Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions site/app/api/media/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
8 changes: 4 additions & 4 deletions site/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
43 changes: 38 additions & 5 deletions site/app/upload/page.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<UploadedFile[]>([])
const [dragActive, setDragActive] = useState(false)
const [loading, setLoading] = useState(true)

const handleFileUpload = async (files: FileList) => {
setUploading(true)
Expand All @@ -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) {
Expand All @@ -43,6 +43,9 @@ export default function UploadPage() {
}

setUploading(false)

// Reload all files after upload completes
await loadAllFiles()
}

const handleDrop = (e: React.DragEvent) => {
Expand Down Expand Up @@ -84,6 +87,28 @@ export default function UploadPage() {
return <File className="w-5 h-5 text-gray-500" />
}

// 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 (
<div className="min-h-screen bg-gray-100 dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-4 py-8">
Expand Down Expand Up @@ -144,11 +169,19 @@ export default function UploadPage() {
</div>
)}

{/* Loading indicator */}
{loading && uploadedFiles.length === 0 && (
<div className="mt-6 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading files...</p>
</div>
)}

{/* Uploaded Files */}
{uploadedFiles.length > 0 && (
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Uploaded Files ({uploadedFiles.length})
All Uploaded Files ({uploadedFiles.length})
</h2>

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
Expand Down
Empty file added site/public/media/.gitkeep
Empty file.