feat(ui): improve ui for test, drills and htm screens
This commit is contained in:
@ -2,16 +2,185 @@ 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";
|
||||
import { usePageTitle } from "../../hooks/usePageTitle";
|
||||
|
||||
interface LocationState {
|
||||
from?: {
|
||||
pathname: string;
|
||||
};
|
||||
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;filter:blur(52px);opacity:0.38; }
|
||||
.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<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@ -20,14 +189,10 @@ export const Login = () => {
|
||||
|
||||
const from = (location.state as LocationState)?.from?.pathname || "/student";
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate("/student/home", { replace: true });
|
||||
}
|
||||
if (isAuthenticated) navigate("/student/home", { replace: true });
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
// Clear error when component unmounts or inputs change
|
||||
useEffect(() => {
|
||||
return () => clearError();
|
||||
}, [clearError]);
|
||||
@ -35,122 +200,140 @@ export const Login = () => {
|
||||
const handleSubmit = async (e: FormEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
clearError();
|
||||
|
||||
const success = await login({ email, password });
|
||||
if (success) {
|
||||
navigate(from, { replace: true });
|
||||
}
|
||||
if (success) navigate(from, { replace: true });
|
||||
};
|
||||
|
||||
// Don't render login form if already authenticated
|
||||
if (isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
if (isAuthenticated) return null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center ">
|
||||
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-sm border border-gray-300 space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src="src/assets/ed_logo.png"
|
||||
alt="EdBridge logo"
|
||||
className="h-15 w-auto object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-3xl font-satoshi-bold text-center text-gray-800">
|
||||
Welcome Back
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-satoshi-medium text-gray-700 mb-2"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
placeholder="Enter your email"
|
||||
<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">
|
||||
<div className="lg-logo-badge">
|
||||
<img
|
||||
src="src/assets/ed_logo.png"
|
||||
alt="EdBridge"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: "contain",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-satoshi-medium text-gray-700 mb-2"
|
||||
>
|
||||
Password
|
||||
<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>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<div className="flex items-center mt-4">
|
||||
<div className="lg-input-wrap">
|
||||
<Mail size={15} className="lg-input-icon" />
|
||||
<input
|
||||
id="rememberMe"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
id="email"
|
||||
type="email"
|
||||
className="lg-input"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<label
|
||||
htmlFor="rememberMe"
|
||||
className="ml-2 block text-sm font-satoshi-medium text-gray-700"
|
||||
>
|
||||
Remember me
|
||||
</label>
|
||||
</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="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
||||
{error}
|
||||
<div className="lg-error">
|
||||
<span>⚠️</span> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
className="lg-btn"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || !email || !password}
|
||||
className="w-full bg-linear-to-br from-indigo-500 to-indigo-600 text-white py-3 rounded-2xl hover:bg-indigo-700 transition font-medium disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center justify-center font-satoshi hover:cursor-pointer"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
<Loader2 size={18} className="lg-spinner" /> Signing in...
|
||||
</>
|
||||
) : (
|
||||
"Sign In"
|
||||
"Sign In →"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="lg-footer">
|
||||
By signing in you agree to Edbridge's Terms & Privacy Policy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user