325 lines
9.8 KiB
TypeScript
325 lines
9.8 KiB
TypeScript
'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
|
||
}
|
||
|
||
export default function AdminPage() {
|
||
const router = useRouter()
|
||
const [drops, setDrops] = useState<Drop[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [uploadingImage, setUploadingImage] = useState(false)
|
||
const [formData, setFormData] = useState({
|
||
item: '',
|
||
size: '',
|
||
unit: 'g',
|
||
ppu: '',
|
||
imageFile: null as File | null,
|
||
imagePreview: '',
|
||
})
|
||
|
||
useEffect(() => {
|
||
fetchDrops()
|
||
}, [])
|
||
|
||
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<HTMLInputElement>) => {
|
||
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: parseFloat(formData.ppu),
|
||
imageUrl: imageUrl,
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
// Reset form
|
||
setFormData({
|
||
item: '',
|
||
size: '',
|
||
unit: 'g',
|
||
ppu: '',
|
||
imageFile: null,
|
||
imagePreview: '',
|
||
})
|
||
// 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
|
||
}
|
||
|
||
return (
|
||
<div className="admin-page">
|
||
<div className="container">
|
||
<div className="admin-header">
|
||
<h1>Admin Panel</h1>
|
||
<button onClick={() => router.push('/')}>View Site</button>
|
||
</div>
|
||
|
||
<div className="admin-grid">
|
||
{/* Create Drop Form */}
|
||
<div className="admin-section">
|
||
<h2>Create New Drop</h2>
|
||
<form onSubmit={handleSubmit} className="admin-form">
|
||
<div className="form-group">
|
||
<label htmlFor="item">Product Name</label>
|
||
<input
|
||
type="text"
|
||
id="item"
|
||
value={formData.item}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, item: e.target.value })
|
||
}
|
||
required
|
||
placeholder="e.g. Harlequin – Collective Drop"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-grid">
|
||
<div className="form-group">
|
||
<label htmlFor="size">Batch Size</label>
|
||
<input
|
||
type="number"
|
||
id="size"
|
||
value={formData.size}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, size: e.target.value })
|
||
}
|
||
required
|
||
placeholder="1000"
|
||
min="1"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="unit">Unit</label>
|
||
<input
|
||
type="text"
|
||
id="unit"
|
||
value={formData.unit}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, unit: e.target.value })
|
||
}
|
||
placeholder="g, kg, ml, etc."
|
||
required
|
||
maxLength={12}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="ppu">Price Per Gram (CHF)</label>
|
||
<input
|
||
type="number"
|
||
id="ppu"
|
||
value={formData.ppu}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, ppu: e.target.value })
|
||
}
|
||
required
|
||
placeholder="2.50"
|
||
step="0.01"
|
||
min="0"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="imageFile">Product Image (optional)</label>
|
||
<input
|
||
type="file"
|
||
id="imageFile"
|
||
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||
onChange={handleImageChange}
|
||
className="file-input"
|
||
/>
|
||
{formData.imagePreview && (
|
||
<div className="image-preview">
|
||
<img src={formData.imagePreview} alt="Preview" />
|
||
</div>
|
||
)}
|
||
<p className="file-hint">
|
||
Max file size: 5MB. Allowed formats: JPEG, PNG, WebP
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={submitting || uploadingImage}
|
||
className="cta"
|
||
style={{ width: '100%', marginTop: '20px' }}
|
||
>
|
||
{uploadingImage
|
||
? 'Uploading image...'
|
||
: submitting
|
||
? 'Creating...'
|
||
: 'Create Drop'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Drops List */}
|
||
<div className="admin-section">
|
||
<h2>All Drops</h2>
|
||
{loading ? (
|
||
<p style={{ color: 'var(--muted)' }}>Loading...</p>
|
||
) : !Array.isArray(drops) || drops.length === 0 ? (
|
||
<p style={{ color: 'var(--muted)' }}>No drops yet</p>
|
||
) : (
|
||
<div className="drops-list">
|
||
{drops.map((drop) => (
|
||
<div
|
||
key={drop.id}
|
||
className="drop-card"
|
||
style={{
|
||
background: isSoldOut(drop.fill, drop.size)
|
||
? 'var(--bg-soft)'
|
||
: 'var(--card)',
|
||
opacity: isSoldOut(drop.fill, drop.size) ? 0.7 : 1,
|
||
}}
|
||
>
|
||
<div className="drop-card-header">
|
||
<div className="drop-card-info">
|
||
<h3>{drop.item}</h3>
|
||
<p>
|
||
ID: {drop.id} · Created:{' '}
|
||
{new Date(drop.created_at).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
{isSoldOut(drop.fill, drop.size) && (
|
||
<span className="sold-out-badge">Sold Out</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="drop-card-progress">
|
||
<div className="drop-card-progress-header">
|
||
<span style={{ color: 'var(--muted)' }}>
|
||
{drop.fill}
|
||
{drop.unit} / {drop.size}
|
||
{drop.unit}
|
||
</span>
|
||
<span>
|
||
{getProgressPercentage(drop.fill, drop.size)}%
|
||
</span>
|
||
</div>
|
||
<div className="progress">
|
||
<span
|
||
style={{
|
||
width: `${getProgressPercentage(
|
||
drop.fill,
|
||
drop.size
|
||
)}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="drop-card-price">
|
||
{drop.ppu.toFixed(2)} CHF / {drop.unit}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|