Compare commits
4 Commits
d69af49e24
...
v0.1.4.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e60daa511b | ||
|
|
19706abacb | ||
|
|
170e7b5402 | ||
|
|
ade1043c3c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -52,3 +52,5 @@ next-env.d.ts
|
||||
.release-years-migrated
|
||||
.covers-migrated
|
||||
docker-compose.yml
|
||||
scripts/scrape-bahn-expert-statements.js
|
||||
docs/bahn-expert-statements.txt
|
||||
|
||||
@@ -70,6 +70,20 @@ export default async function AboutPage({ params }: AboutPageProps) {
|
||||
{t("costsTitle")}
|
||||
</h2>
|
||||
<p style={{ marginBottom: "0.5rem" }}>{t("costsIntro")}</p>
|
||||
<p style={{ marginBottom: "0.5rem" }}>
|
||||
{t.rich("costsDonationNote", {
|
||||
link: (chunks) => (
|
||||
<a
|
||||
href="https://politicalbeauty.de/ueber-das-ZPS.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ textDecoration: "underline" }}
|
||||
>
|
||||
{chunks}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<ul
|
||||
style={{
|
||||
marginLeft: "1.25rem",
|
||||
|
||||
@@ -1,99 +1,94 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { PrismaClient, PoliticalStatement as PrismaPoliticalStatement } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export type PoliticalStatement = {
|
||||
id: number;
|
||||
locale: string;
|
||||
text: string;
|
||||
active?: boolean;
|
||||
source?: string;
|
||||
active: boolean;
|
||||
source?: string | null;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
function mapFromPrisma(stmt: PrismaPoliticalStatement): PoliticalStatement {
|
||||
return {
|
||||
id: stmt.id,
|
||||
locale: stmt.locale,
|
||||
text: stmt.text,
|
||||
active: stmt.active,
|
||||
source: stmt.source,
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
const all = await prisma.politicalStatement.findMany({
|
||||
where: {
|
||||
locale: safeLocale,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (all.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const index = Math.floor(Math.random() * active.length);
|
||||
return active[index] ?? null;
|
||||
|
||||
const index = Math.floor(Math.random() * all.length);
|
||||
return mapFromPrisma(all[index]);
|
||||
}
|
||||
|
||||
export async function getAllStatements(locale: string): Promise<PoliticalStatement[]> {
|
||||
return readStatementsFile(locale);
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
const all = await prisma.politicalStatement.findMany({
|
||||
where: { locale: safeLocale },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
return all.map(mapFromPrisma);
|
||||
}
|
||||
|
||||
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 createStatement(locale: string, input: Omit<PoliticalStatement, 'id' | 'locale'>): Promise<PoliticalStatement> {
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
const created = await prisma.politicalStatement.create({
|
||||
data: {
|
||||
locale: safeLocale,
|
||||
text: input.text,
|
||||
active: input.active ?? true,
|
||||
source: input.source ?? null,
|
||||
},
|
||||
});
|
||||
return mapFromPrisma(created);
|
||||
}
|
||||
|
||||
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;
|
||||
export async function updateStatement(locale: string, id: number, input: Partial<Omit<PoliticalStatement, 'id' | 'locale'>>): Promise<PoliticalStatement | null> {
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
|
||||
const updated: PoliticalStatement = {
|
||||
...statements[index],
|
||||
...input,
|
||||
id,
|
||||
};
|
||||
statements[index] = updated;
|
||||
await writeStatementsFile(locale, statements);
|
||||
return updated;
|
||||
// Optional: sicherstellen, dass das Statement zu dieser Locale gehört
|
||||
const existing = await prisma.politicalStatement.findUnique({ where: { id } });
|
||||
if (!existing || existing.locale !== safeLocale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated = await prisma.politicalStatement.update({
|
||||
where: { id },
|
||||
data: {
|
||||
text: input.text ?? existing.text,
|
||||
active: input.active ?? existing.active,
|
||||
source: input.source !== undefined ? input.source : existing.source,
|
||||
},
|
||||
});
|
||||
|
||||
return mapFromPrisma(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) {
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
|
||||
const existing = await prisma.politicalStatement.findUnique({ where: { id } });
|
||||
if (!existing || existing.locale !== safeLocale) {
|
||||
return false;
|
||||
}
|
||||
await writeStatementsFile(locale, filtered);
|
||||
|
||||
await prisma.politicalStatement.delete({ where: { id } });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"deletePuzzle": "Löschen",
|
||||
"wrongPassword": "Falsches Passwort"
|
||||
},
|
||||
"About": {
|
||||
"About": {
|
||||
"title": "Über Hördle & Impressum",
|
||||
"intro": "Hördle ist ein nicht-kommerzielles, privat betriebenes Hobbyprojekt. Es gibt keine Werbeanzeigen, keine gesponserten Inhalte und keine versteckten Abo-Modelle.",
|
||||
"projectTitle": "Über dieses Projekt",
|
||||
@@ -171,6 +171,7 @@
|
||||
"imprintEmailLabel": "E-Mail:",
|
||||
"costsTitle": "Laufende Kosten des Projekts",
|
||||
"costsIntro": "Auch wenn Hördle ein privates Projekt ist, entstehen für den Betrieb laufende Kosten, zum Beispiel:",
|
||||
"costsDonationNote": "Alle Einnahmen, die die Betriebskosten des Projekts übersteigen, werden am Jahresende an die Aktion <link>Zentrum für politische Schönheit</link> gespendet.",
|
||||
"costsDomain": "Domains (z. B. hördle.de / hoerdle.de)",
|
||||
"costsServer": "Server / vServer für App und Tracking",
|
||||
"costsEmail": "E-Mail-Hosting",
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"imprintEmailLabel": "Email:",
|
||||
"costsTitle": "Ongoing costs of the project",
|
||||
"costsIntro": "Even though Hördle is a private project, there are ongoing costs for running it, for example:",
|
||||
"costsDonationNote": "All income that exceeds the operating costs of the project will be donated at the end of the year to the campaign <link>Zentrum für politische Schönheit</link>.",
|
||||
"costsDomain": "Domains (e.g. hördle.de / hoerdle.de)",
|
||||
"costsServer": "Servers / vServers for the app and tracking",
|
||||
"costsEmail": "Email hosting",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoerdle",
|
||||
"version": "0.1.4.8",
|
||||
"version": "0.1.4.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "PoliticalStatement" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"locale" TEXT NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"source" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PoliticalStatement_locale_active_idx" ON "PoliticalStatement"("locale", "active");
|
||||
@@ -101,3 +101,15 @@ model PlayerState {
|
||||
@@unique([identifier, genreKey])
|
||||
@@index([identifier])
|
||||
}
|
||||
|
||||
model PoliticalStatement {
|
||||
id Int @id @default(autoincrement())
|
||||
locale String
|
||||
text String
|
||||
active Boolean @default(true)
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([locale, active])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user