This commit is contained in:
2025-12-20 10:32:36 +05:30
commit 91c68831bf
23 changed files with 2414 additions and 0 deletions

324
app/admin/page.tsx Normal file
View 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>
)
}