diff --git a/app/api/political-statements/route.ts b/app/api/political-statements/route.ts new file mode 100644 index 0000000..d1e2010 --- /dev/null +++ b/app/api/political-statements/route.ts @@ -0,0 +1,113 @@ +import { NextResponse } from 'next/server'; +import { requireAdminAuth } from '@/lib/auth'; +import { + getRandomActiveStatement, + getAllStatements, + createStatement, + updateStatement, + deleteStatement, +} from '@/lib/politicalStatements'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const locale = searchParams.get('locale') || 'en'; + const admin = searchParams.get('admin') === 'true'; + + if (admin) { + const authError = await requireAdminAuth(request as any); + if (authError) { + return authError; + } + const statements = await getAllStatements(locale); + return NextResponse.json(statements); + } + + const statement = await getRandomActiveStatement(locale); + return NextResponse.json(statement); + } catch (error) { + console.error('[political-statements] GET failed:', error); + return NextResponse.json({ error: 'Failed to load political statements' }, { status: 500 }); + } +} + +export async function POST(request: Request) { + const authError = await requireAdminAuth(request as any); + if (authError) { + return authError; + } + + try { + const body = await request.json(); + const { locale, text, active = true, source } = body; + + if (!locale || typeof text !== 'string' || !text.trim()) { + return NextResponse.json({ error: 'locale and text are required' }, { status: 400 }); + } + + const created = await createStatement(locale, { text: text.trim(), active, source }); + return NextResponse.json(created, { status: 201 }); + } catch (error) { + console.error('[political-statements] POST failed:', error); + return NextResponse.json({ error: 'Failed to create statement' }, { status: 500 }); + } +} + +export async function PUT(request: Request) { + const authError = await requireAdminAuth(request as any); + if (authError) { + return authError; + } + + try { + const body = await request.json(); + const { locale, id, text, active, source } = body; + + if (!locale || typeof id !== 'number') { + return NextResponse.json({ error: 'locale and numeric id are required' }, { status: 400 }); + } + + const updated = await updateStatement(locale, id, { + text: typeof text === 'string' ? text.trim() : undefined, + active, + source, + }); + + if (!updated) { + return NextResponse.json({ error: 'Statement not found' }, { status: 404 }); + } + + return NextResponse.json(updated); + } catch (error) { + console.error('[political-statements] PUT failed:', error); + return NextResponse.json({ error: 'Failed to update statement' }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + const authError = await requireAdminAuth(request as any); + if (authError) { + return authError; + } + + try { + const body = await request.json(); + const { locale, id } = body; + + if (!locale || typeof id !== 'number') { + return NextResponse.json({ error: 'locale and numeric id are required' }, { status: 400 }); + } + + const ok = await deleteStatement(locale, id); + if (!ok) { + return NextResponse.json({ error: 'Statement not found' }, { status: 404 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('[political-statements] DELETE failed:', error); + return NextResponse.json({ error: 'Failed to delete statement' }, { status: 500 }); + } +} + + diff --git a/components/PoliticalStatementBanner.tsx b/components/PoliticalStatementBanner.tsx new file mode 100644 index 0000000..174c3d9 --- /dev/null +++ b/components/PoliticalStatementBanner.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useLocale } from 'next-intl'; + +interface ApiStatement { + id: number; + text: string; + active?: boolean; +} + +export default function PoliticalStatementBanner() { + const locale = useLocale(); + const [statement, setStatement] = useState(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const today = new Date().toISOString().split('T')[0]; + const storageKey = `hoerdle_political_statement_shown_${today}_${locale}`; + + try { + const alreadyShown = typeof window !== 'undefined' && window.localStorage.getItem(storageKey); + if (alreadyShown) { + return; + } + } catch { + // ignore localStorage errors + } + + let timeoutId: number | undefined; + + const fetchStatement = async () => { + try { + const res = await fetch(`/api/political-statements?locale=${encodeURIComponent(locale)}`, { + cache: 'no-store', + }); + if (!res.ok) return; + const data = await res.json(); + if (!data || !data.text) return; + setStatement(data); + setVisible(true); + + timeoutId = window.setTimeout(() => { + setVisible(false); + try { + window.localStorage.setItem(storageKey, 'true'); + } catch { + // ignore + } + }, 5000); + } catch (e) { + console.warn('[PoliticalStatementBanner] Failed to load statement', e); + } + }; + + fetchStatement(); + + return () => { + if (timeoutId) { + window.clearTimeout(timeoutId); + } + }; + }, [locale]); + + if (!visible || !statement) return null; + + return ( +
+ + {statement.text} +
+ ); +} + + diff --git a/lib/politicalStatements.ts b/lib/politicalStatements.ts new file mode 100644 index 0000000..29e99b9 --- /dev/null +++ b/lib/politicalStatements.ts @@ -0,0 +1,99 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +export type PoliticalStatement = { + id: number; + text: string; + active?: boolean; + source?: string; +}; + +function getFilePath(locale: string): string { + const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en'; + return path.join(process.cwd(), 'data', `political-statements.${safeLocale}.json`); +} + +async function readStatementsFile(locale: string): Promise { + const filePath = getFilePath(locale); + try { + const raw = await fs.readFile(filePath, 'utf-8'); + const data = JSON.parse(raw); + if (Array.isArray(data)) { + return data; + } + return []; + } catch (err: any) { + if (err.code === 'ENOENT') { + // File does not exist yet + return []; + } + console.error('[politicalStatements] Failed to read file', filePath, err); + return []; + } +} + +async function writeStatementsFile(locale: string, statements: PoliticalStatement[]): Promise { + const filePath = getFilePath(locale); + const dir = path.dirname(filePath); + try { + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(statements, null, 2), 'utf-8'); + } catch (err) { + console.error('[politicalStatements] Failed to write file', filePath, err); + throw err; + } +} + +export async function getRandomActiveStatement(locale: string): Promise { + const statements = await readStatementsFile(locale); + const active = statements.filter((s) => s.active !== false); + if (active.length === 0) { + return null; + } + const index = Math.floor(Math.random() * active.length); + return active[index] ?? null; +} + +export async function getAllStatements(locale: string): Promise { + return readStatementsFile(locale); +} + +export async function createStatement(locale: string, input: Omit): Promise { + const statements = await readStatementsFile(locale); + const nextId = statements.length > 0 ? Math.max(...statements.map((s) => s.id)) + 1 : 1; + const newStatement: PoliticalStatement = { + id: nextId, + active: true, + ...input, + }; + statements.push(newStatement); + await writeStatementsFile(locale, statements); + return newStatement; +} + +export async function updateStatement(locale: string, id: number, input: Partial>): Promise { + const statements = await readStatementsFile(locale); + const index = statements.findIndex((s) => s.id === id); + if (index === -1) return null; + + const updated: PoliticalStatement = { + ...statements[index], + ...input, + id, + }; + statements[index] = updated; + await writeStatementsFile(locale, statements); + return updated; +} + +export async function deleteStatement(locale: string, id: number): Promise { + const statements = await readStatementsFile(locale); + const filtered = statements.filter((s) => s.id !== id); + if (filtered.length === statements.length) { + return false; + } + await writeStatementsFile(locale, filtered); + return true; +} + +