Spaces:
Runtime error
Runtime error
File size: 9,005 Bytes
054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a 5ee5085 054d73a | 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 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | import { useState } from "react";
import { GOOGLE_AUTH_URL, AUTH_STATUS_URL } from "../lib/config";
type AuthMode = 'google' | 'csv-only';
interface AuthStatusProps {
teacherEmail: string;
setTeacherEmail: (email: string) => void;
isAuthenticated: boolean;
setIsAuthenticated: (auth: boolean) => void;
}
export function AuthStatus({
teacherEmail,
setTeacherEmail,
isAuthenticated,
setIsAuthenticated,
}: AuthStatusProps) {
const [emailInput, setEmailInput] = useState("");
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const [authMode, setAuthMode] = useState<AuthMode | null>(null);
const checkAuthStatus = async (email: string) => {
try {
setIsChecking(true);
const response = await fetch(`${AUTH_STATUS_URL}?teacher_email=${encodeURIComponent(email)}`);
const data = await response.json();
if (data.authenticated) {
setTeacherEmail(email);
setIsAuthenticated(true);
}
return data.authenticated;
} catch (err) {
console.error("Error checking auth status:", err);
return false;
} finally {
setIsChecking(false);
}
};
const handleConnect = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!emailInput.trim()) {
setError("請輸入您的電子郵件地址");
return;
}
// Check if already authenticated
const alreadyAuth = await checkAuthStatus(emailInput);
if (alreadyAuth) {
return;
}
// Try to start OAuth - catch errors if not configured
try {
const response = await fetch(`${GOOGLE_AUTH_URL}?teacher_email=${encodeURIComponent(emailInput)}`, {
method: 'GET',
redirect: 'manual'
});
// If we get a redirect, OAuth is configured - follow it
if (response.type === 'opaqueredirect' || response.status === 302) {
window.location.href = `${GOOGLE_AUTH_URL}?teacher_email=${encodeURIComponent(emailInput)}`;
} else if (response.status === 500) {
const data = await response.json();
if (data.detail?.includes('not configured')) {
setAuthMode('csv-only');
setError("Google OAuth 未設定。您仍可在聊天中直接上傳 CSV 檔案使用應用程式!");
// Set as "connected" with just email for CSV fallback
setTeacherEmail(emailInput);
setIsAuthenticated(true);
} else {
setAuthMode('google');
setError(data.detail || "Failed to connect");
}
} else {
// Redirect to OAuth
window.location.href = `${GOOGLE_AUTH_URL}?teacher_email=${encodeURIComponent(emailInput)}`;
}
} catch (err) {
// Network error or CORS - just redirect
window.location.href = `${GOOGLE_AUTH_URL}?teacher_email=${encodeURIComponent(emailInput)}`;
}
};
const handleDisconnect = async () => {
try {
await fetch(`/auth/disconnect?teacher_email=${encodeURIComponent(teacherEmail)}`, {
method: 'POST'
});
setIsAuthenticated(false);
setTeacherEmail("");
} catch (err) {
console.error("Error disconnecting:", err);
}
};
return (
<section id="connect" className="py-20 px-6">
<div className="max-w-xl mx-auto">
<div className="card p-8">
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-light)] flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h2 className="font-display text-2xl font-bold text-[var(--color-text)] mb-2">
{isAuthenticated ? "已連接!" : "連接您的 Google 帳號"}
</h2>
<p className="text-[var(--color-text-muted)]">
{isAuthenticated
? "您的 Google 帳號已連結。現在可以分析考試答案了。"
: "我們只會存取您的 Google 表單回應試算表。不會在我們的伺服器上儲存任何資料。"}
</p>
</div>
{isAuthenticated ? (
<div className="space-y-4">
<div className="flex items-center justify-center gap-3 p-4 rounded-xl bg-[var(--color-success)]/10 border border-[var(--color-success)]/20">
<div className="w-10 h-10 rounded-full bg-[var(--color-success)]/20 flex items-center justify-center">
<svg className="w-5 h-5 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="text-left">
<div className="font-medium text-[var(--color-text)]">{teacherEmail}</div>
<div className="text-sm text-[var(--color-text-muted)]">
{authMode === 'csv-only' ? 'CSV 上傳模式(Google OAuth 未設定)' : 'Google 帳號已連接'}
</div>
</div>
</div>
{authMode === 'csv-only' && (
<div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-sm">
<strong>注意:</strong>您可以在聊天中直接貼上 CSV 資料,或設定 Google OAuth 從 Google 表單取得資料。
</div>
)}
<button
onClick={handleDisconnect}
className="w-full btn btn-outline text-red-500 border-red-200 hover:border-red-500 hover:text-red-600"
>
斷開帳號
</button>
</div>
) : (
<form onSubmit={handleConnect} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-[var(--color-text)] mb-2">
您的學校電子郵件
</label>
<input
type="email"
id="email"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
placeholder="teacher@school.edu"
className="input"
required
/>
</div>
{error && (
<div className="p-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
{error}
</div>
)}
<button
type="submit"
className="w-full btn btn-primary"
disabled={isChecking}
>
{isChecking ? (
<>
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<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" />
</svg>
檢查中...
</>
) : (
<>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
使用 Google 繼續
</>
)}
</button>
<p className="text-center text-xs text-[var(--color-text-muted)]">
連接即表示您同意我們的{" "}
<a href="#" className="underline hover:text-[var(--color-primary)]">服務條款</a>
{" "}和{" "}
<a href="#" className="underline hover:text-[var(--color-primary)]">隱私政策</a>
</p>
</form>
)}
</div>
</div>
</section>
);
}
|