mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Merge branch 'V2' into update-legal-links
This commit is contained in:
commit
7d9be942f1
@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,6 +38,6 @@ export const loginSlides: LoginCarouselSlide[] = [
|
||||
followMouseTilt: true,
|
||||
tiltMaxDeg: 5,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export default loginSlides
|
||||
export default loginSlides;
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }} />;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,5 +18,5 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {
|
||||
<p className="login-subtitle">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,5 +15,5 @@ export default function NavigationLink({ onClick, text, isDisabled = false }: Na
|
||||
{text}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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');
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user