diff --git a/.gitignore b/.gitignore index b692c2d..3d470f1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,10 @@ debug.log *.zip # big local data -site/media/ -media/ +# Allow media directory structure but ignore uploaded files +# Note: Using trailing slash to only match directories, not files/folders containing 'media' in path +site/public/media/* +!site/public/media/.gitkeep chrome-user-data/ .dev.db swapfile diff --git a/site/app/api/media/route.ts b/site/app/api/media/route.ts new file mode 100644 index 0000000..e1ef20d --- /dev/null +++ b/site/app/api/media/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from 'next/server' +import { readdir, stat } from 'fs/promises' +import { join } from 'path' +import { existsSync } from 'fs' + +export async function GET() { + try { + const mediaDir = join(process.cwd(), 'public', 'media') + + // Check if media directory exists + if (!existsSync(mediaDir)) { + return NextResponse.json({ + success: true, + files: [] + }) + } + + // Read all files in the media directory + const files = await readdir(mediaDir) + + // Filter out .gitkeep and other hidden files + const visibleFiles = files.filter(filename => !filename.startsWith('.')) + + // Get file stats for each file + const fileDetails = await Promise.all( + visibleFiles.map(async (filename) => { + const filepath = join(mediaDir, filename) + const stats = await stat(filepath) + + // Determine file type based on extension + const ext = filename.split('.').pop()?.toLowerCase() || '' + let type = 'application/octet-stream' + + if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) { + type = `image/${ext === 'jpg' ? 'jpeg' : ext}` + } else if (['mp4', 'webm', 'avi', 'mov'].includes(ext)) { + type = `video/${ext}` + } + + return { + name: filename, + originalName: filename, + size: stats.size, + type: type, + url: `/media/${filename}`, + path: filepath, + uploadedAt: stats.mtime.toISOString() + } + }) + ) + + // Sort by upload date (newest first) + fileDetails.sort((a, b) => + new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime() + ) + + return NextResponse.json({ + success: true, + files: fileDetails, + total: fileDetails.length + }) + + } catch (error) { + console.error('Error listing media files:', error) + return NextResponse.json( + { success: false, message: 'Failed to list media files' }, + { status: 500 } + ) + } +} diff --git a/site/app/api/upload/route.ts b/site/app/api/upload/route.ts index fdf7409..ee02581 100644 --- a/site/app/api/upload/route.ts +++ b/site/app/api/upload/route.ts @@ -15,8 +15,8 @@ export async function POST(request: NextRequest) { const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) - // All files go to media directory - const uploadDir = 'media' + // All files go to public/media directory so they're accessible via URLs + const uploadDir = join('public', 'media') // Ensure directory exists const uploadPath = join(process.cwd(), uploadDir) @@ -31,8 +31,8 @@ export async function POST(request: NextRequest) { // Write file await writeFile(filepath, buffer) - // Return file info - const fileUrl = `/${uploadDir}/${filename}` + // Return file info (URLs in Next.js don't include 'public' - files in public/ are served from root) + const fileUrl = `/media/${filename}` return NextResponse.json({ success: true, diff --git a/site/app/upload/page.tsx b/site/app/upload/page.tsx index f2f3dec..d349017 100644 --- a/site/app/upload/page.tsx +++ b/site/app/upload/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Upload, File, Video, Image, CheckCircle, AlertCircle } from 'lucide-react' interface UploadedFile { @@ -10,12 +10,14 @@ interface UploadedFile { type: string url: string path: string + uploadedAt?: string } export default function UploadPage() { const [uploading, setUploading] = useState(false) const [uploadedFiles, setUploadedFiles] = useState([]) const [dragActive, setDragActive] = useState(false) + const [loading, setLoading] = useState(true) const handleFileUpload = async (files: FileList) => { setUploading(true) @@ -32,9 +34,7 @@ export default function UploadPage() { const result = await response.json() - if (result.success) { - setUploadedFiles(prev => [...prev, result.file]) - } else { + if (!result.success) { console.error('Upload failed:', result.message) } } catch (error) { @@ -43,6 +43,9 @@ export default function UploadPage() { } setUploading(false) + + // Reload all files after upload completes + await loadAllFiles() } const handleDrop = (e: React.DragEvent) => { @@ -84,6 +87,28 @@ export default function UploadPage() { return } + // Reusable function to load all files from the server + const loadAllFiles = async () => { + try { + setLoading(true) + const response = await fetch('/api/media') + const data = await response.json() + + if (data.success) { + setUploadedFiles(data.files) + } + } catch (error) { + console.error('Failed to load files:', error) + } finally { + setLoading(false) + } + } + + // Load all existing files on mount + useEffect(() => { + loadAllFiles() + }, []) + return (
@@ -144,11 +169,19 @@ export default function UploadPage() {
)} + {/* Loading indicator */} + {loading && uploadedFiles.length === 0 && ( +
+
+

Loading files...

+
+ )} + {/* Uploaded Files */} {uploadedFiles.length > 0 && (

- Uploaded Files ({uploadedFiles.length}) + All Uploaded Files ({uploadedFiles.length})

diff --git a/site/public/media/.gitkeep b/site/public/media/.gitkeep new file mode 100644 index 0000000..e69de29