Spaces:
Paused
Paused
chih.yikuan Claude Opus 4.6 commited on
Commit ·
459bded
1
Parent(s): bc45e54
Add invite code gate page before login
Browse filesUsers must enter "taboola-npo-cz" to access the app. Code is saved in localStorage so it only needs to be entered once per browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chatkit/frontend/src/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { useState, useEffect, useCallback } from "react";
|
| 2 |
import { Header } from "./components/Header";
|
|
|
|
| 3 |
import { LoginForm } from "./components/LoginForm";
|
| 4 |
import { StepIndicator } from "./components/StepIndicator";
|
| 5 |
import { FileUploadPanel } from "./components/step1/FileUploadPanel";
|
|
@@ -8,6 +9,8 @@ import { PromptEditor } from "./components/step2/PromptEditor";
|
|
| 8 |
import { ReportViewer } from "./components/step3/ReportViewer";
|
| 9 |
import { apiGet, apiPost, getToken, clearToken } from "./lib/api";
|
| 10 |
|
|
|
|
|
|
|
| 11 |
interface User {
|
| 12 |
id: number;
|
| 13 |
email: string;
|
|
@@ -15,6 +18,7 @@ interface User {
|
|
| 15 |
}
|
| 16 |
|
| 17 |
export default function App() {
|
|
|
|
| 18 |
const [user, setUser] = useState<User | null>(null);
|
| 19 |
const [loading, setLoading] = useState(true);
|
| 20 |
const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
|
|
@@ -131,6 +135,19 @@ export default function App() {
|
|
| 131 |
);
|
| 132 |
}
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
// Login gate
|
| 135 |
if (!user) {
|
| 136 |
return <LoginForm onLogin={setUser} />;
|
|
|
|
| 1 |
import { useState, useEffect, useCallback } from "react";
|
| 2 |
import { Header } from "./components/Header";
|
| 3 |
+
import { InviteGate } from "./components/InviteGate";
|
| 4 |
import { LoginForm } from "./components/LoginForm";
|
| 5 |
import { StepIndicator } from "./components/StepIndicator";
|
| 6 |
import { FileUploadPanel } from "./components/step1/FileUploadPanel";
|
|
|
|
| 9 |
import { ReportViewer } from "./components/step3/ReportViewer";
|
| 10 |
import { apiGet, apiPost, getToken, clearToken } from "./lib/api";
|
| 11 |
|
| 12 |
+
const INVITE_CODE = "taboola-npo-cz";
|
| 13 |
+
|
| 14 |
interface User {
|
| 15 |
id: number;
|
| 16 |
email: string;
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
export default function App() {
|
| 21 |
+
const [invited, setInvited] = useState(() => localStorage.getItem("invite_code") === INVITE_CODE);
|
| 22 |
const [user, setUser] = useState<User | null>(null);
|
| 23 |
const [loading, setLoading] = useState(true);
|
| 24 |
const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
|
|
|
|
| 135 |
);
|
| 136 |
}
|
| 137 |
|
| 138 |
+
// Invite code gate
|
| 139 |
+
if (!invited) {
|
| 140 |
+
return (
|
| 141 |
+
<InviteGate
|
| 142 |
+
onSuccess={() => {
|
| 143 |
+
localStorage.setItem("invite_code", INVITE_CODE);
|
| 144 |
+
setInvited(true);
|
| 145 |
+
}}
|
| 146 |
+
correctCode={INVITE_CODE}
|
| 147 |
+
/>
|
| 148 |
+
);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
// Login gate
|
| 152 |
if (!user) {
|
| 153 |
return <LoginForm onLogin={setUser} />;
|
chatkit/frontend/src/components/InviteGate.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { CLASSLENS_ICON } from "../lib/icon";
|
| 3 |
+
|
| 4 |
+
interface InviteGateProps {
|
| 5 |
+
onSuccess: () => void;
|
| 6 |
+
correctCode: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export function InviteGate({ onSuccess, correctCode }: InviteGateProps) {
|
| 10 |
+
const [code, setCode] = useState("");
|
| 11 |
+
const [error, setError] = useState("");
|
| 12 |
+
|
| 13 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 14 |
+
e.preventDefault();
|
| 15 |
+
if (code.trim() === correctCode) {
|
| 16 |
+
onSuccess();
|
| 17 |
+
} else {
|
| 18 |
+
setError("邀請碼不正確,請再試一次");
|
| 19 |
+
}
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f1419] via-[#1a2332] to-[#0f1419]">
|
| 24 |
+
<div className="relative">
|
| 25 |
+
<div className="absolute -top-20 -left-20 w-40 h-40 bg-[var(--color-primary)]/20 rounded-full blur-3xl" />
|
| 26 |
+
<div className="absolute -bottom-20 -right-20 w-40 h-40 bg-[var(--color-accent)]/20 rounded-full blur-3xl" />
|
| 27 |
+
|
| 28 |
+
<div className="relative bg-[var(--color-surface)] border border-[var(--color-border)] rounded-2xl p-8 max-w-md w-full shadow-2xl">
|
| 29 |
+
<div className="text-center mb-8">
|
| 30 |
+
<img
|
| 31 |
+
src={CLASSLENS_ICON}
|
| 32 |
+
alt="ClassLens"
|
| 33 |
+
className="w-24 h-24 mx-auto mb-4 rounded-2xl shadow-lg"
|
| 34 |
+
/>
|
| 35 |
+
<h1 className="text-2xl font-bold text-[var(--color-text)] font-display">
|
| 36 |
+
ClassLens
|
| 37 |
+
</h1>
|
| 38 |
+
<p className="text-[var(--color-text-muted)] mt-2">
|
| 39 |
+
請輸入邀請碼以繼續
|
| 40 |
+
</p>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 44 |
+
<div>
|
| 45 |
+
<label className="block text-sm font-medium text-[var(--color-text-muted)] mb-2">
|
| 46 |
+
邀請碼
|
| 47 |
+
</label>
|
| 48 |
+
<input
|
| 49 |
+
type="text"
|
| 50 |
+
value={code}
|
| 51 |
+
onChange={(e) => { setCode(e.target.value); setError(""); }}
|
| 52 |
+
placeholder="請輸入邀請碼..."
|
| 53 |
+
required
|
| 54 |
+
className="input text-center tracking-widest"
|
| 55 |
+
autoFocus
|
| 56 |
+
/>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{error && (
|
| 60 |
+
<p className="text-sm text-red-400 text-center flex items-center justify-center gap-1">
|
| 61 |
+
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 62 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 63 |
+
</svg>
|
| 64 |
+
{error}
|
| 65 |
+
</p>
|
| 66 |
+
)}
|
| 67 |
+
|
| 68 |
+
<button
|
| 69 |
+
type="submit"
|
| 70 |
+
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"
|
| 71 |
+
>
|
| 72 |
+
進入
|
| 73 |
+
</button>
|
| 74 |
+
</form>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
}
|