diff --git a/frontend/src/components/shared/DividerWithText.tsx b/frontend/src/components/shared/DividerWithText.tsx index 9b82240a1..86984969e 100644 --- a/frontend/src/components/shared/DividerWithText.tsx +++ b/frontend/src/components/shared/DividerWithText.tsx @@ -1,4 +1,4 @@ -import './dividerWithText/DividerWithText.css' +import './dividerWithText/DividerWithText.css'; interface TextDividerProps { text?: string @@ -10,9 +10,9 @@ interface TextDividerProps { } export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) { - const variantClass = variant === 'subcategory' ? 'subcategory' : '' - const themeClass = respondsToDarkMode ? '' : 'force-light' - const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style + const variantClass = variant === 'subcategory' ? 'subcategory' : ''; + const themeClass = respondsToDarkMode ? '' : 'force-light'; + const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style; if (text) { return ( @@ -24,7 +24,7 @@ export default function DividerWithText({ text, className = '', style, variant = {text}
- ) + ); } return ( @@ -32,5 +32,5 @@ export default function DividerWithText({ text, className = '', style, variant = className={`h-px my-2.5 ${themeClass} ${className}`} style={styleWithOpacity} /> - ) + ); } diff --git a/frontend/src/components/shared/LoginRightCarousel.tsx b/frontend/src/components/shared/LoginRightCarousel.tsx index c2ebb29bf..eb7e7ab73 100644 --- a/frontend/src/components/shared/LoginRightCarousel.tsx +++ b/frontend/src/components/shared/LoginRightCarousel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react'; import { BASE_PATH } from '../../constants/app'; type ImageSlide = { src: string; alt?: string; cornerModelUrl?: string; title?: string; subtitle?: string; followMouseTilt?: boolean; tiltMaxDeg?: number } @@ -14,53 +14,53 @@ export default function LoginRightCarousel({ initialSeconds?: number slideSeconds?: number }) { - const totalSlides = imageSlides.length - const [index, setIndex] = useState(0) - const mouse = useRef({ x: 0, y: 0 }) + const totalSlides = imageSlides.length; + const [index, setIndex] = useState(0); + const mouse = useRef({ x: 0, y: 0 }); const durationsMs = useMemo(() => { - if (imageSlides.length === 0) return [] - return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000) - }, [imageSlides, initialSeconds, slideSeconds]) + if (imageSlides.length === 0) return []; + return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000); + }, [imageSlides, initialSeconds, slideSeconds]); useEffect(() => { - if (totalSlides <= 1) return + if (totalSlides <= 1) return; const timeout = setTimeout(() => { - setIndex((i) => (i + 1) % totalSlides) - }, durationsMs[index] ?? slideSeconds * 1000) - return () => clearTimeout(timeout) - }, [index, totalSlides, durationsMs, slideSeconds]) + setIndex((i) => (i + 1) % totalSlides); + }, durationsMs[index] ?? slideSeconds * 1000); + return () => clearTimeout(timeout); + }, [index, totalSlides, durationsMs, slideSeconds]); useEffect(() => { const onMove = (e: MouseEvent) => { - mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1 - mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1 - } - window.addEventListener('mousemove', onMove) - return () => window.removeEventListener('mousemove', onMove) - }, []) + mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1; + }; + window.addEventListener('mousemove', onMove); + return () => window.removeEventListener('mousemove', onMove); + }, []); function TiltImage({ src, alt, enabled, maxDeg = 6 }: { src: string; alt?: string; enabled: boolean; maxDeg?: number }) { - const imgRef = useRef(null) + const imgRef = useRef(null); useEffect(() => { - const el = imgRef.current - if (!el) return + const el = imgRef.current; + if (!el) return; - let raf = 0 + let raf = 0; const tick = () => { if (enabled) { - const rotY = (mouse.current.x || 0) * maxDeg - const rotX = -(mouse.current.y || 0) * maxDeg - el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)` + const rotY = (mouse.current.x || 0) * maxDeg; + const rotX = -(mouse.current.y || 0) * maxDeg; + el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)`; } else { - el.style.transform = 'translateY(-2rem)' + el.style.transform = 'translateY(-2rem)'; } - raf = requestAnimationFrame(tick) - } - raf = requestAnimationFrame(tick) - return () => cancelAnimationFrame(raf) - }, [enabled, maxDeg]) + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [enabled, maxDeg]); return ( - ) + ); } return ( @@ -155,5 +155,5 @@ export default function LoginRightCarousel({ ))} - ) + ); } diff --git a/frontend/src/components/shared/loginSlides.ts b/frontend/src/components/shared/loginSlides.ts index aa3d7f443..690f84f6c 100644 --- a/frontend/src/components/shared/loginSlides.ts +++ b/frontend/src/components/shared/loginSlides.ts @@ -38,6 +38,6 @@ export const loginSlides: LoginCarouselSlide[] = [ followMouseTilt: true, tiltMaxDeg: 5, }, -] +]; -export default loginSlides +export default loginSlides; diff --git a/frontend/src/routes/AuthCallback.tsx b/frontend/src/routes/AuthCallback.tsx index 285d58251..be4cd5424 100644 --- a/frontend/src/routes/AuthCallback.tsx +++ b/frontend/src/routes/AuthCallback.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { useAuth } from '../auth/UseSession' +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth/UseSession'; /** * OAuth Callback Handler @@ -10,50 +10,50 @@ import { useAuth } from '../auth/UseSession' * We extract it, store in localStorage, and redirect to the home page. */ export default function AuthCallback() { - const navigate = useNavigate() - const { refreshSession } = useAuth() + const navigate = useNavigate(); + const { refreshSession } = useAuth(); useEffect(() => { const handleCallback = async () => { try { - console.log('[AuthCallback] Handling OAuth callback...') + console.log('[AuthCallback] Handling OAuth callback...'); // Extract JWT from URL fragment (#access_token=...) - const hash = window.location.hash.substring(1) // Remove '#' - const params = new URLSearchParams(hash) - const token = params.get('access_token') + const hash = window.location.hash.substring(1); // Remove '#' + const params = new URLSearchParams(hash); + const token = params.get('access_token'); if (!token) { - console.error('[AuthCallback] No access_token in URL fragment') + console.error('[AuthCallback] No access_token in URL fragment'); navigate('/login', { replace: true, state: { error: 'OAuth login failed - no token received.' } - }) - return + }); + return; } // Store JWT in localStorage - localStorage.setItem('stirling_jwt', token) - console.log('[AuthCallback] JWT stored in localStorage') + localStorage.setItem('stirling_jwt', token); + console.log('[AuthCallback] JWT stored in localStorage'); // Refresh session to load user info into state - await refreshSession() + await refreshSession(); - console.log('[AuthCallback] Session refreshed, redirecting to home') + console.log('[AuthCallback] Session refreshed, redirecting to home'); // Clear the hash from URL and redirect to home page - navigate('/', { replace: true }) + navigate('/', { replace: true }); } catch (error) { - console.error('[AuthCallback] Error:', error) + console.error('[AuthCallback] Error:', error); navigate('/login', { replace: true, state: { error: 'OAuth login failed. Please try again.' } - }) + }); } - } + }; - handleCallback() - }, [navigate, refreshSession]) + handleCallback(); + }, [navigate, refreshSession]); return (
- ) + ); } diff --git a/frontend/src/routes/Landing.tsx b/frontend/src/routes/Landing.tsx index 0eb2ba091..6f473f6f1 100644 --- a/frontend/src/routes/Landing.tsx +++ b/frontend/src/routes/Landing.tsx @@ -1,8 +1,8 @@ -import { Navigate, useLocation } from 'react-router-dom' -import { useAuth } from '../auth/UseSession' -import { useAppConfig } from '../hooks/useAppConfig' -import HomePage from '../pages/HomePage' -import Login from './Login' +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../auth/UseSession'; +import { useAppConfig } from '../hooks/useAppConfig'; +import HomePage from '../pages/HomePage'; +import Login from './Login'; /** * Landing component - Smart router based on authentication status @@ -12,18 +12,18 @@ import Login from './Login' * If user is not authenticated: Show Login or redirect to /login */ export default function Landing() { - const { session, loading: authLoading } = useAuth() - const { config, loading: configLoading } = useAppConfig() - const location = useLocation() + const { session, loading: authLoading } = useAuth(); + const { config, loading: configLoading } = useAppConfig(); + const location = useLocation(); - const loading = authLoading || configLoading + const loading = authLoading || configLoading; console.log('[Landing] State:', { pathname: location.pathname, loading, hasSession: !!session, loginEnabled: config?.enableLogin, - }) + }); // Show loading while checking auth and config if (loading) { @@ -36,27 +36,27 @@ export default function Landing() { - ) + ); } // If login is disabled, show app directly (anonymous mode) if (config?.enableLogin === false) { - console.debug('[Landing] Login disabled - showing app in anonymous mode') - return + console.debug('[Landing] Login disabled - showing app in anonymous mode'); + return ; } // If we have a session, show the main app if (session) { - return + return ; } // If we're at home route ("/"), show login directly (marketing/landing page) // Otherwise navigate to login (fixes URL mismatch for tool routes) - const isHome = location.pathname === '/' || location.pathname === '' + const isHome = location.pathname === '/' || location.pathname === ''; if (isHome) { - return + return ; } // For non-home routes without auth, navigate to login (preserves from location) - return + return ; } diff --git a/frontend/src/routes/Login.tsx b/frontend/src/routes/Login.tsx index 61efb0c74..04c345936 100644 --- a/frontend/src/routes/Login.tsx +++ b/frontend/src/routes/Login.tsx @@ -1,42 +1,42 @@ -import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { springAuth } from '../auth/springAuthClient' -import { useAuth } from '../auth/UseSession' -import { useTranslation } from 'react-i18next' -import { useDocumentMeta } from '../hooks/useDocumentMeta' -import AuthLayout from './authShared/AuthLayout' +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { springAuth } from '../auth/springAuthClient'; +import { useAuth } from '../auth/UseSession'; +import { useTranslation } from 'react-i18next'; +import { useDocumentMeta } from '../hooks/useDocumentMeta'; +import AuthLayout from './authShared/AuthLayout'; // Import login components -import LoginHeader from './login/LoginHeader' -import ErrorMessage from './login/ErrorMessage' -import EmailPasswordForm from './login/EmailPasswordForm' -import OAuthButtons from './login/OAuthButtons' -import DividerWithText from '../components/shared/DividerWithText' -import LoggedInState from './login/LoggedInState' -import { BASE_PATH } from '../constants/app' +import LoginHeader from './login/LoginHeader'; +import ErrorMessage from './login/ErrorMessage'; +import EmailPasswordForm from './login/EmailPasswordForm'; +import OAuthButtons from './login/OAuthButtons'; +import DividerWithText from '../components/shared/DividerWithText'; +import LoggedInState from './login/LoggedInState'; +import { BASE_PATH } from '../constants/app'; export default function Login() { - const navigate = useNavigate() - const { session, loading } = useAuth() - const { t } = useTranslation() - const [isSigningIn, setIsSigningIn] = useState(false) - const [error, setError] = useState(null) - const [showEmailForm, setShowEmailForm] = useState(false) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') + const navigate = useNavigate(); + const { session, loading } = useAuth(); + const { t } = useTranslation(); + const [isSigningIn, setIsSigningIn] = useState(false); + const [error, setError] = useState(null); + const [showEmailForm, setShowEmailForm] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); // Prefill email from query param (e.g. after password reset) useEffect(() => { try { - const url = new URL(window.location.href) - const emailFromQuery = url.searchParams.get('email') + const url = new URL(window.location.href); + const emailFromQuery = url.searchParams.get('email'); if (emailFromQuery) { - setEmail(emailFromQuery) + setEmail(emailFromQuery); } } catch (_) { // ignore } - }, []) + }, []); const baseUrl = window.location.origin + BASE_PATH; @@ -48,74 +48,74 @@ export default function Login() { ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), ogImage: `${baseUrl}/og_images/home.png`, ogUrl: `${window.location.origin}${window.location.pathname}` - }) + }); // Show logged in state if authenticated if (session && !loading) { - return + return ; } const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => { try { - setIsSigningIn(true) - setError(null) + setIsSigningIn(true); + setError(null); - console.log(`[Login] Signing in with ${provider}`) + console.log(`[Login] Signing in with ${provider}`); // Redirect to Spring OAuth2 endpoint const { error } = await springAuth.signInWithOAuth({ provider, options: { redirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - console.error(`[Login] ${provider} error:`, error) - setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`) + console.error(`[Login] ${provider} error:`, error); + setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`); } } catch (err) { - console.error(`[Login] Unexpected error:`, err) - setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') + console.error(`[Login] Unexpected error:`, err); + setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred'); } finally { - setIsSigningIn(false) + setIsSigningIn(false); } - } + }; const signInWithEmail = async () => { if (!email || !password) { - setError(t('login.pleaseEnterBoth') || 'Please enter both email and password') - return + setError(t('login.pleaseEnterBoth') || 'Please enter both email and password'); + return; } try { - setIsSigningIn(true) - setError(null) + setIsSigningIn(true); + setError(null); - console.log('[Login] Signing in with email:', email) + console.log('[Login] Signing in with email:', email); const { user, session, error } = await springAuth.signInWithPassword({ email: email.trim(), password: password - }) + }); if (error) { - console.error('[Login] Email sign in error:', error) - setError(error.message) + console.error('[Login] Email sign in error:', error); + setError(error.message); } else if (user && session) { - console.log('[Login] Email sign in successful') + console.log('[Login] Email sign in successful'); // Auth state will update automatically and Landing will redirect to home // No need to navigate manually here } } catch (err) { - console.error('[Login] Unexpected error:', err) - setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') + console.error('[Login] Unexpected error:', err); + setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred'); } finally { - setIsSigningIn(false) + setIsSigningIn(false); } - } + }; const handleForgotPassword = () => { - navigate('/auth/reset') - } + navigate('/auth/reset'); + }; return ( @@ -185,5 +185,5 @@ export default function Login() { - ) + ); } diff --git a/frontend/src/routes/Signup.tsx b/frontend/src/routes/Signup.tsx index 0a5ee32ac..465ff338b 100644 --- a/frontend/src/routes/Signup.tsx +++ b/frontend/src/routes/Signup.tsx @@ -1,28 +1,28 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useTranslation } from 'react-i18next' -import { useDocumentMeta } from '../hooks/useDocumentMeta' -import AuthLayout from './authShared/AuthLayout' -import './authShared/auth.css' -import { BASE_PATH } from '../constants/app' +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useDocumentMeta } from '../hooks/useDocumentMeta'; +import AuthLayout from './authShared/AuthLayout'; +import './authShared/auth.css'; +import { BASE_PATH } from '../constants/app'; // Import signup components -import LoginHeader from './login/LoginHeader' -import ErrorMessage from './login/ErrorMessage' -import DividerWithText from '../components/shared/DividerWithText' -import SignupForm from './signup/SignupForm' -import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation' -import { useAuthService } from './signup/AuthService' +import LoginHeader from './login/LoginHeader'; +import ErrorMessage from './login/ErrorMessage'; +import DividerWithText from '../components/shared/DividerWithText'; +import SignupForm from './signup/SignupForm'; +import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation'; +import { useAuthService } from './signup/AuthService'; export default function Signup() { - const navigate = useNavigate() - const { t } = useTranslation() - const [isSigningUp, setIsSigningUp] = useState(false) - const [error, setError] = useState(null) - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [fieldErrors, setFieldErrors] = useState({}) + const navigate = useNavigate(); + const { t } = useTranslation(); + const [isSigningUp, setIsSigningUp] = useState(false); + const [error, setError] = useState(null); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [fieldErrors, setFieldErrors] = useState({}); const baseUrl = window.location.origin + BASE_PATH; @@ -34,38 +34,38 @@ export default function Signup() { ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), ogImage: `${baseUrl}/og_images/home.png`, ogUrl: `${window.location.origin}${window.location.pathname}` - }) + }); - const { validateSignupForm } = useSignupFormValidation() - const { signUp } = useAuthService() + const { validateSignupForm } = useSignupFormValidation(); + const { signUp } = useAuthService(); const handleSignUp = async () => { - const validation = validateSignupForm(email, password, confirmPassword) + const validation = validateSignupForm(email, password, confirmPassword); if (!validation.isValid) { - setError(validation.error) - setFieldErrors(validation.fieldErrors || {}) - return + setError(validation.error); + setFieldErrors(validation.fieldErrors || {}); + return; } try { - setIsSigningUp(true) - setError(null) - setFieldErrors({}) + setIsSigningUp(true); + setError(null); + setFieldErrors({}); - const result = await signUp(email, password, '') + const result = await signUp(email, password, ''); if (result.user) { // Show success message and redirect to login - setError(null) - setTimeout(() => navigate('/login'), 2000) + setError(null); + setTimeout(() => navigate('/login'), 2000); } } catch (err) { - console.error('[Signup] Unexpected error:', err) - setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' })) + console.error('[Signup] Unexpected error:', err); + setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' })); } finally { - setIsSigningUp(false) + setIsSigningUp(false); } - } + }; return ( @@ -101,5 +101,5 @@ export default function Signup() { - ) + ); } diff --git a/frontend/src/routes/authShared/AuthLayout.tsx b/frontend/src/routes/authShared/AuthLayout.tsx index e32f629e0..99c4b69de 100644 --- a/frontend/src/routes/authShared/AuthLayout.tsx +++ b/frontend/src/routes/authShared/AuthLayout.tsx @@ -1,51 +1,51 @@ -import React, { useEffect, useRef, useState } from 'react' -import LoginRightCarousel from '../../components/shared/LoginRightCarousel' -import loginSlides from '../../components/shared/loginSlides' -import styles from './AuthLayout.module.css' +import React, { useEffect, useRef, useState } from 'react'; +import LoginRightCarousel from '../../components/shared/LoginRightCarousel'; +import loginSlides from '../../components/shared/loginSlides'; +import styles from './AuthLayout.module.css'; interface AuthLayoutProps { children: React.ReactNode } export default function AuthLayout({ children }: AuthLayoutProps) { - const cardRef = useRef(null) - const [hideRightPanel, setHideRightPanel] = useState(false) + const cardRef = useRef(null); + const [hideRightPanel, setHideRightPanel] = useState(false); // Force light mode on auth pages useEffect(() => { - const htmlElement = document.documentElement - const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme') + const htmlElement = document.documentElement; + const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme'); // Set light mode - htmlElement.setAttribute('data-mantine-color-scheme', 'light') + htmlElement.setAttribute('data-mantine-color-scheme', 'light'); // Cleanup: restore previous theme when leaving auth pages return () => { if (previousColorScheme) { - htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme) + htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme); } - } - }, []) + }; + }, []); useEffect(() => { const update = () => { // Use viewport to avoid hysteresis when the card is already in single-column mode - const viewportWidth = window.innerWidth - const viewportHeight = window.innerHeight - const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96) // matches min(73.75rem, 96vw) - const columnWidth = cardWidthIfTwoCols / 2 - const tooNarrow = columnWidth < 470 - const tooShort = viewportHeight < 740 - setHideRightPanel(tooNarrow || tooShort) - } - update() - window.addEventListener('resize', update) - window.addEventListener('orientationchange', update) + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // matches min(73.75rem, 96vw) + const columnWidth = cardWidthIfTwoCols / 2; + const tooNarrow = columnWidth < 470; + const tooShort = viewportHeight < 740; + setHideRightPanel(tooNarrow || tooShort); + }; + update(); + window.addEventListener('resize', update); + window.addEventListener('orientationchange', update); return () => { - window.removeEventListener('resize', update) - window.removeEventListener('orientationchange', update) - } - }, []) + window.removeEventListener('resize', update); + window.removeEventListener('orientationchange', update); + }; + }, []); return (
@@ -64,5 +64,5 @@ export default function AuthLayout({ children }: AuthLayoutProps) { )}
- ) + ); } diff --git a/frontend/src/routes/login/EmailPasswordForm.tsx b/frontend/src/routes/login/EmailPasswordForm.tsx index 7036e24cf..dff38026f 100644 --- a/frontend/src/routes/login/EmailPasswordForm.tsx +++ b/frontend/src/routes/login/EmailPasswordForm.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from 'react-i18next' -import '../authShared/auth.css' +import { useTranslation } from 'react-i18next'; +import '../authShared/auth.css'; interface EmailPasswordFormProps { email: string @@ -27,12 +27,12 @@ export default function EmailPasswordForm({ showPasswordField = true, fieldErrors = {} }: EmailPasswordFormProps) { - const { t } = useTranslation() + const { t } = useTranslation(); const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - onSubmit() - } + e.preventDefault(); + onSubmit(); + }; return (
@@ -82,5 +82,5 @@ export default function EmailPasswordForm({ {submitButtonText}
- ) + ); } diff --git a/frontend/src/routes/login/ErrorMessage.tsx b/frontend/src/routes/login/ErrorMessage.tsx index 4f237b77c..4563a2450 100644 --- a/frontend/src/routes/login/ErrorMessage.tsx +++ b/frontend/src/routes/login/ErrorMessage.tsx @@ -3,11 +3,11 @@ interface ErrorMessageProps { } export default function ErrorMessage({ error }: ErrorMessageProps) { - if (!error) return null + if (!error) return null; return (

{error}

- ) + ); } diff --git a/frontend/src/routes/login/LoggedInState.tsx b/frontend/src/routes/login/LoggedInState.tsx index 19483b8bc..7caf92bcf 100644 --- a/frontend/src/routes/login/LoggedInState.tsx +++ b/frontend/src/routes/login/LoggedInState.tsx @@ -1,20 +1,20 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { useAuth } from '../../auth/UseSession' -import { useTranslation } from 'react-i18next' +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/UseSession'; +import { useTranslation } from 'react-i18next'; export default function LoggedInState() { - const navigate = useNavigate() - const { user } = useAuth() - const { t } = useTranslation() + const navigate = useNavigate(); + const { user } = useAuth(); + const { t } = useTranslation(); useEffect(() => { const timer = setTimeout(() => { - navigate('/') - }, 2000) + navigate('/'); + }, 2000); - return () => clearTimeout(timer) - }, [navigate]) + return () => clearTimeout(timer); + }, [navigate]); return (
- ) + ); } diff --git a/frontend/src/routes/login/LoginHeader.tsx b/frontend/src/routes/login/LoginHeader.tsx index 093100574..dde6867ab 100644 --- a/frontend/src/routes/login/LoginHeader.tsx +++ b/frontend/src/routes/login/LoginHeader.tsx @@ -18,5 +18,5 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {

{subtitle}

)} - ) + ); } diff --git a/frontend/src/routes/login/NavigationLink.tsx b/frontend/src/routes/login/NavigationLink.tsx index 965a659b9..935654381 100644 --- a/frontend/src/routes/login/NavigationLink.tsx +++ b/frontend/src/routes/login/NavigationLink.tsx @@ -15,5 +15,5 @@ export default function NavigationLink({ onClick, text, isDisabled = false }: Na {text} - ) + ); } diff --git a/frontend/src/routes/login/OAuthButtons.tsx b/frontend/src/routes/login/OAuthButtons.tsx index 0de8f3179..b5d5ec54c 100644 --- a/frontend/src/routes/login/OAuthButtons.tsx +++ b/frontend/src/routes/login/OAuthButtons.tsx @@ -1,5 +1,5 @@ -import { useTranslation } from 'react-i18next' -import { BASE_PATH } from '../../constants/app' +import { useTranslation } from 'react-i18next'; +import { BASE_PATH } from '../../constants/app'; // OAuth provider configuration const oauthProviders = [ @@ -7,7 +7,7 @@ const oauthProviders = [ { id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false }, { id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true }, { id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true } -] +]; interface OAuthButtonsProps { onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void @@ -16,10 +16,10 @@ interface OAuthButtonsProps { } export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical' }: OAuthButtonsProps) { - const { t } = useTranslation() + const { t } = useTranslation(); // Filter out disabled providers - don't show them at all - const enabledProviders = oauthProviders.filter(p => !p.isDisabled) + const enabledProviders = oauthProviders.filter(p => !p.isDisabled); if (layout === 'icons') { return ( @@ -37,7 +37,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } if (layout === 'grid') { @@ -56,7 +56,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } return ( @@ -74,5 +74,5 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' ))} - ) + ); } diff --git a/frontend/src/routes/signup/AuthService.ts b/frontend/src/routes/signup/AuthService.ts index ce2426fa2..a68e72d24 100644 --- a/frontend/src/routes/signup/AuthService.ts +++ b/frontend/src/routes/signup/AuthService.ts @@ -1,5 +1,5 @@ -import { springAuth } from '../../auth/springAuthClient' -import { BASE_PATH } from '../../constants/app' +import { springAuth } from '../../auth/springAuthClient'; +import { BASE_PATH } from '../../constants/app'; export const useAuthService = () => { @@ -8,7 +8,7 @@ export const useAuthService = () => { password: string, name: string ) => { - console.log('[Signup] Creating account for:', email) + console.log('[Signup] Creating account for:', email); const { user, session, error } = await springAuth.signUp({ email: email.trim(), @@ -17,38 +17,38 @@ export const useAuthService = () => { data: { full_name: name }, emailRedirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - console.error('[Signup] Sign up error:', error) - throw new Error(error.message) + console.error('[Signup] Sign up error:', error); + throw new Error(error.message); } if (user) { - console.log('[Signup] Sign up successful:', user) + console.log('[Signup] Sign up successful:', user); return { user: user, session: session, requiresEmailConfirmation: user && !session - } + }; } - throw new Error('Unknown error occurred during signup') - } + throw new Error('Unknown error occurred during signup'); + }; const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => { const { error } = await springAuth.signInWithOAuth({ provider, options: { redirectTo: `${BASE_PATH}/auth/callback` } - }) + }); if (error) { - throw new Error(error.message) + throw new Error(error.message); } - } + }; return { signUp, signInWithProvider - } -} \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/frontend/src/routes/signup/SignupForm.tsx b/frontend/src/routes/signup/SignupForm.tsx index 2ae809467..9ff7bc7f4 100644 --- a/frontend/src/routes/signup/SignupForm.tsx +++ b/frontend/src/routes/signup/SignupForm.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react' -import '../authShared/auth.css' -import { useTranslation } from 'react-i18next' -import { SignupFieldErrors } from './SignupFormValidation' +import { useEffect } from 'react'; +import '../authShared/auth.css'; +import { useTranslation } from 'react-i18next'; +import { SignupFieldErrors } from './SignupFormValidation'; interface SignupFormProps { name?: string @@ -38,14 +38,14 @@ export default function SignupForm({ showName = false, showTerms = false }: SignupFormProps) { - const { t } = useTranslation() - const showConfirm = password.length >= 4 + const { t } = useTranslation(); + const showConfirm = password.length >= 4; useEffect(() => { if (!showConfirm && confirmPassword) { - setConfirmPassword('') + setConfirmPassword(''); } - }, [showConfirm, confirmPassword, setConfirmPassword]) + }, [showConfirm, confirmPassword, setConfirmPassword]); return ( <> @@ -158,5 +158,5 @@ export default function SignupForm({ {isSubmitting ? t('signup.creatingAccount') : t('signup.signUp')} - ) + ); } \ No newline at end of file diff --git a/frontend/src/routes/signup/SignupFormValidation.ts b/frontend/src/routes/signup/SignupFormValidation.ts index 7abe498a0..40c1ad7e7 100644 --- a/frontend/src/routes/signup/SignupFormValidation.ts +++ b/frontend/src/routes/signup/SignupFormValidation.ts @@ -1,4 +1,4 @@ -import { useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next'; export interface SignupFieldErrors { name?: string @@ -14,7 +14,7 @@ export interface SignupValidationResult { } export const useSignupFormValidation = () => { - const { t } = useTranslation() + const { t } = useTranslation(); const validateSignupForm = ( email: string, @@ -22,45 +22,45 @@ export const useSignupFormValidation = () => { confirmPassword: string, name?: string ): SignupValidationResult => { - const fieldErrors: SignupFieldErrors = {} + const fieldErrors: SignupFieldErrors = {}; // Validate name if (name !== undefined && name !== null && !name.trim()) { - fieldErrors.name = t('signup.nameRequired', 'Name is required') + fieldErrors.name = t('signup.nameRequired', 'Name is required'); } // Validate email - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email) { - fieldErrors.email = t('signup.emailRequired', 'Email is required') + fieldErrors.email = t('signup.emailRequired', 'Email is required'); } else if (!emailRegex.test(email)) { - fieldErrors.email = t('signup.invalidEmail') + fieldErrors.email = t('signup.invalidEmail'); } // Validate password if (!password) { - fieldErrors.password = t('signup.passwordRequired', 'Password is required') + fieldErrors.password = t('signup.passwordRequired', 'Password is required'); } else if (password.length < 6) { - fieldErrors.password = t('signup.passwordTooShort') + fieldErrors.password = t('signup.passwordTooShort'); } // Validate confirm password if (!confirmPassword) { - fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password') + fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password'); } else if (password !== confirmPassword) { - fieldErrors.confirmPassword = t('signup.passwordsDoNotMatch') + fieldErrors.confirmPassword = t('signup.passwordsDoNotMatch'); } - const hasErrors = Object.keys(fieldErrors).length > 0 + const hasErrors = Object.keys(fieldErrors).length > 0; return { isValid: !hasErrors, error: null, // Don't show generic error, field errors are more specific fieldErrors: hasErrors ? fieldErrors : undefined - } - } + }; + }; return { validateSignupForm - } -} \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/frontend/src/tests/missingTranslations.test.ts b/frontend/src/tests/missingTranslations.test.ts index 908ab1504..53d635e16 100644 --- a/frontend/src/tests/missingTranslations.test.ts +++ b/frontend/src/tests/missingTranslations.test.ts @@ -3,7 +3,7 @@ import path from 'path'; import ts from 'typescript'; import { describe, expect, test } from 'vitest'; -const REPO_ROOT = path.join(__dirname, '../../../') +const REPO_ROOT = path.join(__dirname, '../../../'); const SRC_ROOT = path.join(__dirname, '..'); const EN_GB_FILE = path.join(__dirname, '../../public/locales/en-GB/translation.json');