mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
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:
parent
c67859a1ff
commit
3e9c55243e
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,6 @@ export const loginSlides: LoginCarouselSlide[] = [
|
|||||||
followMouseTilt: true,
|
followMouseTilt: true,
|
||||||
tiltMaxDeg: 5,
|
tiltMaxDeg: 5,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export default loginSlides
|
export default loginSlides;
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,5 +15,5 @@ export default function NavigationLink({ onClick, text, isDisabled = false }: Na
|
|||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
@ -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');
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user