Files
cbd420/app/admin/page.tsx
2025-12-20 10:32:36 +05:30

325 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}