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>
This commit is contained in:
Ludy 2025-10-27 17:22:56 +01:00 committed by GitHub
parent c67859a1ff
commit 3e9c55243e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 274 additions and 274 deletions

View File

@ -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 =
<span className="text-divider__label">{text}</span>
<div className="text-divider__rule" />
</div>
)
);
}
return (
@ -32,5 +32,5 @@ export default function DividerWithText({ text, className = '', style, variant =
className={`h-px my-2.5 ${themeClass} ${className}`}
style={styleWithOpacity}
/>
)
);
}

View File

@ -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<HTMLImageElement | null>(null)
const imgRef = useRef<HTMLImageElement | null>(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 (
<img
@ -79,7 +79,7 @@ export default function LoginRightCarousel({
transformOrigin: '50% 50%',
}}
/>
)
);
}
return (
@ -155,5 +155,5 @@ export default function LoginRightCarousel({
))}
</div>
</div>
)
);
}

View File

@ -38,6 +38,6 @@ export const loginSlides: LoginCarouselSlide[] = [
followMouseTilt: true,
tiltMaxDeg: 5,
},
]
];
export default loginSlides
export default loginSlides;

View File

@ -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 (
<div style={{
@ -69,5 +69,5 @@ export default function AuthCallback() {
</div>
</div>
</div>
)
);
}

View File

@ -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() {
</div>
</div>
</div>
)
);
}
// If login is disabled, show app directly (anonymous mode)
if (config?.enableLogin === false) {
console.debug('[Landing] Login disabled - showing app in anonymous mode')
return <HomePage />
console.debug('[Landing] Login disabled - showing app in anonymous mode');
return <HomePage />;
}
// If we have a session, show the main app
if (session) {
return <HomePage />
return <HomePage />;
}
// 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 <Login />
return <Login />;
}
// For non-home routes without auth, navigate to login (preserves from location)
return <Navigate to="/login" replace state={{ from: location }} />
return <Navigate to="/login" replace state={{ from: location }} />;
}

View File

@ -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<string | null>(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<string | null>(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 <LoggedInState />
return <LoggedInState />;
}
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 (
<AuthLayout>
@ -185,5 +185,5 @@ export default function Login() {
</div>
</AuthLayout>
)
);
}

View File

@ -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<string | null>(null)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [fieldErrors, setFieldErrors] = useState<SignupFieldErrors>({})
const navigate = useNavigate();
const { t } = useTranslation();
const [isSigningUp, setIsSigningUp] = useState(false);
const [error, setError] = useState<string | null>(null);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [fieldErrors, setFieldErrors] = useState<SignupFieldErrors>({});
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 (
<AuthLayout>
@ -101,5 +101,5 @@ export default function Signup() {
</button>
</div>
</AuthLayout>
)
);
}

View File

@ -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<HTMLDivElement | null>(null)
const [hideRightPanel, setHideRightPanel] = useState(false)
const cardRef = useRef<HTMLDivElement | null>(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 (
<div className={styles.authContainer}>
@ -64,5 +64,5 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
)}
</div>
</div>
)
);
}

View File

@ -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 (
<form onSubmit={handleSubmit}>
@ -82,5 +82,5 @@ export default function EmailPasswordForm({
{submitButtonText}
</button>
</form>
)
);
}

View File

@ -3,11 +3,11 @@ interface ErrorMessageProps {
}
export default function ErrorMessage({ error }: ErrorMessageProps) {
if (!error) return null
if (!error) return null;
return (
<div className="error-message">
<p className="error-message-text">{error}</p>
</div>
)
);
}

View File

@ -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 (
<div style={{
@ -50,5 +50,5 @@ export default function LoggedInState() {
</div>
</div>
</div>
)
);
}

View File

@ -18,5 +18,5 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {
<p className="login-subtitle">{subtitle}</p>
)}
</div>
)
);
}

View File

@ -15,5 +15,5 @@ export default function NavigationLink({ onClick, text, isDisabled = false }: Na
{text}
</button>
</div>
)
);
}

View File

@ -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 = '
</div>
))}
</div>
)
);
}
if (layout === 'grid') {
@ -56,7 +56,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
</div>
))}
</div>
)
);
}
return (
@ -74,5 +74,5 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
</button>
))}
</div>
)
);
}

View File

@ -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
}
}
};
};

View File

@ -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')}
</button>
</>
)
);
}

View File

@ -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
}
}
};
};

View File

@ -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');