'use client' import { useState, useEffect, Suspense } from 'react' import Image from 'next/image' import AuthModal from './AuthModal' import UnlockModal from './UnlockModal' import { useI18n } from '@/lib/i18n' interface DropData { id: number item: string description?: string | null size: number fill: number unit: string ppu: number price_chf?: number | null price_eur?: number | null wholesale_price_chf?: number | null wholesale_price_eur?: number | null image_url: string | null images?: string[] // Array of image URLs (up to 4) 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 referral_points?: number } export default function Drop() { const { t, language } = useI18n() const [drop, setDrop] = useState(null) const [loading, setLoading] = useState(true) const [selectedSize, setSelectedSize] = useState(50) const [customQuantity, setCustomQuantity] = useState('') const [quantityError, setQuantityError] = useState('') 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) const [isWholesaleUnlocked, setIsWholesaleUnlocked] = useState(false) const [showUnlockModal, setShowUnlockModal] = useState(false) const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [shippingFee, setShippingFee] = useState(null) const [loadingShippingFee, setLoadingShippingFee] = useState(false) // Currency is based on language: English (en) โ†’ EUR, German (de) โ†’ CHF const currency: 'CHF' | 'EUR' = language === 'de' ? 'CHF' : 'EUR' const [referralPoints, setReferralPoints] = useState(0) const [pointsToEur, setPointsToEur] = useState(100) const [pointsToChf, setPointsToChf] = useState(100) // Keep for backward compatibility const [pointsToUse, setPointsToUse] = useState(0) const [loadingPoints, setLoadingPoints] = useState(false) useEffect(() => { fetchActiveDrop() checkAuth() checkWholesaleStatus() fetchShippingFee() // Fetch currency info on mount fetchReferralPoints() // Poll active drop every 30 seconds const interval = setInterval(() => { fetchActiveDrop() }, 30000) // 30 seconds return () => clearInterval(interval) }, []) // Fetch referral points when user is authenticated useEffect(() => { if (user) { fetchReferralPoints() } else { setReferralPoints(0) setPointsToUse(0) } }, [user]) const checkWholesaleStatus = async () => { try { const response = await fetch('/api/referrals/status', { credentials: 'include', }) if (response.ok) { const data = await response.json() setIsWholesaleUnlocked(data.isUnlocked || false) } } catch (error) { console.error('Error checking wholesale status:', error) } } const fetchReferralPoints = async () => { if (!user) { setReferralPoints(0) setPointsToUse(0) return } setLoadingPoints(true) try { const response = await fetch('/api/referral-points', { credentials: 'include', }) if (response.ok) { const data = await response.json() setReferralPoints(data.referral_points || 0) // Use EUR-based setting (preferred), fallback to CHF converted to EUR if (data.points_to_eur) { setPointsToEur(data.points_to_eur) } else { // Convert CHF to EUR (1 CHF โ‰ˆ 1.0309 EUR) setPointsToEur((data.points_to_chf || 100) / 1.030927835) } setPointsToChf(data.points_to_chf || 100) // Keep for backward compatibility } } catch (error) { console.error('Error fetching referral points:', error) } finally { setLoadingPoints(false) } } // 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', { // Add cache control to prevent stale data cache: 'no-store', }) 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 getRemainingInGrams = () => { if (!drop) return 0 if (drop.unit === 'kg') { return (drop.size - drop.fill) * 1000 } return drop.size - drop.fill } const getMinimumGrams = () => { if (!drop) return 0 // Minimum price is 5 in user's currency // Calculate minimum grams needed for 5 (EUR or CHF) const pricePerGram = getPricePerGram() const minPrice = 5 // 5 in user's currency return Math.ceil(minPrice / pricePerGram) } const handleCustomQuantityChange = (value: string) => { setCustomQuantity(value) setQuantityError('') // Clear selected preset size when custom input is used if (value.trim() !== '') { const numValue = parseInt(value, 10) if (!isNaN(numValue) && numValue > 0) { setSelectedSize(numValue) } } } const validateCustomQuantity = () => { if (!drop || !customQuantity.trim()) { setQuantityError('') return true } const numValue = parseInt(customQuantity, 10) const remaining = getRemainingInGrams() const minimum = getMinimumGrams() if (isNaN(numValue) || numValue <= 0) { setQuantityError(t('drop.enterValidNumber')) return false } if (numValue < minimum) { setQuantityError(t('drop.minimumRequired', { minimum })) return false } if (numValue > remaining) { setQuantityError(t('drop.maximumAvailable', { maximum: remaining })) return false } setQuantityError('') return true } const handleQuantityButtonClick = (size: number) => { setSelectedSize(size) setCustomQuantity('') setQuantityError('') } 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 fetchShippingFee = async () => { setLoadingShippingFee(true) try { const response = await fetch('/api/shipping-fee', { credentials: 'include', }) if (response.ok) { const data = await response.json() setShippingFee(data.shipping_fee || 40) // Currency is now based on language, not geolocation } else { // Default to 40 EUR if fetch fails setShippingFee(40) } } catch (error) { console.error('Error fetching shipping fee:', error) // Default to 40 EUR on error setShippingFee(40) } finally { setLoadingShippingFee(false) } } // Get price per gram based on user's currency and wholesale status const getPricePerGramFromDrop = (): number => { if (!drop) return 0 // Use new price fields if available, otherwise fall back to ppu calculation if (isWholesaleUnlocked) { // Wholesale price if (currency === 'CHF' && drop.wholesale_price_chf != null) { return Number(drop.wholesale_price_chf) || 0 } else if (currency === 'EUR' && drop.wholesale_price_eur != null) { return Number(drop.wholesale_price_eur) || 0 } // Fallback to ppu calculation if new fields not set const pricePerGramEur = Number(drop.ppu) / 1000 return currency === 'CHF' ? pricePerGramEur * 0.97 : pricePerGramEur } else { // Regular price if (currency === 'CHF' && drop.price_chf != null) { return Number(drop.price_chf) || 0 } else if (currency === 'EUR' && drop.price_eur != null) { return Number(drop.price_eur) || 0 } // Fallback to ppu calculation if new fields not set const pricePerGramEur = Number(drop.ppu) / 1000 return currency === 'CHF' ? pricePerGramEur * 0.97 : pricePerGramEur } } const handleJoinDrop = () => { // Validate custom quantity if entered if (customQuantity && !validateCustomQuantity()) { return } // Check if user is logged in if (!user) { setShowAuthModal(true) return } // Fetch available currencies, buyer data, and shipping fee when opening confirm modal fetchAvailableCurrencies() fetchBuyerData() fetchShippingFee() setShowConfirmModal(true) } const handleLogin = (loggedInUser: User) => { setUser(loggedInUser) setShowAuthModal(false) // After login, fetch buyer data and show the confirmation modal fetchAvailableCurrencies() fetchBuyerData() fetchShippingFee() setShowConfirmModal(true) } const handleConfirmPurchase = async () => { if (!drop) return // Validate buyer data fields if (!buyerFullname.trim() || !buyerAddress.trim() || !buyerPhone.trim()) { setErrorMessage(t('drop.fillDeliveryInfo')) 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 points_to_use: pointsToUse, // Points to use for discount currency: currency, // Display currency based on language (EUR for en, CHF for de) }), }) 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() // Refresh referral points if any were used if (pointsToUse > 0) { fetchReferralPoints() setPointsToUse(0) // Reset points used } // 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) setPointsToUse(0) // Reset points when canceling } const handlePointsToUseChange = (value: string) => { const numValue = parseFloat(value) || 0 // Calculate max points based on EUR price (universal base) const priceEur = currency === 'CHF' ? calculatePriceBeforeDiscount() / 0.97 : calculatePriceBeforeDiscount() const maxPoints = Math.min(referralPoints, priceEur * pointsToEur) setPointsToUse(Math.max(0, Math.min(numValue, maxPoints))) } const calculatePriceBeforeDiscount = () => { if (!drop) return 0 const pricePerGram = getPricePerGramFromDrop() return selectedSize * pricePerGram } const getMaxDiscountFromPoints = () => { if (pointsToEur === 0) return 0 // Calculate discount in EUR (universal base) const discountEur = referralPoints / pointsToEur // Convert to user's currency for display if (currency === 'CHF') { return discountEur * 0.97 // Convert EUR to CHF } else { return discountEur // Already in EUR } } const calculateDiscountFromPoints = () => { if (pointsToUse === 0 || pointsToEur === 0) return 0 // Calculate discount in EUR first (universal base) const discountEur = pointsToUse / pointsToEur // Convert to user's currency if (currency === 'CHF') { return discountEur * 0.97 // Convert EUR to CHF } else { return discountEur // Already in EUR } } const calculatePrice = () => { if (!drop) return 0 const pricePerGram = getPricePerGramFromDrop() const price = selectedSize * pricePerGram // Apply points discount const discount = calculateDiscountFromPoints() return Math.max(0, price - discount) } const calculateStandardPrice = (): number => { if (!drop) return 0 // Get regular price (not wholesale) let pricePerGram: number if (currency === 'CHF' && drop.price_chf != null) { pricePerGram = Number(drop.price_chf) || 0 } else if (currency === 'EUR' && drop.price_eur != null) { pricePerGram = Number(drop.price_eur) || 0 } else { // Fallback to ppu calculation const pricePerGramEur = Number(drop.ppu) / 1000 pricePerGram = pricePerGramEur * (currency === 'CHF' ? 0.97 : 1) } return selectedSize * pricePerGram } const calculateWholesalePrice = (): number => { if (!drop) return 0 // Get wholesale price let pricePerGram: number if (currency === 'CHF' && drop.wholesale_price_chf != null) { pricePerGram = Number(drop.wholesale_price_chf) || 0 } else if (currency === 'EUR' && drop.wholesale_price_eur != null) { pricePerGram = Number(drop.wholesale_price_eur) || 0 } else { // Fallback to ppu calculation const pricePerGramEur = Number(drop.ppu) / 1000 pricePerGram = (pricePerGramEur * 0.76) * (currency === 'CHF' ? 0.97 : 1) } return selectedSize * pricePerGram } // Get price per gram in user's currency (regular price) const getPricePerGram = (): number => { if (!drop) return 0 if (currency === 'CHF' && drop.price_chf != null) { return Number(drop.price_chf) || 0 } else if (currency === 'EUR' && drop.price_eur != null) { return Number(drop.price_eur) || 0 } // Fallback to ppu calculation const pricePerGramEur = Number(drop.ppu) / 1000 return currency === 'CHF' ? pricePerGramEur * 0.97 : pricePerGramEur } // Get wholesale price per gram in user's currency const getWholesalePricePerGram = (): number => { if (!drop) return 0 if (currency === 'CHF' && drop.wholesale_price_chf != null) { return Number(drop.wholesale_price_chf) || 0 } else if (currency === 'EUR' && drop.wholesale_price_eur != null) { return Number(drop.wholesale_price_eur) || 0 } // Fallback to ppu calculation const pricePerGramEur = Number(drop.ppu) / 1000 return (pricePerGramEur * 0.76) * (currency === 'CHF' ? 0.97 : 1) } 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) { const dayText = diffDays === 1 ? t('drop.day') : t('drop.days') const hourText = diffHours > 0 ? (diffHours === 1 ? t('drop.hour') : t('drop.hours')) : '' return `${diffDays} ${dayText}${diffHours > 0 ? ` ${diffHours} ${hourText}` : ''}` } else if (diffHours > 0) { const hourText = diffHours === 1 ? t('drop.hour') : t('drop.hours') const minuteText = diffMinutes > 0 ? (diffMinutes === 1 ? t('drop.minute') : t('drop.minutes')) : '' return `${diffHours} ${hourText}${diffMinutes > 0 ? ` ${diffMinutes} ${minuteText}` : ''}` } else { const minuteText = diffMinutes === 1 ? t('drop.minute') : t('drop.minutes') return `${diffMinutes} ${minuteText}` } } // Get images array (prioritize new images array, fallback to legacy image_url) // Must be defined before early returns to maintain hook order const images = drop?.images && drop.images.length > 0 ? drop.images : (drop?.image_url ? [drop.image_url] : []) // Reset selected image index when images change // Must be called before early returns to maintain hook order useEffect(() => { if (images.length > 0 && selectedImageIndex >= images.length) { setSelectedImageIndex(0) } else if (images.length === 0) { setSelectedImageIndex(0) } }, [images, selectedImageIndex]) if (loading) { return (

{t('drop.loading')}

) } if (!drop) { return (

{t('drop.dropSoldOut')}

{t('drop.fullyReserved')}

{t('drop.nextDropComingSoon')}

) } const progressPercentage = getProgressPercentage(drop.fill, drop.size) // Calculate separate percentages for sales and pending const salesFill = Number(drop.sales_fill) || 0 const pendingFill = Number(drop.pending_fill) || 0 const salesPercentage = getProgressPercentage(salesFill, drop.size) const pendingPercentage = getProgressPercentage(pendingFill, 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 (
{images.length > 0 ? (
{/* Main large image */}
{`${drop.item}
{/* Thumbnails */} {images.length > 1 && (
{images.slice(0, 4).map((imgUrl, index) => ( ))}
)}
) : (
{t('common.noImage')}
)}

{drop.item}

{drop.description && (
{drop.description}
)}
{formatSize(drop.size, drop.unit)} {t('drop.batch')}
{(() => { // Get prices in user's currency const pricePerGram = getPricePerGram(); const wholesalePricePerGram = getWholesalePricePerGram(); if (isWholesaleUnlocked) { return ( <> {t('drop.wholesalePriceLabel')} {wholesalePricePerGram.toFixed(2)} {currency} / g {t('drop.standard')}: {pricePerGram.toFixed(2)} {currency} / g ); } return ( <> {t('drop.standardPriceLabel')} {pricePerGram.toFixed(2)} {currency} / g {t('drop.wholesale')}: {wholesalePricePerGram.toFixed(2)} {currency} / g ๐Ÿ”’ { e.preventDefault(); setShowUnlockModal(true); }}>{t('drop.unlock')}
{t('drop.unlockOnce')}
); })()}
{isUpcoming ? (

{t('drop.dropStartsIn')} {timeUntilStart}

) : ( <>
{salesPercentage > 0 && ( )} {pendingPercentage > 0 && ( )}
{(() => { const fillDisplay = drop.unit === 'kg' ? Math.round(drop.fill * 1000) : Math.round(drop.fill); const sizeDisplay = drop.unit === 'kg' ? Math.round(drop.size * 1000) : drop.size; return `${fillDisplay}g ${t('drop.of')} ${sizeDisplay}g ${t('drop.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} {t('drop.onHold')}
) })()} )} {!isUpcoming && hasRemaining && ( <>
{availableSizes.length > 0 && (
{availableSizes.map((size) => ( ))}
)}
0 ? '1' : '100%', minWidth: '150px' }}> handleCustomQuantityChange(e.target.value)} onBlur={validateCustomQuantity} placeholder={t('drop.custom')} min={getMinimumGrams()} max={getRemainingInGrams()} style={{ width: '100%', padding: '14px', borderRadius: '12px', border: `1px solid ${quantityError ? '#dc2626' : 'var(--border)'}`, background: 'var(--bg-soft)', color: 'var(--text)', fontSize: '14px', }} /> {quantityError && (
{quantityError}
)} {!quantityError && customQuantity && (
{t('drop.min')}: {getMinimumGrams()}g ยท {t('drop.max')}: {getRemainingInGrams()}g
)}
{isWholesaleUnlocked ? ( <>
{t('drop.total')}: {calculatePrice().toFixed(2)} {currency}
{t('drop.standardTotal')}: {calculateStandardPrice().toFixed(2)} {currency}
) : ( <>
{t('drop.total')}: {calculateStandardPrice().toFixed(2)} {currency}
{t('drop.wholesaleTotal')}: {calculateWholesalePrice().toFixed(2)} {currency} ๐Ÿ”’
)}
{t('drop.noSubscription')}
)} {hasRemaining && availableSizes.length === 0 && (

{t('drop.lessThanRemaining', { amount: 50, unit: drop.unit })}

)} {!hasRemaining && (

{t('drop.fullyReservedText')}

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

{t('drop.confirmPurchase')}

{t('drop.item')}: {drop.item}

{t('drop.quantity')}: {selectedSize}g

{t('drop.pricePerUnit', { unit: drop.unit })}: {getPricePerGram().toFixed(2)} {currency}

{/* Delivery Information */}

{t('drop.deliveryInformation')}

setBuyerFullname(e.target.value)} placeholder={t('drop.enterFullName')} 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', }} />