Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
+154
-29
@@ -1,63 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { trackSignup } from '@/lib/analytics';
|
||||
import Wordmark from '@/components/Wordmark';
|
||||
|
||||
function SignupInner() {
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const next = search.get('next') || '/dashboard';
|
||||
const { signUp, signInWithGoogle } = useAuth();
|
||||
|
||||
export default function SignupPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [ageOk, setAgeOk] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const handleSignup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
// TODO: Integrate with Supabase Auth
|
||||
// const { error } = await supabase.auth.signUp({ email, password });
|
||||
setLoading(false);
|
||||
setError('Auth integration pending. Backend is ready.');
|
||||
if (password.length < 8) return setError('Password must be at least 8 characters.');
|
||||
if (password !== confirm) return setError('Passwords do not match.');
|
||||
if (!ageOk) return setError('You must confirm you are 21 or older.');
|
||||
|
||||
setBusy(true);
|
||||
const { error: err } = await signUp(email, password, ageOk);
|
||||
setBusy(false);
|
||||
if (err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
trackSignup({ method: 'password' });
|
||||
setDone(true);
|
||||
// Supabase confirms via email by default. If session is immediate, redirect.
|
||||
setTimeout(() => router.replace(next), 1500);
|
||||
};
|
||||
|
||||
const handleGoogle = async () => {
|
||||
setBusy(true);
|
||||
await signInWithGoogle();
|
||||
};
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 16px' }}>
|
||||
<div className="surface diagonal-cut" style={{ maxWidth: 420, padding: 32, textAlign: 'center' }}>
|
||||
<div className="mono" style={{ fontSize: 48, color: 'var(--grade-a)', marginBottom: 16 }}>✓</div>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 12 }}>You're in.</h2>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
|
||||
Check your email for a confirmation link, then come back and grade something.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="min-h-[80vh] flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<h1 className="text-3xl font-bold text-center mb-2">Create Account</h1>
|
||||
<p className="text-center text-[var(--text-muted)] text-sm mb-8">5 free scans. No credit card required.</p>
|
||||
<form onSubmit={handleSignup} className="space-y-4">
|
||||
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 16px' }}>
|
||||
<div className="surface diagonal-cut animate-fade-up" style={{ width: '100%', maxWidth: 420, padding: 32 }}>
|
||||
<a
|
||||
href="/"
|
||||
style={{ display: 'flex', justifyContent: 'center', color: 'var(--text-0)', textDecoration: 'none', marginBottom: 24 }}
|
||||
aria-label="VYNDR — home"
|
||||
>
|
||||
<Wordmark size={26} />
|
||||
</a>
|
||||
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, textAlign: 'center', marginBottom: 6 }}>Get started — free</h1>
|
||||
<p style={{ textAlign: 'center', color: 'var(--text-secondary)', fontSize: 13, marginBottom: 24 }}>
|
||||
5 free reads every month. Your first read is fully unlocked. No credit card.
|
||||
</p>
|
||||
|
||||
<button onClick={handleGoogle} disabled={busy} className="btn-ghost" style={{ width: '100%', marginBottom: 16, padding: 12 }}>
|
||||
Continue with Google
|
||||
</button>
|
||||
|
||||
<div style={dividerStyle}>
|
||||
<span style={dividerLine} />
|
||||
<span className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>OR</span>
|
||||
<span style={dividerLine} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSignup} style={{ display: 'grid', gap: 12 }}>
|
||||
<div>
|
||||
<label className="block text-sm text-[var(--text-muted)] mb-1">Email</label>
|
||||
<label className="mono" style={labelStyle}>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
autoComplete="email"
|
||||
className="input-field"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-[var(--text-muted)] mb-1">Password</label>
|
||||
<label className="mono" style={labelStyle}>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
|
||||
autoComplete="new-password"
|
||||
className="input-field"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-[var(--grade-d)] text-sm">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-[var(--accent)] text-white rounded-xl font-medium hover:opacity-90 transition disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Sign Up — Free'}
|
||||
<div>
|
||||
<label className="mono" style={labelStyle}>Confirm password</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
className="input-field"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', fontSize: 13, color: 'var(--text-secondary)', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ageOk}
|
||||
onChange={(e) => setAgeOk(e.target.checked)}
|
||||
style={{ marginTop: 2 }}
|
||||
/>
|
||||
<span>I confirm I am 21 years of age or older.</span>
|
||||
</label>
|
||||
|
||||
{error && <p style={{ color: 'var(--grade-d)', fontSize: 13, margin: 0 }}>{error}</p>}
|
||||
|
||||
<button type="submit" disabled={busy} className="btn-primary" style={{ padding: 14, marginTop: 4 }}>
|
||||
{busy ? 'Creating account…' : 'Get started — free'}
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-center text-sm text-[var(--text-muted)] mt-6">
|
||||
Already have an account? <a href="/login" className="text-[var(--accent)] hover:underline">Log in</a>
|
||||
|
||||
<p style={{ textAlign: 'center', fontSize: 12, color: 'var(--text-tertiary)', marginTop: 16, lineHeight: 1.6 }}>
|
||||
By signing up you agree to our{' '}
|
||||
<a href="/terms" style={{ color: 'var(--text-secondary)' }}>Terms</a> and{' '}
|
||||
<a href="/privacy" style={{ color: 'var(--text-secondary)' }}>Privacy Policy</a>.
|
||||
</p>
|
||||
|
||||
<p style={{ textAlign: 'center', fontSize: 13, color: 'var(--text-secondary)', marginTop: 12 }}>
|
||||
Already have an account?{' '}
|
||||
<a href={`/login${next ? `?next=${encodeURIComponent(next)}` : ''}`} style={{ color: 'var(--grade-a)' }}>
|
||||
Log in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SignupPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<SignupInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--text-tertiary)',
|
||||
marginBottom: 6,
|
||||
};
|
||||
|
||||
const dividerStyle: React.CSSProperties = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto 1fr',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
margin: '16px 0',
|
||||
};
|
||||
|
||||
const dividerLine: React.CSSProperties = {
|
||||
height: 1,
|
||||
background: 'var(--border)',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user