feat: initial commit — Curio mastery tutor
Full Next.js App Router application with: - AI-graded lesson checkpoints (BKT/Elo mastery tracking) - Auth.js v5: credentials, Google, GitHub, generic OIDC - Anonymous-session-first with migrate-on-signin - Admin panel: users, blueprints, reports, site settings - Password reset + email verification (nodemailer/SMTP) - Site config: require_auth + signups_enabled flags - server-only guards on all DB/generation/verification modules - PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM - 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Report = { id: string; checkpointId: string; gradeId: string | null; reason: string; notes: string | null; createdAt: Date };
|
||||
|
||||
export default function AdminReportsClient({ reports }: { reports: Report[] }) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
const act = async (id: string, action: 'dismiss' | 're-verify') => {
|
||||
setBusy(id);
|
||||
await fetch(`/api/admin/reports/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
});
|
||||
setBusy(null);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={s.heading}>Content reports</h1>
|
||||
{reports.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-ink-muted)' }}>No pending reports.</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
|
||||
{reports.map((r) => (
|
||||
<div key={r.id} style={s.card}>
|
||||
<div style={s.cardMeta}>
|
||||
<span style={s.tag}>{r.reason}</span>
|
||||
<span style={s.date}>{new Date(r.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{r.notes && <p style={s.notes}>{r.notes}</p>}
|
||||
<p style={{ fontSize: '0.8125rem', color: 'var(--color-ink-faint)', fontFamily: 'var(--font-display)' }}>
|
||||
checkpoint {r.checkpointId.slice(0, 8)}…
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-3)' }}>
|
||||
<button disabled={busy === r.id} onClick={() => act(r.id, 'dismiss')} style={s.btn}>Dismiss</button>
|
||||
<button disabled={busy === r.id} onClick={() => act(r.id, 're-verify')} style={s.btn}>Flag for re-verify</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const s = {
|
||||
heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-6)' },
|
||||
card: { padding: 'var(--space-5)', border: '1px solid var(--color-border)', borderRadius: '4px', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' },
|
||||
cardMeta: { display: 'flex', gap: 'var(--space-3)', alignItems: 'center' },
|
||||
tag: { fontFamily: 'var(--font-display)', fontSize: '0.75rem', fontWeight: 600, padding: '2px 8px', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', borderRadius: '3px' },
|
||||
date: { fontSize: '0.8125rem', color: 'var(--color-ink-faint)', fontFamily: 'var(--font-display)' },
|
||||
notes: { fontSize: '0.9375rem', color: 'var(--color-ink-muted)' },
|
||||
btn: { padding: 'var(--space-1) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', fontFamily: 'var(--font-display)', fontSize: '0.875rem', color: 'var(--color-ink)' },
|
||||
} as const;
|
||||
Reference in New Issue
Block a user