From e965cb55efb6cd836cce0f9818731e1c64f9865e Mon Sep 17 00:00:00 2001 From: blaisadmin Date: Sat, 30 May 2026 15:50:23 -0400 Subject: [PATCH] Revert education feature from main --- backend/src/app.ts | 198 ------ backend/src/db/schema.ts | 34 -- .../src/repositories/educationRepository.ts | 153 ----- backend/src/types.ts | 29 - frontend/src/App.tsx | 569 +----------------- frontend/src/index.css | 122 ---- 6 files changed, 2 insertions(+), 1103 deletions(-) delete mode 100644 backend/src/repositories/educationRepository.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 6e11a8d..f01146b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -61,19 +61,6 @@ import { updateVetVisitForBird, } from './repositories/birdRepository.js'; import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js'; -import { - deleteDailyEducation, - deleteEducationQuestion, - createEducationQuestion, - getDailyEducationForDate, - getEducationOptOut, - listDailyEducationForAdmin, - listDailyEducationQuestions, - listEducationQuestionsForAdmin, - updateEducationOptOut, - updateEducationQuestion, - upsertDailyEducation, -} from './repositories/educationRepository.js'; import { buildBirdPhotoObjectKey, getImageExtensionFromContentType, @@ -112,8 +99,6 @@ import type { AuthContext, BillingInterval, BillingPlan, - DailyEducationRow, - EducationQuestionRow, BirdGender, BirdMilestoneReminderCandidateRow, BirdRow, @@ -339,27 +324,6 @@ const integrationTokenCreateSchema = z.object({ expiresInDays: z.coerce.number().int().min(1).max(3650).optional(), }); -const educationQuestionSchema = z - .object({ - prompt: z.string().trim().min(1).max(500), - options: z.array(z.string().trim().min(1).max(240)).min(2).max(4), - correctAnswerIndex: z.coerce.number().int().min(0).max(3), - explanation: z.string().trim().max(800).optional().or(z.literal('')), - }) - .refine((value) => value.correctAnswerIndex < value.options.length, { - message: 'Correct answer must match one of the quiz options.', - path: ['correctAnswerIndex'], - }); - -const dailyEducationSchema = z.object({ - publishDate: dateStringSchema, - fact: z.string().trim().min(1).max(2000), -}); - -const educationPreferenceSchema = z.object({ - educationOptOut: z.boolean(), -}); - const emptyToNull = (value?: string) => { const trimmed = value?.trim() ?? ''; return trimmed ? trimmed : null; @@ -554,25 +518,6 @@ const normalizeWorkspaceMember = (row: WorkspaceMemberRow) => ({ createdAt: row.created_at, }); -const normalizeEducationQuestion = (row: EducationQuestionRow) => ({ - id: row.id, - prompt: row.prompt, - options: row.options, - correctAnswerIndex: Number(row.correct_answer_index), - explanation: row.explanation ?? null, - createdAt: row.created_at, - updatedAt: row.updated_at, -}); - -const normalizeDailyEducation = (row: DailyEducationRow, questions: EducationQuestionRow[] = []) => ({ - id: row.id, - publishDate: row.publish_date, - fact: row.fact, - quizQuestions: questions.map(normalizeEducationQuestion), - createdAt: row.created_at, - updatedAt: row.updated_at, -}); - const signBirdPhotoAccessToken = (row: BirdRow) => { if (!row.photo_object_key) { return ''; @@ -2437,149 +2382,6 @@ app.get('/api/admin/rescue-workspaces', requireAuth, requireSessionAuth, require } }); -app.get('/api/admin/daily-education', requireAuth, requireSessionAuth, requireAdmin, async (_req: Request, res: Response, next: NextFunction) => { - try { - const education = await listDailyEducationForAdmin(); - res.json({ education: education.map((entry) => normalizeDailyEducation(entry)) }); - } catch (error) { - next(error); - } -}); - -app.get('/api/admin/education-questions', requireAuth, requireSessionAuth, requireAdmin, async (_req: Request, res: Response, next: NextFunction) => { - try { - const questions = await listEducationQuestionsForAdmin(); - res.json({ questions: questions.map(normalizeEducationQuestion) }); - } catch (error) { - next(error); - } -}); - -app.put('/api/admin/daily-education', requireAuth, requireSessionAuth, requireAdmin, async (req: Request, res: Response, next: NextFunction) => { - const parsed = dailyEducationSchema.safeParse(req.body); - - if (!parsed.success) { - res.status(400).json({ error: 'Invalid daily education payload', details: parsed.error.flatten() }); - return; - } - - try { - const education = await upsertDailyEducation({ - publishDate: parsed.data.publishDate, - fact: parsed.data.fact, - createdByUserId: req.auth!.user.id, - }); - res.json({ education: normalizeDailyEducation(education) }); - } catch (error) { - next(error); - } -}); - -app.post('/api/admin/education-questions', requireAuth, requireSessionAuth, requireAdmin, async (req: Request, res: Response, next: NextFunction) => { - const parsed = educationQuestionSchema.safeParse(req.body); - - if (!parsed.success) { - res.status(400).json({ error: 'Invalid education question payload', details: parsed.error.flatten() }); - return; - } - - try { - const question = await createEducationQuestion({ - question: { ...parsed.data, explanation: emptyToNull(parsed.data.explanation) }, - createdByUserId: req.auth!.user.id, - }); - res.status(201).json({ question: normalizeEducationQuestion(question) }); - } catch (error) { - next(error); - } -}); - -app.put('/api/admin/education-questions/:questionId', requireAuth, requireSessionAuth, requireAdmin, async (req: Request, res: Response, next: NextFunction) => { - const parsed = educationQuestionSchema.safeParse(req.body); - - if (!parsed.success) { - res.status(400).json({ error: 'Invalid education question payload', details: parsed.error.flatten() }); - return; - } - - try { - const question = await updateEducationQuestion(req.params.questionId, { - ...parsed.data, - explanation: emptyToNull(parsed.data.explanation), - }); - - if (!question) { - res.status(404).json({ error: 'Education question not found.' }); - return; - } - - res.json({ question: normalizeEducationQuestion(question) }); - } catch (error) { - next(error); - } -}); - -app.delete('/api/admin/education-questions/:questionId', requireAuth, requireSessionAuth, requireAdmin, async (req: Request, res: Response, next: NextFunction) => { - try { - const deleted = await deleteEducationQuestion(req.params.questionId); - - if (!deleted) { - res.status(404).json({ error: 'Education question not found.' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } -}); - -app.delete('/api/admin/daily-education/:educationId', requireAuth, requireSessionAuth, requireAdmin, async (req: Request, res: Response, next: NextFunction) => { - try { - const deleted = await deleteDailyEducation(req.params.educationId); - - if (!deleted) { - res.status(404).json({ error: 'Daily education item not found.' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } -}); - -app.get('/api/education/today', requireAuth, requireSessionAuth, async (req: Request, res: Response, next: NextFunction) => { - try { - const educationOptOut = await getEducationOptOut(req.auth!.user.id); - const education = educationOptOut ? null : await getDailyEducationForDate(); - const questions = education ? await listDailyEducationQuestions(education.publish_date) : []; - - res.json({ - educationOptOut, - education: education ? normalizeDailyEducation(education, questions) : null, - }); - } catch (error) { - next(error); - } -}); - -app.patch('/api/education/preferences', requireAuth, requireSessionAuth, async (req: Request, res: Response, next: NextFunction) => { - const parsed = educationPreferenceSchema.safeParse(req.body); - - if (!parsed.success) { - res.status(400).json({ error: 'Invalid education preference payload', details: parsed.error.flatten() }); - return; - } - - try { - const educationOptOut = await updateEducationOptOut(req.auth!.user.id, parsed.data.educationOptOut); - res.json({ educationOptOut }); - } catch (error) { - next(error); - } -}); - app.patch('/api/admin/rescue-workspaces/:workspaceId', requireAuth, requireAdmin, requireWriteAccess, async (req: Request, res: Response, next: NextFunction) => { const parsed = z.object({ rescueVerificationStatus: rescueVerificationStatusSchema }).safeParse(req.body); diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index beb31c0..875fd1d 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -30,9 +30,6 @@ export const ensureSchema = async (database: DatabaseClient = db) => { ALTER TABLE workspaces DROP CONSTRAINT IF EXISTS workspaces_id_check; - ALTER TABLE users - ADD COLUMN IF NOT EXISTS education_opt_out BOOLEAN NOT NULL DEFAULT FALSE; - ALTER TABLE workspaces ADD COLUMN IF NOT EXISTS billing_email VARCHAR(255), ADD COLUMN IF NOT EXISTS billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic', @@ -142,37 +139,6 @@ export const ensureSchema = async (database: DatabaseClient = db) => { CREATE INDEX IF NOT EXISTS idx_auth_sessions_created_user ON auth_sessions (created_at DESC, user_id); - CREATE TABLE IF NOT EXISTS daily_education ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - publish_date DATE NOT NULL UNIQUE, - fact TEXT NOT NULL, - quiz_questions JSONB NOT NULL DEFAULT '[]'::jsonb, - created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - ALTER TABLE daily_education - ALTER COLUMN quiz_questions SET DEFAULT '[]'::jsonb; - - CREATE INDEX IF NOT EXISTS idx_daily_education_publish_date - ON daily_education (publish_date DESC); - - CREATE TABLE IF NOT EXISTS education_question_bank ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - prompt VARCHAR(500) NOT NULL, - options JSONB NOT NULL, - correct_answer_index INTEGER NOT NULL, - explanation VARCHAR(800), - created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - CHECK (correct_answer_index >= 0 AND correct_answer_index <= 3) - ); - - CREATE INDEX IF NOT EXISTS idx_education_question_bank_created - ON education_question_bank (created_at DESC); - CREATE TABLE IF NOT EXISTS integration_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/backend/src/repositories/educationRepository.ts b/backend/src/repositories/educationRepository.ts deleted file mode 100644 index 817a07e..0000000 --- a/backend/src/repositories/educationRepository.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { db } from '../db/client.js'; -import type { DailyEducationQuestion, DailyEducationRow, EducationQuestionRow } from '../types.js'; - -export const getEducationOptOut = async (userId: string) => { - const result = await db.query<{ education_opt_out: boolean }>( - `SELECT education_opt_out - FROM users - WHERE id = $1`, - [userId], - ); - - return result.rows[0]?.education_opt_out ?? false; -}; - -export const updateEducationOptOut = async (userId: string, educationOptOut: boolean) => { - const result = await db.query<{ education_opt_out: boolean }>( - `UPDATE users - SET education_opt_out = $2 - WHERE id = $1 - RETURNING education_opt_out`, - [userId, educationOptOut], - ); - - return result.rows[0]?.education_opt_out ?? educationOptOut; -}; - -export const getDailyEducationForDate = async (publishDate?: string) => { - const result = publishDate - ? await db.query( - `SELECT id, publish_date::text, fact, quiz_questions, created_by_user_id, created_at, updated_at - FROM daily_education - WHERE publish_date = $1`, - [publishDate], - ) - : await db.query( - `SELECT id, publish_date::text, fact, quiz_questions, created_by_user_id, created_at, updated_at - FROM daily_education - WHERE publish_date = CURRENT_DATE`, - ); - - return result.rows[0] ?? null; -}; - -export const listDailyEducationForAdmin = async () => { - const result = await db.query( - `SELECT id, publish_date::text, fact, quiz_questions, created_by_user_id, created_at, updated_at - FROM daily_education - ORDER BY publish_date DESC - LIMIT 120`, - ); - - return result.rows; -}; - -export const upsertDailyEducation = async ({ - publishDate, - fact, - createdByUserId, -}: { - publishDate: string; - fact: string; - createdByUserId: string; -}) => { - const result = await db.query( - `INSERT INTO daily_education (publish_date, fact, created_by_user_id) - VALUES ($1, $2, $3) - ON CONFLICT (publish_date) DO UPDATE - SET fact = EXCLUDED.fact, - updated_at = CURRENT_TIMESTAMP - RETURNING id, publish_date::text, fact, quiz_questions, created_by_user_id, created_at, updated_at`, - [publishDate, fact, createdByUserId], - ); - - return result.rows[0]; -}; - -export const listEducationQuestionsForAdmin = async () => { - const result = await db.query( - `SELECT id, prompt, options, correct_answer_index, explanation, created_by_user_id, created_at, updated_at - FROM education_question_bank - ORDER BY updated_at DESC, created_at DESC - LIMIT 400`, - ); - - return result.rows; -}; - -export const listDailyEducationQuestions = async (seedDate?: string) => { - const result = await db.query( - `SELECT id, prompt, options, correct_answer_index, explanation, created_by_user_id, created_at, updated_at - FROM education_question_bank - ORDER BY md5(COALESCE($1::text, CURRENT_DATE::text) || id::text) - LIMIT 4`, - [seedDate ?? null], - ); - - return result.rows; -}; - -export const createEducationQuestion = async ({ - question, - createdByUserId, -}: { - question: DailyEducationQuestion; - createdByUserId: string; -}) => { - const result = await db.query( - `INSERT INTO education_question_bank (prompt, options, correct_answer_index, explanation, created_by_user_id) - VALUES ($1, $2::jsonb, $3, $4, $5) - RETURNING id, prompt, options, correct_answer_index, explanation, created_by_user_id, created_at, updated_at`, - [question.prompt, JSON.stringify(question.options), question.correctAnswerIndex, question.explanation, createdByUserId], - ); - - return result.rows[0]; -}; - -export const updateEducationQuestion = async (questionId: string, question: DailyEducationQuestion) => { - const result = await db.query( - `UPDATE education_question_bank - SET prompt = $2, - options = $3::jsonb, - correct_answer_index = $4, - explanation = $5, - updated_at = CURRENT_TIMESTAMP - WHERE id = $1 - RETURNING id, prompt, options, correct_answer_index, explanation, created_by_user_id, created_at, updated_at`, - [questionId, question.prompt, JSON.stringify(question.options), question.correctAnswerIndex, question.explanation], - ); - - return result.rows[0] ?? null; -}; - -export const deleteEducationQuestion = async (questionId: string) => { - const result = await db.query<{ id: string }>( - `DELETE FROM education_question_bank - WHERE id = $1 - RETURNING id`, - [questionId], - ); - - return Boolean(result.rowCount); -}; - -export const deleteDailyEducation = async (educationId: string) => { - const result = await db.query<{ id: string }>( - `DELETE FROM daily_education - WHERE id = $1 - RETURNING id`, - [educationId], - ); - - return Boolean(result.rowCount); -}; diff --git a/backend/src/types.ts b/backend/src/types.ts index 97c2e22..93f43cd 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -13,38 +13,9 @@ export type UserRow = { email: string; password_hash: string | null; name: string; - education_opt_out?: boolean; created_at: string; }; -export type DailyEducationQuestion = { - prompt: string; - options: string[]; - correctAnswerIndex: number; - explanation: string | null; -}; - -export type DailyEducationRow = { - id: string; - publish_date: string; - fact: string; - quiz_questions: DailyEducationQuestion[]; - created_by_user_id: string | null; - created_at: string; - updated_at: string; -}; - -export type EducationQuestionRow = { - id: string; - prompt: string; - options: string[]; - correct_answer_index: number; - explanation: string | null; - created_by_user_id: string | null; - created_at: string; - updated_at: string; -}; - export type WorkspaceRow = { id: number; name: string; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 23d5ce5..1478dfd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -162,23 +162,6 @@ type AdminRescueWorkspace = { memberCount: number; }; -type DailyEducationQuestion = { - id: string; - prompt: string; - options: string[]; - correctAnswerIndex: number; - explanation: string | null; -}; - -type DailyEducation = { - id: string; - publishDate: string; - fact: string; - quizQuestions: DailyEducationQuestion[]; - createdAt: string; - updatedAt: string; -}; - type IntegrationTokenSummary = { id: string; userId: string; @@ -311,18 +294,6 @@ type BillingNotice = { message: string; }; -type DailyEducationQuestionFormState = { - prompt: string; - options: [string, string, string, string]; - correctAnswerIndex: number; - explanation: string; -}; - -type DailyEducationFormState = { - publishDate: string; - fact: string; -}; - type BulkWeightRowState = { weightGrams: string; }; @@ -690,18 +661,6 @@ const emptyIntegrationTokenForm: IntegrationTokenFormState = { expiresInDays: '', }; -const emptyDailyEducationQuestion = (): DailyEducationQuestionFormState => ({ - prompt: '', - options: ['', '', '', ''], - correctAnswerIndex: 0, - explanation: '', -}); - -const emptyDailyEducationForm = (): DailyEducationFormState => ({ - publishDate: new Date().toISOString().slice(0, 10), - fact: '', -}); - const defaultAuthProviders: AuthProvider[] = [ { providerKey: 'google', displayName: 'Google', enabled: false }, { providerKey: 'microsoft', displayName: 'Microsoft', enabled: false }, @@ -1471,20 +1430,6 @@ function App() { const [integrationTokens, setIntegrationTokens] = useState([]); const [adminSummary, setAdminSummary] = useState(null); const [adminRescueWorkspaces, setAdminRescueWorkspaces] = useState([]); - const [adminDailyEducation, setAdminDailyEducation] = useState([]); - const [adminEducationQuestions, setAdminEducationQuestions] = useState([]); - const [dailyEducationForm, setDailyEducationForm] = useState(emptyDailyEducationForm); - const [educationQuestionForm, setEducationQuestionForm] = useState(emptyDailyEducationQuestion); - const [editingEducationQuestionId, setEditingEducationQuestionId] = useState(''); - const [savingDailyEducation, setSavingDailyEducation] = useState(false); - const [savingEducationQuestion, setSavingEducationQuestion] = useState(false); - const [deletingDailyEducationId, setDeletingDailyEducationId] = useState(''); - const [deletingEducationQuestionId, setDeletingEducationQuestionId] = useState(''); - const [todayEducation, setTodayEducation] = useState(null); - const [educationOptOut, setEducationOptOut] = useState(false); - const [savingEducationPreference, setSavingEducationPreference] = useState(false); - const [educationAnswers, setEducationAnswers] = useState>({}); - const [dailyEducationOpen, setDailyEducationOpen] = useState(false); const [birds, setBirds] = useState([]); const [memorializedBirds, setMemorializedBirds] = useState([]); const [selectedBirdId, setSelectedBirdId] = useState(''); @@ -2027,15 +1972,6 @@ function App() { setIntegrationTokens([]); setAdminSummary(null); setAdminRescueWorkspaces([]); - setAdminDailyEducation([]); - setAdminEducationQuestions([]); - setDailyEducationForm(emptyDailyEducationForm()); - setEducationQuestionForm(emptyDailyEducationQuestion()); - setEditingEducationQuestionId(''); - setTodayEducation(null); - setEducationOptOut(false); - setEducationAnswers({}); - setDailyEducationOpen(false); setBirds([]); setMemorializedBirds([]); setWeights([]); @@ -2274,27 +2210,20 @@ function App() { const loadAdminDashboard = async () => { try { - const [summaryResponse, rescuesResponse, educationResponse, educationQuestionsResponse] = await Promise.all([ + const [summaryResponse, rescuesResponse] = await Promise.all([ apiFetch('/admin/summary', authToken), apiFetch('/admin/rescue-workspaces', authToken), - apiFetch('/admin/daily-education', authToken), - apiFetch('/admin/education-questions', authToken), ]); - if (!summaryResponse.ok || !rescuesResponse.ok || !educationResponse.ok || !educationQuestionsResponse.ok) { + if (!summaryResponse.ok || !rescuesResponse.ok) { throw new Error('Unable to load admin dashboard.'); } const summaryData = (await readJsonSafely<{ summary?: AdminSummary }>(summaryResponse)) ?? {}; const rescuesData = (await readJsonSafely<{ rescueWorkspaces?: AdminRescueWorkspace[] }>(rescuesResponse)) ?? {}; - const educationData = (await readJsonSafely<{ education?: DailyEducation[] }>(educationResponse)) ?? {}; - const educationQuestionsData = - (await readJsonSafely<{ questions?: DailyEducationQuestion[] }>(educationQuestionsResponse)) ?? {}; setAdminSummary(summaryData.summary ?? null); setAdminRescueWorkspaces(rescuesData.rescueWorkspaces ?? []); - setAdminDailyEducation(educationData.education ?? []); - setAdminEducationQuestions(educationQuestionsData.questions ?? []); } catch (adminError) { setError(adminError instanceof Error ? adminError.message : 'Unable to load admin dashboard.'); } @@ -2303,34 +2232,6 @@ function App() { void loadAdminDashboard(); }, [activePage, authSession?.isAdmin, authToken]); - useEffect(() => { - if (!authToken || !authSession) { - return; - } - - const loadTodayEducation = async () => { - try { - const response = await apiFetch('/education/today', authToken); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to load daily education.')); - } - - const data = - (await readJsonSafely<{ education?: DailyEducation | null; educationOptOut?: boolean }>(response)) ?? {}; - - setEducationOptOut(Boolean(data.educationOptOut)); - setTodayEducation(data.education ?? null); - setEducationAnswers({}); - setDailyEducationOpen(false); - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to load daily education.'); - } - }; - - void loadTodayEducation(); - }, [authSession, authToken]); - useEffect(() => { if (!selectedBird?.id) { setWeights([]); @@ -2749,221 +2650,6 @@ function App() { } }; - const loadDailyEducationIntoForm = (education: DailyEducation) => { - setDailyEducationForm({ - publishDate: education.publishDate, - fact: education.fact, - }); - }; - - const loadEducationQuestionIntoForm = (question: DailyEducationQuestion) => { - setEditingEducationQuestionId(question.id); - setEducationQuestionForm({ - prompt: question.prompt, - options: [ - question.options[0] ?? '', - question.options[1] ?? '', - question.options[2] ?? '', - question.options[3] ?? '', - ], - correctAnswerIndex: question.correctAnswerIndex, - explanation: question.explanation ?? '', - }); - }; - - const resetEducationQuestionForm = () => { - setEditingEducationQuestionId(''); - setEducationQuestionForm(emptyDailyEducationQuestion()); - }; - - const handleDailyEducationSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!authToken) { - return; - } - - setError(''); - setSavingDailyEducation(true); - - try { - const response = await apiFetch('/admin/daily-education', authToken, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - publishDate: dailyEducationForm.publishDate, - fact: dailyEducationForm.fact.trim(), - }), - }); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to save daily education.')); - } - - const data = (await readJsonSafely<{ education?: DailyEducation }>(response)) ?? {}; - - if (!data.education) { - throw new Error('Unable to save daily education.'); - } - - setAdminDailyEducation((current) => - [data.education!, ...current.filter((education) => education.id !== data.education!.id && education.publishDate !== data.education!.publishDate)] - .sort((left, right) => right.publishDate.localeCompare(left.publishDate)), - ); - if (data.education.publishDate === new Date().toISOString().slice(0, 10) && !educationOptOut) { - const todayResponse = await apiFetch('/education/today', authToken); - const todayData = todayResponse.ok - ? await readJsonSafely<{ education?: DailyEducation | null }>(todayResponse) - : null; - setTodayEducation(todayData?.education ?? null); - setEducationAnswers({}); - } - setDailyEducationForm(emptyDailyEducationForm()); - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to save daily education.'); - } finally { - setSavingDailyEducation(false); - } - }; - - const handleEducationQuestionSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!authToken) { - return; - } - - setError(''); - setSavingEducationQuestion(true); - - try { - const response = await apiFetch( - editingEducationQuestionId ? `/admin/education-questions/${editingEducationQuestionId}` : '/admin/education-questions', - authToken, - { - method: editingEducationQuestionId ? 'PUT' : 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - prompt: educationQuestionForm.prompt.trim(), - options: educationQuestionForm.options.map((option) => option.trim()), - correctAnswerIndex: educationQuestionForm.correctAnswerIndex, - explanation: educationQuestionForm.explanation.trim(), - }), - }, - ); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to save education question.')); - } - - const data = (await readJsonSafely<{ question?: DailyEducationQuestion }>(response)) ?? {}; - - if (!data.question) { - throw new Error('Unable to save education question.'); - } - - setAdminEducationQuestions((current) => [ - data.question!, - ...current.filter((question) => question.id !== data.question!.id), - ]); - resetEducationQuestionForm(); - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to save education question.'); - } finally { - setSavingEducationQuestion(false); - } - }; - - const handleDeleteEducationQuestion = async (questionId: string) => { - if (!authToken) { - return; - } - - setError(''); - setDeletingEducationQuestionId(questionId); - - try { - const response = await apiFetch(`/admin/education-questions/${questionId}`, authToken, { method: 'DELETE' }); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to delete education question.')); - } - - setAdminEducationQuestions((current) => current.filter((question) => question.id !== questionId)); - if (editingEducationQuestionId === questionId) { - resetEducationQuestionForm(); - } - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to delete education question.'); - } finally { - setDeletingEducationQuestionId(''); - } - }; - - const handleDeleteDailyEducation = async (educationId: string) => { - if (!authToken) { - return; - } - - setError(''); - setDeletingDailyEducationId(educationId); - - try { - const response = await apiFetch(`/admin/daily-education/${educationId}`, authToken, { method: 'DELETE' }); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to delete daily education.')); - } - - setAdminDailyEducation((current) => current.filter((education) => education.id !== educationId)); - if (todayEducation?.id === educationId) { - setTodayEducation(null); - } - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to delete daily education.'); - } finally { - setDeletingDailyEducationId(''); - } - }; - - const handleEducationPreferenceChange = async (nextEducationOptOut: boolean) => { - if (!authToken) { - return; - } - - setError(''); - setSavingEducationPreference(true); - - try { - const response = await apiFetch('/education/preferences', authToken, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ educationOptOut: nextEducationOptOut }), - }); - - if (!response.ok) { - throw new Error(await readErrorMessage(response, 'Unable to save education preference.')); - } - - setEducationOptOut(nextEducationOptOut); - if (nextEducationOptOut) { - setTodayEducation(null); - setDailyEducationOpen(false); - } else { - const todayResponse = await apiFetch('/education/today', authToken); - const data = todayResponse.ok - ? await readJsonSafely<{ education?: DailyEducation | null }>(todayResponse) - : null; - setTodayEducation(data?.education ?? null); - } - setEducationAnswers({}); - } catch (educationError) { - setError(educationError instanceof Error ? educationError.message : 'Unable to save education preference.'); - } finally { - setSavingEducationPreference(false); - } - }; - const handleCreateWorkspace = async (event: React.FormEvent) => { event.preventDefault(); @@ -4826,74 +4512,6 @@ function App() { - {todayEducation ? ( -
-
-
-

Fid fact of the day

-

{dailyEducationOpen ? formatDate(todayEducation.publishDate) : 'Daily learning'}

-
- -
- {dailyEducationOpen ? ( - <> -

{todayEducation.fact}

-
- {todayEducation.quizQuestions.map((question, questionIndex) => { - const selectedAnswer = educationAnswers[questionIndex]; - const hasAnswer = selectedAnswer !== undefined; - - return ( -
- {question.prompt} -
- {question.options.map((option, optionIndex) => ( - - ))} -
- {hasAnswer ? ( -

- {selectedAnswer === question.correctAnswerIndex ? 'Correct.' : 'Correct answer shown.'}{' '} - {question.explanation} -

- ) : null} -
- ); - })} -
- - ) : ( -

- Open today's fact and {todayEducation.quizQuestions.length || 'the'} quiz questions. -

- )} -
- ) : null} -
@@ -5044,168 +4662,6 @@ function App() { )}
- -
-
-
-

Education

-

Fid facts

-
- -
-
-
- -