Files
edbridge-scholars/src/pages/auth/Login.tsx
2026-02-22 03:38:16 +06:00

339 lines
11 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, useEffect } from "react";
import type { FormEvent } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../../stores/authStore";
import { Loader2, Mail, Lock } from "lucide-react";
interface LocationState {
from?: { pathname: string };
}
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');
.lg-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 */
.lg-blob { position:fixed;pointer-events:none;z-index:0; }
.lg-blob-1 { width:280px;height:280px;background:#fde68a;top:-100px;left:-100px;border-radius:60% 40% 70% 30%/50% 60% 40% 50%;animation:lgWobble1 14s ease-in-out infinite; }
.lg-blob-2 { width:220px;height:220px;background:#a5f3c0;bottom:-60px;left:4%;border-radius:40% 60% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 16s ease-in-out infinite; }
.lg-blob-3 { width:250px;height:250px;background:#fbcfe8;top:10%;right:-70px;border-radius:70% 30% 50% 50%/40% 60% 40% 60%;animation:lgWobble1 18s ease-in-out infinite reverse; }
.lg-blob-4 { width:180px;height:180px;background:#bfdbfe;bottom:8%;right:0;border-radius:50% 50% 30% 70%/60% 40% 60% 40%;animation:lgWobble2 12s ease-in-out infinite; }
@keyframes lgWobble1 {
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 lgWobble2 {
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);}
}
.lg-dot { position:fixed;border-radius:50%;pointer-events:none;z-index:0;opacity:0.28;animation:lgFloat 7s ease-in-out infinite; }
@keyframes lgFloat {
0%,100%{transform:translateY(0) rotate(0deg);}
50%{transform:translateY(-14px) rotate(180deg);}
}
/* Card */
.lg-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: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
}
@keyframes lgPopIn {
from { opacity:0; transform:scale(0.9) translateY(20px); }
to { opacity:1; transform:scale(1) translateY(0); }
}
/* Logo area */
.lg-logo-wrap {
display: flex; flex-direction: column; align-items: center; gap: 0.85rem;
}
.lg-logo-badge {
width: 64px; height: 64px; border-radius: 20px;
background: linear-gradient(135deg, #a855f7, #7c3aed);
display: flex; align-items: center; justify-content: center;
box-shadow: 0 6px 0 #5b21b655, 0 10px 24px rgba(124,58,237,0.25);
font-size: 1.75rem;
animation: lgPopIn 0.5s cubic-bezier(0.34,1.56,0.64,1) 0.1s both;
}
.lg-title {
font-size: 1.5rem; font-weight: 900; color: #1e1b4b;
letter-spacing: -0.02em; text-align: center;
}
.lg-sub {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.82rem; font-weight: 600; color: #9ca3af;
text-align: center; margin-top: -0.25rem;
}
/* Form fields */
.lg-fields { display: flex; flex-direction: column; gap: 1rem; }
.lg-field { display: flex; flex-direction: column; gap: 0.4rem; }
.lg-label {
font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em;
text-transform: uppercase; color: #6b7280;
padding-left: 0.25rem;
}
.lg-input-wrap { position: relative; }
.lg-input-icon {
position: absolute; left: 0.85rem; top: 50%;
transform: translateY(-50%); pointer-events: none; color: #9ca3af;
transition: color 0.2s ease;
}
.lg-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;
}
.lg-input:focus {
background: white; border-color: #c4b5fd;
box-shadow: 0 0 0 3px rgba(168,85,247,0.1);
}
.lg-input:focus ~ .lg-input-icon { color: #a855f7; }
.lg-input:disabled { opacity: 0.5; cursor: not-allowed; }
.lg-input::placeholder { color: #d1d5db; }
/* Remember me */
.lg-remember {
display: flex; align-items: center; gap: 0.5rem;
padding: 0 0.1rem;
}
.lg-checkbox {
width: 18px; height: 18px; border-radius: 6px;
accent-color: #a855f7; cursor: pointer; flex-shrink: 0;
}
.lg-remember-label {
font-family: 'Nunito Sans', sans-serif;
font-size: 0.8rem; font-weight: 600; color: #6b7280;
cursor: pointer;
}
/* Error */
.lg-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 */
.lg-btn {
width: 100%; padding: 0.95rem;
background: #f97316; 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 #c2560e, 0 8px 20px rgba(249,115,22,0.25);
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.lg-btn:hover { transform:translateY(-2px); box-shadow:0 8px 0 #c2560e,0 12px 24px rgba(249,115,22,0.3); }
.lg-btn:active { transform:translateY(3px); box-shadow:0 3px 0 #c2560e; }
.lg-btn:disabled {
background: #e5e7eb; color: #9ca3af;
cursor: not-allowed; box-shadow: 0 4px 0 #d1d5db;
}
.lg-btn:disabled:hover { transform: none; box-shadow: 0 4px 0 #d1d5db; }
.lg-spinner { animation: lgSpin 0.8s linear infinite; }
@keyframes lgSpin { to { transform: rotate(360deg); } }
/* Footer hint */
.lg-footer {
text-align: center;
font-family: 'Nunito Sans', sans-serif;
font-size: 0.75rem; font-weight: 600; color: #9ca3af;
}
`;
export const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const { login, isAuthenticated, isLoading, error, clearError } =
useAuthStore();
const from = (location.state as LocationState)?.from?.pathname || "/student";
useEffect(() => {
if (isAuthenticated) navigate("/student/home", { replace: true });
}, [isAuthenticated, navigate]);
useEffect(() => {
return () => clearError();
}, [clearError]);
const handleSubmit = async (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault();
clearError();
const success = await login({ email, password });
if (success) navigate(from, { replace: true });
};
if (isAuthenticated) return null;
return (
<div className="lg-screen">
<style>{STYLES}</style>
{/* Blobs */}
<div className="lg-blob lg-blob-1" />
<div className="lg-blob lg-blob-2" />
<div className="lg-blob lg-blob-3" />
<div className="lg-blob lg-blob-4" />
{/* Dots */}
{DOTS.map((d, i) => (
<div
key={i}
className="lg-dot"
style={
{
width: d.size,
height: d.size,
background: d.color,
top: d.top,
left: d.left,
right: d.right,
animationDelay: d.delay,
animationDuration: `${5.5 + i * 0.4}s`,
} as React.CSSProperties
}
/>
))}
<div className="lg-card">
{/* Logo + heading */}
<div className="lg-logo-wrap space-y-5">
<img
src="src/assets/ed_logo.png"
alt="EdBridge"
style={{
width: 600,
height: 70,
objectFit: "contain",
borderRadius: 8,
}}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<div>
<h1 className="lg-title">Welcome back 👋</h1>
<p className="lg-sub">Sign in to continue your SAT prep</p>
</div>
</div>
{/* Fields */}
<div className="lg-fields">
{/* Email */}
<div className="lg-field">
<label className="lg-label" htmlFor="email">
Email
</label>
<div className="lg-input-wrap">
<Mail size={15} className="lg-input-icon" />
<input
id="email"
type="email"
className="lg-input"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
</div>
</div>
{/* Password */}
<div className="lg-field">
<label className="lg-label" htmlFor="password">
Password
</label>
<div className="lg-input-wrap">
<Lock size={15} className="lg-input-icon" />
<input
id="password"
type="password"
className="lg-input"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
</div>
</div>
{/* Remember me */}
<div className="lg-remember">
<input id="rememberMe" type="checkbox" className="lg-checkbox" />
<label htmlFor="rememberMe" className="lg-remember-label">
Keep me signed in
</label>
</div>
{/* Error */}
{error && (
<div className="lg-error">
<span></span> {error}
</div>
)}
{/* Submit */}
<button
className="lg-btn"
onClick={handleSubmit}
disabled={isLoading || !email || !password}
>
{isLoading ? (
<>
<Loader2 size={18} className="lg-spinner" /> Signing in...
</>
) : (
"Sign In →"
)}
</button>
</div>
<p className="lg-footer">
By signing in you agree to Edbridge's Terms & Privacy Policy.
</p>
</div>
</div>
);
};