'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' interface Drop { id: number item: string size: number fill: number unit: string ppu: number created_at: string } interface Buyer { id: number username: string email: string created_at?: string } interface Sale { id: number drop_id: number buyer_id: number size: number payment_id: string | null created_at: string drop_item?: string drop_unit?: string drop_ppu?: number buyer_username?: string buyer_email?: string } export default function AdminPage() { const router = useRouter() const [drops, setDrops] = useState([]) const [buyers, setBuyers] = useState([]) const [sales, setSales] = useState([]) const [loading, setLoading] = useState(true) const [buyersLoading, setBuyersLoading] = useState(false) const [salesLoading, setSalesLoading] = useState(false) const [submitting, setSubmitting] = useState(false) const [uploadingImage, setUploadingImage] = useState(false) const [editingBuyer, setEditingBuyer] = useState(null) const [editingSale, setEditingSale] = useState(null) const [formData, setFormData] = useState({ item: '', size: '', unit: 'g', ppu: '', imageFile: null as File | null, imagePreview: '', startTime: '', }) const [buyerFormData, setBuyerFormData] = useState({ username: '', email: '', password: '', }) const [saleFormData, setSaleFormData] = useState({ drop_id: '', buyer_id: '', size: '', payment_id: '', }) useEffect(() => { fetchDrops() fetchBuyers() fetchSales() }, []) const fetchDrops = async () => { try { const response = await fetch('/api/drops') if (!response.ok) { throw new Error('Failed to fetch drops') } const data = await response.json() // Ensure data is always an array setDrops(Array.isArray(data) ? data : []) } catch (error) { console.error('Error fetching drops:', error) setDrops([]) // Set to empty array on error } finally { setLoading(false) } } const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { setFormData({ ...formData, imageFile: file, imagePreview: URL.createObjectURL(file), }) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSubmitting(true) try { let imageUrl = '' // Upload image if provided if (formData.imageFile) { setUploadingImage(true) const uploadFormData = new FormData() uploadFormData.append('file', formData.imageFile) const uploadResponse = await fetch('/api/upload', { method: 'POST', body: uploadFormData, }) if (!uploadResponse.ok) { const error = await uploadResponse.json() alert(`Image upload failed: ${error.error}`) setUploadingImage(false) setSubmitting(false) return } const uploadData = await uploadResponse.json() imageUrl = uploadData.url setUploadingImage(false) } // Create drop const response = await fetch('/api/drops', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ item: formData.item, size: parseInt(formData.size), unit: formData.unit.trim() || 'g', ppu: parseInt(formData.ppu, 10), imageUrl: imageUrl, startTime: formData.startTime || null, }), }) if (response.ok) { // Reset form setFormData({ item: '', size: '', unit: 'g', ppu: '', imageFile: null, imagePreview: '', startTime: '', }) // Clear file input const fileInput = document.getElementById('imageFile') as HTMLInputElement if (fileInput) fileInput.value = '' // Refresh drops list fetchDrops() alert('Drop created successfully!') } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error creating drop:', error) alert('Failed to create drop') } finally { setSubmitting(false) setUploadingImage(false) } } const getProgressPercentage = (fill: number, size: number) => { return Math.min((fill / size) * 100, 100).toFixed(1) } const isSoldOut = (fill: number, size: number) => { return fill >= size } const fetchBuyers = async () => { setBuyersLoading(true) try { const response = await fetch('/api/buyers') if (response.ok) { const data = await response.json() setBuyers(Array.isArray(data) ? data : []) } } catch (error) { console.error('Error fetching buyers:', error) setBuyers([]) } finally { setBuyersLoading(false) } } const fetchSales = async () => { setSalesLoading(true) try { const response = await fetch('/api/sales/list') if (response.ok) { const data = await response.json() setSales(Array.isArray(data) ? data : []) } } catch (error) { console.error('Error fetching sales:', error) setSales([]) } finally { setSalesLoading(false) } } const handleEditBuyer = (buyer: Buyer) => { setEditingBuyer(buyer) setBuyerFormData({ username: buyer.username, email: buyer.email, password: '', }) } const handleSaveBuyer = async () => { if (!editingBuyer) return try { const updateData: any = {} if (buyerFormData.username !== editingBuyer.username) { updateData.username = buyerFormData.username } if (buyerFormData.email !== editingBuyer.email) { updateData.email = buyerFormData.email } if (buyerFormData.password) { updateData.password = buyerFormData.password } if (Object.keys(updateData).length === 0) { setEditingBuyer(null) return } const response = await fetch(`/api/buyers/${editingBuyer.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData), }) if (response.ok) { alert('Buyer updated successfully') setEditingBuyer(null) fetchBuyers() } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error updating buyer:', error) alert('Failed to update buyer') } } const handleDeleteBuyer = async (id: number) => { if (!confirm('Are you sure you want to delete this buyer? This will also delete all their sales.')) { return } try { const response = await fetch(`/api/buyers/${id}`, { method: 'DELETE', }) if (response.ok) { alert('Buyer deleted successfully') fetchBuyers() } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error deleting buyer:', error) alert('Failed to delete buyer') } } const handleEditSale = (sale: Sale) => { setEditingSale(sale) setSaleFormData({ drop_id: sale.drop_id.toString(), buyer_id: sale.buyer_id.toString(), size: sale.size.toString(), payment_id: sale.payment_id || '', }) } const handleSaveSale = async () => { if (!editingSale) return try { const updateData: any = {} if (parseInt(saleFormData.drop_id) !== editingSale.drop_id) { updateData.drop_id = parseInt(saleFormData.drop_id) } if (parseInt(saleFormData.buyer_id) !== editingSale.buyer_id) { updateData.buyer_id = parseInt(saleFormData.buyer_id) } if (parseInt(saleFormData.size) !== editingSale.size) { updateData.size = parseInt(saleFormData.size) } if (saleFormData.payment_id !== (editingSale.payment_id || '')) { updateData.payment_id = saleFormData.payment_id || null } if (Object.keys(updateData).length === 0) { setEditingSale(null) return } const response = await fetch(`/api/sales/${editingSale.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData), }) if (response.ok) { alert('Sale updated successfully') setEditingSale(null) fetchSales() fetchDrops() // Refresh drops to update fill } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error updating sale:', error) alert('Failed to update sale') } } const handleDeleteSale = async (id: number) => { if (!confirm('Are you sure you want to delete this sale?')) { return } try { const response = await fetch(`/api/sales/${id}`, { method: 'DELETE', }) if (response.ok) { alert('Sale deleted successfully') fetchSales() fetchDrops() // Refresh drops to update fill } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error deleting sale:', error) alert('Failed to delete sale') } } return (

Admin Panel

{/* Create Drop Form */}

Create New Drop

setFormData({ ...formData, item: e.target.value }) } required placeholder="e.g. Harlequin – Collective Drop" />
setFormData({ ...formData, size: e.target.value }) } required placeholder="1000" min="1" />
setFormData({ ...formData, unit: e.target.value }) } placeholder="g, kg, ml, etc." required maxLength={12} />
setFormData({ ...formData, ppu: e.target.value }) } required placeholder="2500" step="1" min="1" />

Enter price in smallest currency unit. 1000 = 1.00 CHF, 2500 = 2.50 CHF, 10 = 0.01 CHF

setFormData({ ...formData, startTime: e.target.value }) } />

When should this drop become available? Leave empty to make it available immediately.

{formData.imagePreview && (
Preview
)}

Max file size: 5MB. Allowed formats: JPEG, PNG, WebP

{/* Drops List */}

All Drops

{loading ? (

Loading...

) : !Array.isArray(drops) || drops.length === 0 ? (

No drops yet

) : (
{drops.map((drop) => (

{drop.item}

ID: {drop.id} · Created:{' '} {new Date(drop.created_at).toLocaleDateString()}

{isSoldOut(drop.fill, drop.size) && ( Sold Out )}
{drop.fill} {drop.unit} / {drop.size} {drop.unit} {getProgressPercentage(drop.fill, drop.size)}%
{(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit}
))}
)}
{/* Buyers Section */}

Buyers

{buyersLoading ? (

Loading...

) : buyers.length === 0 ? (

No buyers yet

) : (
{buyers.map((buyer) => (
{editingBuyer?.id === buyer.id ? (
setBuyerFormData({ ...buyerFormData, username: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
setBuyerFormData({ ...buyerFormData, email: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
setBuyerFormData({ ...buyerFormData, password: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
) : (

{buyer.username}

{buyer.email} · ID: {buyer.id}

{buyer.created_at && (

Joined: {new Date(buyer.created_at).toLocaleDateString()}

)}
)}
))}
)}
{/* Sales Section */}

Sales

{salesLoading ? (

Loading...

) : sales.length === 0 ? (

No sales yet

) : (
{sales.map((sale) => (
{editingSale?.id === sale.id ? (
setSaleFormData({ ...saleFormData, drop_id: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
setSaleFormData({ ...saleFormData, buyer_id: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
setSaleFormData({ ...saleFormData, size: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
setSaleFormData({ ...saleFormData, payment_id: e.target.value }) } style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)', }} />
) : (

Sale #{sale.id}

{sale.drop_item || `Drop #${sale.drop_id}`} · {sale.size}g

Buyer: {sale.buyer_username || `#${sale.buyer_id}`} ({sale.buyer_email || 'N/A'})

{sale.drop_ppu && (

Price: {((sale.drop_ppu / 1000) * sale.size).toFixed(2)} CHF

)}

{new Date(sale.created_at).toLocaleString()} {sale.payment_id && ` · Payment: ${sale.payment_id}`}

)}
))}
)}
) }