All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m17s
🎨 Color harmony improvements for better visual balance: ## Changes Made - **Icons**: User/Lock icons changed from bright maritime-teal to subdued deep-sea-blue/60 - **Input Focus**: Focus rings changed from bright teal to elegant deep-sea-blue/50 - **Sign In Button**: Simplified from teal-green gradient to solid deep-sea-blue - **Error Messages**: More subdued red-900/30 background for better harmony ## Result - More cohesive professional appearance - Better contrast balance against Monaco harbor background - Elegant, understated design that doesn't compete with background - WCAG AA compliant contrast ratios maintained 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, FormEvent } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Lock, User, Loader2 } from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import Image from 'next/image';
|
|
|
|
export default function AdminLogin() {
|
|
const router = useRouter();
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
setError(data.error || 'Login failed');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Successful login - redirect to dashboard
|
|
router.push('/admin/dashboard');
|
|
} catch (err) {
|
|
setError('Network error. Please try again.');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen w-full flex items-center justify-center p-4 relative overflow-hidden">
|
|
{/* Monaco Background Image */}
|
|
<div
|
|
className="absolute inset-0 w-full h-full bg-cover bg-center"
|
|
style={{
|
|
backgroundImage: 'url(/monaco_high_res.jpg)',
|
|
filter: 'brightness(0.6) contrast(1.1)'
|
|
}}
|
|
/>
|
|
|
|
{/* Overlay gradient for better readability */}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/30 to-black/50" />
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="relative w-full max-w-md z-10"
|
|
>
|
|
{/* Logo and Title */}
|
|
<div className="text-center mb-8">
|
|
<motion.div
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
transition={{ delay: 0.2, duration: 0.5 }}
|
|
className="flex justify-center mb-4"
|
|
>
|
|
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center shadow-lg p-2">
|
|
<Image
|
|
src="/puffinOffset.png"
|
|
alt="Puffin Offset Logo"
|
|
width={64}
|
|
height={64}
|
|
className="object-contain"
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
<motion.h1
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.3, duration: 0.5 }}
|
|
className="text-3xl font-bold mb-2 text-off-white"
|
|
>
|
|
Admin Portal
|
|
</motion.h1>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.4, duration: 0.5 }}
|
|
className="font-medium text-off-white/80"
|
|
>
|
|
Puffin Offset Management
|
|
</motion.p>
|
|
</div>
|
|
|
|
{/* Login Card */}
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: 0.5, duration: 0.5 }}
|
|
className="bg-white border border-light-gray-border rounded-xl p-8 shadow-2xl"
|
|
>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Error Message */}
|
|
{error && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="bg-red-900/30 border border-red-800/60 rounded-lg p-3 text-red-100 text-sm"
|
|
>
|
|
{error}
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Username Field */}
|
|
<div>
|
|
<label htmlFor="username" className="block text-sm font-medium text-deep-sea-blue mb-2">
|
|
Username
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<User className="h-5 w-5 text-deep-sea-blue/60" />
|
|
</div>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-3 bg-white border border-light-gray-border rounded-lg text-deep-sea-blue placeholder-deep-sea-blue/40 focus:outline-none focus:ring-2 focus:ring-deep-sea-blue/50 focus:border-deep-sea-blue transition-all"
|
|
placeholder="Enter your username"
|
|
required
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-deep-sea-blue mb-2">
|
|
Password
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Lock className="h-5 w-5 text-deep-sea-blue/60" />
|
|
</div>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-3 bg-white border border-light-gray-border rounded-lg text-deep-sea-blue placeholder-deep-sea-blue/40 focus:outline-none focus:ring-2 focus:ring-deep-sea-blue/50 focus:border-deep-sea-blue transition-all"
|
|
placeholder="Enter your password"
|
|
required
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<motion.button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
|
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
|
className={`w-full py-3 px-4 rounded-lg font-semibold text-white shadow-lg transition-all ${
|
|
isLoading
|
|
? 'bg-deep-sea-blue/50 cursor-not-allowed'
|
|
: 'bg-deep-sea-blue hover:bg-deep-sea-blue/90'
|
|
}`}
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center justify-center">
|
|
<Loader2 className="animate-spin mr-2" size={20} />
|
|
Signing in...
|
|
</span>
|
|
) : (
|
|
'Sign In'
|
|
)}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
|
|
{/* Footer */}
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.6, duration: 0.5 }}
|
|
className="text-center mt-6 text-sm font-medium text-off-white/70"
|
|
>
|
|
© 2024 Puffin Offset. Secure admin access.
|
|
</motion.p>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|