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 { interface TextDividerProps {
text?: string text?: string
@ -10,9 +10,9 @@ interface TextDividerProps {
} }
export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) { export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) {
const variantClass = variant === 'subcategory' ? 'subcategory' : '' const variantClass = variant === 'subcategory' ? 'subcategory' : '';
const themeClass = respondsToDarkMode ? '' : 'force-light' const themeClass = respondsToDarkMode ? '' : 'force-light';
const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style;
if (text) { if (text) {
return ( return (
@ -24,7 +24,7 @@ export default function DividerWithText({ text, className = '', style, variant =
<span className="text-divider__label">{text}</span> <span className="text-divider__label">{text}</span>
<div className="text-divider__rule" /> <div className="text-divider__rule" />
</div> </div>
) );
} }
return ( return (
@ -32,5 +32,5 @@ export default function DividerWithText({ text, className = '', style, variant =
className={`h-px my-2.5 ${themeClass} ${className}`} className={`h-px my-2.5 ${themeClass} ${className}`}
style={styleWithOpacity} 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'; import { BASE_PATH } from '../../constants/app';
type ImageSlide = { src: string; alt?: string; cornerModelUrl?: string; title?: string; subtitle?: string; followMouseTilt?: boolean; tiltMaxDeg?: number } 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 initialSeconds?: number
slideSeconds?: number slideSeconds?: number
}) { }) {
const totalSlides = imageSlides.length const totalSlides = imageSlides.length;
const [index, setIndex] = useState(0) const [index, setIndex] = useState(0);
const mouse = useRef({ x: 0, y: 0 }) const mouse = useRef({ x: 0, y: 0 });
const durationsMs = useMemo(() => { const durationsMs = useMemo(() => {
if (imageSlides.length === 0) return [] if (imageSlides.length === 0) return [];
return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000) return imageSlides.map((_, i) => (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000);
}, [imageSlides, initialSeconds, slideSeconds]) }, [imageSlides, initialSeconds, slideSeconds]);
useEffect(() => { useEffect(() => {
if (totalSlides <= 1) return if (totalSlides <= 1) return;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
setIndex((i) => (i + 1) % totalSlides) setIndex((i) => (i + 1) % totalSlides);
}, durationsMs[index] ?? slideSeconds * 1000) }, durationsMs[index] ?? slideSeconds * 1000);
return () => clearTimeout(timeout) return () => clearTimeout(timeout);
}, [index, totalSlides, durationsMs, slideSeconds]) }, [index, totalSlides, durationsMs, slideSeconds]);
useEffect(() => { useEffect(() => {
const onMove = (e: MouseEvent) => { const onMove = (e: MouseEvent) => {
mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1 mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1 mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1;
} };
window.addEventListener('mousemove', onMove) window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove) return () => window.removeEventListener('mousemove', onMove);
}, []) }, []);
function TiltImage({ src, alt, enabled, maxDeg = 6 }: { src: string; alt?: string; enabled: boolean; maxDeg?: number }) { 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(() => { useEffect(() => {
const el = imgRef.current const el = imgRef.current;
if (!el) return if (!el) return;
let raf = 0 let raf = 0;
const tick = () => { const tick = () => {
if (enabled) { if (enabled) {
const rotY = (mouse.current.x || 0) * maxDeg const rotY = (mouse.current.x || 0) * maxDeg;
const rotX = -(mouse.current.y || 0) * maxDeg const rotX = -(mouse.current.y || 0) * maxDeg;
el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)` el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)`;
} else { } else {
el.style.transform = 'translateY(-2rem)' el.style.transform = 'translateY(-2rem)';
} }
raf = requestAnimationFrame(tick) raf = requestAnimationFrame(tick);
} };
raf = requestAnimationFrame(tick) raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf) return () => cancelAnimationFrame(raf);
}, [enabled, maxDeg]) }, [enabled, maxDeg]);
return ( return (
<img <img
@ -79,7 +79,7 @@ export default function LoginRightCarousel({
transformOrigin: '50% 50%', transformOrigin: '50% 50%',
}} }}
/> />
) );
} }
return ( return (
@ -155,5 +155,5 @@ export default function LoginRightCarousel({
))} ))}
</div> </div>
</div> </div>
) );
} }

View File

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

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react' import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { useAuth } from '../auth/UseSession' import { useAuth } from '../auth/UseSession';
/** /**
* OAuth Callback Handler * OAuth Callback Handler
@ -10,50 +10,50 @@ import { useAuth } from '../auth/UseSession'
* We extract it, store in localStorage, and redirect to the home page. * We extract it, store in localStorage, and redirect to the home page.
*/ */
export default function AuthCallback() { export default function AuthCallback() {
const navigate = useNavigate() const navigate = useNavigate();
const { refreshSession } = useAuth() const { refreshSession } = useAuth();
useEffect(() => { useEffect(() => {
const handleCallback = async () => { const handleCallback = async () => {
try { try {
console.log('[AuthCallback] Handling OAuth callback...') console.log('[AuthCallback] Handling OAuth callback...');
// Extract JWT from URL fragment (#access_token=...) // Extract JWT from URL fragment (#access_token=...)
const hash = window.location.hash.substring(1) // Remove '#' const hash = window.location.hash.substring(1); // Remove '#'
const params = new URLSearchParams(hash) const params = new URLSearchParams(hash);
const token = params.get('access_token') const token = params.get('access_token');
if (!token) { if (!token) {
console.error('[AuthCallback] No access_token in URL fragment') console.error('[AuthCallback] No access_token in URL fragment');
navigate('/login', { navigate('/login', {
replace: true, replace: true,
state: { error: 'OAuth login failed - no token received.' } state: { error: 'OAuth login failed - no token received.' }
}) });
return return;
} }
// Store JWT in localStorage // Store JWT in localStorage
localStorage.setItem('stirling_jwt', token) localStorage.setItem('stirling_jwt', token);
console.log('[AuthCallback] JWT stored in localStorage') console.log('[AuthCallback] JWT stored in localStorage');
// Refresh session to load user info into state // 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 // Clear the hash from URL and redirect to home page
navigate('/', { replace: true }) navigate('/', { replace: true });
} catch (error) { } catch (error) {
console.error('[AuthCallback] Error:', error) console.error('[AuthCallback] Error:', error);
navigate('/login', { navigate('/login', {
replace: true, replace: true,
state: { error: 'OAuth login failed. Please try again.' } state: { error: 'OAuth login failed. Please try again.' }
}) });
} }
} };
handleCallback() handleCallback();
}, [navigate, refreshSession]) }, [navigate, refreshSession]);
return ( return (
<div style={{ <div style={{
@ -69,5 +69,5 @@ export default function AuthCallback() {
</div> </div>
</div> </div>
</div> </div>
) );
} }

View File

@ -1,8 +1,8 @@
import { Navigate, useLocation } from 'react-router-dom' import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../auth/UseSession' import { useAuth } from '../auth/UseSession';
import { useAppConfig } from '../hooks/useAppConfig' import { useAppConfig } from '../hooks/useAppConfig';
import HomePage from '../pages/HomePage' import HomePage from '../pages/HomePage';
import Login from './Login' import Login from './Login';
/** /**
* Landing component - Smart router based on authentication status * 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 * If user is not authenticated: Show Login or redirect to /login
*/ */
export default function Landing() { export default function Landing() {
const { session, loading: authLoading } = useAuth() const { session, loading: authLoading } = useAuth();
const { config, loading: configLoading } = useAppConfig() const { config, loading: configLoading } = useAppConfig();
const location = useLocation() const location = useLocation();
const loading = authLoading || configLoading const loading = authLoading || configLoading;
console.log('[Landing] State:', { console.log('[Landing] State:', {
pathname: location.pathname, pathname: location.pathname,
loading, loading,
hasSession: !!session, hasSession: !!session,
loginEnabled: config?.enableLogin, loginEnabled: config?.enableLogin,
}) });
// Show loading while checking auth and config // Show loading while checking auth and config
if (loading) { if (loading) {
@ -36,27 +36,27 @@ export default function Landing() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
// If login is disabled, show app directly (anonymous mode) // If login is disabled, show app directly (anonymous mode)
if (config?.enableLogin === false) { if (config?.enableLogin === false) {
console.debug('[Landing] Login disabled - showing app in anonymous mode') console.debug('[Landing] Login disabled - showing app in anonymous mode');
return <HomePage /> return <HomePage />;
} }
// If we have a session, show the main app // If we have a session, show the main app
if (session) { if (session) {
return <HomePage /> return <HomePage />;
} }
// If we're at home route ("/"), show login directly (marketing/landing page) // If we're at home route ("/"), show login directly (marketing/landing page)
// Otherwise navigate to login (fixes URL mismatch for tool routes) // Otherwise navigate to login (fixes URL mismatch for tool routes)
const isHome = location.pathname === '/' || location.pathname === '' const isHome = location.pathname === '/' || location.pathname === '';
if (isHome) { if (isHome) {
return <Login /> return <Login />;
} }
// For non-home routes without auth, navigate to login (preserves from location) // 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 { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { springAuth } from '../auth/springAuthClient' import { springAuth } from '../auth/springAuthClient';
import { useAuth } from '../auth/UseSession' import { useAuth } from '../auth/UseSession';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { useDocumentMeta } from '../hooks/useDocumentMeta' import { useDocumentMeta } from '../hooks/useDocumentMeta';
import AuthLayout from './authShared/AuthLayout' import AuthLayout from './authShared/AuthLayout';
// Import login components // Import login components
import LoginHeader from './login/LoginHeader' import LoginHeader from './login/LoginHeader';
import ErrorMessage from './login/ErrorMessage' import ErrorMessage from './login/ErrorMessage';
import EmailPasswordForm from './login/EmailPasswordForm' import EmailPasswordForm from './login/EmailPasswordForm';
import OAuthButtons from './login/OAuthButtons' import OAuthButtons from './login/OAuthButtons';
import DividerWithText from '../components/shared/DividerWithText' import DividerWithText from '../components/shared/DividerWithText';
import LoggedInState from './login/LoggedInState' import LoggedInState from './login/LoggedInState';
import { BASE_PATH } from '../constants/app' import { BASE_PATH } from '../constants/app';
export default function Login() { export default function Login() {
const navigate = useNavigate() const navigate = useNavigate();
const { session, loading } = useAuth() const { session, loading } = useAuth();
const { t } = useTranslation() const { t } = useTranslation();
const [isSigningIn, setIsSigningIn] = useState(false) const [isSigningIn, setIsSigningIn] = useState(false);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
const [showEmailForm, setShowEmailForm] = useState(false) const [showEmailForm, setShowEmailForm] = useState(false);
const [email, setEmail] = useState('') const [email, setEmail] = useState('');
const [password, setPassword] = useState('') const [password, setPassword] = useState('');
// Prefill email from query param (e.g. after password reset) // Prefill email from query param (e.g. after password reset)
useEffect(() => { useEffect(() => {
try { try {
const url = new URL(window.location.href) const url = new URL(window.location.href);
const emailFromQuery = url.searchParams.get('email') const emailFromQuery = url.searchParams.get('email');
if (emailFromQuery) { if (emailFromQuery) {
setEmail(emailFromQuery) setEmail(emailFromQuery);
} }
} catch (_) { } catch (_) {
// ignore // ignore
} }
}, []) }, []);
const baseUrl = window.location.origin + BASE_PATH; 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)'), ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
ogImage: `${baseUrl}/og_images/home.png`, ogImage: `${baseUrl}/og_images/home.png`,
ogUrl: `${window.location.origin}${window.location.pathname}` ogUrl: `${window.location.origin}${window.location.pathname}`
}) });
// Show logged in state if authenticated // Show logged in state if authenticated
if (session && !loading) { if (session && !loading) {
return <LoggedInState /> return <LoggedInState />;
} }
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => { const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => {
try { try {
setIsSigningIn(true) setIsSigningIn(true);
setError(null) setError(null);
console.log(`[Login] Signing in with ${provider}`) console.log(`[Login] Signing in with ${provider}`);
// Redirect to Spring OAuth2 endpoint // Redirect to Spring OAuth2 endpoint
const { error } = await springAuth.signInWithOAuth({ const { error } = await springAuth.signInWithOAuth({
provider, provider,
options: { redirectTo: `${BASE_PATH}/auth/callback` } options: { redirectTo: `${BASE_PATH}/auth/callback` }
}) });
if (error) { if (error) {
console.error(`[Login] ${provider} error:`, error) console.error(`[Login] ${provider} error:`, error);
setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`) setError(t('login.failedToSignIn', { provider, message: error.message }) || `Failed to sign in with ${provider}`);
} }
} catch (err) { } catch (err) {
console.error(`[Login] Unexpected error:`, err) console.error(`[Login] Unexpected error:`, err);
setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred');
} finally { } finally {
setIsSigningIn(false) setIsSigningIn(false);
} }
} };
const signInWithEmail = async () => { const signInWithEmail = async () => {
if (!email || !password) { if (!email || !password) {
setError(t('login.pleaseEnterBoth') || 'Please enter both email and password') setError(t('login.pleaseEnterBoth') || 'Please enter both email and password');
return return;
} }
try { try {
setIsSigningIn(true) setIsSigningIn(true);
setError(null) 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({ const { user, session, error } = await springAuth.signInWithPassword({
email: email.trim(), email: email.trim(),
password: password password: password
}) });
if (error) { if (error) {
console.error('[Login] Email sign in error:', error) console.error('[Login] Email sign in error:', error);
setError(error.message) setError(error.message);
} else if (user && session) { } 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 // Auth state will update automatically and Landing will redirect to home
// No need to navigate manually here // No need to navigate manually here
} }
} catch (err) { } catch (err) {
console.error('[Login] Unexpected error:', err) console.error('[Login] Unexpected error:', err);
setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred') setError(t('login.unexpectedError', { message: err instanceof Error ? err.message : 'Unknown error' }) || 'An unexpected error occurred');
} finally { } finally {
setIsSigningIn(false) setIsSigningIn(false);
} }
} };
const handleForgotPassword = () => { const handleForgotPassword = () => {
navigate('/auth/reset') navigate('/auth/reset');
} };
return ( return (
<AuthLayout> <AuthLayout>
@ -185,5 +185,5 @@ export default function Login() {
</div> </div>
</AuthLayout> </AuthLayout>
) );
} }

View File

@ -1,28 +1,28 @@
import { useState } from 'react' import { useState } from 'react';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { useDocumentMeta } from '../hooks/useDocumentMeta' import { useDocumentMeta } from '../hooks/useDocumentMeta';
import AuthLayout from './authShared/AuthLayout' import AuthLayout from './authShared/AuthLayout';
import './authShared/auth.css' import './authShared/auth.css';
import { BASE_PATH } from '../constants/app' import { BASE_PATH } from '../constants/app';
// Import signup components // Import signup components
import LoginHeader from './login/LoginHeader' import LoginHeader from './login/LoginHeader';
import ErrorMessage from './login/ErrorMessage' import ErrorMessage from './login/ErrorMessage';
import DividerWithText from '../components/shared/DividerWithText' import DividerWithText from '../components/shared/DividerWithText';
import SignupForm from './signup/SignupForm' import SignupForm from './signup/SignupForm';
import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation' import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation';
import { useAuthService } from './signup/AuthService' import { useAuthService } from './signup/AuthService';
export default function Signup() { export default function Signup() {
const navigate = useNavigate() const navigate = useNavigate();
const { t } = useTranslation() const { t } = useTranslation();
const [isSigningUp, setIsSigningUp] = useState(false) const [isSigningUp, setIsSigningUp] = useState(false);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
const [email, setEmail] = useState('') const [email, setEmail] = useState('');
const [password, setPassword] = useState('') const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('');
const [fieldErrors, setFieldErrors] = useState<SignupFieldErrors>({}) const [fieldErrors, setFieldErrors] = useState<SignupFieldErrors>({});
const baseUrl = window.location.origin + BASE_PATH; 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)'), ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
ogImage: `${baseUrl}/og_images/home.png`, ogImage: `${baseUrl}/og_images/home.png`,
ogUrl: `${window.location.origin}${window.location.pathname}` ogUrl: `${window.location.origin}${window.location.pathname}`
}) });
const { validateSignupForm } = useSignupFormValidation() const { validateSignupForm } = useSignupFormValidation();
const { signUp } = useAuthService() const { signUp } = useAuthService();
const handleSignUp = async () => { const handleSignUp = async () => {
const validation = validateSignupForm(email, password, confirmPassword) const validation = validateSignupForm(email, password, confirmPassword);
if (!validation.isValid) { if (!validation.isValid) {
setError(validation.error) setError(validation.error);
setFieldErrors(validation.fieldErrors || {}) setFieldErrors(validation.fieldErrors || {});
return return;
} }
try { try {
setIsSigningUp(true) setIsSigningUp(true);
setError(null) setError(null);
setFieldErrors({}) setFieldErrors({});
const result = await signUp(email, password, '') const result = await signUp(email, password, '');
if (result.user) { if (result.user) {
// Show success message and redirect to login // Show success message and redirect to login
setError(null) setError(null);
setTimeout(() => navigate('/login'), 2000) setTimeout(() => navigate('/login'), 2000);
} }
} catch (err) { } catch (err) {
console.error('[Signup] Unexpected error:', err) console.error('[Signup] Unexpected error:', err);
setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' })) setError(err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' }));
} finally { } finally {
setIsSigningUp(false) setIsSigningUp(false);
} }
} };
return ( return (
<AuthLayout> <AuthLayout>
@ -101,5 +101,5 @@ export default function Signup() {
</button> </button>
</div> </div>
</AuthLayout> </AuthLayout>
) );
} }

View File

@ -1,51 +1,51 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react';
import LoginRightCarousel from '../../components/shared/LoginRightCarousel' import LoginRightCarousel from '../../components/shared/LoginRightCarousel';
import loginSlides from '../../components/shared/loginSlides' import loginSlides from '../../components/shared/loginSlides';
import styles from './AuthLayout.module.css' import styles from './AuthLayout.module.css';
interface AuthLayoutProps { interface AuthLayoutProps {
children: React.ReactNode children: React.ReactNode
} }
export default function AuthLayout({ children }: AuthLayoutProps) { export default function AuthLayout({ children }: AuthLayoutProps) {
const cardRef = useRef<HTMLDivElement | null>(null) const cardRef = useRef<HTMLDivElement | null>(null);
const [hideRightPanel, setHideRightPanel] = useState(false) const [hideRightPanel, setHideRightPanel] = useState(false);
// Force light mode on auth pages // Force light mode on auth pages
useEffect(() => { useEffect(() => {
const htmlElement = document.documentElement const htmlElement = document.documentElement;
const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme') const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme');
// Set light mode // 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 // Cleanup: restore previous theme when leaving auth pages
return () => { return () => {
if (previousColorScheme) { if (previousColorScheme) {
htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme) htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme);
} }
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
const update = () => { const update = () => {
// Use viewport to avoid hysteresis when the card is already in single-column mode // Use viewport to avoid hysteresis when the card is already in single-column mode
const viewportWidth = window.innerWidth const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight const viewportHeight = window.innerHeight;
const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96) // matches min(73.75rem, 96vw) const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // matches min(73.75rem, 96vw)
const columnWidth = cardWidthIfTwoCols / 2 const columnWidth = cardWidthIfTwoCols / 2;
const tooNarrow = columnWidth < 470 const tooNarrow = columnWidth < 470;
const tooShort = viewportHeight < 740 const tooShort = viewportHeight < 740;
setHideRightPanel(tooNarrow || tooShort) setHideRightPanel(tooNarrow || tooShort);
} };
update() update();
window.addEventListener('resize', update) window.addEventListener('resize', update);
window.addEventListener('orientationchange', update) window.addEventListener('orientationchange', update);
return () => { return () => {
window.removeEventListener('resize', update) window.removeEventListener('resize', update);
window.removeEventListener('orientationchange', update) window.removeEventListener('orientationchange', update);
} };
}, []) }, []);
return ( return (
<div className={styles.authContainer}> <div className={styles.authContainer}>
@ -64,5 +64,5 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
)} )}
</div> </div>
</div> </div>
) );
} }

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import '../authShared/auth.css' import '../authShared/auth.css';
interface EmailPasswordFormProps { interface EmailPasswordFormProps {
email: string email: string
@ -27,12 +27,12 @@ export default function EmailPasswordForm({
showPasswordField = true, showPasswordField = true,
fieldErrors = {} fieldErrors = {}
}: EmailPasswordFormProps) { }: EmailPasswordFormProps) {
const { t } = useTranslation() const { t } = useTranslation();
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
onSubmit() onSubmit();
} };
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -82,5 +82,5 @@ export default function EmailPasswordForm({
{submitButtonText} {submitButtonText}
</button> </button>
</form> </form>
) );
} }

View File

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

View File

@ -1,20 +1,20 @@
import { useEffect } from 'react' import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../auth/UseSession' import { useAuth } from '../../auth/UseSession';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
export default function LoggedInState() { export default function LoggedInState() {
const navigate = useNavigate() const navigate = useNavigate();
const { user } = useAuth() const { user } = useAuth();
const { t } = useTranslation() const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
navigate('/') navigate('/');
}, 2000) }, 2000);
return () => clearTimeout(timer) return () => clearTimeout(timer);
}, [navigate]) }, [navigate]);
return ( return (
<div style={{ <div style={{
@ -50,5 +50,5 @@ export default function LoggedInState() {
</div> </div>
</div> </div>
</div> </div>
) );
} }

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { BASE_PATH } from '../../constants/app' import { BASE_PATH } from '../../constants/app';
// OAuth provider configuration // OAuth provider configuration
const oauthProviders = [ const oauthProviders = [
@ -7,7 +7,7 @@ const oauthProviders = [
{ id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false }, { id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false },
{ id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true }, { id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true },
{ id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true } { id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true }
] ];
interface OAuthButtonsProps { interface OAuthButtonsProps {
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void
@ -16,10 +16,10 @@ interface OAuthButtonsProps {
} }
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical' }: 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 // 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') { if (layout === 'icons') {
return ( return (
@ -37,7 +37,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
</div> </div>
))} ))}
</div> </div>
) );
} }
if (layout === 'grid') { if (layout === 'grid') {
@ -56,7 +56,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
</div> </div>
))} ))}
</div> </div>
) );
} }
return ( return (
@ -74,5 +74,5 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
</button> </button>
))} ))}
</div> </div>
) );
} }

View File

@ -1,5 +1,5 @@
import { springAuth } from '../../auth/springAuthClient' import { springAuth } from '../../auth/springAuthClient';
import { BASE_PATH } from '../../constants/app' import { BASE_PATH } from '../../constants/app';
export const useAuthService = () => { export const useAuthService = () => {
@ -8,7 +8,7 @@ export const useAuthService = () => {
password: string, password: string,
name: string name: string
) => { ) => {
console.log('[Signup] Creating account for:', email) console.log('[Signup] Creating account for:', email);
const { user, session, error } = await springAuth.signUp({ const { user, session, error } = await springAuth.signUp({
email: email.trim(), email: email.trim(),
@ -17,38 +17,38 @@ export const useAuthService = () => {
data: { full_name: name }, data: { full_name: name },
emailRedirectTo: `${BASE_PATH}/auth/callback` emailRedirectTo: `${BASE_PATH}/auth/callback`
} }
}) });
if (error) { if (error) {
console.error('[Signup] Sign up error:', error) console.error('[Signup] Sign up error:', error);
throw new Error(error.message) throw new Error(error.message);
} }
if (user) { if (user) {
console.log('[Signup] Sign up successful:', user) console.log('[Signup] Sign up successful:', user);
return { return {
user: user, user: user,
session: session, session: session,
requiresEmailConfirmation: user && !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 signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => {
const { error } = await springAuth.signInWithOAuth({ const { error } = await springAuth.signInWithOAuth({
provider, provider,
options: { redirectTo: `${BASE_PATH}/auth/callback` } options: { redirectTo: `${BASE_PATH}/auth/callback` }
}) });
if (error) { if (error) {
throw new Error(error.message) throw new Error(error.message);
} }
} };
return { return {
signUp, signUp,
signInWithProvider signInWithProvider
} };
} };

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react';
import '../authShared/auth.css' import '../authShared/auth.css';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { SignupFieldErrors } from './SignupFormValidation' import { SignupFieldErrors } from './SignupFormValidation';
interface SignupFormProps { interface SignupFormProps {
name?: string name?: string
@ -38,14 +38,14 @@ export default function SignupForm({
showName = false, showName = false,
showTerms = false showTerms = false
}: SignupFormProps) { }: SignupFormProps) {
const { t } = useTranslation() const { t } = useTranslation();
const showConfirm = password.length >= 4 const showConfirm = password.length >= 4;
useEffect(() => { useEffect(() => {
if (!showConfirm && confirmPassword) { if (!showConfirm && confirmPassword) {
setConfirmPassword('') setConfirmPassword('');
} }
}, [showConfirm, confirmPassword, setConfirmPassword]) }, [showConfirm, confirmPassword, setConfirmPassword]);
return ( return (
<> <>
@ -158,5 +158,5 @@ export default function SignupForm({
{isSubmitting ? t('signup.creatingAccount') : t('signup.signUp')} {isSubmitting ? t('signup.creatingAccount') : t('signup.signUp')}
</button> </button>
</> </>
) );
} }

View File

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
export interface SignupFieldErrors { export interface SignupFieldErrors {
name?: string name?: string
@ -14,7 +14,7 @@ export interface SignupValidationResult {
} }
export const useSignupFormValidation = () => { export const useSignupFormValidation = () => {
const { t } = useTranslation() const { t } = useTranslation();
const validateSignupForm = ( const validateSignupForm = (
email: string, email: string,
@ -22,45 +22,45 @@ export const useSignupFormValidation = () => {
confirmPassword: string, confirmPassword: string,
name?: string name?: string
): SignupValidationResult => { ): SignupValidationResult => {
const fieldErrors: SignupFieldErrors = {} const fieldErrors: SignupFieldErrors = {};
// Validate name // Validate name
if (name !== undefined && name !== null && !name.trim()) { 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 // Validate email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) { if (!email) {
fieldErrors.email = t('signup.emailRequired', 'Email is required') fieldErrors.email = t('signup.emailRequired', 'Email is required');
} else if (!emailRegex.test(email)) { } else if (!emailRegex.test(email)) {
fieldErrors.email = t('signup.invalidEmail') fieldErrors.email = t('signup.invalidEmail');
} }
// Validate password // Validate password
if (!password) { if (!password) {
fieldErrors.password = t('signup.passwordRequired', 'Password is required') fieldErrors.password = t('signup.passwordRequired', 'Password is required');
} else if (password.length < 6) { } else if (password.length < 6) {
fieldErrors.password = t('signup.passwordTooShort') fieldErrors.password = t('signup.passwordTooShort');
} }
// Validate confirm password // Validate confirm password
if (!confirmPassword) { if (!confirmPassword) {
fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password') fieldErrors.confirmPassword = t('signup.confirmPasswordRequired', 'Please confirm your password');
} else if (password !== confirmPassword) { } 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 { return {
isValid: !hasErrors, isValid: !hasErrors,
error: null, // Don't show generic error, field errors are more specific error: null, // Don't show generic error, field errors are more specific
fieldErrors: hasErrors ? fieldErrors : undefined fieldErrors: hasErrors ? fieldErrors : undefined
} };
} };
return { return {
validateSignupForm validateSignupForm
} };
} };

View File

@ -3,7 +3,7 @@ import path from 'path';
import ts from 'typescript'; import ts from 'typescript';
import { describe, expect, test } from 'vitest'; 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 SRC_ROOT = path.join(__dirname, '..');
const EN_GB_FILE = path.join(__dirname, '../../public/locales/en-GB/translation.json'); const EN_GB_FILE = path.join(__dirname, '../../public/locales/en-GB/translation.json');