| import React, { useState, useEffect, useRef } from 'react'; | |
| import { Mode } from '@/types'; | |
| interface LoginModalProps { | |
| onLoginSuccess: (mode: Mode) => void; | |
| onClose: () => void; | |
| } | |
| const LoginModal: React.FC<LoginModalProps> = ({ onLoginSuccess, onClose }) => { | |
| const [username, setUsername] = useState(''); | |
| const [password, setPassword] = useState(''); | |
| const [hasFocused, setHasFocused] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const modalRef = useRef<HTMLDivElement>(null); | |
| const usernameRef = useRef<HTMLInputElement>(null); | |
| const CONTROLLER_NAME = process.env.CONTROLLER_NAME || ''; | |
| const CONTROLLER_PASSWORD = process.env.CONTROLLER_PASSWORD || ''; | |
| if (!CONTROLLER_NAME || !CONTROLLER_PASSWORD) { | |
| throw new Error("Environment variables for login credentials are not set."); | |
| } | |
| useEffect(() => { | |
| if (!hasFocused) { | |
| setHasFocused(true); | |
| usernameRef.current?.focus(); | |
| } | |
| const handleEsc = (event: KeyboardEvent) => { | |
| if (event.key === 'Escape') { | |
| onClose(); | |
| } | |
| }; | |
| window.addEventListener('keydown', handleEsc); | |
| return () => { | |
| window.removeEventListener('keydown', handleEsc); | |
| }; | |
| }, [onClose]); | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setError(null); | |
| setIsSubmitting(true); | |
| setTimeout(() => { | |
| if (username === CONTROLLER_NAME && password === CONTROLLER_PASSWORD) { | |
| onLoginSuccess('controller'); | |
| } else { | |
| setError('Username atau PIN/Password salah. Silakan coba lagi.'); | |
| } | |
| setIsSubmitting(false); | |
| }, 500); | |
| }; | |
| const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { | |
| if (modalRef.current && e.target === modalRef.current) { | |
| onClose(); | |
| } | |
| }; | |
| return ( | |
| <div | |
| ref={modalRef} | |
| onClick={handleBackdropClick} | |
| className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 transition-opacity duration-300 animate-fade-in" | |
| > | |
| <div className="bg-gray-800 border border-gray-700 rounded-lg shadow-xl p-6 sm:p-8 w-full max-w-sm transform animate-scale-in"> | |
| <h2 className="text-2xl font-bold text-center text-indigo-400 mb-6">Login</h2> | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div> | |
| <label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-1"> | |
| Username | |
| </label> | |
| <input | |
| ref={usernameRef} | |
| type="text" | |
| id="username" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| className="w-full bg-gray-900 border border-gray-600 rounded-md px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| required | |
| /> | |
| </div> | |
| <div> | |
| <label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-1"> | |
| PIN / Password | |
| </label> | |
| <input | |
| type="password" | |
| id="password" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| className="w-full bg-gray-900 border border-gray-600 rounded-md px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| required | |
| /> | |
| </div> | |
| {error && <p className="text-red-400 text-sm text-center">{error}</p>} | |
| <div className="pt-2 flex flex-col sm:flex-row-reverse gap-3"> | |
| <button | |
| type="submit" | |
| disabled={isSubmitting} | |
| className="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-md font-semibold text-white transition-colors disabled:bg-indigo-800 disabled:cursor-not-allowed flex items-center justify-center" | |
| > | |
| {isSubmitting && <SpinnerIcon />} | |
| {isSubmitting ? 'Memverifikasi...' : 'Login'} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={onClose} | |
| className="w-full px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md font-semibold text-white transition-colors" | |
| > | |
| Batal | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| <style>{` | |
| @keyframes fade-in { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| @keyframes scale-in { | |
| from { opacity: 0; transform: scale(0.95); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| .animate-fade-in { animation: fade-in 0.3s ease-out forwards; } | |
| .animate-scale-in { animation: scale-in 0.3s ease-out forwards; } | |
| `}</style> | |
| </div> | |
| ); | |
| }; | |
| const SpinnerIcon = () => ( | |
| <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| ); | |
| export default LoginModal; | |