"use client"; import { useState, useEffect, useRef } from "react"; import { useRouter, useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useAuthStore } from "@/stores/auth-store"; import { Mail, AlertCircle, Loader2, X } from "lucide-react"; export default function LoginPage() { const router = useRouter(); const params = useParams(); const t = useTranslations("login"); const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore(); const serverUrl = process.env.NEXT_PUBLIC_JMAP_SERVER_URL; const appName = process.env.NEXT_PUBLIC_APP_NAME || 'Webmail'; // All hooks must be called unconditionally at the top const [formData, setFormData] = useState({ username: "", password: "", }); const [savedUsernames, setSavedUsernames] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [filteredSuggestions, setFilteredSuggestions] = useState([]); const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); const suggestionsRef = useRef(null); const inputRef = useRef(null); const justSelectedSuggestion = useRef(false); // Set page title useEffect(() => { if (serverUrl) { document.title = appName; } }, [appName, serverUrl]); // Load saved usernames from localStorage on mount useEffect(() => { if (!serverUrl) return; const saved = localStorage.getItem("webmail_usernames"); if (saved) { try { const usernames = JSON.parse(saved); setSavedUsernames(usernames); } catch { console.error("Failed to parse saved usernames"); } } }, [serverUrl]); useEffect(() => { if (isAuthenticated) { router.push(`/${params.locale}`); } }, [isAuthenticated, router, params.locale]); useEffect(() => { clearError(); }, [formData, clearError]); // Filter suggestions based on input useEffect(() => { if (!serverUrl) return; // Skip showing suggestions if we just selected one if (justSelectedSuggestion.current) { justSelectedSuggestion.current = false; return; } if (formData.username && savedUsernames.length > 0) { const filtered = savedUsernames.filter(username => username.toLowerCase().includes(formData.username.toLowerCase()) ); setFilteredSuggestions(filtered); setShowSuggestions(filtered.length > 0); } else if (formData.username === "" && savedUsernames.length > 0) { setFilteredSuggestions(savedUsernames); setShowSuggestions(false); // Don't show on empty input } else { setShowSuggestions(false); } setSelectedSuggestionIndex(-1); }, [formData.username, savedUsernames, serverUrl]); // Close suggestions when clicking outside useEffect(() => { if (!serverUrl) return; const handleClickOutside = (event: MouseEvent) => { if (suggestionsRef.current && !suggestionsRef.current.contains(event.target as Node) && inputRef.current && !inputRef.current.contains(event.target as Node)) { setShowSuggestions(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [serverUrl]); // Show error if JMAP server URL is not configured if (!serverUrl) { return (

Configuration Error

NEXT_PUBLIC_JMAP_SERVER_URL environment variable is not set.

); } // Save username on successful login const saveUsername = (username: string) => { const saved = localStorage.getItem("webmail_usernames"); let usernames: string[] = []; if (saved) { try { usernames = JSON.parse(saved); } catch { console.error("Failed to parse saved usernames"); } } // Add username if not already present, keep max 5 recent usernames if (!usernames.includes(username)) { usernames = [username, ...usernames].slice(0, 5); localStorage.setItem("webmail_usernames", JSON.stringify(usernames)); setSavedUsernames(usernames); } }; // Remove a username from saved list const removeUsername = (username: string, e: React.MouseEvent) => { e.stopPropagation(); const updated = savedUsernames.filter(u => u !== username); localStorage.setItem("webmail_usernames", JSON.stringify(updated)); setSavedUsernames(updated); setFilteredSuggestions(updated.filter(u => u.toLowerCase().includes(formData.username.toLowerCase()) )); }; const handleUsernameChange = (e: React.ChangeEvent) => { setFormData({ ...formData, username: e.target.value }); }; const handleUsernameFocus = () => { if (savedUsernames.length > 0 && formData.username === "") { setFilteredSuggestions(savedUsernames); setShowSuggestions(true); } else if (filteredSuggestions.length > 0) { setShowSuggestions(true); } }; const selectSuggestion = (username: string) => { justSelectedSuggestion.current = true; setFormData({ ...formData, username }); setShowSuggestions(false); // Focus password field document.getElementById("password")?.focus(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions || filteredSuggestions.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); setSelectedSuggestionIndex(prev => prev < filteredSuggestions.length - 1 ? prev + 1 : prev ); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelectedSuggestionIndex(prev => prev > 0 ? prev - 1 : -1); } else if (e.key === "Enter" && selectedSuggestionIndex >= 0) { e.preventDefault(); selectSuggestion(filteredSuggestions[selectedSuggestionIndex]); } else if (e.key === "Escape") { setShowSuggestions(false); setSelectedSuggestionIndex(-1); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const success = await login( serverUrl, formData.username, formData.password ); if (success) { saveUsername(formData.username); router.push(`/${params.locale}`); } }; return (
{/* Logo */}

{appName}

{/* Error Message */} {error && (

{t(`error.${error}`) || t("error.generic")}

)} {/* Login Form */}
{/* Custom autocomplete dropdown */} {showSuggestions && filteredSuggestions.length > 0 && (
{filteredSuggestions.map((username, index) => (
selectSuggestion(username)} > {username}
))}
)}
setFormData({ ...formData, password: e.target.value })} className="h-12 px-4 bg-secondary/50 border-border/50 focus:bg-secondary focus:border-primary/50 transition-colors" placeholder={t("password_placeholder")} required autoComplete="current-password" />
); }