fix: photo display, duplicate detection, memory photos
- Remove duplicate FamilyUploadSection from public page - Remove 'Von Anonym' caption from user-uploaded gallery photos - Add SHA-256 duplicate detection in upload route (same file → same path) - Fix timeline photos: use object-contain instead of object-cover (no clipping) - Fix timeline modal photos: remove fixed h-48 height - Add photo display support to MemorySection component - Include media_filenames in memory contribution queries - Add media_filenames to Memory type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFile, mkdir } from 'fs/promises'
|
||||
import { writeFile, mkdir, readdir, readFile } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { cookies } from 'next/headers'
|
||||
import { createHash, randomUUID } from 'crypto'
|
||||
@@ -19,6 +19,35 @@ async function isAdmin() {
|
||||
|
||||
const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data')
|
||||
|
||||
// In-memory hash cache (populated on first use)
|
||||
let hashCache: Map<string, string> | null = null
|
||||
|
||||
async function getFileHash(buffer: Buffer): Promise<string> {
|
||||
return createHash('sha256').update(buffer).digest('hex')
|
||||
}
|
||||
|
||||
async function buildHashCache(folder: string): Promise<void> {
|
||||
if (hashCache) return
|
||||
hashCache = new Map()
|
||||
const uploadsDir = path.join(DATA_DIR, 'uploads', folder)
|
||||
try {
|
||||
const files = await readdir(uploadsDir)
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await readFile(path.join(uploadsDir, file))
|
||||
const hash = await getFileHash(content)
|
||||
hashCache.set(hash, `${folder}/${file}`)
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function findDuplicate(buffer: Buffer, folder: string): Promise<string | null> {
|
||||
await buildHashCache(folder)
|
||||
const hash = await getFileHash(buffer)
|
||||
return hashCache?.get(hash) || null
|
||||
}
|
||||
|
||||
const MIME_TO_FOLDER: Record<string, string> = {
|
||||
'image/jpeg': 'photos',
|
||||
'image/jpg': 'photos',
|
||||
@@ -75,11 +104,22 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const filename = `${folder}/${randomUUID()}${ext || '.bin'}`
|
||||
const filePath = path.join(DATA_DIR, 'uploads', filename)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
// Check for duplicate
|
||||
const existingFile = await findDuplicate(buffer, folder)
|
||||
if (existingFile) {
|
||||
uploadedFiles.push(existingFile)
|
||||
continue
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(filePath), { recursive: true })
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await writeFile(filePath, buffer)
|
||||
|
||||
// Add to hash cache
|
||||
const hash = await getFileHash(buffer)
|
||||
hashCache?.set(hash, filename)
|
||||
|
||||
uploadedFiles.push(filename)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -41,7 +41,7 @@ export default async function HomePage() {
|
||||
try {
|
||||
userMemories = plain(
|
||||
db.prepare(`
|
||||
SELECT id, name, title, content, created_at
|
||||
SELECT id, name, title, content, media_filenames, created_at
|
||||
FROM contributions
|
||||
WHERE status = 'approved' AND type = 'memory'
|
||||
ORDER BY created_at DESC
|
||||
@@ -58,6 +58,8 @@ export default async function HomePage() {
|
||||
id: m.id,
|
||||
title: m.title || 'Erinnerung',
|
||||
content: m.content,
|
||||
author: m.name || null,
|
||||
media_filenames: m.media_filenames || null,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.created_at,
|
||||
}))
|
||||
@@ -144,7 +146,7 @@ export default async function HomePage() {
|
||||
filename: filename.trim(),
|
||||
original_name: null,
|
||||
type: 'photo' as const,
|
||||
caption: `Von ${c.name || 'Anonym'}`,
|
||||
caption: null,
|
||||
sort_order: 9998,
|
||||
status: 'approved' as const,
|
||||
created_at: c.created_at,
|
||||
|
||||
@@ -34,7 +34,9 @@ export default function MemorySection({ memories }: { memories: Memory[] }) {
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-10">
|
||||
{memories.map((memory, i) => (
|
||||
{memories.map((memory, i) => {
|
||||
const photos = memory.media_filenames ? memory.media_filenames.split(',').filter(Boolean) : []
|
||||
return (
|
||||
<motion.article
|
||||
key={memory.id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
@@ -55,6 +57,20 @@ export default function MemorySection({ memories }: { memories: Memory[] }) {
|
||||
{memory.title}
|
||||
</h3>
|
||||
|
||||
{/* Photos */}
|
||||
{photos.length > 0 && (
|
||||
<div className={`grid gap-3 mb-5 ${photos.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}`}>
|
||||
{photos.map((filename, j) => (
|
||||
<img
|
||||
key={j}
|
||||
src={`/api/files/${filename.trim()}`}
|
||||
alt=""
|
||||
className="w-full object-contain rounded-xl bg-warm-brown/5 max-h-64"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="font-lora text-warm-brown/80 text-base sm:text-lg leading-relaxed whitespace-pre-wrap">
|
||||
{memory.content}
|
||||
</p>
|
||||
@@ -66,7 +82,8 @@ export default function MemorySection({ memories }: { memories: Memory[] }) {
|
||||
)}
|
||||
</p>
|
||||
</motion.article>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function TimelineSection({ entries }: TimelineSectionProps) {
|
||||
key={i}
|
||||
src={`/api/files/${filename.trim()}`}
|
||||
alt=""
|
||||
className="w-full h-24 object-cover rounded-lg"
|
||||
className="w-full max-h-40 object-contain rounded-lg bg-warm-brown/5"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -310,7 +310,7 @@ export default function TimelineSection({ entries }: TimelineSectionProps) {
|
||||
key={i}
|
||||
src={`/api/files/${filename.trim()}`}
|
||||
alt=""
|
||||
className="w-full h-48 object-cover rounded-lg"
|
||||
className="w-full object-contain rounded-lg bg-warm-brown/5"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ export type Memory = {
|
||||
title: string
|
||||
content: string
|
||||
author: string | null
|
||||
media_filenames: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user