Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19706abacb | ||
|
|
170e7b5402 | ||
|
|
ade1043c3c | ||
|
|
d69af49e24 | ||
|
|
63687524e7 | ||
|
|
0246cb58ee | ||
|
|
d76aa9f4e9 |
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
|
||||
|
||||
@@ -68,6 +68,14 @@ interface News {
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface PoliticalStatement {
|
||||
id: number;
|
||||
text: string;
|
||||
active?: boolean;
|
||||
source?: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
@@ -169,6 +177,11 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
||||
const [showSpecials, setShowSpecials] = useState(false);
|
||||
const [showGenres, setShowGenres] = useState(false);
|
||||
const [showNews, setShowNews] = useState(false);
|
||||
const [showPoliticalStatements, setShowPoliticalStatements] = useState(false);
|
||||
const [politicalStatementsLocale, setPoliticalStatementsLocale] = useState<'de' | 'en'>('de');
|
||||
const [politicalStatements, setPoliticalStatements] = useState<PoliticalStatement[]>([]);
|
||||
const [newPoliticalStatementText, setNewPoliticalStatementText] = useState('');
|
||||
const [newPoliticalStatementActive, setNewPoliticalStatementActive] = useState(true);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Check for existing auth on mount
|
||||
@@ -447,6 +460,100 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Political Statements functions (JSON-backed via API)
|
||||
const fetchPoliticalStatements = async (targetLocale: 'de' | 'en') => {
|
||||
const res = await fetch(`/api/political-statements?locale=${encodeURIComponent(targetLocale)}&admin=true`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const enriched: PoliticalStatement[] = data.map((s: any) => ({
|
||||
id: s.id,
|
||||
text: s.text,
|
||||
active: s.active !== false,
|
||||
source: s.source,
|
||||
locale: targetLocale,
|
||||
}));
|
||||
setPoliticalStatements(prev => {
|
||||
const others = prev.filter(p => p.locale !== targetLocale);
|
||||
return [...others, ...enriched];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePoliticalStatement = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newPoliticalStatementText.trim()) return;
|
||||
|
||||
const res = await fetch('/api/political-statements', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
locale: politicalStatementsLocale,
|
||||
text: newPoliticalStatementText.trim(),
|
||||
active: newPoliticalStatementActive,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setNewPoliticalStatementText('');
|
||||
setNewPoliticalStatementActive(true);
|
||||
fetchPoliticalStatements(politicalStatementsLocale);
|
||||
} else {
|
||||
alert('Failed to create statement');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditPoliticalStatementText = (locale: string, id: number, text: string) => {
|
||||
setPoliticalStatements(prev =>
|
||||
prev.map(s => (s.locale === locale && s.id === id ? { ...s, text } : s)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditPoliticalStatementActive = (locale: string, id: number, active: boolean) => {
|
||||
setPoliticalStatements(prev =>
|
||||
prev.map(s => (s.locale === locale && s.id === id ? { ...s, active } : s)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSavePoliticalStatement = async (locale: string, id: number) => {
|
||||
const stmt = politicalStatements.find(s => s.locale === locale && s.id === id);
|
||||
if (!stmt) return;
|
||||
|
||||
const res = await fetch('/api/political-statements', {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
locale,
|
||||
id,
|
||||
text: stmt.text,
|
||||
active: stmt.active !== false,
|
||||
source: stmt.source,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
alert('Failed to save statement');
|
||||
fetchPoliticalStatements(locale as 'de' | 'en');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePoliticalStatement = async (locale: string, id: number) => {
|
||||
if (!confirm('Delete this statement?')) return;
|
||||
|
||||
const res = await fetch('/api/political-statements', {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ locale, id }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setPoliticalStatements(prev => prev.filter(s => !(s.locale === locale && s.id === id)));
|
||||
} else {
|
||||
alert('Failed to delete statement');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNews = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if ((!newNewsTitle.de.trim() && !newNewsTitle.en.trim()) || (!newNewsContent.de.trim() && !newNewsContent.en.trim())) return;
|
||||
@@ -524,9 +631,13 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Load specials after auth
|
||||
// Load specials and political statements after auth
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) fetchSpecials();
|
||||
if (isAuthenticated) {
|
||||
fetchSpecials();
|
||||
fetchPoliticalStatements('de');
|
||||
fetchPoliticalStatements('en');
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const deleteGenre = async (id: number) => {
|
||||
@@ -1580,6 +1691,141 @@ export default function AdminPage({ params }: { params: { locale: string } }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Political Statements Management */}
|
||||
<div className="admin-card" style={{ marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>
|
||||
Political Statements
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowPoliticalStatements(!showPoliticalStatements)}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#f3f4f6',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '0.25rem',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
>
|
||||
{showPoliticalStatements ? t('hide') : t('show')}
|
||||
</button>
|
||||
</div>
|
||||
{showPoliticalStatements && (
|
||||
<>
|
||||
{/* Language Tabs */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||
{(['de', 'en'] as const).map(lang => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={() => setPoliticalStatementsLocale(lang)}
|
||||
style={{
|
||||
padding: '0.4rem 0.8rem',
|
||||
borderRadius: '999px',
|
||||
border: '1px solid #d1d5db',
|
||||
background: politicalStatementsLocale === lang ? '#111827' : 'white',
|
||||
color: politicalStatementsLocale === lang ? 'white' : '#111827',
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{lang.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Create Form */}
|
||||
<form onSubmit={handleCreatePoliticalStatement} style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<textarea
|
||||
value={newPoliticalStatementText}
|
||||
onChange={e => setNewPoliticalStatementText(e.target.value)}
|
||||
placeholder="Statement text"
|
||||
className="form-input"
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.875rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newPoliticalStatementActive}
|
||||
onChange={e => setNewPoliticalStatementActive(e.target.checked)}
|
||||
/>
|
||||
Active
|
||||
</label>
|
||||
<button type="submit" className="btn-primary" style={{ fontSize: '0.875rem' }}>
|
||||
Add Statement ({politicalStatementsLocale.toUpperCase()})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* List */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{politicalStatements
|
||||
.filter(s => s.locale === politicalStatementsLocale)
|
||||
.map(stmt => (
|
||||
<div
|
||||
key={`${stmt.locale}-${stmt.id}`}
|
||||
style={{
|
||||
background: stmt.active ? '#ecfdf5' : '#f3f4f6',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem'
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
value={stmt.text}
|
||||
onChange={e => handleEditPoliticalStatementText(stmt.locale, stmt.id, e.target.value)}
|
||||
className="form-input"
|
||||
rows={3}
|
||||
style={{ fontSize: '0.85rem' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.8rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stmt.active !== false}
|
||||
onChange={e => handleEditPoliticalStatementActive(stmt.locale, stmt.id, e.target.checked)}
|
||||
/>
|
||||
Active
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
style={{ padding: '0.25rem 0.6rem', fontSize: '0.75rem' }}
|
||||
onClick={() => handleSavePoliticalStatement(stmt.locale, stmt.id)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger"
|
||||
style={{ padding: '0.25rem 0.6rem', fontSize: '0.75rem' }}
|
||||
onClick={() => handleDeletePoliticalStatement(stmt.locale, stmt.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{politicalStatements.filter(s => s.locale === politicalStatementsLocale).length === 0 && (
|
||||
<p style={{ color: '#666', fontSize: '0.875rem', textAlign: 'center', padding: '0.5rem' }}>
|
||||
No statements for this language yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="admin-card" style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>{t('uploadSongs')}</h2>
|
||||
<form onSubmit={handleBatchUpload}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { config } from "@/lib/config";
|
||||
import { generateBaseMetadata } from "@/lib/metadata";
|
||||
import InstallPrompt from "@/components/InstallPrompt";
|
||||
import AppFooter from "@/components/AppFooter";
|
||||
import PoliticalStatementBanner from "@/components/PoliticalStatementBanner";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -89,6 +90,7 @@ export default async function LocaleLayout({
|
||||
{children}
|
||||
<InstallPrompt />
|
||||
<AppFooter />
|
||||
<PoliticalStatementBanner />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
94
lib/politicalStatements.ts
Normal file
94
lib/politicalStatements.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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 | null;
|
||||
};
|
||||
|
||||
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 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() * all.length);
|
||||
return mapFromPrisma(all[index]);
|
||||
}
|
||||
|
||||
export async function getAllStatements(locale: string): Promise<PoliticalStatement[]> {
|
||||
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' | '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' | 'locale'>>): Promise<PoliticalStatement | null> {
|
||||
const safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
|
||||
// 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 safeLocale = ['de', 'en'].includes(locale) ? locale : 'en';
|
||||
|
||||
const existing = await prisma.politicalStatement.findUnique({ where: { id } });
|
||||
if (!existing || existing.locale !== safeLocale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await prisma.politicalStatement.delete({ where: { id } });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoerdle",
|
||||
"version": "0.1.4.6",
|
||||
"version": "0.1.4.9",
|
||||
"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