bf070ec47f
- Rewrote CandleSection with properly aligned animated flames (fix Framer Motion transform-overwrite bug by using left:0 / pixel offsets instead of translateX(-50%)); added size prop for inline flames - Fixed Timeline: CSS dots/line replacing broken SVG estimates, mobile padding (pl-10), larger dots, cards grow with content - Fixed MusicPlayer: actual play/pause (not mute), visibilitychange + pagehide handlers so music stops on iOS lock screen / app switch - Fixed nav: horizontal scroll on mobile (overflow-x-auto whitespace-nowrap) - Fixed footer: Impressum centered, admin link moved to absolute/hidden - Removed meine-oma link from TributeSection (page still accessible) - Added admin auth (isAdmin cookie check) to /api/upload POST, /api/contributions/[id] PUT/DELETE, and /api/candles/[id] PUT/DELETE - Extracted sp(), relativeTime(), formatDate() to src/lib/utils.ts - Added Vitest with 15 unit tests for utility functions (npm test) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.3 KiB
TypeScript
85 lines
2.3 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { getDb } from '@/lib/db'
|
|
import { cookies } from 'next/headers'
|
|
import { createHash } from 'crypto'
|
|
|
|
async function isAdmin() {
|
|
const cookieStore = await cookies()
|
|
const token = cookieStore.get('admin_auth')?.value
|
|
const expected = createHash('sha256')
|
|
.update(process.env.ADMIN_PASSWORD || 'change-me')
|
|
.digest('hex')
|
|
return token === expected
|
|
}
|
|
|
|
export const runtime = 'nodejs'
|
|
|
|
export async function PUT(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
if (!await isAdmin()) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const { id } = await params
|
|
const body = await request.json()
|
|
|
|
const db = getDb()
|
|
|
|
// Check if only updating status
|
|
if (Object.keys(body).length === 1 && 'status' in body) {
|
|
db.prepare('UPDATE contributions SET status = ? WHERE id = ?').run(body.status, id)
|
|
} else {
|
|
// Full update
|
|
const { name, email, type, year, month, day, title, content, location, media_filenames, status } = body
|
|
|
|
db.prepare(`
|
|
UPDATE contributions
|
|
SET name = ?, email = ?, type = ?, year = ?, month = ?, day = ?, title = ?, content = ?, location = ?, media_filenames = ?, status = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
name,
|
|
email || null,
|
|
type,
|
|
year || null,
|
|
month || null,
|
|
day || null,
|
|
title || null,
|
|
content || null,
|
|
location || null,
|
|
media_filenames || null,
|
|
status || 'pending',
|
|
id
|
|
)
|
|
}
|
|
|
|
return NextResponse.json({ success: true })
|
|
} catch (error) {
|
|
console.error('Error updating contribution:', error)
|
|
return NextResponse.json({ error: 'Failed to update contribution' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
export async function DELETE(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
if (!await isAdmin()) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const { id } = await params
|
|
const db = getDb()
|
|
|
|
db.prepare('DELETE FROM contributions WHERE id = ?').run(id)
|
|
|
|
return NextResponse.json({ success: true })
|
|
} catch (error) {
|
|
console.error('Error deleting contribution:', error)
|
|
return NextResponse.json({ error: 'Failed to delete contribution' }, { status: 500 })
|
|
}
|
|
}
|