diff --git a/src/modules/trading/components/AlertsPanel.tsx b/src/modules/trading/components/AlertsPanel.tsx new file mode 100644 index 0000000..d97ef26 --- /dev/null +++ b/src/modules/trading/components/AlertsPanel.tsx @@ -0,0 +1,457 @@ +/** + * AlertsPanel Component + * Manages price alerts for trading symbols + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + BellIcon, + BellAlertIcon, + PlusIcon, + TrashIcon, + ArrowPathIcon, + CheckCircleIcon, + XCircleIcon, + ArrowUpIcon, + ArrowDownIcon, +} from '@heroicons/react/24/solid'; +import { + getAlerts, + createAlert, + deleteAlert, + enableAlert, + disableAlert, + getAlertStats, + type PriceAlert, + type AlertCondition, + type AlertStats, + type CreateAlertInput, +} from '../../../services/trading.service'; + +interface AlertsPanelProps { + symbol: string; + currentPrice?: number; +} + +export const AlertsPanel: React.FC = ({ symbol, currentPrice }) => { + const [alerts, setAlerts] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showForm, setShowForm] = useState(false); + const [filterActive, setFilterActive] = useState(undefined); + + // Form state + const [formPrice, setFormPrice] = useState(''); + const [formCondition, setFormCondition] = useState('above'); + const [formNote, setFormNote] = useState(''); + const [formNotifyPush, setFormNotifyPush] = useState(true); + const [formNotifyEmail, setFormNotifyEmail] = useState(false); + const [formIsRecurring, setFormIsRecurring] = useState(false); + const [submitting, setSubmitting] = useState(false); + + // Fetch alerts + const fetchAlerts = useCallback(async () => { + setLoading(true); + setError(null); + try { + const [alertsData, statsData] = await Promise.all([ + getAlerts({ isActive: filterActive }), + getAlertStats(), + ]); + setAlerts(alertsData); + setStats(statsData); + } catch (err) { + setError('Failed to fetch alerts'); + console.error('Alerts fetch error:', err); + } finally { + setLoading(false); + } + }, [filterActive]); + + useEffect(() => { + fetchAlerts(); + }, [fetchAlerts]); + + // Set current price when showing form + useEffect(() => { + if (showForm && currentPrice) { + setFormPrice(currentPrice.toFixed(2)); + } + }, [showForm, currentPrice]); + + // Handle create alert + const handleCreateAlert = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formPrice || isNaN(parseFloat(formPrice))) return; + + setSubmitting(true); + try { + const input: CreateAlertInput = { + symbol: symbol.toUpperCase(), + condition: formCondition, + price: parseFloat(formPrice), + note: formNote || undefined, + notifyPush: formNotifyPush, + notifyEmail: formNotifyEmail, + isRecurring: formIsRecurring, + }; + await createAlert(input); + setShowForm(false); + setFormPrice(''); + setFormNote(''); + setFormCondition('above'); + setFormIsRecurring(false); + fetchAlerts(); + } catch (err) { + setError('Failed to create alert'); + } finally { + setSubmitting(false); + } + }; + + // Handle toggle alert + const handleToggleAlert = async (alert: PriceAlert) => { + try { + if (alert.isActive) { + await disableAlert(alert.id); + } else { + await enableAlert(alert.id); + } + fetchAlerts(); + } catch (err) { + setError('Failed to toggle alert'); + } + }; + + // Handle delete alert + const handleDeleteAlert = async (alertId: string) => { + try { + await deleteAlert(alertId); + fetchAlerts(); + } catch (err) { + setError('Failed to delete alert'); + } + }; + + // Get condition display + const getConditionDisplay = (condition: AlertCondition) => { + switch (condition) { + case 'above': + return { text: 'Price Above', icon: ArrowUpIcon, color: 'text-green-400' }; + case 'below': + return { text: 'Price Below', icon: ArrowDownIcon, color: 'text-red-400' }; + case 'crosses_above': + return { text: 'Crosses Above', icon: ArrowUpIcon, color: 'text-green-400' }; + case 'crosses_below': + return { text: 'Crosses Below', icon: ArrowDownIcon, color: 'text-red-400' }; + } + }; + + // Filter alerts for current symbol + const symbolAlerts = alerts.filter((a) => a.symbol === symbol.toUpperCase()); + const otherAlerts = alerts.filter((a) => a.symbol !== symbol.toUpperCase()); + + return ( +
+ {/* Header */} +
+
+ +

Price Alerts

+
+
+ + +
+
+ + {/* Stats */} + {stats && ( +
+
+

Total

+

{stats.total}

+
+
+

Active

+

{stats.active}

+
+
+

Triggered

+

{stats.triggered}

+
+
+ )} + + {/* Error message */} + {error && ( +
+ + {error} +
+ )} + + {/* Create Alert Form */} + {showForm && ( +
+
+

Create Alert for {symbol}

+ +
+ +
+
+ + +
+
+ + setFormPrice(e.target.value)} + placeholder="0.00" + className="w-full px-2 py-1.5 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-yellow-500" + required + /> +
+
+ +
+ + setFormNote(e.target.value)} + placeholder="Reminder note..." + className="w-full px-2 py-1.5 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-yellow-500" + /> +
+ +
+ + + +
+ + +
+ )} + + {/* Filter Tabs */} +
+ + + +
+ + {/* Alerts for Current Symbol */} + {symbolAlerts.length > 0 && ( +
+

{symbol} Alerts

+ {symbolAlerts.map((alert) => { + const condition = getConditionDisplay(alert.condition); + const ConditionIcon = condition.icon; + return ( +
+
+
+ + {condition.text} + ${alert.price.toFixed(2)} +
+
+ + +
+
+ {alert.note && ( +

{alert.note}

+ )} +
+ {alert.isRecurring && ( + Recurring + )} + {alert.triggeredAt && ( + + + Triggered at ${alert.triggeredPrice?.toFixed(2)} + + )} +
+
+ ); + })} +
+ )} + + {/* Other Alerts */} + {otherAlerts.length > 0 && ( +
+

Other Symbols

+ {otherAlerts.map((alert) => { + const condition = getConditionDisplay(alert.condition); + const ConditionIcon = condition.icon; + return ( +
+
+
+ {alert.symbol} + + ${alert.price.toFixed(2)} +
+
+ + +
+
+
+ ); + })} +
+ )} + + {/* Empty State */} + {alerts.length === 0 && !loading && ( +
+ +

No price alerts yet

+

Create an alert to get notified when price reaches your target

+
+ )} +
+ ); +}; + +export default AlertsPanel; diff --git a/src/modules/trading/pages/Trading.tsx b/src/modules/trading/pages/Trading.tsx index 70dcd23..d0d1ab4 100644 --- a/src/modules/trading/pages/Trading.tsx +++ b/src/modules/trading/pages/Trading.tsx @@ -5,6 +5,7 @@ import CandlestickChartWithML from '../components/CandlestickChartWithML'; import WatchlistSidebar from '../components/WatchlistSidebar'; import PaperTradingPanel from '../components/PaperTradingPanel'; import MLSignalsPanel from '../components/MLSignalsPanel'; +import AlertsPanel from '../components/AlertsPanel'; import type { Interval, CrosshairData } from '../../../types/trading.types'; import type { MLSignal } from '../../../services/mlService'; @@ -14,6 +15,7 @@ export default function Trading() { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isMLSignalsOpen, setIsMLSignalsOpen] = useState(true); const [isPaperTradingOpen, setIsPaperTradingOpen] = useState(true); + const [isAlertsOpen, setIsAlertsOpen] = useState(false); // ML Overlay states const [enableMLOverlays, setEnableMLOverlays] = useState(true); @@ -239,6 +241,25 @@ export default function Trading() { /> +