diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 914b0dc..748c0c7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import Login from './pages/Login'; import AdminDashboard from './pages/AdminDashboard'; import UserDashboard from './pages/UserDashboard'; +import './i18n'; // Initialize translations function App() { const [token, setToken] = useState(localStorage.getItem('token')); diff --git a/frontend/src/components/LanguageSelector.jsx b/frontend/src/components/LanguageSelector.jsx new file mode 100644 index 0000000..c07469f --- /dev/null +++ b/frontend/src/components/LanguageSelector.jsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import { Globe } from 'lucide-react'; + +function LanguageSelector() { + const { i18n } = useTranslation(); + + const toggleLanguage = () => { + const newLang = i18n.language === 'es' ? 'en' : 'es'; + i18n.changeLanguage(newLang); + }; + + return ( + + ); +} + +export default LanguageSelector; diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js new file mode 100644 index 0000000..68b9e3a --- /dev/null +++ b/frontend/src/i18n.js @@ -0,0 +1,143 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// Translations +const resources = { + en: { + translation: { + // General + "title": "VigIA Control", + "logout": "Logout", + "login_title": "Login", + "login_subtitle": "Sign in to access the control panel", + "username": "Username", + "password": "Password", + "sign_in": "Sign In", + "welcome": "Welcome", + + // Admin Dashboard + "monitor_area": "Monitor Area", + "plate_approvals": "Plate Approvals", + "user_management": "User Management", + "visitor_approvals": "Visitor Approvals", + "pending_approvals": "Pending Approvals", + "approve": "Approve", + "deny": "Deny", + "all_plates": "All Registered Plates", + "create_user": "Create New User", + "user_list": "User List", + "delete": "Delete", + + // User Dashboard + "my_plates_title": "My Registered Plates", + "register_plate": "Register New Plate", + "plate_number": "Plate Number (e.g. AA-BB-11)", + "owner_desc": "Owner / Description", + "submit_plate": "Register Plate", + "my_visitors_title": "My Visitors", + "register_visitor": "Register Visitor", + "visitor_rut": "RUT (12345678-9)", + "full_name": "Full Name", + "submit_visitor": "Register Visitor", + "access_days": "Access Duration", + "1_day": "1 Day Access", + "2_days": "2 Days Access", + "1_week": "1 Week Access", + + // Status + "active": "ACTIVE", + "pending": "PENDING", + "denied": "DENIED", + "approved": "APPROVED", + + // Messages + "confirm_delete_plate": "Are you sure you want to delete this plate?", + "confirm_delete_visitor": "Are you sure you want to delete this visitor?", + + // Visitor Check + "visitor_check": "Visitor Check", + "check_status": "Check Status", + "enter_rut": "Enter RUT", + "visitor_found": "Visitor Found", + "visitor_not_found": "Visitor Not Found", + "access_granted_until": "Access granted until", + "registered_by": "Registered by", + "no_record": "No record found for this RUT." + } + }, + es: { + translation: { + // General + "title": "Control VigIA", + "logout": "Cerrar sesión", + "login_title": "Iniciar Sesión", + "login_subtitle": "Ingresa para acceder al panel de control", + "username": "Nombre de usuario", + "password": "Contraseña", + "sign_in": "Ingresar", + "welcome": "Bienvenido", + + // Admin Dashboard + "monitor_area": "Monitor", + "plate_approvals": "Aprobación Patentes", + "user_management": "Gestión Usuarios", + "visitor_approvals": "Aprobación Visitas", + "pending_approvals": "Aprobaciones Pendientes", + "approve": "Aprobar", + "deny": "Rechazar", + "all_plates": "Todas las Patentes", + "create_user": "Crear Usuario", + "user_list": "Lista de Usuarios", + "delete": "Eliminar", + + // User Dashboard + "my_plates_title": "Mis Patentes Registradas", + "register_plate": "Registrar Nueva Patente", + "plate_number": "Patente (ej. AA-BB-11)", + "owner_desc": "Propietario / Descripción", + "submit_plate": "Registrar Patente", + "my_visitors_title": "Mis Visitas", + "register_visitor": "Registrar Visita", + "visitor_rut": "RUT (12345678-9)", + "full_name": "Nombre Completo", + "submit_visitor": "Registrar Visita", + "access_days": "Duración Acceso", + "1_day": "Acceso 1 Día", + "2_days": "Acceso 2 Días", + "1_week": "Acceso 1 Semana", + + // Status + "active": "ACTIVO", + "pending": "PENDIENTE", + "denied": "DENEGADO", + "approved": "APROBADO", + + // Messages + "confirm_delete_plate": "¿Estás seguro de que quieres eliminar esta patente?", + "confirm_delete_visitor": "¿Estás seguro de que quieres eliminar esta visita?", + + // Visitor Check + "visitor_check": "Consultar Visita", + "check_status": "Verificar Estado", + "enter_rut": "Ingresar RUT", + "visitor_found": "Visita Encontrada", + "visitor_not_found": "Visita No Encontrada", + "access_granted_until": "Acceso permitido hasta", + "registered_by": "Registrado por", + "no_record": "No se encontró registro para este RUT." + } + } +}; + +i18n + .use(initReactI18next) + .init({ + resources, + lng: "es", // Default language + fallbackLng: "en", + interpolation: { + escapeValue: false + } + }); + +export default i18n; diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 4c8f6a4..218aee0 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -1,12 +1,15 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import io from 'socket.io-client'; -import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, Clock, Calendar, AlertCircle } from 'lucide-react'; +import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, AlertCircle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../components/LanguageSelector'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const socket = io(API_URL); function AdminDashboard({ token }) { + const { t } = useTranslation(); const [users, setUsers] = useState([]); const [plates, setPlates] = useState([]); const [newUser, setNewUser] = useState({ username: '', password: '', role: 'USER' }); @@ -162,30 +165,33 @@ function AdminDashboard({ token }) {

- Admin Portal + {t('title')}

-
- - - - +
+
+ + + + +
+
@@ -193,7 +199,7 @@ function AdminDashboard({ token }) {

- Live Monitor + {t('monitor_area')}

@@ -204,17 +210,17 @@ function AdminDashboard({ token }) { {/* Visitor Lookup Banner */}

- Visitor Check + {t('visitor_check')}

setSearchRut(e.target.value)} />
@@ -233,12 +239,12 @@ function AdminDashboard({ token }) {
-

Access Authorized

+

{t('visitor_found')}

Visitor: {searchResult.data.name}

Status: {searchResult.data.status}

- Authorized by: @{searchResult.data.addedBy?.username || 'Unknown'} + {t('registered_by')}: @{searchResult.data.addedBy?.username || 'Unknown'}

@@ -249,8 +255,8 @@ function AdminDashboard({ token }) {
-

No Record Found

-

This RUT is not listed in the visitor registry.

+

{t('visitor_not_found')}

+

{t('no_record')}

)} @@ -312,7 +318,7 @@ function AdminDashboard({ token }) { {activeTab === 'plates' && (
-

Pending Approvals

+

{t('pending_approvals')}

{plates.filter(p => p.status === 'PENDING').length === 0 && (

No pending plates.

@@ -328,20 +334,20 @@ function AdminDashboard({ token }) { onClick={() => handleApprovePlate(plate.id, 'ALLOWED')} className="px-4 py-2 bg-green-600 hover:bg-green-500 rounded-lg text-sm font-bold flex items-center gap-2" > - Approve + {t('approve')}
))}
-

All Plates

+

{t('all_plates')}

{plates.filter(p => p.status !== 'PENDING').map(plate => (
@@ -352,7 +358,7 @@ function AdminDashboard({ token }) {
Added by: {plate.addedBy?.username || 'System'}
- {plate.status} + {t(plate.status.toLowerCase()) || plate.status}
@@ -364,18 +370,18 @@ function AdminDashboard({ token }) { {activeTab === 'users' && (
-

Create User

+

{t('create_user')}

setNewUser({ ...newUser, username: e.target.value })} /> setNewUser({ ...newUser, password: e.target.value })} /> @@ -387,12 +393,12 @@ function AdminDashboard({ token }) { - +
-

Existing Users

+

{t('user_list')}

{users.map(u => (
@@ -419,7 +425,7 @@ function AdminDashboard({ token }) { {activeTab === 'visitors' && (
-

Visitor Approvals

+

{t('visitor_approvals')}

{Object.values(activePeople.filter(p => p.status === 'PENDING').reduce((acc, p) => { const uId = p.addedById || 0; @@ -454,3 +460,5 @@ function AdminDashboard({ token }) { } export default AdminDashboard; + +export default AdminDashboard; diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index c983e6e..dde262e 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,65 +1,81 @@ import { useState } from 'react'; import axios from 'axios'; -import { useNavigate } from 'react-router-dom'; +import { Lock, User } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../components/LanguageSelector'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; -function Login({ setToken, setUserRole }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const navigate = useNavigate(); +function Login({ setToken, setUserRole, setUsername }) { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ username: '', password: '' }); + const [error, setError] = useState(''); - const handleLogin = async (e) => { + const handleSubmit = async (e) => { e.preventDefault(); try { - const res = await axios.post(`${API_URL}/api/auth/login`, { username, password }); - const { token, role, username: user } = res.data; + const res = await axios.post(`${API_URL}/api/auth/login`, formData); + localStorage.setItem('token', res.data.token); + localStorage.setItem('role', res.data.role); + localStorage.setItem('username', res.data.username); - localStorage.setItem('token', token); - localStorage.setItem('role', role); - localStorage.setItem('username', user); - - setToken(token); - setUserRole(role); - - if (role === 'ADMIN') { - navigate('/admin'); - } else { - navigate('/user'); - } + setToken(res.data.token); + setUserRole(res.data.role); + setUsername(res.data.username); } catch (err) { - alert('Login failed: ' + (err.response?.data?.error || err.message)); + setError(err.response?.data?.error || 'Login failed'); } }; return ( -
-
-

Control Patente AI

-
-
- - setUsername(e.target.value)} - className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-blue-500 outline-none" - /> +
+
+ +
+
+
+

+ {t('title')} +

+

{t('login_subtitle')}

+
+ + {error && ( +
+ {error}
+ )} + +
- - setPassword(e.target.value)} - className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-blue-500 outline-none" - /> + +
+ + setFormData({ ...formData, username: e.target.value })} + /> +
-
diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx index 6ba2d50..5459637 100644 --- a/frontend/src/pages/UserDashboard.jsx +++ b/frontend/src/pages/UserDashboard.jsx @@ -2,11 +2,14 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import { Car, Clock, CheckCircle, AlertCircle, Users, Trash2, PlusCircle, UserPlus, AlertTriangle } from 'lucide-react'; import io from 'socket.io-client'; +import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../components/LanguageSelector'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const socket = io(API_URL); function UserDashboard({ token, username }) { + const { t } = useTranslation(); const [plates, setPlates] = useState([]); const [newPlate, setNewPlate] = useState({ number: '', owner: '' }); const [detections, setDetections] = useState([]); @@ -91,7 +94,7 @@ function UserDashboard({ token, username }) { }; const handleDeletePlate = async (id) => { - if (!confirm('Are you sure you want to delete this plate?')) return; + if (!confirm(t('confirm_delete_plate'))) return; try { await axios.delete(`${API_URL}/api/plates/${id}`, { headers: { Authorization: `Bearer ${token}` } @@ -103,7 +106,7 @@ function UserDashboard({ token, username }) { }; const handleDeletePerson = async (id) => { - if (!confirm('Are you sure you want to delete this visitor?')) return; + if (!confirm(t('confirm_delete_visitor'))) return; try { await axios.delete(`${API_URL}/api/people/${id}`, { headers: { Authorization: `Bearer ${token}` } @@ -118,48 +121,51 @@ function UserDashboard({ token, username }) {
-
-

- Welcome, {username} -

-

Manage your vehicles and visitors.

+
+
+

+ {t('welcome')}, {username} +

+

Manage your vehicles and visitors.

+
+
{/* Plate Registration Form (Existing) */}

- Register New Plate + {t('register_plate')}

setNewPlate({ ...newPlate, number: e.target.value.toUpperCase() })} required /> setNewPlate({ ...newPlate, owner: e.target.value })} required /> - +
{/* Visitor Registration Form (Existing) */}

- Register Visitor + {t('register_visitor')}

setNewPerson({ ...newPerson, rut: e.target.value })} required @@ -170,26 +176,26 @@ function UserDashboard({ token, username }) { onChange={e => setNewPerson({ ...newPerson, durationDays: parseInt(e.target.value) })} required > - - - + + +
setNewPerson({ ...newPerson, name: e.target.value })} required /> - +
{/* My Plates List */}
-

My Registered Plates

+

{t('my_plates_title')}

{plates.length === 0 &&

No plates registered.

} {plates.map(plate => ( @@ -201,23 +207,23 @@ function UserDashboard({ token, username }) {
{plate.status === 'ALLOWED' && ( - ACTIVE + {t('active')} )} {plate.status === 'PENDING' && ( - PENDING + {t('pending')} )} {plate.status === 'DENIED' && ( - DENIED + {t('denied')} )} @@ -229,7 +235,7 @@ function UserDashboard({ token, username }) { {/* My Visitors List */}
-

My Visitors

+

{t('my_visitors_title')}

{people.length === 0 &&

No visitors registered.

} {people.map(p => ( @@ -241,14 +247,14 @@ function UserDashboard({ token, username }) {
- {p.status} + {p.status === 'APPROVED' ? t('approved') : p.status === 'DENIED' ? t('denied') : t('pending')}