init
This commit is contained in:
324
app/admin/page.tsx
Normal file
324
app/admin/page.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user