Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 61 additions & 53 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { DashboardPage } from '@/features/dashboard/DashboardPage';
import { LiveMarketDashboard } from '@/pages/LiveMarketDashboard';
import { AuthGuard } from '@/features/auth/AuthGuard';
import { useUserStore } from '@/stores/userStore';
import { PriceAlertModal } from '@/components/PriceAlertModal';
import { ActiveAlertsDrawer } from '@/components/ActiveAlertsDrawer';
import { ROUTES } from '@/constants/routes.constants';

function AppContent() {
Expand Down Expand Up @@ -80,61 +82,67 @@ function AppContent() {
}

return (
<Routes>
{/* Landing */}
<Route path={ROUTES.HOME} element={<GlobeView onEnterApp={() => navigate(ROUTES.LOGIN)} />} />
<>
<Routes>
{/* Landing */}
<Route path={ROUTES.HOME} element={<GlobeView onEnterApp={() => navigate(ROUTES.LOGIN)} />} />

{/* Auth Flows */}
<Route
path={ROUTES.LOGIN}
element={
<Login
onLoginSuccess={() => navigate(ROUTES.DASHBOARD)}
onNavigateToSignup={() => navigate(ROUTES.REGISTER)}
onNavigateToForgot={() => navigate(ROUTES.FORGOT_PASSWORD)}
/>
}
/>
<Route
path={ROUTES.REGISTER}
element={
<Register
onRegisterSuccess={() => navigate(ROUTES.SUCCESS)}
onNavigateToLogin={() => navigate(ROUTES.LOGIN)}
/>
}
/>
<Route
path={ROUTES.SUCCESS}
element={
<SuccessPage
onNavigateToDashboard={() => navigate(ROUTES.LOGIN)} // Needs to login first
onBackToLogin={() => navigate(ROUTES.LOGIN)}
/>
}
/>
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmail />} />
<Route path={ROUTES.AUTH_CALLBACK} element={<AuthCallback />} />
<Route
path={ROUTES.FORGOT_PASSWORD}
element={<ForgotPassword onBackToLogin={() => navigate(ROUTES.LOGIN)} />}
/>
<Route
path={ROUTES.RESET_PASSWORD}
element={<ResetPassword onBackToLogin={() => navigate(ROUTES.LOGIN)} />}
/>
<Route path={ROUTES.EXPLORE} element={<LiveMarketDashboard />} />
{/* Auth Flows */}
<Route
path={ROUTES.LOGIN}
element={
<Login
onLoginSuccess={() => navigate(ROUTES.DASHBOARD)}
onNavigateToSignup={() => navigate(ROUTES.REGISTER)}
onNavigateToForgot={() => navigate(ROUTES.FORGOT_PASSWORD)}
/>
}
/>
<Route
path={ROUTES.REGISTER}
element={
<Register
onRegisterSuccess={() => navigate(ROUTES.SUCCESS)}
onNavigateToLogin={() => navigate(ROUTES.LOGIN)}
/>
}
/>
<Route
path={ROUTES.SUCCESS}
element={
<SuccessPage
onNavigateToDashboard={() => navigate(ROUTES.LOGIN)} // Needs to login first
onBackToLogin={() => navigate(ROUTES.LOGIN)}
/>
}
/>
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmail />} />
<Route path={ROUTES.AUTH_CALLBACK} element={<AuthCallback />} />
<Route
path={ROUTES.FORGOT_PASSWORD}
element={<ForgotPassword onBackToLogin={() => navigate(ROUTES.LOGIN)} />}
/>
<Route
path={ROUTES.RESET_PASSWORD}
element={<ResetPassword onBackToLogin={() => navigate(ROUTES.LOGIN)} />}
/>
<Route path={ROUTES.EXPLORE} element={<LiveMarketDashboard />} />

{/* Protected Routes */}
<Route
path={ROUTES.DASHBOARD}
element={
<AuthGuard>
<DashboardPage user={user} onLogout={handleLogout} />
</AuthGuard>
}
/>
</Routes>
{/* Protected Routes */}
<Route
path={ROUTES.DASHBOARD}
element={
<AuthGuard>
<DashboardPage user={user} onLogout={handleLogout} />
</AuthGuard>
}
/>
</Routes>

{/* Global Price Target Alarm Modal & Drawer */}
<PriceAlertModal />
<ActiveAlertsDrawer />
</>
);
}

Expand Down
169 changes: 169 additions & 0 deletions src/components/ActiveAlertsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useEffect } from 'react';
import { X, Bell, Trash2, Plus, TrendingUp, TrendingDown, RefreshCw } from 'lucide-react';
import { usePriceAlertStore } from '@/stores/priceAlertStore';
import '@/styles/components/price-alerts.css';

export const ActiveAlertsDrawer: React.FC = () => {
const {
alerts,
isDrawerOpen,
setDrawerOpen,
fetchAlerts,
removeAlert,
openModal,
isLoading,
} = usePriceAlertStore();

useEffect(() => {
if (isDrawerOpen) {
fetchAlerts();
}
}, [isDrawerOpen, fetchAlerts]);

if (!isDrawerOpen) return null;

const activeAlerts = alerts.filter((a) => !a.is_triggered);
const triggeredAlerts = alerts.filter((a) => a.is_triggered);

const formatDate = (dateStr?: string | null) => {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};

return (
<div className="vercel-drawer-backdrop" onClick={() => setDrawerOpen(false)}>
<div className="vercel-drawer-content" onClick={(e) => e.stopPropagation()}>
{/* Drawer Header */}
<div className="vercel-drawer-header">
<div className="vercel-drawer-title-group">
<Bell size={18} strokeWidth={2.5} style={{ color: '#00e599' }} />
<h3 className="vercel-drawer-title">Price Target Alarms</h3>
<span className="vercel-drawer-count-badge">
{activeAlerts.length} / 10 Active
</span>
</div>

<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
className="vercel-icon-btn"
onClick={() => fetchAlerts()}
title="Refresh Alerts"
disabled={isLoading}
>
<RefreshCw size={14} className={isLoading ? 'spin' : ''} />
</button>
<button
className="vercel-drawer-close-btn"
onClick={() => setDrawerOpen(false)}
title="Close Drawer"
>
<X size={16} strokeWidth={2} />
</button>
</div>
</div>

{/* Drawer Subheader Action */}
<div className="vercel-drawer-subbar">
<button
className="vercel-btn-create-alert"
onClick={() => {
openModal();
}}
>
<Plus size={14} strokeWidth={3} />
<span>Create New Price Alarm</span>
</button>
Comment on lines +68 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Opening the create-alert modal doesn't close the drawer.

openModal() here doesn't set isDrawerOpen to false, and the store's openModal action doesn't touch it either. Both the drawer and modal backdrops (position: fixed; inset: 0;, same z-index: 1100) can be open simultaneously, causing overlapping full-screen overlays.

🐛 Proposed fix
           <button
             className="vercel-btn-create-alert"
             onClick={() => {
+              setDrawerOpen(false);
               openModal();
             }}
           >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className="vercel-btn-create-alert"
onClick={() => {
openModal();
}}
>
<Plus size={14} strokeWidth={3} />
<span>Create New Price Alarm</span>
</button>
<button
className="vercel-btn-create-alert"
onClick={() => {
setDrawerOpen(false);
openModal();
}}
>
<Plus size={14} strokeWidth={3} />
<span>Create New Price Alarm</span>
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ActiveAlertsDrawer.tsx` around lines 68 - 76, Update the
Create New Price Alarm button handler in ActiveAlertsDrawer to close the drawer
by setting isDrawerOpen to false before or when invoking openModal(). Preserve
the existing modal-opening behavior and ensure only the modal overlay remains
open after the click.

</div>

{/* Alerts List */}
<div className="vercel-drawer-body">
{isLoading && alerts.length === 0 ? (
<div className="vercel-drawer-empty">Loading active price alarms...</div>
) : alerts.length === 0 ? (
<div className="vercel-drawer-empty">
<Bell size={32} strokeWidth={1.5} style={{ color: '#444444', marginBottom: '12px' }} />
<p style={{ margin: 0, fontWeight: 600, color: '#ededed' }}>No Price Alarms Set</p>
<p style={{ margin: '4px 0 0 0', fontSize: '12px', color: '#888888' }}>
Set target price alarms for your stocks to receive instant Web and Email notifications.
</p>
</div>
) : (
<div className="vercel-alerts-list">
{/* Active Alarms Section */}
{activeAlerts.length > 0 && (
<div className="vercel-alert-group">
<h4 className="vercel-group-label">Active Alarms ({activeAlerts.length})</h4>
{activeAlerts.map((alert) => (
<div key={alert.id} className="vercel-alert-item">
<div className="vercel-item-left">
<div className="vercel-symbol-avatar">
{alert.symbol.substring(0, 2)}
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span className="vercel-symbol-name">{alert.symbol}</span>
<span className={`vercel-condition-badge ${alert.condition.toLowerCase()}`}>
{alert.condition === 'ABOVE' ? (
<TrendingUp size={11} strokeWidth={3} />
) : (
<TrendingDown size={11} strokeWidth={3} />
)}
{alert.condition} ${alert.target_price.toFixed(2)}
</span>
</div>
<span className="vercel-alert-time">Set {formatDate(alert.created_at)}</span>
</div>
</div>

<button
className="vercel-alert-delete-btn"
onClick={() => removeAlert(alert.id)}
title="Delete Alert"
>
<Trash2 size={14} strokeWidth={2} />
</button>
Comment on lines +119 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled rejection on delete/dismiss failure; no error surfaced.

removeAlert in the store rethrows the error after setting error in state, but neither call site here catches the promise, and this component never destructures/render's the store's error. A failed delete produces an unhandled promise rejection and silent failure from the user's perspective.

🐛 Proposed fix
-  const {
-    alerts,
-    isDrawerOpen,
-    setDrawerOpen,
-    fetchAlerts,
-    removeAlert,
-    openModal,
-    isLoading,
-  } = usePriceAlertStore();
+  const {
+    alerts,
+    isDrawerOpen,
+    setDrawerOpen,
+    fetchAlerts,
+    removeAlert,
+    openModal,
+    isLoading,
+    error,
+  } = usePriceAlertStore();
                       <button
                         className="vercel-alert-delete-btn"
-                        onClick={() => removeAlert(alert.id)}
+                        onClick={() => removeAlert(alert.id).catch(() => {})}
                         title="Delete Alert"

(apply the same .catch to the triggered-list delete button, and render error in the drawer body similar to the modal's error banner.)

Also applies to: 152-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ActiveAlertsDrawer.tsx` around lines 119 - 125, Update
ActiveAlertsDrawer’s delete and dismiss button handlers to catch rejected
promises from the store’s removeAlert action, preventing unhandled rejections
and surfacing the failure. Destructure the store error state and render it in
the drawer body using the modal’s existing error-banner pattern.

</div>
))}
</div>
)}

{/* Triggered Alarms Section */}
{triggeredAlerts.length > 0 && (
<div className="vercel-alert-group" style={{ marginTop: '20px' }}>
<h4 className="vercel-group-label">Triggered History ({triggeredAlerts.length})</h4>
{triggeredAlerts.map((alert) => (
<div key={alert.id} className="vercel-alert-item triggered">
<div className="vercel-item-left">
<div className="vercel-symbol-avatar triggered">🔔</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span className="vercel-symbol-name">{alert.symbol}</span>
<span className="vercel-triggered-tag">
Triggered @ ${alert.target_price.toFixed(2)}
</span>
</div>
<span className="vercel-alert-time">
Triggered {formatDate(alert.triggered_at)}
</span>
</div>
</div>

<button
className="vercel-alert-delete-btn"
onClick={() => removeAlert(alert.id)}
title="Dismiss Alert"
>
<Trash2 size={14} strokeWidth={2} />
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
45 changes: 36 additions & 9 deletions src/components/PortfolioView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useState, useEffect, useTransition } from 'react';
import { Bell } from 'lucide-react';
import { portfolioService } from '@/services/portfolio.service';
import type { Portfolio } from '@/services/portfolio.service';
import { usePriceAlertStore } from '@/stores/priceAlertStore';
import '@/styles/components/portfolio.css';

interface PortfolioViewProps {
Expand All @@ -14,6 +16,7 @@ export const PortfolioView: React.FC<PortfolioViewProps> = ({ onSelectStock }) =
const [newSymbol, setNewSymbol] = useState('');
const [error, setError] = useState<string | null>(null);
const [, startTransition] = useTransition();
const openModal = usePriceAlertStore((state) => state.openModal);

useEffect(() => {
fetchPortfolio();
Expand Down Expand Up @@ -270,15 +273,39 @@ export const PortfolioView: React.FC<PortfolioViewProps> = ({ onSelectStock }) =
</div>
)}

<button
onClick={(e) => {
e.stopPropagation();
handleRemoveHolding(holding.symbol);
}}
className="remove-btn"
>
Remove
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
onClick={(e) => {
e.stopPropagation();
openModal(holding.symbol, holding.price || undefined);
}}
className="portfolio-btn"
style={{
padding: '6px 12px',
fontSize: '0.8rem',
background: 'rgba(0, 229, 153, 0.1)',
border: '1px solid rgba(0, 229, 153, 0.3)',
color: '#00e599',
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
fontWeight: 600,
}}
title="Set Price Target Alarm"
>
<Bell size={12} strokeWidth={2.5} /> Alarm
</button>

<button
onClick={(e) => {
e.stopPropagation();
handleRemoveHolding(holding.symbol);
}}
className="remove-btn"
>
Remove
</button>
</div>
</div>
))
)}
Expand Down
Loading
Loading