Files
cbd420/app/admin/drops/page.tsx
2025-12-21 17:36:44 +01:00

1200 lines
46 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
image_url: string | null
images?: string[]
created_at: string
start_time: string | null
}
interface Sale {
id: number
drop_id: number
buyer_id: number
size: number
payment_id: string | null
created_at: string
buyer_username?: string
buyer_email?: string
buyer_fullname?: string
buyer_address?: string
buyer_phone?: string
}
export default function DropsManagementPage() {
const router = useRouter()
const [drops, setDrops] = useState<Drop[]>([])
const [loading, setLoading] = useState(true)
const [authenticated, setAuthenticated] = useState(false)
const [editingDrop, setEditingDrop] = useState<Drop | null>(null)
const [creatingDrop, setCreatingDrop] = useState(false)
const [showSalesPopup, setShowSalesPopup] = useState(false)
const [salesForDrop, setSalesForDrop] = useState<Sale[]>([])
const [selectedDropId, setSelectedDropId] = useState<number | null>(null)
const [formData, setFormData] = useState({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
const [imageFiles, setImageFiles] = useState<File[]>([])
const [imagePreviews, setImagePreviews] = useState<string[]>([])
const [existingImages, setExistingImages] = useState<string[]>([])
const [uploadingImage, setUploadingImage] = useState(false)
useEffect(() => {
// Check authentication
fetch('/api/admin/check')
.then((res) => res.json())
.then((data) => {
if (data.authenticated) {
setAuthenticated(true)
fetchDrops()
} else {
router.push('/admin/login')
}
})
.catch(() => {
router.push('/admin/login')
})
}, [router])
const fetchDrops = async () => {
try {
const response = await fetch('/api/drops')
if (response.ok) {
const data = await response.json()
setDrops(Array.isArray(data) ? data : [])
}
} catch (error) {
console.error('Error fetching drops:', error)
} finally {
setLoading(false)
}
}
const handleEdit = async (drop: Drop) => {
setEditingDrop(drop)
setFormData({
item: drop.item,
size: drop.size.toString(),
unit: drop.unit,
ppu: drop.ppu.toString(),
imageUrl: drop.image_url || '',
startTime: drop.start_time ? new Date(drop.start_time).toISOString().slice(0, 16) : '',
})
// Fetch existing images for this drop
try {
const response = await fetch(`/api/drops/images?drop_id=${drop.id}`)
if (response.ok) {
const data = await response.json()
const imageUrls = data.map((img: any) => img.image_url)
setExistingImages(imageUrls)
} else {
setExistingImages(drop.image_url ? [drop.image_url] : [])
}
} catch (error) {
console.error('Error fetching drop images:', error)
setExistingImages(drop.image_url ? [drop.image_url] : [])
}
// Clear file selections
setImageFiles([])
setImagePreviews([])
}
const handleSave = async () => {
if (!editingDrop) return
try {
setUploadingImage(true)
const imageUrls: string[] = [...existingImages]
// Upload new image files
for (const file of imageFiles) {
const uploadFormData = new FormData()
uploadFormData.append('file', file)
const uploadResponse = await fetch('/api/upload', {
method: 'POST',
body: uploadFormData,
})
if (uploadResponse.ok) {
const uploadData = await uploadResponse.json()
imageUrls.push(uploadData.url)
} else {
const error = await uploadResponse.json()
alert(`Image upload failed: ${error.error}`)
setUploadingImage(false)
return
}
}
// Add URL if provided
if (formData.imageUrl) {
imageUrls.push(formData.imageUrl)
}
// Limit to 4 images total
const finalImageUrls = imageUrls.slice(0, 4)
// Update drop basic info
const response = await fetch(`/api/drops/${editingDrop.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: formData.item,
size: parseInt(formData.size),
unit: formData.unit,
ppu: parseInt(formData.ppu),
imageUrl: finalImageUrls[0] || null, // Keep first image for legacy support
startTime: formData.startTime || null,
}),
})
if (!response.ok) {
const error = await response.json()
alert(`Error: ${error.error}`)
setUploadingImage(false)
return
}
// Save images (always save, even if empty array to clear all images)
const imagesResponse = await fetch('/api/drops/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
drop_id: editingDrop.id,
image_urls: finalImageUrls,
}),
})
if (!imagesResponse.ok) {
const error = await imagesResponse.json()
alert(`Error saving images: ${error.error}`)
setUploadingImage(false)
return
}
alert('Drop updated successfully')
setEditingDrop(null)
setImageFiles([])
setImagePreviews([])
setExistingImages([])
// Clean up preview URLs
imagePreviews.forEach(url => URL.revokeObjectURL(url))
fetchDrops()
} catch (error) {
console.error('Error updating drop:', error)
alert('Failed to update drop')
} finally {
setUploadingImage(false)
}
}
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this drop? This will also delete all related sales.')) {
return
}
try {
const response = await fetch(`/api/drops/${id}`, {
method: 'DELETE',
})
if (response.ok) {
alert('Drop deleted successfully')
fetchDrops()
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} catch (error) {
console.error('Error deleting drop:', error)
alert('Failed to delete drop')
}
}
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
if (files.length > 0) {
// Calculate how many more images we can add (max 4 total)
const currentTotal = existingImages.length + imagePreviews.length
const remainingSlots = Math.max(0, 4 - currentTotal)
if (remainingSlots === 0) {
alert('Maximum of 4 images allowed. Please remove some images first.')
// Clear the file input
e.target.value = ''
return
}
// Limit to remaining slots
const limitedFiles = files.slice(0, remainingSlots)
setImageFiles([...imageFiles, ...limitedFiles])
// Create previews for new files
const newPreviews = limitedFiles.map(file => URL.createObjectURL(file))
setImagePreviews([...imagePreviews, ...newPreviews])
// Clear the file input
e.target.value = ''
}
}
const removeImage = (index: number) => {
const newFiles = imageFiles.filter((_, i) => i !== index)
const newPreviews = imagePreviews.filter((_, i) => i !== index)
setImageFiles(newFiles)
setImagePreviews(newPreviews)
// Revoke the URL to free memory
URL.revokeObjectURL(imagePreviews[index])
}
const removeExistingImage = (index: number) => {
const newImages = existingImages.filter((_, i) => i !== index)
setExistingImages(newImages)
}
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
try {
setUploadingImage(true)
const imageUrls: string[] = []
// Add URL if provided
if (formData.imageUrl) {
imageUrls.push(formData.imageUrl)
}
// Upload image files
for (const file of imageFiles) {
const uploadFormData = new FormData()
uploadFormData.append('file', file)
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)
return
}
const uploadData = await uploadResponse.json()
imageUrls.push(uploadData.url)
}
// Limit to 4 images
const finalImageUrls = imageUrls.slice(0, 4)
// 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,
ppu: parseInt(formData.ppu),
imageUrl: finalImageUrls[0] || null, // Keep first image for legacy support
startTime: formData.startTime || null,
}),
})
if (!response.ok) {
const error = await response.json()
alert(`Error: ${error.error}`)
setUploadingImage(false)
return
}
const dropData = await response.json()
const dropId = dropData.id
// Save multiple images if we have more than one
if (finalImageUrls.length > 0) {
const imagesResponse = await fetch('/api/drops/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
drop_id: dropId,
image_urls: finalImageUrls,
}),
})
if (!imagesResponse.ok) {
const error = await imagesResponse.json()
alert(`Drop created but failed to save images: ${error.error}`)
}
}
alert('Drop created successfully')
setFormData({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
setImageFiles([])
setImagePreviews([])
// Clean up preview URLs
imagePreviews.forEach(url => URL.revokeObjectURL(url))
// Clear file input
const fileInput = document.getElementById('imageFiles') as HTMLInputElement
if (fileInput) fileInput.value = ''
setCreatingDrop(false)
fetchDrops()
} catch (error) {
console.error('Error creating drop:', error)
alert('Failed to create drop')
} finally {
setUploadingImage(false)
}
}
const handleViewSales = async (dropId: number) => {
try {
const response = await fetch(`/api/sales/drop/${dropId}`)
if (response.ok) {
const data = await response.json()
setSalesForDrop(Array.isArray(data) ? data : [])
setSelectedDropId(dropId)
setShowSalesPopup(true)
} else {
alert('Failed to fetch sales')
}
} catch (error) {
console.error('Error fetching sales:', error)
alert('Failed to fetch sales')
}
}
const getProgressPercentage = (fill: number, size: number) => {
return Math.min((fill / size) * 100, 100)
}
if (loading) {
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)'
}}>
<p style={{ color: 'var(--muted)' }}>Loading...</p>
</div>
)
}
if (!authenticated) {
return null
}
return (
<div style={{
minHeight: '100vh',
background: 'var(--bg)',
padding: '40px 20px'
}}>
<div className="container" style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<h1>Drops Management</h1>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => {
setCreatingDrop(!creatingDrop)
if (creatingDrop) {
// Reset form when canceling
setFormData({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
setImageFiles([])
setImagePreviews([])
setExistingImages([])
imagePreviews.forEach(url => URL.revokeObjectURL(url))
const fileInput = document.getElementById('imageFiles') as HTMLInputElement
if (fileInput) fileInput.value = ''
}
}}
className="cta"
style={{ padding: '10px 20px', fontSize: '14px' }}
>
{creatingDrop ? 'Cancel' : 'Create New Drop'}
</button>
<button
onClick={() => router.push('/admin')}
style={{
padding: '10px 20px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '8px',
color: 'var(--text)',
cursor: 'pointer',
fontSize: '14px'
}}
>
Back to Dashboard
</button>
</div>
</div>
{creatingDrop && (
<div className="drop-card" style={{ marginBottom: '30px', padding: '20px' }}>
<h2 style={{ marginTop: 0, marginBottom: '20px' }}>Create New Drop</h2>
<form onSubmit={handleCreate}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Item Name *</label>
<input
type="text"
value={formData.item}
onChange={(e) => setFormData({ ...formData, item: e.target.value })}
required
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Size *</label>
<input
type="number"
value={formData.size}
onChange={(e) => setFormData({ ...formData, size: e.target.value })}
required
min="1"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Unit *</label>
<input
type="text"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
required
maxLength={12}
placeholder="g, kg, ml, etc."
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Price Per Unit (cents) *</label>
<input
type="number"
value={formData.ppu}
onChange={(e) => setFormData({ ...formData, ppu: e.target.value })}
required
min="1"
placeholder="2500 = 2.50 CHF"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<label style={{ display: 'block', marginBottom: '8px' }}>
Product Images (up to 4)
</label>
<input
type="file"
id="imageFiles"
accept="image/jpeg,image/jpg,image/png,image/webp"
onChange={handleImageChange}
multiple
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
cursor: 'pointer'
}}
/>
{(imagePreviews.length > 0 || existingImages.length > 0) && (
<div style={{
marginTop: '12px',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '12px'
}}>
{existingImages.map((imgUrl, index) => (
<div key={`existing-${index}`} style={{ position: 'relative' }}>
<img
src={imgUrl}
alt={`Existing ${index + 1}`}
style={{
width: '100%',
height: '150px',
borderRadius: '8px',
objectFit: 'cover',
border: '1px solid var(--border)'
}}
/>
<button
type="button"
onClick={() => removeExistingImage(index)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
background: 'rgba(220, 38, 38, 0.9)',
color: '#fff',
border: 'none',
borderRadius: '4px',
width: '24px',
height: '24px',
cursor: 'pointer',
fontSize: '16px',
lineHeight: '1'
}}
>
×
</button>
</div>
))}
{imagePreviews.map((preview, index) => (
<div key={`preview-${index}`} style={{ position: 'relative' }}>
<img
src={preview}
alt={`Preview ${index + 1}`}
style={{
width: '100%',
height: '150px',
borderRadius: '8px',
objectFit: 'cover',
border: '1px solid var(--border)'
}}
/>
<button
type="button"
onClick={() => removeImage(index)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
background: 'rgba(220, 38, 38, 0.9)',
color: '#fff',
border: 'none',
borderRadius: '4px',
width: '24px',
height: '24px',
cursor: 'pointer',
fontSize: '16px',
lineHeight: '1'
}}
>
×
</button>
</div>
))}
</div>
)}
<p style={{ marginTop: '8px', fontSize: '12px', color: 'var(--muted)' }}>
Max 4 images. Max file size: 5MB each. Allowed formats: JPEG, PNG, WebP
</p>
<p style={{ marginTop: '4px', fontSize: '12px', color: 'var(--muted)' }}>
Or enter an image URL (will be added to uploaded images):
</p>
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({ ...formData, imageUrl: e.target.value })
}}
placeholder="https://example.com/image.jpg"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
marginTop: '8px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Start Time</label>
<input
type="datetime-local"
value={formData.startTime}
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
</div>
<button
type="submit"
className="cta"
disabled={uploadingImage}
style={{ padding: '10px 20px', fontSize: '14px' }}
>
{uploadingImage ? 'Uploading image...' : 'Create Drop'}
</button>
</form>
</div>
)}
{drops.length === 0 ? (
<p style={{ color: 'var(--muted)' }}>No drops found</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{drops.map((drop) => (
<div
key={drop.id}
className="drop-card"
style={{
background: editingDrop?.id === drop.id ? 'var(--bg-soft)' : 'var(--card)',
padding: '20px'
}}
>
{editingDrop?.id === drop.id ? (
<div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Item Name</label>
<input
type="text"
value={formData.item}
onChange={(e) => setFormData({ ...formData, item: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Size</label>
<input
type="number"
value={formData.size}
onChange={(e) => setFormData({ ...formData, size: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Unit</label>
<input
type="text"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
maxLength={12}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Price Per Unit (cents)</label>
<input
type="number"
value={formData.ppu}
onChange={(e) => setFormData({ ...formData, ppu: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Start Time</label>
<input
type="datetime-local"
value={formData.startTime}
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
</div>
<div style={{ gridColumn: '1 / -1', marginTop: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px' }}>
Product Images (up to 4)
</label>
<input
type="file"
id="imageFilesEdit"
accept="image/jpeg,image/jpg,image/png,image/webp"
onChange={handleImageChange}
multiple
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
cursor: 'pointer'
}}
/>
{(imagePreviews.length > 0 || existingImages.length > 0) && (
<div style={{
marginTop: '12px',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '12px'
}}>
{existingImages.map((imgUrl, index) => (
<div key={`existing-${index}`} style={{ position: 'relative' }}>
<img
src={imgUrl}
alt={`Existing ${index + 1}`}
style={{
width: '100%',
height: '150px',
borderRadius: '8px',
objectFit: 'cover',
border: '1px solid var(--border)'
}}
/>
<button
type="button"
onClick={() => removeExistingImage(index)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
background: 'rgba(220, 38, 38, 0.9)',
color: '#fff',
border: 'none',
borderRadius: '4px',
width: '24px',
height: '24px',
cursor: 'pointer',
fontSize: '16px',
lineHeight: '1'
}}
>
×
</button>
</div>
))}
{imagePreviews.map((preview, index) => (
<div key={`preview-${index}`} style={{ position: 'relative' }}>
<img
src={preview}
alt={`Preview ${index + 1}`}
style={{
width: '100%',
height: '150px',
borderRadius: '8px',
objectFit: 'cover',
border: '1px solid var(--border)'
}}
/>
<button
type="button"
onClick={() => removeImage(index)}
style={{
position: 'absolute',
top: '4px',
right: '4px',
background: 'rgba(220, 38, 38, 0.9)',
color: '#fff',
border: 'none',
borderRadius: '4px',
width: '24px',
height: '24px',
cursor: 'pointer',
fontSize: '16px',
lineHeight: '1'
}}
>
×
</button>
</div>
))}
</div>
)}
<p style={{ marginTop: '8px', fontSize: '12px', color: 'var(--muted)' }}>
Max 4 images. Max file size: 5MB each. Allowed formats: JPEG, PNG, WebP
</p>
<p style={{ marginTop: '4px', fontSize: '12px', color: 'var(--muted)' }}>
Or enter an image URL (will be added to uploaded images):
</p>
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({ ...formData, imageUrl: e.target.value })
}}
placeholder="https://example.com/image.jpg"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
marginTop: '8px'
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={handleSave}
className="cta"
disabled={uploadingImage}
style={{ padding: '8px 16px', fontSize: '14px' }}
>
{uploadingImage ? 'Uploading images...' : 'Save'}
</button>
<button
onClick={() => {
setEditingDrop(null)
setImageFiles([])
setImagePreviews([])
setExistingImages([])
// Clean up preview URLs
imagePreviews.forEach(url => URL.revokeObjectURL(url))
}}
style={{
padding: '8px 16px',
fontSize: '14px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '8px',
color: 'var(--text)',
cursor: 'pointer'
}}
>
Cancel
</button>
</div>
</div>
) : (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div style={{ flex: 1 }}>
<h3 style={{ marginBottom: '8px' }}>{drop.item}</h3>
<p style={{ color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
ID: {drop.id} · Size: {drop.size}{drop.unit} · Price: {(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit}
</p>
<p style={{ color: 'var(--muted)', fontSize: '12px', marginBottom: '12px' }}>
Created: {new Date(drop.created_at).toLocaleString()}
{drop.start_time && ` · Start: ${new Date(drop.start_time).toLocaleString()}`}
</p>
{/* Fill Bar */}
<div className="progress" style={{ marginBottom: '8px' }}>
<span style={{ width: `${getProgressPercentage(drop.fill, drop.size)}%` }}></span>
</div>
<p style={{ color: 'var(--muted)', fontSize: '14px', marginBottom: '12px' }}>
{drop.unit === 'kg' ? drop.fill.toFixed(2) : Math.round(drop.fill)}{drop.unit} of {drop.size}{drop.unit} reserved
</p>
{/* Display all images */}
{(() => {
const images = drop.images && drop.images.length > 0
? drop.images
: (drop.image_url ? [drop.image_url] : [])
if (images.length === 0) return null
return (
<div style={{ marginTop: '12px' }}>
<div style={{
display: 'grid',
gridTemplateColumns: images.length > 1 ? 'repeat(auto-fit, minmax(150px, 1fr))' : '1fr',
gap: '8px',
maxWidth: images.length === 1 ? '200px' : '100%'
}}>
{images.slice(0, 4).map((imgUrl, index) => (
<img
key={index}
src={imgUrl}
alt={`${drop.item} - Image ${index + 1}`}
style={{
width: '100%',
height: '150px',
objectFit: 'cover',
borderRadius: '8px',
border: '1px solid var(--border)'
}}
/>
))}
</div>
{images.length > 4 && (
<p style={{
marginTop: '8px',
fontSize: '12px',
color: 'var(--muted)'
}}>
+{images.length - 4} more image{images.length - 4 > 1 ? 's' : ''}
</p>
)}
</div>
)
})()}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button
onClick={() => handleViewSales(drop.id)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
>
View Sales
</button>
<button
onClick={() => handleEdit(drop)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
cursor: 'pointer'
}}
>
Edit
</button>
<button
onClick={() => handleDelete(drop.id)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: '#dc2626',
border: 'none',
borderRadius: '6px',
color: '#fff',
cursor: 'pointer'
}}
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Sales Popup */}
{showSalesPopup && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={() => setShowSalesPopup(false)}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '800px',
width: '100%',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: 0 }}>Sales for Drop #{selectedDropId}</h2>
<button
onClick={() => setShowSalesPopup(false)}
style={{
background: 'transparent',
border: 'none',
color: 'var(--text)',
fontSize: '24px',
cursor: 'pointer',
padding: '0',
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
{salesForDrop.length === 0 ? (
<p style={{ color: 'var(--muted)', textAlign: 'center', padding: '40px' }}>
No sales found for this drop
</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{salesForDrop.map((sale) => (
<div
key={sale.id}
style={{
background: 'var(--bg-soft)',
padding: '16px',
borderRadius: '8px',
border: '1px solid var(--border)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<p style={{ margin: 0, marginBottom: '4px', fontWeight: '600' }}>
Sale #{sale.id}
</p>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
Buyer: {sale.buyer_username || `#${sale.buyer_id}`} ({sale.buyer_email || 'N/A'})
</p>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
Size: {sale.size}g
</p>
{(sale.buyer_fullname || sale.buyer_address || sale.buyer_phone) && (
<div style={{
background: 'var(--card)',
padding: '10px',
borderRadius: '6px',
marginTop: '8px',
marginBottom: '8px'
}}>
<p style={{ margin: 0, color: 'var(--text)', fontSize: '12px', fontWeight: '600', marginBottom: '4px' }}>
Delivery Information:
</p>
{sale.buyer_fullname && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Name: {sale.buyer_fullname}
</p>
)}
{sale.buyer_address && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Address: {sale.buyer_address}
</p>
)}
{sale.buyer_phone && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Phone: {sale.buyer_phone}
</p>
)}
</div>
)}
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px' }}>
{new Date(sale.created_at).toLocaleString()}
{sale.payment_id && ` · Payment ID: ${sale.payment_id}`}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</div>
)
}