Include political statements feature files
This commit is contained in:
113
app/api/political-statements/route.ts
Normal file
113
app/api/political-statements/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
95
components/PoliticalStatementBanner.tsx
Normal file
95
components/PoliticalStatementBanner.tsx
Normal file
@@ -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<ApiStatement | null>(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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '1.25rem',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
maxWidth: '640px',
|
||||||
|
width: 'calc(100% - 2.5rem)',
|
||||||
|
zIndex: 1050,
|
||||||
|
background: 'rgba(17,24,39,0.95)',
|
||||||
|
color: '#e5e7eb',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
boxShadow: '0 10px 25px rgba(0,0,0,0.45)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '0.9rem' }}>✊</span>
|
||||||
|
<span style={{ flex: 1 }}>{statement.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
99
lib/politicalStatements.ts
Normal file
99
lib/politicalStatements.ts
Normal file
@@ -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<PoliticalStatement[]> {
|
||||||
|
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<void> {
|
||||||
|
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<PoliticalStatement | null> {
|
||||||
|
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<PoliticalStatement[]> {
|
||||||
|
return readStatementsFile(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStatement(locale: string, input: Omit<PoliticalStatement, 'id'>): Promise<PoliticalStatement> {
|
||||||
|
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<Omit<PoliticalStatement, 'id'>>): Promise<PoliticalStatement | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user