Files
edbridge-scholars/src/pages/auth/Register.tsx

796 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import type { FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "../../stores/authStore";
import {
Loader2,
Mail,
Lock,
User,
ImageIcon,
BookOpen,
Star,
Zap,
Trophy,
} from "lucide-react";
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;
}
/* Blobs inside left panel */
.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); }
}
/* Floating decorative shapes */
.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); }
}
/* Stars scattered */
.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); }
}
/* Left panel content */
.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;
}
/* Feature pills */
.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;
}
/* Social proof */
.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 (form) ─── */
.rg-right {
flex: 1;
display: flex; align-items: center; justify-content: center;
padding: 3rem 4rem;
position: relative; overflow: hidden;
}
/* Subtle bg dots on right */
.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); }
}
/* Form header */
.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 row */
.rg-avatar-row {
display: flex; align-items: center; gap: 1.1rem;
background: #f9fafb; border: 2.5px solid #f3f4f6;
border-radius: 18px; padding: 0.9rem 1.1rem;
transition: border-color 0.2s;
}
.rg-avatar-row:focus-within { border-color: #c4b5fd; background: white; }
.rg-avatar-ring {
width: 52px; height: 52px; border-radius: 50%;
border: 2.5px dashed #e5e7eb;
display: flex; align-items: center; justify-content: center;
overflow: hidden; background: white; flex-shrink: 0;
transition: border-color 0.25s, border-style 0.25s;
}
.rg-avatar-ring.filled { border-style: solid; border-color: #a855f7; }
.rg-avatar-ring img { width: 100%; height: 100%; object-fit: cover; }
.rg-avatar-input-col { flex: 1; display: flex; flex-direction: column; gap: 0.2rem; }
.rg-avatar-label {
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.1em;
text-transform: uppercase; color: #6b7280;
}
.rg-avatar-input {
background: transparent; border: none; outline: none;
font-family: 'Nunito Sans', sans-serif;
font-size: 0.85rem; font-weight: 600; color: #1e1b4b;
width: 100%;
}
.rg-avatar-input::placeholder { color: #d1d5db; }
.rg-avatar-hint {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.7rem; font-weight: 600; color: #c4b5fd;
}
/* Fields grid */
.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; }
/* Error */
.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;
}
/* Submit */
.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; }
@keyframes spin { to { transform: rotate(360deg); } }
.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; }
/* Responsive */
@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: <BookOpen size={18} color="#fff" />,
bg: "#a855f7",
title: "Adaptive Practice",
sub: "Questions tailored to your skill level",
},
{
icon: <Zap size={18} color="#fff" />,
bg: "#f97316",
title: "Instant Feedback",
sub: "Know exactly where you went wrong",
},
{
icon: <Trophy size={18} color="#fff" />,
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"];
export const Register = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [avatarUrl, setAvatarUrl] = useState("");
const [password, setPassword] = useState("");
const [avatarError, setAvatarError] = useState(false);
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<HTMLButtonElement>) => {
e.preventDefault();
clearError();
const success = await register({
email,
name,
avatar_url: avatarUrl,
password,
});
if (success) navigate("/student/home", { replace: true });
};
return (
<div className="rg-root">
<style>{STYLES}</style>
{/* ── LEFT PANEL ── */}
<div className="rg-left">
<div className="rg-panel-blob rg-panel-blob-1" />
<div className="rg-panel-blob rg-panel-blob-2" />
<div className="rg-panel-blob rg-panel-blob-3" />
{/* Decorative floating dots */}
{PANEL_DOTS.map((d, i) => (
<div
key={i}
className="rg-shape"
style={
{
width: d.size,
height: d.size,
background: d.color,
borderRadius: "50%",
opacity: 0.12,
top: d.top,
left: d.left,
animationDelay: d.delay,
animationDuration: d.dur,
} as React.CSSProperties
}
/>
))}
{/* Stars */}
{[
{ 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) => (
<Star
key={i}
className="rg-star"
size={s.size}
style={
{
top: s.top,
left: s.left,
animationDelay: s.delay,
fill: "#fde68a",
} as React.CSSProperties
}
/>
))}
{/* Decorative ring shapes */}
{[
{ 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) => (
<div
key={i}
className="rg-shape"
style={
{
width: r.size,
height: r.size,
border: "2.5px solid rgba(255,255,255,0.1)",
borderRadius: "50%",
top: r.top,
left: r.left,
animationDelay: r.delay,
animationDuration: r.dur,
} as React.CSSProperties
}
/>
))}
<div className="rg-panel-content">
{/* Logo */}
<div className="rg-panel-logo">
<div className="rg-panel-logo-badge">📚</div>
<span className="rg-panel-logo-text">EdBridge</span>
</div>
{/* Headline */}
<div className="rg-panel-headline">
<h2>
Ace the SAT.
<br />
<span>Start for free.</span>
</h2>
<p>
Join thousands of students who improved their
<br />
SAT scores with personalized practice.
</p>
</div>
{/* Feature pills */}
<div className="rg-features">
{FEATURES.map((f, i) => (
<div className="rg-feature" key={i}>
<div className="rg-feature-icon" style={{ background: f.bg }}>
{f.icon}
</div>
<div className="rg-feature-text">
<strong>{f.title}</strong>
<span>{f.sub}</span>
</div>
</div>
))}
</div>
{/* Social proof */}
<div className="rg-social-proof">
<div className="rg-avatars">
{INITIALS.map((s, i) => (
<div className="rg-av" key={i}>
{s}
</div>
))}
</div>
<p>
<strong>2,400+</strong> students already enrolled
</p>
</div>
</div>
</div>
{/* ── RIGHT PANEL ── */}
<div className="rg-right">
{BG_DOTS.map((d, i) => (
<div
key={i}
className="rg-bg-dot"
style={
{
width: d.size,
height: d.size,
background: d.color,
top: (d as any).top,
right: (d as any).right,
bottom: (d as any).bottom,
left: (d as any).left,
animationDelay: d.delay,
animationDuration: d.dur,
} as React.CSSProperties
}
/>
))}
<div className="rg-form-wrap">
{/* Header */}
<div className="rg-form-header">
<h1>Create your account </h1>
<p>Fill in the details below to get started</p>
</div>
{/* Avatar URL row */}
<div className="rg-avatar-row">
<div
className={`rg-avatar-ring ${avatarUrl && !avatarError ? "filled" : ""}`}
>
{avatarUrl && !avatarError ? (
<img
src={avatarUrl}
alt="Avatar"
onError={() => setAvatarError(true)}
/>
) : (
<ImageIcon size={20} color="#d1d5db" />
)}
</div>
<div className="rg-avatar-input-col">
<span className="rg-avatar-label">
Avatar URL{" "}
<span
style={{
fontWeight: 600,
textTransform: "none",
letterSpacing: 0,
color: "#c4b5fd",
fontSize: "0.68rem",
}}
>
(optional)
</span>
</span>
<input
className="rg-avatar-input"
type="url"
placeholder="https://example.com/photo.jpg"
value={avatarUrl}
onChange={(e) => {
setAvatarUrl(e.target.value);
setAvatarError(false);
}}
disabled={isLoading}
/>
<span className="rg-avatar-hint">
Paste any image URL to set your profile photo
</span>
</div>
</div>
{/* Fields */}
<div className="rg-fields">
{/* Name + Email row */}
<div className="rg-row">
<div className="rg-field">
<label className="rg-label" htmlFor="name">
Full Name
</label>
<div className="rg-input-wrap">
<input
id="name"
type="text"
className="rg-input"
style={{ paddingLeft: "1rem" }}
placeholder="Jane Doe"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isLoading}
/>
</div>
</div>
</div>
{/* Email */}
<div className="rg-field">
<label className="rg-label" htmlFor="email">
Email
</label>
<div className="rg-input-wrap">
<Mail size={15} className="rg-input-icon" />
<input
id="email"
type="email"
className="rg-input"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
</div>
</div>
{/* Password */}
<div className="rg-field">
<label className="rg-label" htmlFor="password">
Password
</label>
<div className="rg-input-wrap">
<Lock size={15} className="rg-input-icon" />
<input
id="password"
type="password"
className="rg-input"
placeholder="Min. 6 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
</div>
{password && (
<>
<div className="rg-strength-bar">
{[1, 2, 3].map((seg) => (
<div
key={seg}
className={`rg-strength-seg ${strength >= seg ? S_CLASS[strength] : ""}`}
/>
))}
</div>
<p className={`rg-strength-hint ${S_CLASS[strength]}`}>
{S_LABEL[strength]} password
</p>
</>
)}
</div>
{/* Error */}
{error && (
<div className="rg-error">
<span></span> {error}
</div>
)}
{/* Submit */}
<button
className="rg-btn"
onClick={handleSubmit}
disabled={isLoading || !isValid}
>
{isLoading ? (
<>
<Loader2 size={18} className="rg-spinner" /> Creating
account...
</>
) : (
"Create Account →"
)}
</button>
</div>
<p className="rg-form-footer">
Already have an account?{" "}
<a href="/login" className="rg-link">
Sign in
</a>
</p>
</div>
</div>
</div>
);
};