'use client' import { useState, useEffect } from 'react' import Image from 'next/image' import AuthModal from './AuthModal' interface DropData { id: number item: string size: number fill: number unit: string ppu: number image_url: string | null created_at: string start_time: string | null is_upcoming?: boolean sales_fill?: number // Only confirmed sales pending_fill?: number // Items on hold (pending orders) } interface User { id: number username: string email: string } export default function Drop() { const [drop, setDrop] = useState(null) const [loading, setLoading] = useState(true) const [selectedSize, setSelectedSize] = useState(50) const [showConfirmModal, setShowConfirmModal] = useState(false) const [showAuthModal, setShowAuthModal] = useState(false) const [processing, setProcessing] = useState(false) const [user, setUser] = useState(null) const [checkingAuth, setCheckingAuth] = useState(true) useEffect(() => { fetchActiveDrop() checkAuth() }, []) const checkAuth = async () => { try { const response = await fetch('/api/auth/session', { credentials: 'include', }) if (response.ok) { const data = await response.json() setUser(data.user) } } catch (error) { console.error('Error checking auth:', error) } finally { setCheckingAuth(false) } } const fetchActiveDrop = async () => { try { const response = await fetch('/api/drops/active') if (response.ok) { const data = await response.json() // Handle both null response and actual drop data setDrop(data) // data can be null if no active drop } else { // If response is not ok, log the error const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) console.error('Error fetching active drop:', errorData) setDrop(null) } } catch (error) { console.error('Error fetching active drop:', error) setDrop(null) } finally { setLoading(false) } } const getProgressPercentage = (fill: number, size: number) => { return Math.min((fill / size) * 100, 100) } const formatSize = (size: number, unit: string) => { if (unit === 'g' && size >= 1000) { return `${(size / 1000).toFixed(1)}kg` } return `${size}${unit}` } const getAvailableSizes = () => { if (!drop) return [] const sizes = [50, 100, 250] // Always in grams // Calculate remaining inventory in grams let remainingInGrams = 0 if (drop.unit === 'kg') { remainingInGrams = (drop.size - drop.fill) * 1000 } else { // For 'g' or any other unit, assume same unit remainingInGrams = drop.size - drop.fill } // Only show sizes that don't exceed remaining inventory return sizes.filter((size) => size <= remainingInGrams) } const handleJoinDrop = () => { // Check if user is logged in if (!user) { setShowAuthModal(true) return } setShowConfirmModal(true) } const handleLogin = (loggedInUser: User) => { setUser(loggedInUser) setShowAuthModal(false) // After login, show the confirmation modal setShowConfirmModal(true) } const handleConfirmPurchase = async () => { if (!drop) return setProcessing(true) try { // Create NOWPayments invoice and sale record const response = await fetch('/api/payments/create-invoice', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', // Important for cookies body: JSON.stringify({ drop_id: drop.id, size: selectedSize, // Size in grams }), }) if (!response.ok) { const error = await response.json() if (response.status === 401) { // User not authenticated - show login modal setShowConfirmModal(false) setShowAuthModal(true) setProcessing(false) return } alert(`Error: ${error.error || 'Failed to create payment invoice'}`) setProcessing(false) return } const data = await response.json() // Close modal setShowConfirmModal(false) // Redirect to NOWPayments invoice if (data.invoice_url) { window.location.href = data.invoice_url } else { alert('Payment invoice created but no redirect URL received') await fetchActiveDrop() } } catch (error) { console.error('Error creating payment invoice:', error) alert('Failed to create payment invoice. Please try again.') setProcessing(false) } } const handleCancelPurchase = () => { setShowConfirmModal(false) } const calculatePrice = () => { if (!drop) return 0 // ppu is stored as integer where 1000 = $1.00, so divide by 1000 to get actual price const pricePerUnit = drop.ppu / 1000 if (drop.unit === 'kg') { return (selectedSize / 1000) * pricePerUnit } return selectedSize * pricePerUnit } const getTimeUntilStart = () => { if (!drop || !drop.is_upcoming || !drop.start_time) return null const startTime = new Date(drop.start_time) const now = new Date() const diffMs = startTime.getTime() - now.getTime() if (diffMs <= 0) return null const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) if (diffDays > 0) { return `${diffDays} day${diffDays > 1 ? 's' : ''}${diffHours > 0 ? ` ${diffHours} hour${diffHours > 1 ? 's' : ''}` : ''}` } else if (diffHours > 0) { return `${diffHours} hour${diffHours > 1 ? 's' : ''}${diffMinutes > 0 ? ` ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}` : ''}` } else { return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}` } } if (loading) { return (

Loading...

) } if (!drop) { return (

Drop Sold Out

The current collective drop has been fully reserved.

Next collective drop coming soon.

) } const progressPercentage = getProgressPercentage(drop.fill, drop.size) const availableSizes = getAvailableSizes() const timeUntilStart = getTimeUntilStart() const isUpcoming = drop.is_upcoming && timeUntilStart // Calculate remaining in the drop's unit const remaining = drop.size - drop.fill const hasRemaining = remaining > 0 return (
{drop.image_url ? ( {drop.item} ) : (
No Image
)}

{drop.item}

{formatSize(drop.size, drop.unit)} Batch
{(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit} ยท incl. 2.5% VAT
{isUpcoming ? (

Drop starts in {timeUntilStart}

) : ( <>
{drop.unit === 'kg' ? drop.fill.toFixed(2) : Math.round(drop.fill)} {drop.unit} of {drop.size} {drop.unit} reserved
{(() => { const pendingFill = Number(drop.pending_fill) || 0; console.log(`pending fill:${pendingFill}`) return pendingFill > 0 && (
{drop.unit === 'kg' ? pendingFill.toFixed(2) : Math.round(pendingFill)} {drop.unit} on hold (10 min checkout window)
) })()} )} {!isUpcoming && hasRemaining && availableSizes.length > 0 && ( <>
{availableSizes.map((size) => ( ))}
)} {hasRemaining && availableSizes.length === 0 && (

Less than 50{drop.unit} remaining. This drop is almost fully reserved.

)} {!hasRemaining && (

This drop is fully reserved

)}
{/* Confirmation Modal */} {showConfirmModal && drop && (
e.stopPropagation()} >

Confirm Purchase

Item: {drop.item}

Quantity: {selectedSize}g

Price per {drop.unit}: {(drop.ppu / 1000).toFixed(2)} CHF

Total: {calculatePrice().toFixed(2)} CHF

incl. 2.5% VAT

)} {/* Auth Modal */} setShowAuthModal(false)} onLogin={handleLogin} />
) }