Files
Synapsis/src/app/login/page.tsx
T
cyph3rasi 988bd8ab38 Prevent email autofill as display name and autosave profile media
Hop-State: A_06FP8EYP9H40CXQH16XPSN8
Hop-Proposal: R_06FP8EY0KW03D22K71DN4KG
Hop-Task: T_06FP8DNXSH7CAHXCHDZ2Z1R
Hop-Attempt: AT_06FP8DNXSKPDEVNXEZRQ5W8
2026-07-14 22:24:30 -07:00

934 lines
44 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { TriangleAlert, X } from 'lucide-react';
import { decryptPrivateKey } from '@/lib/crypto/private-key-client';
import { keyStore, importPrivateKey } from '@/lib/crypto/user-signing';
import { useAuth } from '@/lib/contexts/AuthContext';
declare global {
interface Window {
turnstile?: {
render: (element: string | HTMLElement, options: {
sitekey: string;
callback?: (token: string) => void;
'error-callback'?: () => void;
'expired-callback'?: () => void;
}) => string;
reset: (widgetId: string) => void;
remove: (widgetId: string) => void;
};
}
}
interface AuthScreenProps {
modal?: boolean;
onClose?: () => void;
onSuccess?: () => void;
}
export function AuthScreen({ modal = false, onClose, onSuccess }: AuthScreenProps) {
const router = useRouter();
const [mode, setMode] = useState<'login' | 'register' | 'import'>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [handle, setHandle] = useState('');
const [displayName, setDisplayName] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [nodeInfoLoaded, setNodeInfoLoaded] = useState(false);
const [nodeInfo, setNodeInfo] = useState<{ name: string; description: string; logoUrl?: string; isNsfw?: boolean; turnstileSiteKey?: string | null }>({ name: '', description: '' });
const [handleStatus, setHandleStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle');
const [ageVerified, setAgeVerified] = useState(false);
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
const [turnstileLoaded, setTurnstileLoaded] = useState(false);
const turnstileRef = useRef<HTMLDivElement>(null);
const turnstileWidgetId = useRef<string | null>(null);
const { unlockIdentity, login } = useAuth();
const [importFile, setImportFile] = useState<File | null>(null);
const [importPassword, setImportPassword] = useState('');
const [importHandle, setImportHandle] = useState('');
const [acceptedCompliance, setAcceptedCompliance] = useState(false);
const [importAgeVerified, setImportAgeVerified] = useState(false);
const [importSuccess, setImportSuccess] = useState<string | null>(null);
// Fetch node info
useEffect(() => {
fetch('/api/node')
.then(res => res.json())
.then(data => {
setNodeInfo({
name: data.name || '',
description: data.description || 'Synapsis is designed to function like a global signal layer rather than a culture-bound platform. Anyone can run their own node and still participate in a shared, interconnected network, with global identity, clean terminology, and a modern interface that feels current rather than experimental. Synapsis aims to be neutral, resilient infrastructure for human and machine discourse, more like a protocol or nervous system than a social club.',
logoUrl: data.logoUrl || undefined,
isNsfw: data.isNsfw || false,
turnstileSiteKey: data.turnstileSiteKey || null,
});
// Update page title
if (data.name && data.name !== 'Synapsis') {
document.title = data.name;
}
setNodeInfoLoaded(true);
})
.catch(() => {
setNodeInfoLoaded(true);
});
}, []);
// Load Turnstile script if site key is available
useEffect(() => {
if (!nodeInfo.turnstileSiteKey) return;
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
script.async = true;
script.defer = true;
script.onload = () => setTurnstileLoaded(true);
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, [nodeInfo.turnstileSiteKey]);
// Render Turnstile widget when ready
useEffect(() => {
if (!turnstileLoaded || !nodeInfo.turnstileSiteKey || !turnstileRef.current || mode === 'import') return;
// Clean up previous widget
if (turnstileWidgetId.current && window.turnstile) {
try {
window.turnstile.remove(turnstileWidgetId.current);
} catch (e) {
// Ignore errors
}
}
// Render new widget
if (window.turnstile) {
turnstileWidgetId.current = window.turnstile.render(turnstileRef.current, {
sitekey: nodeInfo.turnstileSiteKey,
callback: (token: string) => {
setTurnstileToken(token);
},
'error-callback': () => {
setTurnstileToken(null);
},
'expired-callback': () => {
setTurnstileToken(null);
},
});
}
return () => {
if (turnstileWidgetId.current && window.turnstile) {
try {
window.turnstile.remove(turnstileWidgetId.current);
} catch (e) {
// Ignore errors
}
}
};
}, [turnstileLoaded, nodeInfo.turnstileSiteKey, mode]);
// Handle availability check
useEffect(() => {
const checkHandle = mode === 'register' ? handle : (mode === 'import' ? importHandle : '');
if (!checkHandle || checkHandle.length < 3) {
setHandleStatus('idle');
return;
}
const timer = setTimeout(async () => {
setHandleStatus('checking');
try {
const res = await fetch(`/api/auth/check-handle?handle=${checkHandle}`);
const data = await res.json();
if (data.available) {
setHandleStatus('available');
} else {
setHandleStatus('taken');
}
} catch {
setHandleStatus('idle');
}
}, 500);
return () => clearTimeout(timer);
}, [handle, importHandle, mode]);
const handleImport = async (e: React.FormEvent) => {
e.preventDefault();
if (!importFile || !importPassword || !importHandle || !acceptedCompliance) {
setError('Please fill in all fields and accept the compliance agreement');
return;
}
if (nodeInfo.isNsfw && !importAgeVerified) {
setError('You must verify your age to import an account on this node');
return;
}
setLoading(true);
setError('');
setImportSuccess(null);
try {
const fileContent = await importFile.text();
const exportData = JSON.parse(fileContent);
const res = await fetch('/api/account/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
exportData,
password: importPassword,
newHandle: importHandle,
acceptedCompliance,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Import failed');
}
setImportSuccess(data.message);
// Soft navigation to preserve AuthContext/KeyStore state
setTimeout(() => {
router.refresh();
if (onSuccess) {
onSuccess();
} else {
router.push('/');
}
}, 2000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Import failed');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (mode === 'register' && password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (mode === 'register' && nodeInfo.isNsfw && !ageVerified) {
setError('You must verify your age to register on this node');
return;
}
// Check if Turnstile is required but not completed
if (nodeInfo.turnstileSiteKey && !turnstileToken) {
setError('Please complete the verification challenge');
return;
}
setLoading(true);
try {
const endpoint = mode === 'login' ? '/api/auth/login' : '/api/auth/register';
// Only include turnstileToken if Turnstile is enabled (site key exists)
const body = mode === 'login'
? {
email,
password,
...(nodeInfo.turnstileSiteKey ? { turnstileToken } : {})
}
: {
email,
password,
handle,
displayName,
...(nodeInfo.turnstileSiteKey ? { turnstileToken } : {})
};
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Authentication failed');
}
// Decrypt and store private key if available
if (data.user?.privateKeyEncrypted) {
try {
const privateKeyDecrypted = await decryptPrivateKey(
data.user.privateKeyEncrypted,
password
);
// Import and set in memory store
// Remove PEM headers if present and clean whitespace
let cleanKey = privateKeyDecrypted
.replace(/-----BEGIN [A-Z ]+-----/, '')
.replace(/-----END [A-Z ]+-----/, '')
.replace(/\s/g, '');
const binaryDer = Buffer.from(cleanKey, 'base64');
const cryptoKey = await importPrivateKey(binaryDer);
keyStore.setPrivateKey(cryptoKey);
console.log('[Auth] Private key decrypted and stored successfully');
} catch (decryptError) {
console.error('[Auth] Failed to decrypt private key:', decryptError);
// Don't block login/registration if decryption fails - user can unlock later
// The identity unlock prompt will be shown in the app
}
} else {
if (process.env.NODE_ENV === 'development') console.log('[Auth] No encrypted private key returned from server');
}
// Sync with global auth state if we have a key (or even if we don't, to trigger load)
// But unlockIdentity specifically needs the key.
// If data.user.privateKeyEncrypted is present, we try to unlock globally.
if (data.user?.privateKeyEncrypted) {
try {
// Update AuthContext first so it has the user and key
login(data.user);
// Now unlock (passing user explicitly to avoid async state delay)
await unlockIdentity(password, data.user);
} catch (e) {
console.error("Failed to auto-unlock identity:", e);
}
}
// Soft navigation to preserve AuthContext/KeyStore state
router.refresh();
if (onSuccess) {
onSuccess();
} else {
router.push('/');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
// Reset Turnstile on error
if (turnstileWidgetId.current && window.turnstile) {
window.turnstile.reset(turnstileWidgetId.current);
setTurnstileToken(null);
}
} finally {
setLoading(false);
}
};
const content = (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: modal ? '0' : '24px',
}}>
<div style={{
width: '100%',
maxWidth: mode === 'register' ? '680px' : '400px',
}}>
{/* Logo */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '32px', minHeight: '120px' }}>
{nodeInfoLoaded && (
<>
{nodeInfo.logoUrl ? (
<Image
src={nodeInfo.logoUrl}
alt={nodeInfo.name || 'Node logo'}
width={200}
height={60}
style={{ marginBottom: '16px', objectFit: 'contain', maxHeight: '60px', width: 'auto' }}
unoptimized
/>
) : (
<Image
src="/logotext.svg"
alt="Synapsis"
width={200}
height={48}
style={{ marginBottom: '16px', objectFit: 'contain' }}
priority
/>
)}
{nodeInfo.name && nodeInfo.name !== 'Synapsis' && !nodeInfo.logoUrl && (
<div style={{ fontSize: '18px', fontWeight: 600, color: 'var(--foreground)', marginBottom: '8px' }}>
{nodeInfo.name}
</div>
)}
<p style={{ color: 'var(--foreground-secondary)', marginTop: '0', textAlign: 'center' }}>
{nodeInfo.description}
</p>
</>
)}
</div>
{/* Mode Switcher */}
<div style={{
display: 'flex',
marginBottom: '24px',
background: 'var(--background-secondary)',
borderRadius: 'var(--radius-md)',
padding: '4px',
gap: '4px'
}}>
<button
onClick={() => setMode('login')}
style={{
flex: 1,
padding: '10px',
border: 'none',
borderRadius: 'var(--radius-sm)',
background: mode === 'login' ? 'var(--background-tertiary)' : 'transparent',
color: mode === 'login' ? 'var(--foreground)' : 'var(--foreground-secondary)',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
>
Login
</button>
<button
onClick={() => setMode('register')}
style={{
flex: 1,
padding: '10px',
border: 'none',
borderRadius: 'var(--radius-sm)',
background: mode === 'register' ? 'var(--background-tertiary)' : 'transparent',
color: mode === 'register' ? 'var(--foreground)' : 'var(--foreground-secondary)',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
>
Register
</button>
<button
onClick={() => setMode('import')}
style={{
flex: 1,
padding: '10px',
border: 'none',
borderRadius: 'var(--radius-sm)',
background: mode === 'import' ? 'var(--background-tertiary)' : 'transparent',
color: mode === 'import' ? 'var(--foreground)' : 'var(--foreground-secondary)',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
>
Import
</button>
</div>
{/* Form */}
{mode !== 'import' ? (
<form onSubmit={handleSubmit} className="card" style={{ padding: '24px' }}>
{error && (
<div style={{
padding: '12px',
marginBottom: '16px',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid var(--error)',
borderRadius: 'var(--radius-md)',
color: 'var(--error)',
fontSize: '14px',
}}>
{error}
</div>
)}
{mode === 'register' && (
<>
{/* Row 1: Handle | Password */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Handle
</label>
<div style={{ position: 'relative' }}>
<span style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--foreground-tertiary)',
}}>@</span>
<input
type="text"
name="username"
autoComplete="username"
className="input"
value={handle}
onChange={(e) => setHandle(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))}
style={{ paddingLeft: '28px' }}
placeholder="yourhandle"
required
minLength={3}
maxLength={20}
/>
</div>
<div style={{
fontSize: '12px',
marginTop: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ color: 'var(--foreground-tertiary)' }}>
3-20 chars, a-z 0-9 _
</span>
{handleStatus === 'checking' && (
<span style={{ color: 'var(--foreground-tertiary)' }}>Checking...</span>
)}
{handleStatus === 'available' && (
<span style={{ color: 'var(--success)', fontWeight: 600 }}></span>
)}
{handleStatus === 'taken' && (
<span style={{ color: 'var(--error)', fontWeight: 600 }}>Taken</span>
)}
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Password
</label>
<input
type="password"
name="password"
autoComplete="new-password"
className="input"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
/>
</div>
</div>
{/* Row 2: Display Name | Confirm Password */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Display Name
</label>
<input
type="text"
name="displayName"
autoComplete="nickname"
className="input"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Your Name"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Confirm Password
</label>
<input
type="password"
name="confirmPassword"
autoComplete="new-password"
className="input"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
/>
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Email
</label>
<input
type="email"
name="email"
autoComplete="email"
className="input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
</div>
<p style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginBottom: '16px' }}>
You can connect your own S3-compatible storage when you first upload media.
</p>
</>
)}
{/* Login Mode - Show email/password only */}
{mode === 'login' && (
<>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Email
</label>
<input
type="email"
className="input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Password
</label>
<input
type="password"
className="input"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
/>
</div>
</>
)}
{mode === 'register' && nodeInfo.isNsfw && (
<div style={{
marginBottom: '20px',
padding: '12px',
background: 'rgba(239, 68, 68, 0.05)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: 'var(--radius-md)',
}}>
<label style={{ display: 'flex', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={ageVerified}
onChange={(e) => setAgeVerified(e.target.checked)}
style={{ marginTop: '3px' }}
/>
<span style={{ fontSize: '12px', color: 'var(--foreground-secondary)', lineHeight: 1.4 }}>
<strong style={{ color: 'var(--error)', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<TriangleAlert size={12} /> Age Verification:
</strong> This node contains adult or sensitive content. I confirm that I am at least 18 years of age.
</span>
</label>
</div>
)}
{nodeInfo.turnstileSiteKey && (
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'center' }}>
<div ref={turnstileRef}></div>
</div>
)}
<button
type="submit"
className="btn btn-primary btn-lg"
style={{ width: '100%' }}
disabled={loading ||
(!!nodeInfo.turnstileSiteKey && !turnstileToken) ||
(mode === 'register' && (
!handle || handle.length < 3 ||
!email ||
!password || password.length < 8 ||
!confirmPassword ||
password !== confirmPassword ||
(nodeInfo.isNsfw && !ageVerified)
))}
>
{loading ? 'Please wait...' : (mode === 'login' ? 'Login' : 'Create Account')}
</button>
</form>
) : (
<form onSubmit={handleImport} className="card" style={{ padding: '24px' }}>
{error && (
<div style={{
padding: '12px',
marginBottom: '16px',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid var(--error)',
borderRadius: 'var(--radius-md)',
color: 'var(--error)',
fontSize: '14px',
}}>
{error}
</div>
)}
{importSuccess && (
<div style={{
padding: '12px',
marginBottom: '16px',
background: 'var(--success)',
border: '1px solid var(--success)',
borderRadius: 'var(--radius-md)',
color: '#000',
fontSize: '14px',
}}>
{importSuccess} Redirecting...
</div>
)}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Export file
</label>
<div style={{ position: 'relative' }}>
<input
type="file"
id="import-file-input"
accept=".json"
onChange={(e) => setImportFile(e.target.files?.[0] || null)}
style={{ display: 'none' }}
/>
<label
htmlFor="import-file-input"
className="input"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
color: importFile ? 'var(--foreground)' : 'var(--foreground-tertiary)',
fontSize: '14px'
}}
>
<span>{importFile ? importFile.name : 'Select export file...'}</span>
<span className="btn btn-ghost btn-sm" style={{ pointerEvents: 'none', padding: '4px 8px', height: 'auto', minHeight: 'unset' }}>
Browse
</span>
</label>
</div>
</div>
<p style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginBottom: '16px' }}>
You can connect your own S3-compatible storage when you first upload media.
</p>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Password (from your old account)
</label>
<input
type="password"
className="input"
value={importPassword}
onChange={(e) => setImportPassword(e.target.value)}
placeholder="Enter the password for this account"
required
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Handle on this node
</label>
<div style={{ position: 'relative' }}>
<span style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--foreground-tertiary)',
}}>@</span>
<input
type="text"
className="input"
value={importHandle}
onChange={(e) => setImportHandle(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))}
style={{ paddingLeft: '28px' }}
placeholder="yourhandle"
required
minLength={3}
maxLength={20}
/>
</div>
<div style={{
fontSize: '12px',
marginTop: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ color: 'var(--foreground-tertiary)' }}>
3-20 chars
</span>
{handleStatus === 'checking' && (
<span style={{ color: 'var(--foreground-tertiary)' }}>Checking...</span>
)}
{handleStatus === 'available' && (
<span style={{ color: 'var(--success)', fontWeight: 600 }}>Available</span>
)}
{handleStatus === 'taken' && (
<span style={{ color: 'var(--error)', fontWeight: 600 }}>Taken</span>
)}
</div>
</div>
<div style={{
marginBottom: '20px',
padding: '12px',
background: 'rgba(245, 158, 11, 0.05)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 'var(--radius-md)',
}}>
<label style={{ display: 'flex', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={acceptedCompliance}
onChange={(e) => setAcceptedCompliance(e.target.checked)}
style={{ marginTop: '3px' }}
/>
<span style={{ fontSize: '12px', color: 'var(--foreground-secondary)', lineHeight: 1.4 }}>
<strong style={{ color: 'var(--warning)', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<TriangleAlert size={12} /> Compliance:
</strong> I agree to comply with this node's rules and take responsibility for my migrated content.
</span>
</label>
</div>
{nodeInfo.isNsfw && (
<div style={{
marginBottom: '20px',
padding: '12px',
background: 'rgba(239, 68, 68, 0.05)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: 'var(--radius-md)',
}}>
<label style={{ display: 'flex', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={importAgeVerified}
onChange={(e) => setImportAgeVerified(e.target.checked)}
style={{ marginTop: '3px' }}
/>
<span style={{ fontSize: '12px', color: 'var(--foreground-secondary)', lineHeight: 1.4 }}>
<strong style={{ color: 'var(--error)', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<TriangleAlert size={12} /> Age Verification:
</strong> This node contains adult or sensitive content. I confirm that I am at least 18 years of age.
</span>
</label>
</div>
)}
<button
type="submit"
className="btn btn-primary btn-lg"
style={{ width: '100%' }}
disabled={loading || !importFile || !importPassword || !importHandle || !acceptedCompliance || (nodeInfo.isNsfw && !importAgeVerified)}
>
{loading ? 'Importing...' : 'Import Account'}
</button>
</form>
)}
{!modal && (
<p style={{ textAlign: 'center', marginTop: '24px', color: 'var(--foreground-tertiary)', fontSize: '14px' }}>
<Link href="/">← Back to home</Link>
</p>
)}
</div>
</div>
);
if (modal) {
return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.72)',
backdropFilter: 'blur(10px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
zIndex: 120,
}}
onClick={onClose}
>
<div
style={{
position: 'relative',
width: 'min(760px, 100%)',
}}
onClick={(event) => event.stopPropagation()}
>
{onClose && (
<button
onClick={onClose}
aria-label="Close"
style={{
position: 'absolute',
top: '18px',
right: '18px',
width: '44px',
height: '44px',
borderRadius: '999px',
border: '1px solid var(--border)',
background: 'rgba(0, 0, 0, 0.78)',
color: 'var(--foreground)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
zIndex: 3,
}}
>
<X size={20} />
</button>
)}
<div
className="card"
style={{
maxHeight: 'calc(100vh - 48px)',
overflowY: 'auto',
padding: '28px',
background: 'rgba(10, 10, 10, 0.98)',
boxShadow: '0 30px 90px rgba(0, 0, 0, 0.55)',
}}
>
{content}
</div>
</div>
</div>
);
}
return (
<div style={{ minHeight: '100vh' }}>
{content}
</div>
);
}
export default function LoginPage() {
return <AuthScreen />;
}