// QuotaApplyModal — opens from QueryPage's "申請更多額度" button. On open it // checks /quota-requests/me?status=pending; if a row exists the modal renders // a "you already have a pending request" state with no textarea. Otherwise // renders a textarea and POSTs to /quota-requests on submit. const QUOTA_APPLY_MIN = 10; const QUOTA_APPLY_MAX = 1000; const QuotaApplyModal = ({ open, lang, onClose }) => { const t = lang === 'zh'; const [reason, setReason] = React.useState(''); const [submitting, setSubmitting] = React.useState(false); const [error, setError] = React.useState(null); const [pendingRow, setPendingRow] = React.useState(null); const [loadingPending, setLoadingPending] = React.useState(false); const [success, setSuccess] = React.useState(false); // Reset state on each open React.useEffect(() => { if (!open) return; setReason(''); setError(null); setSuccess(false); setPendingRow(null); setLoadingPending(true); let cancelled = false; (async () => { try { const res = await apiFetch('/quota-requests/me?status=pending'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const rows = await res.json(); if (!cancelled) setPendingRow(rows.length > 0 ? rows[0] : null); } catch (err) { if (!cancelled) setError(err.message || String(err)); } finally { if (!cancelled) setLoadingPending(false); } })(); return () => { cancelled = true; }; }, [open]); if (!open) return null; const handleSubmit = async () => { if (submitting) return; const trimmed = reason.trim(); if (trimmed.length < QUOTA_APPLY_MIN) { setError(t ? `理由至少 ${QUOTA_APPLY_MIN} 字元` : `Reason must be at least ${QUOTA_APPLY_MIN} characters`); return; } setSubmitting(true); setError(null); try { const res = await apiFetch('/quota-requests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: trimmed }), }); if (res.status === 201) { setSuccess(true); setTimeout(() => { onClose && onClose(); }, 2000); return; } const body = await res.json().catch(() => null); const code = body?.detail?.error_code || body?.error_code; if (res.status === 409 && code === 'quota_request_pending') { // Re-fetch the pending row to render blocked state const me = await apiFetch('/quota-requests/me?status=pending'); if (me.ok) { const rows = await me.json(); setPendingRow(rows[0] || null); } return; } const detailText = body?.detail?.detail || body?.detail || `HTTP ${res.status}`; setError(typeof detailText === 'string' ? detailText : JSON.stringify(detailText)); } catch (err) { setError(err.message || String(err)); } finally { setSubmitting(false); } }; const formatDate = (iso) => { if (!iso) return ''; try { return new Date(iso).toLocaleString(); } catch { return iso; } }; return (
{t ? '載入中…' : 'Loading…'}
)} {!loadingPending && pendingRow && ({t ? `您已有一筆審核中的申請(送出於 ${formatDate(pendingRow.requested_at)})。` : `You already have a pending request (submitted ${formatDate(pendingRow.requested_at)}).`}
{t ? '請耐心等候管理員處理。' : 'Please wait while an admin reviews it.'}
{t ? '已送出,admin 收到通知後會處理' : 'Submitted — admin will be notified.'}
)} {!loadingPending && !pendingRow && !success && ( <>{t ? '請告訴我們你的用途' : "Tell us how you'll use it"}