ClassLens / chatkit /frontend /src /components /LoginForm.tsx
kuanz's picture
Add database management: Supabase persistence, teacher profiles, admin dashboard (#1)
4c4a722
Raw
History Blame Contribute Delete
6.4 kB
import { useState } from "react";
import { apiPost, setToken } from "../lib/api";
import { CLASSLENS_ICON } from "../lib/icon";
import type { User } from "../types";
interface AuthResponse {
token: string;
user: User;
}
interface LoginFormProps {
onLogin: (user: User) => void;
}
export function LoginForm({ onLogin }: LoginFormProps) {
const [mode, setMode] = useState<"login" | "register">("login");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [fullName, setFullName] = useState("");
const [school, setSchool] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const endpoint = mode === "login" ? "/api/auth/login" : "/api/auth/register";
const body =
mode === "login"
? { email, password }
: { email, password, full_name: fullName, school };
const result = await apiPost<AuthResponse>(endpoint, body);
setToken(result.token);
onLogin(result.user);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f1419] via-[#1a2332] to-[#0f1419]">
<div className="relative">
<div className="absolute -top-20 -left-20 w-40 h-40 bg-[var(--color-primary)]/20 rounded-full blur-3xl" />
<div className="absolute -bottom-20 -right-20 w-40 h-40 bg-[var(--color-accent)]/20 rounded-full blur-3xl" />
<div className="relative bg-[var(--color-surface)] border border-[var(--color-border)] rounded-2xl p-8 max-w-md w-full shadow-2xl">
<div className="text-center mb-8">
<img
src={CLASSLENS_ICON}
alt="ClassLens"
className="w-24 h-24 mx-auto mb-4 rounded-2xl shadow-lg"
/>
<h1 className="text-2xl font-bold text-[var(--color-text)] font-display">
ClassLens
</h1>
<p className="text-[var(--color-text-muted)] mt-2">
AI 驅動的考試分析
</p>
</div>
{/* Mode toggle */}
<div className="flex mb-6 bg-[var(--color-background)] rounded-xl p-1">
<button
type="button"
onClick={() => { setMode("login"); setError(""); }}
className={`flex-1 py-2 text-sm font-medium rounded-lg transition-all ${
mode === "login"
? "bg-[var(--color-primary)] text-white shadow"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
>
登入
</button>
<button
type="button"
onClick={() => { setMode("register"); setError(""); }}
className={`flex-1 py-2 text-sm font-medium rounded-lg transition-all ${
mode === "register"
? "bg-[var(--color-primary)] text-white shadow"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
>
註冊
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--color-text-muted)] mb-2">
電子郵件
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="input"
autoFocus
/>
</div>
{mode === "register" && (
<>
<div>
<label className="block text-sm font-medium text-[var(--color-text-muted)] mb-2">
姓名
</label>
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
placeholder="王小明"
required
className="input"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--color-text-muted)] mb-2">
學校
</label>
<input
type="text"
value={school}
onChange={(e) => setSchool(e.target.value)}
placeholder="台北市立第一高級中學"
required
className="input"
/>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-[var(--color-text-muted)] mb-2">
密碼
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={4}
className="input"
/>
</div>
{error && (
<p className="text-sm text-red-400 flex items-center gap-1">
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{error}
</p>
)}
<button
type="submit"
disabled={loading}
className="w-full py-3 px-4 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-accent)] text-white font-semibold rounded-xl hover:opacity-90 transition-opacity disabled:opacity-50"
>
{loading ? "..." : mode === "login" ? "登入" : "建立帳號"}
</button>
</form>
</div>
</div>
</div>
);
}