semillero-2-AI-ATS/components/AppContext.tsx

239 lines
9.5 KiB
TypeScript

"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
type Language = "en" | "es";
type Theme = "light" | "dark";
interface AppContextType {
lang: Language;
setLang: (lang: Language) => void;
theme: Theme;
setTheme: (theme: Theme) => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
const loginTranslations = {
en: {
title: "Secure Access",
subtitle: "Please enter the password to access the Semillero AI Recruitment Platform.",
placeholder: "Enter password",
button: "Unlock",
error: "Incorrect password",
required: "Password is required",
unlocking: "Unlocking...",
},
es: {
title: "Acceso Seguro",
subtitle: "Por favor ingrese la contraseña para acceder a la Plataforma de Reclutamiento Semillero IA.",
placeholder: "Ingrese la contraseña",
button: "Desbloquear",
error: "Contraseña incorrecta",
required: "La contraseña es requerida",
unlocking: "Desbloqueando...",
}
};
export function AppProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Language>("en");
const [theme, setThemeState] = useState<Theme>("light");
const [mounted, setMounted] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [password, setPassword] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [loadingAuth, setLoadingAuth] = useState(false);
// Load from localStorage on mount
useEffect(() => {
const storedLang = localStorage.getItem("lang") as Language;
const storedTheme = localStorage.getItem("theme") as Theme;
const sessionTime = localStorage.getItem("auth_session_time");
setTimeout(() => {
if (storedLang === "en" || storedLang === "es") {
setLangState(storedLang);
}
if (storedTheme === "light" || storedTheme === "dark") {
setThemeState(storedTheme);
} else {
// Check system preference
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
if (mediaQuery.matches) {
setThemeState("dark");
}
}
// Check auth session: auto log off after 24 hours (1 day)
if (sessionTime) {
const lastSession = parseInt(sessionTime, 10);
const oneDayMs = 24 * 60 * 60 * 1000;
if (Date.now() - lastSession < oneDayMs) {
setIsAuthenticated(true);
} else {
localStorage.removeItem("auth_session_time");
}
}
setMounted(true);
}, 0);
}, []);
// Sync theme to document element
useEffect(() => {
if (!mounted) return;
const root = window.document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}, [theme, mounted]);
const setLang = (newLang: Language) => {
setLangState(newLang);
localStorage.setItem("lang", newLang);
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem("theme", newTheme);
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!password.trim()) {
setErrorMsg(lang === "es" ? loginTranslations.es.required : loginTranslations.en.required);
return;
}
setLoadingAuth(true);
setErrorMsg("");
try {
const response = await fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
});
if (response.ok) {
localStorage.setItem("auth_session_time", Date.now().toString());
setIsAuthenticated(true);
} else {
await response.json();
setErrorMsg(lang === "es" ? loginTranslations.es.error : loginTranslations.en.error);
}
} catch (err) {
console.error("Authentication failed:", err);
setErrorMsg("Connection error. Please try again.");
} finally {
setLoadingAuth(false);
}
};
const t = lang === "es" ? loginTranslations.es : loginTranslations.en;
if (!mounted) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 transition-colors duration-200">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
if (!isAuthenticated) {
return (
<AppContext.Provider value={{ lang, setLang, theme, setTheme }}>
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 p-4 transition-colors duration-200 relative font-sans">
{/* Top-right Language and Theme Controls */}
<div className="absolute top-4 right-4 flex items-center gap-2">
{/* Language Selector */}
<button
onClick={() => setLang(lang === "en" ? "es" : "en")}
className="px-2.5 py-1.5 text-xs font-semibold text-slate-700 dark:text-slate-200 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-md shadow-xs hover:bg-slate-50 dark:hover:bg-slate-800 transition duration-150 cursor-pointer"
>
{lang === "en" ? "Español (ES)" : "English (EN)"}
</button>
{/* Theme Toggle */}
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="p-1.5 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-md shadow-xs hover:bg-slate-50 dark:hover:bg-slate-800 transition duration-150 cursor-pointer"
>
{theme === "light" ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4.5 h-4.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4.5 h-4.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m0 13.5V21M4.22 4.22l1.59 1.59m12.38 12.38l1.59 1.59M3 12h2.25m13.5 0H21M6.09 18.36l1.59-1.59m12.38-12.38l-1.59 1.59M12 7.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z" />
</svg>
)}
</button>
</div>
{/* Login Lock Card */}
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg shadow-sm p-8 max-w-sm w-full flex flex-col gap-5 transition-colors duration-200">
<div className="flex flex-col items-center text-center gap-2">
{/* Shield Lock Icon */}
<div className="w-12 h-12 bg-blue-50 dark:bg-blue-950/40 rounded-full flex items-center justify-center text-blue-600 dark:text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0V10.5m-2.25 10.5h13.5c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H5.25c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125Z" />
</svg>
</div>
<h2 className="text-xl font-bold text-slate-900 dark:text-white mt-1">
{t.title}
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-normal max-w-[280px]">
{t.subtitle}
</p>
</div>
<form onSubmit={handleLogin} className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t.placeholder}
disabled={loadingAuth}
className="w-full px-3.5 py-2 border border-slate-200 dark:border-slate-800 rounded-md text-slate-900 dark:text-white bg-slate-50 dark:bg-slate-800/40 text-sm focus:outline-none focus:border-blue-500 dark:focus:border-blue-500/80 transition duration-150"
autoFocus
/>
{errorMsg && (
<p className="text-[11px] text-red-600 dark:text-red-400 font-semibold mt-0.5">
{errorMsg}
</p>
)}
</div>
<button
type="submit"
disabled={loadingAuth}
className="w-full py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-semibold text-sm rounded-md transition duration-200 cursor-pointer flex items-center justify-center gap-1.5"
>
{loadingAuth ? t.unlocking : t.button}
</button>
</form>
</div>
</div>
</AppContext.Provider>
);
}
return (
<AppContext.Provider value={{ lang, setLang, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error("useApp must be used within an AppProvider");
}
return context;
}