300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState } from 'react';
|
||
import Link from 'next/link';
|
||
import Image from 'next/image';
|
||
import { useRouter } from 'next/navigation';
|
||
import { Eye, EyeOff, Mail, Lock, AlertCircle, CheckCircle } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||
import { cn } from '@/utils/cn';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
|
||
interface LoginForm {
|
||
email: string;
|
||
password: string;
|
||
}
|
||
|
||
interface FormErrors {
|
||
email?: string;
|
||
password?: string;
|
||
general?: string;
|
||
}
|
||
|
||
export default function LoginPage() {
|
||
const router = useRouter();
|
||
const { login, isLoading: authLoading } = useAuth();
|
||
const [form, setForm] = useState<LoginForm>({
|
||
email: '',
|
||
password: ''
|
||
});
|
||
const [errors, setErrors] = useState<FormErrors>({});
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [rememberMe, setRememberMe] = useState(false);
|
||
|
||
const validateForm = (): boolean => {
|
||
const newErrors: FormErrors = {};
|
||
|
||
// Email validation
|
||
if (!form.email) {
|
||
newErrors.email = 'Email wajib diisi';
|
||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
|
||
newErrors.email = 'Format email tidak valid';
|
||
}
|
||
|
||
// Password validation
|
||
if (!form.password) {
|
||
newErrors.password = 'Password wajib diisi';
|
||
} else if (form.password.length < 6) {
|
||
newErrors.password = 'Password minimal 6 karakter';
|
||
}
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
const handleInputChange = (field: keyof LoginForm, value: string) => {
|
||
setForm(prev => ({ ...prev, [field]: value }));
|
||
// Clear error when user starts typing
|
||
if (errors[field]) {
|
||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||
}
|
||
};
|
||
|
||
const handleQuickLogin = (email: string, password: string) => {
|
||
setForm({ email, password });
|
||
setErrors({});
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!validateForm()) return;
|
||
|
||
setIsLoading(true);
|
||
setErrors({});
|
||
|
||
try {
|
||
const success = await login(form.email, form.password);
|
||
|
||
if (!success) {
|
||
setErrors({ general: 'Email atau password salah' });
|
||
}
|
||
// If successful, AuthContext will handle the redirect
|
||
} catch (error) {
|
||
setErrors({ general: 'Terjadi kesalahan. Silakan coba lagi.' });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 flex items-center justify-center p-4">
|
||
<div className="w-full max-w-md">
|
||
{/* Logo and Title */}
|
||
<div className="text-center mb-8">
|
||
<div className="mx-auto mb-4">
|
||
<Image
|
||
src="https://upload.wikimedia.org/wikipedia/id/thumb/2/29/Logo_Badan_Gizi_Nasional.svg/480px-Logo_Badan_Gizi_Nasional.svg.png"
|
||
alt="Logo Badan Gizi Nasional"
|
||
width={64}
|
||
height={64}
|
||
className="rounded-lg"
|
||
priority
|
||
/>
|
||
</div>
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Selamat Datang</h1>
|
||
<p className="text-gray-600">Masuk ke akun Learning Management System Anda</p>
|
||
</div>
|
||
|
||
<Card className="shadow-xl border-0">
|
||
<CardHeader className="space-y-1 pb-6">
|
||
<CardTitle className="text-2xl font-semibold text-center">Masuk</CardTitle>
|
||
<CardDescription className="text-center">
|
||
Masukkan email dan password untuk mengakses dashboard
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
{/* General Error */}
|
||
{errors.general && (
|
||
<Alert variant="destructive">
|
||
<AlertCircle className="h-4 w-4" />
|
||
<AlertDescription>{errors.general}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* Email Field */}
|
||
<div className="space-y-2">
|
||
<label htmlFor="email" className="text-sm font-medium text-gray-700">
|
||
Email
|
||
</label>
|
||
<div className="relative">
|
||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||
<Input
|
||
id="email"
|
||
type="email"
|
||
placeholder="nama@email.com"
|
||
value={form.email}
|
||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||
className={cn(
|
||
"pl-10",
|
||
errors.email && "border-red-500 focus:border-red-500"
|
||
)}
|
||
disabled={isLoading}
|
||
/>
|
||
</div>
|
||
{errors.email && (
|
||
<p className="text-sm text-red-600 flex items-center gap-1">
|
||
<AlertCircle className="h-3 w-3" />
|
||
{errors.email}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Password Field */}
|
||
<div className="space-y-2">
|
||
<label htmlFor="password" className="text-sm font-medium text-gray-700">
|
||
Password
|
||
</label>
|
||
<div className="relative">
|
||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||
<Input
|
||
id="password"
|
||
type={showPassword ? 'text' : 'password'}
|
||
placeholder="Masukkan password"
|
||
value={form.password}
|
||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||
className={cn(
|
||
"pl-10 pr-10",
|
||
errors.password && "border-red-500 focus:border-red-500"
|
||
)}
|
||
disabled={isLoading}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||
disabled={isLoading}
|
||
>
|
||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||
</button>
|
||
</div>
|
||
{errors.password && (
|
||
<p className="text-sm text-red-600 flex items-center gap-1">
|
||
<AlertCircle className="h-3 w-3" />
|
||
{errors.password}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Remember Me & Forgot Password */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<input
|
||
id="remember"
|
||
type="checkbox"
|
||
checked={rememberMe}
|
||
onChange={(e) => setRememberMe(e.target.checked)}
|
||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||
disabled={isLoading}
|
||
/>
|
||
<label htmlFor="remember" className="text-sm text-gray-600">
|
||
Ingat saya
|
||
</label>
|
||
</div>
|
||
<Link
|
||
href="/forgot-password"
|
||
className="text-sm text-blue-600 hover:text-blue-700 hover:underline"
|
||
>
|
||
Lupa password?
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Submit Button */}
|
||
<Button
|
||
type="submit"
|
||
className="w-full"
|
||
disabled={isLoading}
|
||
>
|
||
{isLoading ? (
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||
Memproses...
|
||
</div>
|
||
) : (
|
||
'Masuk'
|
||
)}
|
||
</Button>
|
||
</form>
|
||
|
||
{/* Demo Credentials */}
|
||
<div className="mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||
<div className="flex items-start gap-2 mb-3">
|
||
<CheckCircle className="h-4 w-4 text-blue-600 mt-0.5" />
|
||
<div className="text-sm">
|
||
<p className="font-medium text-blue-800 mb-2">Demo Credentials:</p>
|
||
|
||
{/* Admin Credentials */}
|
||
<div className="mb-3 p-3 bg-white rounded border border-blue-100">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<p className="font-medium text-blue-800">👨💼 Admin:</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleQuickLogin('admin@lms.com', 'admin123')}
|
||
className="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded transition-colors"
|
||
disabled={isLoading}
|
||
>
|
||
Quick Fill
|
||
</button>
|
||
</div>
|
||
<p className="text-blue-700 text-xs">Email: admin@lms.com</p>
|
||
<p className="text-blue-700 text-xs">Password: admin123</p>
|
||
</div>
|
||
|
||
{/* Student Credentials */}
|
||
<div className="p-3 bg-white rounded border border-blue-100">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<p className="font-medium text-blue-800">👨🎓 Student:</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleQuickLogin('student@lms.com', 'student123')}
|
||
className="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded transition-colors"
|
||
disabled={isLoading}
|
||
>
|
||
Quick Fill
|
||
</button>
|
||
</div>
|
||
<p className="text-blue-700 text-xs">Email: student@lms.com</p>
|
||
<p className="text-blue-700 text-xs">Password: student123</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Register Link */}
|
||
<div className="mt-6 text-center">
|
||
<p className="text-sm text-gray-600">
|
||
Belum punya akun?{' '}
|
||
<Link
|
||
href="/register"
|
||
className="text-blue-600 hover:text-blue-700 font-medium hover:underline"
|
||
>
|
||
Daftar sekarang
|
||
</Link>
|
||
</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Footer */}
|
||
<div className="mt-8 text-center text-sm text-gray-500">
|
||
<p>© 2024 Learning Management System. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |