Spaces:
Runtime error
Runtime error
File size: 6,402 Bytes
bc45e54 4c4a722 bc45e54 4c4a722 bc45e54 4c4a722 bc45e54 4c4a722 bc45e54 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | 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>
);
}
|