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: , }, + { + path: "/register", + element: , + }, { path: "/student", element: , diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index 703d5f8..ce6f738 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -175,6 +175,16 @@ const STYLES = ` font-family: 'Nunito Sans', sans-serif; font-size: 0.75rem; font-weight: 600; color: #9ca3af; } + .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; } `; export const Login = () => { @@ -332,6 +342,12 @@ export const Login = () => {

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.preventDefault(); + clearError(); + const success = await register({ + email, + name, + avatar_url: avatarUrl, + password, + }); + if (success) navigate("/student/home", { replace: true }); + }; + + const isValid = name.trim() && email.trim() && password.length >= 6; + + return ( +
+ + + {/* Blobs */} +
+
+
+
+ + {/* Dots */} + {DOTS.map((d, i) => ( +
+ ))} + +
+ {/* Logo + heading */} +
+ EdBridge { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + + {/* Avatar preview */} +
+ {avatarUrl && !avatarError ? ( + Avatar preview setAvatarError(true)} + /> + ) : ( + + )} +
+ +
+

Create account ✨

+

Join EdBridge and start your SAT prep

+
+
+ + {/* Fields */} +
+ {/* Name */} +
+ +
+ + setName(e.target.value)} + disabled={isLoading} + autoComplete="off" + /> +
+
+ + {/* Email */} +
+ +
+ + setEmail(e.target.value)} + disabled={isLoading} + autoComplete="off" + /> +
+
+ + {/* Avatar URL */} +
+ +
+ + handleAvatarChange(e.target.value)} + disabled={isLoading} + /> +
+
+ + {/* Password */} +
+ +
+ + setPassword(e.target.value)} + disabled={isLoading} + autoComplete="off" + /> +
+ {/* Strength bar */} + {password && ( + <> +
+ {[1, 2, 3].map((seg) => ( +
= seg + ? `active-${STRENGTH_CLASSES[strength]}` + : "" + }`} + /> + ))} +
+

+ {STRENGTH_LABELS[strength]} password +

+ + )} +
+ + {/* Error */} + {error && ( +
+ ⚠️ {error} +
+ )} + + {/* Submit */} + +
+ +

+ Already have an account?{" "} + + Sign in + +

+
+
+ ); +}; diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx index ff6b1ad..bed0f8f 100644 --- a/src/pages/student/practice/Test.tsx +++ b/src/pages/student/practice/Test.tsx @@ -1807,6 +1807,7 @@ export const Test = () => { Go To Question )} + {!isTargeted && ( Promise; + register: (credentials: RegistrationRequest) => Promise; logout: () => void; clearError: () => void; } @@ -21,6 +28,7 @@ export const useAuthStore = create()( isAuthenticated: false, isLoading: false, error: null, + registrationMessage: null, login: async (credentials: LoginRequest) => { set({ isLoading: true, error: null }); @@ -51,6 +59,31 @@ export const useAuthStore = create()( return false; } }, + register: async (credentials: RegistrationRequest) => { + set({ isLoading: true, error: null }); + + try { + const response = await api.register(credentials); + + set({ + registrationMessage: response.message, + }); + + return true; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Registration failed"; + set({ + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + error: errorMessage, + }); + + return false; + } + }, logout: () => { set({ @@ -70,6 +103,6 @@ export const useAuthStore = create()( token: state.token, isAuthenticated: state.isAuthenticated, }), - } - ) + }, + ), ); diff --git a/src/utils/api.ts b/src/utils/api.ts index ed0d40c..5604734 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -22,6 +22,12 @@ export interface LoginRequest { email: string; password: string; } +export interface RegistrationRequest { + email: string; + name: string; + avatar_url: string; + password: string; +} export interface User { email: string; @@ -96,6 +102,14 @@ class ApiClient { body: JSON.stringify(credentials), }); } + async register( + credentials: RegistrationRequest, + ): Promise<{ message: string }> { + return this.request<{ message: string }>("/auth/register/", { + method: "POST", + body: JSON.stringify(credentials), + }); + } // Authenticated request helper async authenticatedRequest(