'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 [selectedCurrency, setSelectedCurrency] = useState('btc') const [availableCurrencies, setAvailableCurrencies] = useState([]) const [loadingCurrencies, setLoadingCurrencies] = useState(false) const [buyerFullname, setBuyerFullname] = useState('') const [buyerAddress, setBuyerAddress] = useState('') const [buyerPhone, setBuyerPhone] = useState('') const [showConfirmModal, setShowConfirmModal] = useState(false) const [showAuthModal, setShowAuthModal] = useState(false) const [showPaymentModal, setShowPaymentModal] = useState(false) const [showSuccessModal, setShowSuccessModal] = useState(false) const [showErrorModal, setShowErrorModal] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [paymentData, setPaymentData] = useState(null) const [processing, setProcessing] = useState(false) const [user, setUser] = useState(null) const [checkingAuth, setCheckingAuth] = useState(true) useEffect(() => { fetchActiveDrop() checkAuth() }, []) // Poll payment status when payment modal is open useEffect(() => { if (!showPaymentModal || !paymentData?.payment_id) return const checkPaymentStatus = async () => { try { const response = await fetch(`/api/payments/check-status?payment_id=${paymentData.payment_id}`, { credentials: 'include', }) if (response.ok) { const status = await response.json() // If pending order is gone and sale exists, payment was processed if (!status.has_pending_order && status.has_sale) { // Close payment modal setShowPaymentModal(false) // Show success modal setShowSuccessModal(true) // Refresh drop data await fetchActiveDrop() } } } catch (error) { console.error('Error checking payment status:', error) } } // Check immediately, then poll every 3 seconds checkPaymentStatus() const interval = setInterval(checkPaymentStatus, 3000) return () => clearInterval(interval) }, [showPaymentModal, paymentData?.payment_id]) 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 fetchAvailableCurrencies = async () => { setLoadingCurrencies(true) try { const response = await fetch('/api/payments/currencies') if (response.ok) { const data = await response.json() // When fixed_rate=true, API returns objects with { currency, min_amount, max_amount } // When fixed_rate=false, API returns array of strings const currencies: string[] = [] if (Array.isArray(data.currencies)) { data.currencies.forEach((c: any) => { let currencyCode: string | null = null // Handle object format (when fixed_rate=true) if (typeof c === 'object' && c !== null && c.currency) { currencyCode = String(c.currency).trim().toLowerCase() } // Handle string format (when fixed_rate=false) else if (typeof c === 'string') { currencyCode = c.trim().toLowerCase() } // Add to array if valid if (currencyCode && currencyCode.length > 0) { currencies.push(currencyCode) } }) } setAvailableCurrencies(currencies) // Set default to BTC if available, otherwise first currency if (currencies.length > 0) { const defaultCurrency = currencies.includes('btc') ? 'btc' : currencies[0] setSelectedCurrency(defaultCurrency) } } } catch (error) { console.error('Error fetching currencies:', error) } finally { setLoadingCurrencies(false) } } const fetchBuyerData = async () => { try { const response = await fetch('/api/buyer-data', { credentials: 'include', }) if (response.ok) { const data = await response.json() if (data.buyer_data) { // Autofill form fields with existing buyer data setBuyerFullname(data.buyer_data.fullname || '') setBuyerAddress(data.buyer_data.address || '') setBuyerPhone(data.buyer_data.phone || '') } } } catch (error) { console.error('Error fetching buyer data:', error) } } const handleJoinDrop = () => { // Check if user is logged in if (!user) { setShowAuthModal(true) return } // Fetch available currencies and buyer data when opening confirm modal fetchAvailableCurrencies() fetchBuyerData() setShowConfirmModal(true) } const handleLogin = (loggedInUser: User) => { setUser(loggedInUser) setShowAuthModal(false) // After login, fetch buyer data and show the confirmation modal fetchAvailableCurrencies() fetchBuyerData() setShowConfirmModal(true) } const handleConfirmPurchase = async () => { if (!drop) return // Validate buyer data fields if (!buyerFullname.trim() || !buyerAddress.trim() || !buyerPhone.trim()) { setErrorMessage('Please fill in all delivery information (full name, address, and phone)') setShowErrorModal(true) return } setProcessing(true) try { // First, get or create buyer_data const buyerDataResponse = await fetch('/api/buyer-data/get-or-create', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ fullname: buyerFullname.trim(), address: buyerAddress.trim(), phone: buyerPhone.trim(), }), }) if (!buyerDataResponse.ok) { const error = await buyerDataResponse.json() setErrorMessage(error.error || 'Failed to save delivery information') setShowErrorModal(true) setProcessing(false) return } const buyerData = await buyerDataResponse.json() // Create NOWPayments payment 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 pay_currency: selectedCurrency, // Selected payment currency buyer_data_id: buyerData.buyer_data_id, // Buyer delivery data ID }), }) 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 } // Show error modal instead of alert setErrorMessage(error.error || 'Failed to create payment') setShowErrorModal(true) setShowConfirmModal(false) setProcessing(false) return } const data = await response.json() // Close confirmation modal setShowConfirmModal(false) setProcessing(false) // Show payment modal with payment details if (data.pay_address) { setPaymentData(data) setShowPaymentModal(true) } else { setErrorMessage('Payment created but no payment address received') setShowErrorModal(true) await fetchActiveDrop() } } catch (error) { console.error('Error creating payment invoice:', error) setErrorMessage('Failed to create payment invoice. Please try again.') setShowErrorModal(true) 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

{/* Delivery Information */}

Delivery Information

setBuyerFullname(e.target.value)} placeholder="Enter your full name" required style={{ width: '100%', padding: '12px', background: 'var(--bg-soft)', border: '1px solid var(--border)', borderRadius: '8px', fontSize: '14px', color: 'var(--text)', boxSizing: 'border-box', }} />