diff --git a/src/App.tsx b/src/App.tsx
index 4b89997..163e9a6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -22,6 +22,7 @@ import { HardTestModules } from "./pages/student/hard-test-modules/page";
import { Analytics } from "./pages/student/Analytics";
import { QuestMap } from "./pages/student/QuestMap";
import ErrorPage from "./pages/ErrorPage";
+import { Register } from "./pages/auth/Register";
function App() {
const router = createBrowserRouter([
@@ -29,6 +30,10 @@ function App() {
path: "/login",
element:
By signing in you agree to Edbridge's Terms & Privacy Policy.
++ Don't have an account?{" "} + + Sign up + +
); diff --git a/src/pages/auth/Register.tsx b/src/pages/auth/Register.tsx new file mode 100644 index 0000000..e6b5319 --- /dev/null +++ b/src/pages/auth/Register.tsx @@ -0,0 +1,451 @@ +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 } from "lucide-react"; + +const DOTS = [ + { size: 12, color: "#f97316", top: "8%", left: "6%", delay: "0s" }, + { size: 7, color: "#a855f7", top: "22%", left: "3%", delay: "1.2s" }, + { size: 9, color: "#22c55e", top: "65%", left: "5%", delay: "0.6s" }, + { size: 8, color: "#f43f5e", top: "80%", left: "8%", delay: "2.1s" }, + { size: 12, color: "#3b82f6", top: "10%", right: "6%", delay: "1.8s" }, + { size: 7, color: "#eab308", top: "40%", right: "3%", delay: "0.9s" }, + { size: 10, color: "#a855f7", top: "72%", right: "5%", delay: "0.4s" }, + { size: 8, color: "#f97316", top: "55%", right: "8%", delay: "1.5s" }, +]; + +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'); + + .rg-screen { + min-height: 100vh; + background: #fffbf4; + font-family: 'Nunito', sans-serif; + position: relative; + overflow: hidden; + display: flex; align-items: center; justify-content: center; + padding: 2rem 1.25rem; + } + + /* Blobs */ + .rg-blob { position:fixed;pointer-events:none;z-index:0; } + .rg-blob-1 { width:280px;height:280px;background:#fde68a;top:-100px;left:-100px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:rgWobble1 14s ease-in-out infinite; } + .rg-blob-2 { width:220px;height:220px;background:#a5f3c0;bottom:-60px;left:4%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:rgWobble2 16s ease-in-out infinite; } + .rg-blob-3 { width:250px;height:250px;background:#fbcfe8;top:10%;right:-70px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:rgWobble1 18s ease-in-out infinite reverse; } + .rg-blob-4 { width:180px;height:180px;background:#bfdbfe;bottom:8%;right:0;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:rgWobble2 12s ease-in-out infinite; } + + @keyframes rgWobble1 { + 0%,100%{border-radius:60% 40% 70% 30%/50% 60% 40% 50%;transform:translate(0,0) rotate(0deg);} + 50%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(14px,18px) rotate(8deg);} + } + @keyframes rgWobble2 { + 0%,100%{border-radius:40% 60% 30% 70%/60% 40% 60% 40%;transform:translate(0,0) rotate(0deg);} + 50%{border-radius:60% 40% 70% 30%/40% 60% 40% 60%;transform:translate(-12px,14px) rotate(-6deg);} + } + + .rg-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.28;animation:rgFloat 7s ease-in-out infinite; } + @keyframes rgFloat { + 0%,100%{transform:translateY(0) rotate(0deg);} + 50%{transform:translateY(-14px) rotate(180deg);} + } + + /* Card */ + .rg-card { + position: relative; z-index: 1; + width: 100%; max-width: 400px; + background: white; border: 2.5px solid #f3f4f6; + border-radius: 28px; + box-shadow: 0 12px 40px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04); + padding: 2.25rem 2rem 2rem; + display: flex; flex-direction: column; gap: 1.75rem; + animation: rgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both; + } + @keyframes rgPopIn { + from { opacity:0; transform:scale(0.9) translateY(20px); } + to { opacity:1; transform:scale(1) translateY(0); } + } + + /* Logo area */ + .rg-logo-wrap { + display: flex; flex-direction: column; align-items: center; gap: 0.85rem; + } + .rg-title { + font-size: 1.5rem; font-weight: 900; color: #1e1b4b; + letter-spacing: -0.02em; text-align: center; + } + .rg-sub { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.82rem; font-weight: 600; color: #9ca3af; + text-align: center; margin-top: -0.25rem; + } + + /* Avatar preview */ + .rg-avatar-preview { + width: 56px; height: 56px; border-radius: 50%; + border: 2.5px dashed #e5e7eb; + display: flex; align-items: center; justify-content: center; + overflow: hidden; background: #f9fafb; + transition: border-color 0.2s ease; + margin-top: 0.25rem; + } + .rg-avatar-preview.has-image { + border-style: solid; border-color: #c4b5fd; + } + .rg-avatar-preview img { + width: 100%; height: 100%; object-fit: cover; + } + + /* Form fields */ + .rg-fields { display: flex; flex-direction: column; gap: 1rem; } + + .rg-field { display: flex; flex-direction: column; gap: 0.4rem; } + .rg-label { + font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em; + text-transform: uppercase; color: #6b7280; + padding-left: 0.25rem; + } + .rg-input-wrap { position: relative; } + .rg-input-icon { + position: absolute; left: 0.85rem; top: 50%; + transform: translateY(-50%); pointer-events: none; color: #9ca3af; + transition: color 0.2s ease; + } + .rg-input { + width: 100%; padding: 0.8rem 1rem 0.8rem 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 ease; + box-sizing: border-box; + } + .rg-input:focus { + background: white; border-color: #c4b5fd; + box-shadow: 0 0 0 3px 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; } + + /* Password strength */ + .rg-strength-bar { + display: flex; gap: 4px; margin-top: 0.35rem; padding: 0 0.1rem; + } + .rg-strength-seg { + flex: 1; height: 4px; border-radius: 999px; + background: #f3f4f6; transition: background 0.3s ease; + } + .rg-strength-seg.active-weak { background: #f43f5e; } + .rg-strength-seg.active-medium { background: #eab308; } + .rg-strength-seg.active-strong { background: #22c55e; } + .rg-strength-label { + font-family: 'Nunito Sans', sans-serif; + font-size: 0.72rem; font-weight: 700; + padding: 0 0.1rem; margin-top: 0.15rem; + color: #9ca3af; + } + .rg-strength-label.weak { color: #f43f5e; } + .rg-strength-label.medium { color: #eab308; } + .rg-strength-label.strong { color: #22c55e; } + + /* Error */ + .rg-error { + background: #fff1f2; border: 2px solid #fecdd3; + border-radius: 14px; padding: 0.75rem 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 button */ + .rg-btn { + width: 100%; padding: 0.95rem; + background: #a855f7; color: white; border: none; + border-radius: 100px; cursor: pointer; + font-family: 'Nunito', sans-serif; font-size: 0.95rem; font-weight: 900; + display: flex; align-items: center; justify-content: center; gap: 0.5rem; + box-shadow: 0 6px 0 #7c3aed, 0 8px 20px rgba(168,85,247,0.25); + transition: transform 0.1s ease, box-shadow 0.1s ease; + } + .rg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #7c3aed,0 12px 24px rgba(168,85,247,0.3); } + .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: rgSpin 0.8s linear infinite; } + @keyframes rgSpin { to { transform: rotate(360deg); } } + + /* Sign-in link */ + .rg-footer { + text-align: center; + font-family: 'Nunito Sans', sans-serif; + font-size: 0.78rem; font-weight: 600; color: #9ca3af; + } + .rg-link { + color: #a855f7; font-weight: 800; text-decoration: none; + transition: color 0.2s ease; + } + .rg-link:hover { color: #7c3aed; } +`; + +function getPasswordStrength(password: string): 0 | 1 | 2 | 3 { + if (!password) return 0; + let score = 0; + if (password.length >= 8) score++; + if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++; + if (/[0-9]/.test(password) || /[^A-Za-z0-9]/.test(password)) score++; + return score as 0 | 1 | 2 | 3; +} + +const STRENGTH_LABELS = ["", "Weak", "Medium", "Strong"]; +const STRENGTH_CLASSES = ["", "weak", "medium", "strong"]; + +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 = getPasswordStrength(password); + + const handleAvatarChange = (url: string) => { + setAvatarUrl(url); + setAvatarError(false); + }; + + const handleSubmit = async (e: FormEvent
{
+ (e.target as HTMLImageElement).style.display = "none";
+ }}
+ />
+
+ {/* Avatar preview */}
+ Join EdBridge and start your SAT prep
++ {STRENGTH_LABELS[strength]} password +
+ > + )} ++ Already have an account?{" "} + + Sign in + +
+