import { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import Layout from '../components/Layout'; import PriceChart from '../components/PriceChart'; import { useToast } from '../context/ToastContext'; import { productsApi, pricesApi, settingsApi, ProductWithStats, PriceHistory, NotificationSettings, } from '../api/client'; export default function ProductDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { showToast } = useToast(); const [product, setProduct] = useState(null); const [prices, setPrices] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isSavingNotifications, setIsSavingNotifications] = useState(false); const [error, setError] = useState(''); const [notificationSettings, setNotificationSettings] = useState(null); const [priceDropThreshold, setPriceDropThreshold] = useState(''); const [targetPrice, setTargetPrice] = useState(''); const [notifyBackInStock, setNotifyBackInStock] = useState(false); const REFRESH_INTERVALS = [ { value: 1800, label: '30 minutes' }, { value: 3600, label: '1 hour' }, { value: 7200, label: '2 hours' }, { value: 14400, label: '4 hours' }, { value: 21600, label: '6 hours' }, { value: 43200, label: '12 hours' }, { value: 86400, label: '24 hours' }, ]; const productId = parseInt(id || '0', 10); const fetchData = async (days?: number) => { try { const [productRes, pricesRes] = await Promise.all([ productsApi.getById(productId), pricesApi.getHistory(productId, days), ]); setProduct(productRes.data); setPrices(pricesRes.data.prices); // Initialize notification form fields from product data if (productRes.data.price_drop_threshold !== null && productRes.data.price_drop_threshold !== undefined) { setPriceDropThreshold(productRes.data.price_drop_threshold.toString()); } if (productRes.data.target_price !== null && productRes.data.target_price !== undefined) { setTargetPrice(productRes.data.target_price.toString()); } setNotifyBackInStock(productRes.data.notify_back_in_stock || false); } catch { setError('Failed to load product details'); } finally { setIsLoading(false); } }; const fetchNotificationSettings = async () => { try { const response = await settingsApi.getNotifications(); setNotificationSettings(response.data); } catch { // Silently fail - notifications just won't be shown } }; useEffect(() => { if (productId) { fetchData(30); fetchNotificationSettings(); } }, [productId]); const handleRefresh = async () => { setIsRefreshing(true); try { await pricesApi.refresh(productId); await fetchData(30); showToast('Price refreshed'); } catch { showToast('Failed to refresh price', 'error'); } finally { setIsRefreshing(false); } }; const handleDelete = async () => { if (!confirm('Are you sure you want to stop tracking this product?')) { return; } try { await productsApi.delete(productId); navigate('/'); } catch { showToast('Failed to delete product', 'error'); } }; const handleRangeChange = (days: number | undefined) => { fetchData(days); }; const handleRefreshIntervalChange = async (newInterval: number) => { if (!product) return; setIsSaving(true); try { await productsApi.update(productId, { refresh_interval: newInterval }); setProduct({ ...product, refresh_interval: newInterval }); showToast('Check interval updated'); } catch { showToast('Failed to update refresh interval', 'error'); } finally { setIsSaving(false); } }; const handleSaveNotifications = async () => { if (!product) return; setIsSavingNotifications(true); try { const threshold = priceDropThreshold ? parseFloat(priceDropThreshold) : null; const target = targetPrice ? parseFloat(targetPrice) : null; await productsApi.update(productId, { price_drop_threshold: threshold, target_price: target, notify_back_in_stock: notifyBackInStock, }); setProduct({ ...product, price_drop_threshold: threshold, target_price: target, notify_back_in_stock: notifyBackInStock, }); showToast('Notification settings saved'); } catch { showToast('Failed to save notification settings', 'error'); } finally { setIsSavingNotifications(false); } }; const formatPrice = (price: number | string | null, currency: string | null) => { if (price === null || price === undefined) return 'N/A'; const numPrice = typeof price === 'string' ? parseFloat(price) : price; if (isNaN(numPrice)) return 'N/A'; const currencySymbol = currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : '$'; return `${currencySymbol}${numPrice.toFixed(2)}`; }; if (isLoading) { return (
); } if (error || !product) { return (
{error || 'Product not found'}
Back to Dashboard
); } const priceChange = (() => { if (!product.stats || prices.length < 1) return null; const currentPrice = typeof product.current_price === 'string' ? parseFloat(product.current_price) : (product.current_price || 0); const firstPrice = typeof prices[0].price === 'string' ? parseFloat(prices[0].price) : prices[0].price; if (firstPrice === 0) return null; return (currentPrice - firstPrice) / firstPrice; })(); return (
← Back to Dashboard
{product.image_url ? ( {product.name ) : (
📦
)}

{product.name || 'Unknown Product'}

{product.url}

{product.stock_status === 'out_of_stock' ? (
Currently Unavailable
) : product.stock_status === 'in_stock' ? (
In Stock
) : null}
{product.stock_status === 'out_of_stock' ? 'Price unavailable' : formatPrice(product.current_price, product.currency)}
{priceChange !== null && priceChange !== 0 && ( 0 ? 'up' : 'down'}`} > {priceChange > 0 ? '↑' : '↓'}{' '} {Math.abs(priceChange * 100).toFixed(1)}% since tracking started )}
Last Checked {product.last_checked ? new Date(product.last_checked).toLocaleString() : 'Never'}
Check Interval
Tracking Since {new Date(product.created_at).toLocaleDateString()}
Price Records {product.stats?.price_count || 0}
{notificationSettings && (notificationSettings.telegram_configured || notificationSettings.discord_configured) && ( <>
🔔

Notification Settings

{notificationSettings.telegram_configured && ( Telegram )} {notificationSettings.discord_configured && ( Discord )}

Get notified when this product's price drops or comes back in stock. Notifications will be sent to your configured channels.

setPriceDropThreshold(e.target.value)} placeholder="Enter amount (e.g., 5.00)" /> Notify when price drops by at least this amount ({product.currency || 'USD'})
setTargetPrice(e.target.value)} placeholder="Enter target price (e.g., 49.99)" /> Notify when price drops to or below this amount ({product.currency || 'USD'})
)}
); }