import { useState, useRef, useCallback } from "react"; import type { FormEvent, DragEvent } from "react"; import { useNavigate } from "react-router-dom"; import { useAuthStore } from "../../stores/authStore"; import { Loader2, Mail, Lock, BookOpen, Star, Zap, Trophy, Upload, X, Camera, } from "lucide-react"; import { api } from "../../utils/api"; const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } .rg-root { min-height: 100vh; display: flex; font-family: 'Nunito', sans-serif; background: #fffbf4; } /* ─── LEFT PANEL ─── */ .rg-left { position: relative; width: 50%; min-height: 100vh; background: linear-gradient(150deg, #1e1b4b 0%, #3b1d8a 50%, #6d28d9 100%); display: flex; flex-direction: column; align-items: flex-start; justify-content: center; padding: 3rem 3.5rem; overflow: hidden; flex-shrink: 0; } .rg-panel-blob { position: absolute; pointer-events: none; border-radius: 50%; opacity: 0.18; } .rg-panel-blob-1 { width: 420px; height: 420px; background: #a855f7; top: -140px; right: -100px; animation: blobDrift1 16s ease-in-out infinite; } .rg-panel-blob-2 { width: 300px; height: 300px; background: #f97316; bottom: -100px; left: -80px; animation: blobDrift2 18s ease-in-out infinite; } .rg-panel-blob-3 { width: 200px; height: 200px; background: #22c55e; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: blobDrift1 14s ease-in-out infinite reverse; } @keyframes blobDrift1 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(24px, 32px) scale(1.08); } } @keyframes blobDrift2 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(-20px, -28px) scale(1.06); } } .rg-shape { position: absolute; pointer-events: none; animation: floatShape 8s ease-in-out infinite; } @keyframes floatShape { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-16px) rotate(12deg); } } .rg-star { position: absolute; pointer-events: none; color: #fde68a; opacity: 0.55; animation: twinkle 3s ease-in-out infinite; } @keyframes twinkle { 0%,100% { opacity: 0.55; transform: scale(1); } 50% { opacity: 0.9; transform: scale(1.3); } } .rg-panel-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 2.5rem; width: 100%; } .rg-panel-logo { display: flex; align-items: center; gap: 0.75rem; } .rg-panel-logo-badge { width: 48px; height: 48px; border-radius: 14px; background: linear-gradient(135deg, #f97316, #ef4444); display: flex; align-items: center; justify-content: center; box-shadow: 0 6px 0 rgba(0,0,0,0.25), 0 8px 20px rgba(249,115,22,0.4); font-size: 1.4rem; } .rg-panel-logo-text { font-size: 1.4rem; font-weight: 900; color: white; letter-spacing: -0.02em; } .rg-panel-headline { display: flex; flex-direction: column; gap: 0.75rem; } .rg-panel-headline h2 { font-size: 2.4rem; font-weight: 900; line-height: 1.15; color: white; letter-spacing: -0.03em; } .rg-panel-headline h2 span { background: linear-gradient(90deg, #fde68a, #f97316); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .rg-panel-headline p { font-family: 'Nunito Sans', sans-serif; font-size: 1rem; font-weight: 600; color: #c4b5fd; line-height: 1.6; } .rg-features { display: flex; flex-direction: column; gap: 0.85rem; } .rg-feature { display: flex; align-items: center; gap: 0.85rem; background: rgba(255,255,255,0.07); border: 1.5px solid rgba(255,255,255,0.12); border-radius: 14px; padding: 0.85rem 1.1rem; backdrop-filter: blur(8px); animation: fadeSlideIn 0.5s ease both; } .rg-feature:nth-child(1) { animation-delay: 0.1s; } .rg-feature:nth-child(2) { animation-delay: 0.2s; } .rg-feature:nth-child(3) { animation-delay: 0.3s; } @keyframes fadeSlideIn { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: translateX(0); } } .rg-feature-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .rg-feature-text strong { display: block; font-size: 0.85rem; font-weight: 800; color: white; } .rg-feature-text span { font-family: 'Nunito Sans', sans-serif; font-size: 0.75rem; font-weight: 600; color: #a5b4fc; } .rg-social-proof { display: flex; align-items: center; gap: 0.75rem; padding: 0.1rem 0; } .rg-avatars { display: flex; } .rg-av { width: 30px; height: 30px; border-radius: 50%; border: 2px solid #3b1d8a; background: linear-gradient(135deg, #a855f7, #6d28d9); margin-left: -8px; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; font-weight: 800; color: white; } .rg-av:first-child { margin-left: 0; } .rg-social-proof p { font-family: 'Nunito Sans', sans-serif; font-size: 0.78rem; font-weight: 700; color: #c4b5fd; } .rg-social-proof p strong { color: #fde68a; } /* ─── RIGHT PANEL ─── */ .rg-right { flex: 1; display: flex; align-items: center; justify-content: center; padding: 3rem 4rem; position: relative; overflow: hidden; } .rg-bg-dot { position: absolute; border-radius: 50%; pointer-events: none; opacity: 0.10; animation: bgDotFloat 9s ease-in-out infinite; } @keyframes bgDotFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-12px); } } .rg-form-wrap { position: relative; z-index: 1; width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 2rem; animation: formPopIn 0.55s cubic-bezier(0.34,1.56,0.64,1) both; } @keyframes formPopIn { from { opacity: 0; transform: translateY(24px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } } .rg-form-header { display: flex; flex-direction: column; gap: 0.4rem; } .rg-form-header h1 { font-size: 2rem; font-weight: 900; color: #1e1b4b; letter-spacing: -0.03em; line-height: 1.2; } .rg-form-header p { font-family: 'Nunito Sans', sans-serif; font-size: 0.88rem; font-weight: 600; color: #9ca3af; } /* ─── AVATAR UPLOAD ─── */ .av-upload-wrap { display: flex; align-items: center; gap: 1.25rem; } /* The circular avatar preview */ .av-circle { position: relative; width: 72px; height: 72px; border-radius: 50%; flex-shrink: 0; cursor: pointer; } .av-circle-inner { width: 100%; height: 100%; border-radius: 50%; overflow: hidden; background: linear-gradient(135deg, #ede9fe, #faf5ff); border: 2.5px solid #e5e7eb; display: flex; align-items: center; justify-content: center; transition: border-color 0.25s, box-shadow 0.25s; position: relative; } .av-circle:hover .av-circle-inner { border-color: #a855f7; box-shadow: 0 0 0 4px rgba(168,85,247,0.12); } .av-circle-inner img { width: 100%; height: 100%; object-fit: cover; } .av-circle-inner.has-image { border-color: #a855f7; border-style: solid; box-shadow: 0 0 0 3px rgba(168,85,247,0.15); } /* Camera overlay on hover */ .av-circle-overlay { position: absolute; inset: 0; border-radius: 50%; background: rgba(109,40,217,0.55); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s; pointer-events: none; } .av-circle:hover .av-circle-overlay { opacity: 1; } /* Remove button */ .av-remove-btn { position: absolute; top: -3px; right: -3px; width: 20px; height: 20px; border-radius: 50%; background: #ef4444; border: 2px solid white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.15s, background 0.15s; z-index: 2; } .av-remove-btn:hover { background: #dc2626; transform: scale(1.15); } /* Drop zone (right side) */ .av-dropzone { flex: 1; border: 2px dashed #e5e7eb; border-radius: 16px; padding: 0.9rem 1.1rem; display: flex; flex-direction: column; align-items: flex-start; gap: 0.35rem; background: #fafafa; cursor: pointer; transition: border-color 0.2s, background 0.2s, transform 0.15s; position: relative; overflow: hidden; } .av-dropzone:hover, .av-dropzone.dragover { border-color: #a855f7; background: #fdf4ff; transform: scale(1.01); } .av-dropzone.dragover { border-style: solid; box-shadow: 0 0 0 4px rgba(168,85,247,0.1); } .av-dropzone-title { font-size: 0.82rem; font-weight: 800; color: #1e1b4b; display: flex; align-items: center; gap: 0.4rem; } .av-dropzone-sub { font-family: 'Nunito Sans', sans-serif; font-size: 0.7rem; font-weight: 600; color: #9ca3af; } .av-dropzone-types { display: flex; gap: 0.3rem; margin-top: 0.1rem; } .av-type-badge { font-family: 'Nunito Sans', sans-serif; font-size: 0.62rem; font-weight: 700; padding: 0.15rem 0.45rem; border-radius: 6px; background: #ede9fe; color: #7c3aed; letter-spacing: 0.04em; text-transform: uppercase; } /* Shimmer drag-over effect */ .av-dropzone.dragover::after { content: ''; position: absolute; inset: 0; background: linear-gradient(120deg, transparent 30%, rgba(168,85,247,0.07) 50%, transparent 70%); animation: shimmer 1s linear infinite; } @keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } } /* Upload progress bar */ .av-progress-wrap { width: 100%; height: 4px; border-radius: 999px; background: #f3f4f6; overflow: hidden; margin-top: 0.4rem; } .av-progress-bar { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #a855f7, #6d28d9); transition: width 0.2s ease; } .av-uploading-label { font-family: 'Nunito Sans', sans-serif; font-size: 0.7rem; font-weight: 700; color: #a855f7; display: flex; align-items: center; gap: 0.3rem; margin-top: 0.2rem; } .av-upload-spinner { animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Upload error */ .av-upload-error { font-family: 'Nunito Sans', sans-serif; font-size: 0.7rem; font-weight: 700; color: #e11d48; margin-top: 0.2rem; } /* Fields */ .rg-fields { display: flex; flex-direction: column; gap: 1rem; } .rg-row { display: flex; gap: 1rem; } .rg-row .rg-field { flex: 1; } .rg-field { display: flex; flex-direction: column; gap: 0.4rem; } .rg-label { font-size: 0.7rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #6b7280; padding-left: 0.2rem; } .rg-input-wrap { position: relative; } .rg-input-icon { position: absolute; left: 0.9rem; top: 50%; transform: translateY(-50%); pointer-events: none; color: #9ca3af; transition: color 0.2s; } .rg-input { width: 100%; padding: 0.85rem 1rem 0.85rem 2.6rem; background: #f9fafb; border: 2.5px solid #f3f4f6; border-radius: 14px; font-family: 'Nunito Sans', sans-serif; font-size: 0.88rem; font-weight: 600; color: #1e1b4b; outline: none; transition: all 0.2s; } .rg-input:focus { background: white; border-color: #c4b5fd; box-shadow: 0 0 0 3.5px rgba(168,85,247,0.1); } .rg-input:focus + .rg-input-icon { color: #a855f7; } .rg-input:disabled { opacity: 0.5; cursor: not-allowed; } .rg-input::placeholder { color: #d1d5db; } /* Strength */ .rg-strength-bar { display: flex; gap: 5px; margin-top: 0.4rem; } .rg-strength-seg { flex: 1; height: 4px; border-radius: 999px; background: #f3f4f6; transition: background 0.3s; } .rg-strength-seg.weak { background: #f43f5e; } .rg-strength-seg.medium { background: #eab308; } .rg-strength-seg.strong { background: #22c55e; } .rg-strength-hint { font-family: 'Nunito Sans', sans-serif; font-size: 0.7rem; font-weight: 700; margin-top: 0.2rem; padding-left: 0.1rem; color: #9ca3af; } .rg-strength-hint.weak { color: #f43f5e; } .rg-strength-hint.medium { color: #eab308; } .rg-strength-hint.strong { color: #22c55e; } .rg-error { background: #fff1f2; border: 2px solid #fecdd3; border-radius: 14px; padding: 0.8rem 1rem; font-family: 'Nunito Sans', sans-serif; font-size: 0.82rem; font-weight: 700; color: #e11d48; display: flex; align-items: center; gap: 0.5rem; } .rg-btn { width: 100%; padding: 1rem; background: #a855f7; color: white; border: none; border-radius: 100px; cursor: pointer; font-family: 'Nunito', sans-serif; font-size: 1rem; font-weight: 900; display: flex; align-items: center; justify-content: center; gap: 0.5rem; box-shadow: 0 6px 0 #7c3aed, 0 10px 24px rgba(168,85,247,0.3); transition: transform 0.1s, box-shadow 0.1s; letter-spacing: 0.01em; } .rg-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 0 #7c3aed, 0 14px 28px rgba(168,85,247,0.35); } .rg-btn:active { transform: translateY(3px); box-shadow: 0 3px 0 #7c3aed; } .rg-btn:disabled { background: #e5e7eb; color: #9ca3af; cursor: not-allowed; box-shadow: 0 4px 0 #d1d5db; } .rg-btn:disabled:hover { transform: none; box-shadow: 0 4px 0 #d1d5db; } .rg-spinner { animation: spin 0.8s linear infinite; } .rg-form-footer { text-align: center; font-family: 'Nunito Sans', sans-serif; font-size: 0.8rem; font-weight: 600; color: #9ca3af; } .rg-link { color: #a855f7; font-weight: 800; text-decoration: none; } .rg-link:hover { color: #7c3aed; } @media (max-width: 860px) { .rg-left { display: none; } .rg-right { padding: 2rem 1.5rem; } } `; function getStrength(p: string): 0 | 1 | 2 | 3 { if (!p) return 0; let s = 0; if (p.length >= 8) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/[0-9]/.test(p) || /[^A-Za-z0-9]/.test(p)) s++; return s as 0 | 1 | 2 | 3; } const S_LABEL = ["", "Weak", "Medium", "Strong"]; const S_CLASS = ["", "weak", "medium", "strong"]; const FEATURES = [ { icon: , bg: "#a855f7", title: "Adaptive Practice", sub: "Questions tailored to your skill level", }, { icon: , bg: "#f97316", title: "Instant Feedback", sub: "Know exactly where you went wrong", }, { icon: , bg: "#22c55e", title: "Score Tracking", sub: "Watch your SAT score climb over time", }, ]; const PANEL_DOTS = [ { size: 70, color: "#f97316", top: "15%", left: "65%", delay: "0s", dur: "9s", }, { size: 45, color: "#22c55e", top: "62%", left: "10%", delay: "1s", dur: "11s", }, { size: 30, color: "#fde68a", top: "35%", left: "75%", delay: "0.5s", dur: "7s", }, { size: 20, color: "#a855f7", top: "78%", left: "50%", delay: "2s", dur: "13s", }, ]; const BG_DOTS = [ { size: 180, color: "#a855f7", top: "5%", right: "5%", delay: "0s", dur: "12s", }, { size: 100, color: "#f97316", bottom: "10%", left: "2%", delay: "1.5s", dur: "10s", }, { size: 60, color: "#22c55e", top: "50%", right: "3%", delay: "0.8s", dur: "8s", }, ]; const INITIALS = ["JD", "AS", "MK", "RP", "LL"]; const ACCEPTED = ["image/jpeg", "image/png", "image/webp", "image/gif"]; // ─── Avatar Upload Component ─────────────────────────────────────────────── interface AvatarUploadProps { avatarUrl: string; onAvatarChange: (url: string) => void; disabled?: boolean; } function AvatarUpload({ avatarUrl, onAvatarChange, disabled, }: AvatarUploadProps) { const [isDragOver, setIsDragOver] = useState(false); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(""); const [previewUrl, setPreviewUrl] = useState(""); const fileInputRef = useRef(null); const processFile = useCallback( async (file: File) => { if (!ACCEPTED.includes(file.type)) { setUploadError("Please upload a JPG, PNG, WebP, or GIF image."); return; } if (file.size > 5 * 1024 * 1024) { setUploadError("Image must be under 5 MB."); return; } setUploadError(""); // Show local preview immediately const local = URL.createObjectURL(file); setPreviewUrl(local); setIsUploading(true); setUploadProgress(0); // Fake progress ticks while uploading const ticker = setInterval(() => { setUploadProgress((p) => Math.min(p + 12, 85)); }, 120); try { const uploadedUrl = await api.uploadAvatar(file); clearInterval(ticker); setUploadProgress(100); onAvatarChange(uploadedUrl); setTimeout(() => { setIsUploading(false); setUploadProgress(0); }, 600); } catch { clearInterval(ticker); setIsUploading(false); setUploadProgress(0); setPreviewUrl(""); setUploadError("Upload failed. Please try again."); } }, [onAvatarChange], ); const handleDrop = (e: DragEvent) => { e.preventDefault(); setIsDragOver(false); const file = e.dataTransfer.files[0]; if (file) processFile(file); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) processFile(file); e.target.value = ""; }; const handleRemove = (ev: React.MouseEvent) => { ev.stopPropagation(); setPreviewUrl(""); onAvatarChange(""); setUploadError(""); }; const displayUrl = previewUrl || avatarUrl; return (
{/* Hidden file input */} {/* Avatar circle */}
!disabled && !isUploading && fileInputRef.current?.click() } title="Click to change photo" >
{displayUrl ? ( Avatar preview ) : ( )}
{displayUrl && !isUploading && (
)}
{/* Drop zone */}
!disabled && !isUploading && fileInputRef.current?.click() } onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={handleDrop} > {isUploading ? ( <>
Uploading photo…
) : ( <>
{displayUrl ? "Change photo" : "Upload a photo"}
{isDragOver ? "Drop it here!" : "Click or drag & drop"}
{["PNG", "JPG", "WebP"].map((t) => ( {t} ))} Max 5 MB
)} {uploadError &&
⚠️ {uploadError}
}
); } // ─── Main Register Page ─────────────────────────────────────────────────── export const Register = () => { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [avatarUrl, setAvatarUrl] = useState(""); const [password, setPassword] = useState(""); const navigate = useNavigate(); const { register, isLoading, error, clearError } = useAuthStore(); const strength = getStrength(password); const isValid = name.trim() && email.trim() && password.length >= 6; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); clearError(); const success = await register({ email, name, avatar_url: avatarUrl, password, }); if (success) navigate("/login", { replace: true }); }; return (
{/* ── LEFT PANEL ── */}
{PANEL_DOTS.map((d, i) => (
))} {[ { top: "14%", left: "20%", size: 14, delay: "0s" }, { top: "28%", left: "78%", size: 10, delay: "0.7s" }, { top: "52%", left: "30%", size: 12, delay: "1.3s" }, { top: "70%", left: "65%", size: 8, delay: "0.4s" }, { top: "88%", left: "22%", size: 10, delay: "1.8s" }, ].map((s, i) => ( ))} {[ { size: 90, top: "72%", left: "5%", delay: "0.2s", dur: "10s" }, { size: 60, top: "8%", left: "55%", delay: "1.1s", dur: "13s" }, ].map((r, i) => (
))}
📚
EdBridge

Ace the SAT.
Start for free.

Join thousands of students who improved their
SAT scores with personalized practice.

{FEATURES.map((f, i) => (
{f.icon}
{f.title} {f.sub}
))}
{INITIALS.map((s, i) => (
{s}
))}

2,400+ students already enrolled

{/* ── RIGHT PANEL ── */}
{BG_DOTS.map((d, i) => (
))}

Create your account ✨

Fill in the details below to get started

{/* ── Avatar Upload ── */} {/* Fields */}
setName(e.target.value)} disabled={isLoading} />
setEmail(e.target.value)} disabled={isLoading} />
setPassword(e.target.value)} disabled={isLoading} />
{password && ( <>
{[1, 2, 3].map((seg) => (
= seg ? S_CLASS[strength] : ""}`} /> ))}

{S_LABEL[strength]} password

)}
{error && (
⚠️ {error}
)}

Already have an account?{" "} Sign in

); };