fix: uploads and contributions display

- Remove duplicate FamilyUploadSection (PhotoUploadSection already handles this)
- Fix contributions POST: don't require content for timeline/media types
- Save all fields (year, month, day, location, media_filenames) in contributions INSERT
- Add user-uploaded photos from contributions to public photo gallery
- Fix PhotoUploadSection to include title in submission

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
denshooter
2026-02-18 12:43:13 +01:00
parent 2fb6dd7279
commit 40ace3522c
3 changed files with 58 additions and 22 deletions
+34 -15
View File
@@ -126,11 +126,25 @@ JSON:`
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json() const body = await request.json()
const { name, type, title, content, photoUrl, date, category } = body const { name, type, title, content, year, month, day, location, media_filenames } = body
if (!content || !type) { if (!type) {
return NextResponse.json( return NextResponse.json(
{ error: 'Content and type are required' }, { error: 'Type is required' },
{ status: 400 }
)
}
// Require content for memory type, title for timeline
if (type === 'memory' && !content) {
return NextResponse.json(
{ error: 'Content is required for memories' },
{ status: 400 }
)
}
if (type === 'timeline' && !title) {
return NextResponse.json(
{ error: 'Title is required for timeline entries' },
{ status: 400 } { status: 400 }
) )
} }
@@ -138,30 +152,35 @@ export async function POST(request: Request) {
const db = getDb() const db = getDb()
// 1. Check bad words instantly - clean content is auto-approved // 1. Check bad words instantly - clean content is auto-approved
const badWordCheck = hasBadWords(content + ' ' + (title || '')) const textToCheck = [content, title].filter(Boolean).join(' ')
const badWordCheck = textToCheck ? hasBadWords(textToCheck) : { flag: false }
const initialStatus = badWordCheck.flag ? 'flagged' : 'approved' const initialStatus = badWordCheck.flag ? 'flagged' : 'approved'
const moderationReason = badWordCheck.flag ? badWordCheck.reason : null const moderationReason = badWordCheck.flag ? (badWordCheck as any).reason : null
// 2. Insert contribution // 2. Insert contribution with all fields
const result = db.prepare(` const result = db.prepare(`
INSERT INTO contributions (name, type, title, content, status, moderation_reason) INSERT INTO contributions (name, type, title, content, year, month, day, location, media_filenames, status, moderation_reason)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
name || 'Anonym', name || 'Anonym',
type, type,
title || null, title || null,
content, content || null,
year || null,
month || null,
day || null,
location || null,
media_filenames || null,
initialStatus, initialStatus,
moderationReason || null moderationReason || null
) )
const contributionId = Number(result.lastInsertRowid) const contributionId = Number(result.lastInsertRowid)
console.log(`[API] Created contribution ${contributionId}, status: ${initialStatus}`) console.log(`[API] Created contribution ${contributionId}, type: ${type}, status: ${initialStatus}`)
// 3. If not already flagged, run AI check in background // 3. If not already flagged and has text, run AI check in background
if (!badWordCheck.flag) { if (!badWordCheck.flag && textToCheck) {
// Fire and forget - don't await moderateWithAI(contributionId, textToCheck).catch(e =>
moderateWithAI(contributionId, content).catch(e =>
console.error('[AI-Mod] Background error:', e) console.error('[AI-Mod] Background error:', e)
) )
} }
@@ -169,7 +188,7 @@ export async function POST(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
id: contributionId, id: contributionId,
message: 'Beitrag wurde gespeichert und wird geprüft' message: 'Beitrag wurde gespeichert'
}) })
} catch (error) { } catch (error) {
+23 -7
View File
@@ -11,7 +11,6 @@ import TimelineSection from '@/components/TimelineSection'
import TimelineUploadSection from '@/components/TimelineUploadSection' import TimelineUploadSection from '@/components/TimelineUploadSection'
import MemoryUploadSection from '@/components/MemoryUploadSection' import MemoryUploadSection from '@/components/MemoryUploadSection'
import PhotoUploadSection from '@/components/PhotoUploadSection' import PhotoUploadSection from '@/components/PhotoUploadSection'
import FamilyUploadSection from '@/components/FamilyUploadSection'
import RecipeSection from '@/components/RecipeSection' import RecipeSection from '@/components/RecipeSection'
import RecipeUploadSection from '@/components/RecipeUploadSection' import RecipeUploadSection from '@/components/RecipeUploadSection'
@@ -123,7 +122,7 @@ export default async function HomePage() {
// Create virtual MediaItem entries for timeline photos // Create virtual MediaItem entries for timeline photos
const timelinePhotos: MediaItem[] = Array.from(timelinePhotoFilenames).map((filename, i) => ({ const timelinePhotos: MediaItem[] = Array.from(timelinePhotoFilenames).map((filename, i) => ({
id: 999000 + i, // High ID to avoid conflicts id: 999000 + i,
filename, filename,
original_name: null, original_name: null,
type: 'photo' as const, type: 'photo' as const,
@@ -133,8 +132,28 @@ export default async function HomePage() {
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
})) }))
// Merge with existing photos // Fetch approved media contributions (user-uploaded photos)
const allPhotos = [...photos, ...timelinePhotos] let mediaContribPhotos: MediaItem[] = []
try {
const mediaContribs = plain(
db.prepare("SELECT id, media_filenames, name, created_at FROM contributions WHERE status = 'approved' AND media_filenames IS NOT NULL AND media_filenames != ''").all()
)
mediaContribPhotos = mediaContribs.flatMap((c: any) =>
c.media_filenames.split(',').filter(Boolean).map((filename: string, i: number) => ({
id: 998000 + c.id * 10 + i,
filename: filename.trim(),
original_name: null,
type: 'photo' as const,
caption: `Von ${c.name || 'Anonym'}`,
sort_order: 9998,
status: 'approved' as const,
created_at: c.created_at,
}))
)
} catch {}
// Merge all photos
const allPhotos = [...photos, ...timelinePhotos, ...mediaContribPhotos]
const recipes = plain<Recipe>( const recipes = plain<Recipe>(
db.prepare('SELECT * FROM recipes ORDER BY sort_order, title').all() db.prepare('SELECT * FROM recipes ORDER BY sort_order, title').all()
@@ -218,9 +237,6 @@ export default async function HomePage() {
{/* Photo Upload */} {/* Photo Upload */}
<PhotoUploadSection /> <PhotoUploadSection />
{/* Family Upload */}
<FamilyUploadSection />
{/* Memories */} {/* Memories */}
<section id="erinnerungen"> <section id="erinnerungen">
<MemorySection memories={combinedMemories} /> <MemorySection memories={combinedMemories} />
+1
View File
@@ -41,6 +41,7 @@ export default function PhotoUploadSection() {
body: JSON.stringify({ body: JSON.stringify({
name: 'Anonym', name: 'Anonym',
type: 'media', type: 'media',
title: 'Foto-Upload',
media_filenames: uploadedFilenames.join(','), media_filenames: uploadedFilenames.join(','),
}), }),
}) })