From 3e9c55243e418642db4a6e04447941a40482c4cb Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 27 Oct 2025 17:22:56 +0100 Subject: [PATCH] style(frontend): enforce semicolons across auth & shared components (#4737) # Description of Changes This pull request primarily focuses on code style improvements across several frontend files, standardizing the use of semicolons and ensuring consistent formatting. No functional or logic changes are introduced; the updates are purely syntactic to improve code readability and maintainability. **Code Style and Formatting Improvements:** * Added missing semicolons and standardized import statements in multiple files, including `DividerWithText.tsx`, `LoginRightCarousel.tsx`, `loginSlides.ts`, `AuthCallback.tsx`, `Landing.tsx`, `Login.tsx`, and `Signup.tsx`. [[1]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL1-R1) [[2]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L1-R1) [[3]](diffhunk://#diff-1fc806abd10f8882945f54b56828db4c4b9a8b986743250b26dd9bdf0ec49bdbL41-R43) [[4]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL1-R3) [[5]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL1-R5) [[6]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL1-R39) [[7]](diffhunk://#diff-0a98c2e661e58f226f98c90b2e82198090b9fd986bbd98c2af6574d19f2ee37aL1-R25) * Updated function bodies and return statements to use consistent semicolon placement and code formatting throughout the affected files. [[1]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL13-R15) [[2]](diffhunk://#diff-5de1b22e63fe3b6c9781c2a476db7440818f18d2aeb5c6c1ddeb446517cf001fL27-R35) [[3]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L17-R63) [[4]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L82-R82) [[5]](diffhunk://#diff-7cc961105816564bebd8656fe59119970d5859b4557f48c37fe920d344a948c3L158-R158) [[6]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL13-R56) [[7]](diffhunk://#diff-540ce2405611334ce0bdff1f48d187218be99ce64fb92f054b9cf5a71cb1ed8cL72-R72) [[8]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL15-R26) [[9]](diffhunk://#diff-d55dde4f28998eb9b30f332a1c96a4c79ec6a70b568bb51eea81d11a3715c35cL39-R61) [[10]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL51-R118) [[11]](diffhunk://#diff-183a38f7c78b7c2950c4bed87ff2843de146d960e28591865d91c3cd86c3fadbL188-R188) [[12]](diffhunk://#diff-0a98c2e661e58f226f98c90b2e82198090b9fd986bbd98c2af6574d19f2ee37aL1-R25) No business logic, UI, or feature behavior has been changed as part of this update. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- .../src/components/shared/DividerWithText.tsx | 12 +- .../components/shared/LoginRightCarousel.tsx | 66 +++++------ frontend/src/components/shared/loginSlides.ts | 4 +- frontend/src/routes/AuthCallback.tsx | 46 ++++---- frontend/src/routes/Landing.tsx | 34 +++--- frontend/src/routes/Login.tsx | 108 +++++++++--------- frontend/src/routes/Signup.tsx | 78 ++++++------- frontend/src/routes/authShared/AuthLayout.tsx | 56 ++++----- .../src/routes/login/EmailPasswordForm.tsx | 14 +-- frontend/src/routes/login/ErrorMessage.tsx | 4 +- frontend/src/routes/login/LoggedInState.tsx | 24 ++-- frontend/src/routes/login/LoginHeader.tsx | 2 +- frontend/src/routes/login/NavigationLink.tsx | 2 +- frontend/src/routes/login/OAuthButtons.tsx | 16 +-- frontend/src/routes/signup/AuthService.ts | 30 ++--- frontend/src/routes/signup/SignupForm.tsx | 18 +-- .../src/routes/signup/SignupFormValidation.ts | 32 +++--- .../src/tests/missingTranslations.test.ts | 2 +- 18 files changed, 274 insertions(+), 274 deletions(-) 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');