chih.yikuan
email-done
5ee5085
import { useState, useEffect } from "react";
import { Header } from "./components/Header";
import { Hero } from "./components/Hero";
import { AuthStatus } from "./components/AuthStatus";
import { ExamAnalyzer } from "./components/ExamAnalyzer";
import { Features } from "./components/Features";
import { SimpleChatPanel } from "./components/SimpleChatPanel";
import { CLASSLENS_ICON } from "./lib/icon";
const VALID_INVITE_CODE = "taboola-npo-cz";
function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
const [code, setCode] = useState("");
const [error, setError] = useState(false);
const [isShaking, setIsShaking] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (code.trim().toLowerCase() === VALID_INVITE_CODE) {
// Save to localStorage so user doesn't need to enter again
localStorage.setItem("classlens_access", "granted");
onSuccess();
} else {
setError(true);
setIsShaking(true);
setTimeout(() => setIsShaking(false), 500);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f1419] via-[#1a2332] to-[#0f1419]">
<div className="relative">
{/* Decorative background elements */}
<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 ${
isShaking ? "animate-shake" : ""
}`}
>
{/* Logo */}
<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>
{/* Invite code form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="invite-code"
className="block text-sm font-medium text-[var(--color-text-muted)] mb-2"
>
輸入邀請碼
</label>
<input
id="invite-code"
type="text"
value={code}
onChange={(e) => {
setCode(e.target.value);
setError(false);
}}
placeholder="請輸入您的邀請碼..."
className={`w-full px-4 py-3 bg-[var(--color-background)] border rounded-xl text-[var(--color-text)] placeholder-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-all ${
error
? "border-red-500 focus:ring-red-500"
: "border-[var(--color-border)]"
}`}
autoFocus
/>
{error && (
<p className="mt-2 text-sm text-red-400 flex items-center gap-1">
<svg className="w-4 h-4" 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>
邀請碼無效,請重試。
</p>
)}
</div>
<button
type="submit"
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 focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface)]"
>
進入應用程式 →
</button>
</form>
{/* Footer */}
<p className="text-center text-xs text-[var(--color-text-muted)] mt-6">
Don't have an invite code?{" "}
<a
href="mailto:kuanz1991@gmail.com"
className="text-[var(--color-primary)] hover:underline"
>
Contact us
</a>
</p>
</div>
</div>
{/* Add shake animation */}
<style>{`
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
20%, 40%, 60%, 80% { transform: translateX(4px); }
}
.animate-shake {
animation: shake 0.5s ease-in-out;
}
`}</style>
</div>
);
}
export default function App() {
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
const [teacherEmail, setTeacherEmail] = useState<string>("");
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showAnalyzer, setShowAnalyzer] = useState(false);
const [testMode, setTestMode] = useState(false);
// Check for existing access on mount
useEffect(() => {
const access = localStorage.getItem("classlens_access");
setHasAccess(access === "granted");
}, []);
// Check URL params for auth callback and test mode
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const authSuccess = params.get("auth_success");
const email = params.get("email");
const authError = params.get("auth_error");
const test = params.get("test");
if (test === "chat") {
setTestMode(true);
return;
}
if (authSuccess === "true" && email) {
setTeacherEmail(email);
setIsAuthenticated(true);
setShowAnalyzer(true);
// Clean URL
window.history.replaceState({}, "", window.location.pathname);
} else if (authError) {
console.error("Auth error:", authError);
window.history.replaceState({}, "", window.location.pathname);
}
}, []);
const handleStartAnalysis = () => {
if (isAuthenticated) {
setShowAnalyzer(true);
}
};
// Loading state
if (hasAccess === null) {
return (
<div className="min-h-screen flex items-center justify-center bg-[var(--color-background)]">
<div className="w-8 h-8 border-2 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin" />
</div>
);
}
// Invite code gate
if (!hasAccess) {
return <InviteCodeGate onSuccess={() => setHasAccess(true)} />;
}
// Test mode - show simple chat panel
if (testMode) {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-slate-100 dark:bg-slate-950 p-4">
<div className="mx-auto w-full max-w-3xl">
<div className="mb-4 text-center">
<h1 className="text-2xl font-bold mb-2">ChatKit Test Mode</h1>
<p className="text-gray-600">Testing basic ChatKit functionality</p>
<a href="/" className="text-blue-500 underline text-sm">← Back to app</a>
</div>
<SimpleChatPanel />
</div>
</main>
);
}
return (
<div className="min-h-screen">
<Header
isAuthenticated={isAuthenticated}
teacherEmail={teacherEmail}
/>
<main>
{!showAnalyzer ? (
<>
<Hero
isAuthenticated={isAuthenticated}
onStartAnalysis={handleStartAnalysis}
/>
<AuthStatus
teacherEmail={teacherEmail}
setTeacherEmail={setTeacherEmail}
isAuthenticated={isAuthenticated}
setIsAuthenticated={setIsAuthenticated}
/>
<Features />
</>
) : (
<ExamAnalyzer
teacherEmail={teacherEmail}
onBack={() => setShowAnalyzer(false)}
/>
)}
</main>
<footer className="py-8 text-center text-sm text-[var(--color-text-muted)]">
<p>© 2026 ClassLens • AI-Powered Teaching Assistant</p>
</footer>
</div>
);
}