From af7b68ee8685f6c535ceff810b20e9e421c1a76c Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Mon, 22 Dec 2025 13:45:02 -0300 Subject: [PATCH] Fix login i18n and forgot password page --- frontend/src/app/forgot-password/page.tsx | 69 +++++++++++++++++++++++ frontend/src/app/login/page.tsx | 60 +++++++++++--------- frontend/src/i18n/en.json | 43 ++++++++++++++ frontend/src/i18n/es.json | 43 ++++++++++++++ frontend/src/i18n/pt-BR.json | 43 ++++++++++++++ frontend/src/lib/i18n.tsx | 27 ++++++++- 6 files changed, 255 insertions(+), 30 deletions(-) create mode 100644 frontend/src/app/forgot-password/page.tsx diff --git a/frontend/src/app/forgot-password/page.tsx b/frontend/src/app/forgot-password/page.tsx new file mode 100644 index 0000000..d3fb836 --- /dev/null +++ b/frontend/src/app/forgot-password/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useTranslation } from "@/lib/i18n"; + +export default function ForgotPasswordPage() { + const { t } = useTranslation(); + const [email, setEmail] = useState(""); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setSubmitted(true); + }; + + return ( +
+
+
+

{t("auth.forgot.title")}

+

{t("auth.forgot.subtitle")}

+
+ + + + {submitted && ( + + {t("auth.forgot.success")} + + )} + +
+
+ + setEmail(event.target.value)} + placeholder={t("auth.forgot.fields.emailPlaceholder")} + required + /> +
+ + +
+
+
+ +
+ + {t("auth.forgot.backLogin")} + +
+
+
+ ); +} diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 16b348f..918cb50 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; @@ -27,20 +27,25 @@ import { motion } from "framer-motion"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; +import { useTranslation } from "@/lib/i18n"; -const loginSchema = z.object({ - email: z.string().min(3, "Usuário deve ter pelo menos 3 caracteres"), - password: z.string().min(3, "Senha deve ter pelo menos 3 caracteres"), - rememberMe: z.boolean().optional(), -}); - -type LoginFormData = z.infer; +type LoginFormData = { + email: string; + password: string; + rememberMe?: boolean; +}; export default function LoginPage() { const router = useRouter(); + const { t } = useTranslation(); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); + const loginSchema = useMemo(() => z.object({ + email: z.string().min(3, t("auth.login.validation.username")), + password: z.string().min(3, t("auth.login.validation.password")), + rememberMe: z.boolean().optional(), + }), [t]); const { register, @@ -71,11 +76,11 @@ export default function LoginPage() { router.push("/dashboard"); } else { - setError("Usuário ou senha inválidos."); + setError(t("auth.login.errors.invalidCredentials")); } } catch (err: any) { console.error(err); - setError(err.message || "Erro ao fazer login. Tente novamente."); + setError(err.message || t("auth.login.errors.generic")); } finally { setLoading(false); } @@ -105,12 +110,11 @@ export default function LoginPage() {

- Conecte-se ao seu futuro profissional + {t("auth.login.hero.title")}

- A plataforma que une talentos e oportunidades. Entre na sua conta e - descubra as melhores vagas do mercado. + {t("auth.login.hero.subtitle")}

@@ -118,19 +122,19 @@ export default function LoginPage() {
- Perfil profissional completo + {t("auth.login.hero.bulletProfile")}
- Empresas de destaque + {t("auth.login.hero.bulletCompanies")}
- Vagas exclusivas + {t("auth.login.hero.bulletJobs")}
@@ -144,9 +148,9 @@ export default function LoginPage() { className="w-full max-w-md space-y-6" >
-

Bem-vindo de volta

+

{t("auth.login.title")}

- Entre com sua conta para continuar + {t("auth.login.subtitle")}

@@ -166,13 +170,13 @@ export default function LoginPage() { )}
- +
@@ -185,13 +189,13 @@ export default function LoginPage() {
- +
@@ -223,14 +227,14 @@ export default function LoginPage() { htmlFor="rememberMe" className="text-sm font-normal cursor-pointer" > - Lembrar de mim + {t("auth.login.rememberMe")}
- - Esqueceu a senha? + {t("auth.login.forgotPassword")}
@@ -239,7 +243,7 @@ export default function LoginPage() { className="w-full h-11 cursor-pointer" disabled={loading} > - {loading ? "Entrando..." : "Entrar"} + {loading ? t("auth.login.loading") : t("auth.login.submit")} @@ -250,7 +254,7 @@ export default function LoginPage() { href="/" className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-2" > - ← Voltar para home + {t("auth.login.backHome")}
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index d904acc..bcb6420 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -62,5 +62,48 @@ "privacy": "Privacy Policy", "terms": "Terms of Use", "copyright": "© {year} GoHorse Jobs. All rights reserved." }, + "auth": { + "login": { + "title": "Welcome back", + "subtitle": "Sign in to continue", + "hero": { + "title": "Connect to your professional future", + "subtitle": "The platform that brings talent and opportunities together. Sign in and discover the best jobs in the market.", + "bulletProfile": "Complete professional profile", + "bulletCompanies": "Featured companies", + "bulletJobs": "Exclusive jobs" + }, + "fields": { + "username": "Username", + "usernamePlaceholder": "Enter your username", + "password": "Password", + "passwordPlaceholder": "••••••••" + }, + "rememberMe": "Remember me", + "forgotPassword": "Forgot password?", + "submit": "Sign in", + "loading": "Signing in...", + "backHome": "← Back to home", + "validation": { + "username": "Username must be at least 3 characters", + "password": "Password must be at least 3 characters" + }, + "errors": { + "invalidCredentials": "Invalid username or password.", + "generic": "Error signing in. Please try again." + } + }, + "forgot": { + "title": "Reset your password", + "subtitle": "Enter your email and we'll send recovery instructions.", + "fields": { + "email": "Email", + "emailPlaceholder": "you@email.com" + }, + "submit": "Send reset link", + "success": "If the email exists, we'll send recovery instructions shortly.", + "backLogin": "← Back to login" + } + }, "common": { "loading": "Loading...", "error": "Error", "retry": "Retry", "noResults": "No results found" } } diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 45056ef..5809205 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -62,5 +62,48 @@ "privacy": "Política de Privacidad", "terms": "Términos de Uso", "copyright": "© {year} GoHorse Jobs. Todos los derechos reservados." }, + "auth": { + "login": { + "title": "Bienvenido de nuevo", + "subtitle": "Inicia sesión para continuar", + "hero": { + "title": "Conéctate con tu futuro profesional", + "subtitle": "La plataforma que une talentos y oportunidades. Inicia sesión y descubre las mejores ofertas del mercado.", + "bulletProfile": "Perfil profesional completo", + "bulletCompanies": "Empresas destacadas", + "bulletJobs": "Vacantes exclusivas" + }, + "fields": { + "username": "Usuario", + "usernamePlaceholder": "Ingresa tu usuario", + "password": "Contraseña", + "passwordPlaceholder": "••••••••" + }, + "rememberMe": "Recordarme", + "forgotPassword": "¿Olvidaste tu contraseña?", + "submit": "Ingresar", + "loading": "Ingresando...", + "backHome": "← Volver al inicio", + "validation": { + "username": "El usuario debe tener al menos 3 caracteres", + "password": "La contraseña debe tener al menos 3 caracteres" + }, + "errors": { + "invalidCredentials": "Usuario o contraseña inválidos.", + "generic": "Error al iniciar sesión. Inténtalo de nuevo." + } + }, + "forgot": { + "title": "Restablecer contraseña", + "subtitle": "Ingresa tu correo y te enviaremos instrucciones de recuperación.", + "fields": { + "email": "Correo electrónico", + "emailPlaceholder": "tu@email.com" + }, + "submit": "Enviar enlace de restablecimiento", + "success": "Si el correo existe, te enviaremos instrucciones en breve.", + "backLogin": "← Volver al login" + } + }, "common": { "loading": "Cargando...", "error": "Error", "retry": "Reintentar", "noResults": "No se encontraron resultados" } } diff --git a/frontend/src/i18n/pt-BR.json b/frontend/src/i18n/pt-BR.json index d541895..afe9945 100644 --- a/frontend/src/i18n/pt-BR.json +++ b/frontend/src/i18n/pt-BR.json @@ -62,5 +62,48 @@ "privacy": "Política de Privacidade", "terms": "Termos de Uso", "copyright": "© {year} GoHorse Jobs. Todos os direitos reservados." }, + "auth": { + "login": { + "title": "Bem-vindo de volta", + "subtitle": "Entre com sua conta para continuar", + "hero": { + "title": "Conecte-se ao seu futuro profissional", + "subtitle": "A plataforma que une talentos e oportunidades. Entre na sua conta e descubra as melhores vagas do mercado.", + "bulletProfile": "Perfil profissional completo", + "bulletCompanies": "Empresas de destaque", + "bulletJobs": "Vagas exclusivas" + }, + "fields": { + "username": "Usuário", + "usernamePlaceholder": "Digite seu usuário", + "password": "Senha", + "passwordPlaceholder": "••••••••" + }, + "rememberMe": "Lembrar de mim", + "forgotPassword": "Esqueceu a senha?", + "submit": "Entrar", + "loading": "Entrando...", + "backHome": "← Voltar para home", + "validation": { + "username": "Usuário deve ter pelo menos 3 caracteres", + "password": "Senha deve ter pelo menos 3 caracteres" + }, + "errors": { + "invalidCredentials": "Usuário ou senha inválidos.", + "generic": "Erro ao fazer login. Tente novamente." + } + }, + "forgot": { + "title": "Recuperar senha", + "subtitle": "Informe seu e-mail e enviaremos instruções de recuperação.", + "fields": { + "email": "E-mail", + "emailPlaceholder": "seu@email.com" + }, + "submit": "Enviar link de recuperação", + "success": "Se o e-mail existir, enviaremos as instruções em instantes.", + "backLogin": "← Voltar para login" + } + }, "common": { "loading": "Carregando...", "error": "Erro", "retry": "Tentar novamente", "noResults": "Nenhum resultado encontrado" } } diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index c9a3d5e..c4a816f 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import en from '@/i18n/en.json'; import es from '@/i18n/es.json'; import ptBR from '@/i18n/pt-BR.json'; @@ -21,8 +21,25 @@ const dictionaries: Record = { const I18nContext = createContext(null); +const localeStorageKey = 'locale'; + +const normalizeLocale = (language: string): Locale => { + if (language.startsWith('pt')) return 'pt-BR'; + if (language.startsWith('es')) return 'es'; + return 'en'; +}; + +const getInitialLocale = (): Locale => { + if (typeof window === 'undefined') return 'en'; + const storedLocale = localStorage.getItem(localeStorageKey); + if (storedLocale && storedLocale in dictionaries) { + return storedLocale as Locale; + } + return normalizeLocale(navigator.language); +}; + export function I18nProvider({ children }: { children: ReactNode }) { - const [locale, setLocale] = useState('en'); + const [locale, setLocale] = useState(getInitialLocale); const t = useCallback((key: string, params?: Record): string => { const keys = key.split('.'); @@ -49,6 +66,12 @@ export function I18nProvider({ children }: { children: ReactNode }) { return value; }, [locale]); + useEffect(() => { + if (typeof window === 'undefined') return; + localStorage.setItem(localeStorageKey, locale); + document.documentElement.lang = locale; + }, [locale]); + return ( {children}