From a36350ce5db1f844c6f372be436a632846ff5862 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 02:02:16 +0000 Subject: [PATCH] [jules] ux: Replace window.confirm with custom ConfirmDialog --- .Jules/changelog.md | 8 ++++ .Jules/knowledge.md | 36 ++++++++++++++- .Jules/todo.md | 12 ++--- test-results/.last-run.json | 4 ++ web/App.tsx | 19 ++++---- web/components/ui/ConfirmDialog.tsx | 69 +++++++++++++++++++++++++++++ web/components/ui/Modal.tsx | 2 + web/contexts/ConfirmContext.tsx | 58 ++++++++++++++++++++++++ web/pages/GroupDetails.tsx | 33 ++++++++++++-- 9 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 test-results/.last-run.json create mode 100644 web/components/ui/ConfirmDialog.tsx create mode 100644 web/contexts/ConfirmContext.tsx diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 16a7b0c..b96ab75 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,14 @@ ## [Unreleased] ### Added +- **Confirmation Dialog System:** Replaced native `window.confirm` alerts with a custom, dual-theme accessible `ConfirmDialog`. + - **Features:** + - `ConfirmProvider` and `useConfirm` hook for easy integration. + - Promise-based API (`await confirm(...)`) allowing linear code flow. + - Dual-theme support (Glassmorphism & Neobrutalism). + - Accessibility improvements: Added `role="dialog"` to Modal component. + - **Technical:** Created `web/contexts/ConfirmContext.tsx` and `web/components/ui/ConfirmDialog.tsx`. Integrated into `web/App.tsx` and `web/pages/GroupDetails.tsx`. + - **Error Boundary System:** Implemented a global React Error Boundary to catch render errors gracefully. - **Features:** - Dual-theme support (Glassmorphism & Neobrutalism) for the error fallback UI. diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index ad48a44..d295aab 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -87,6 +87,38 @@ colors: { ## Component Patterns +### Confirmation Dialog Pattern + +**Date:** 2026-01-23 +**Context:** replacing `window.confirm` with custom UI + +Use `ConfirmProvider` to allow promise-based confirmation without managing local state. + +```tsx +// 1. Wrap App + + + + +// 2. Use Hook +const { confirm } = useConfirm(); + +const handleDelete = async () => { + if (await confirm({ + title: 'Delete?', + message: 'Sure?', + variant: 'danger' + })) { + // delete logic + } +}; +``` + +**Benefits:** +- Cleaner code (no `isModalOpen` state). +- Consistent UI across app. +- Non-blocking (unlike `alert`), but easy to await. + ### Error Boundary Pattern **Date:** 2026-01-14 @@ -176,7 +208,7 @@ When making a div clickable (like a card), you must ensure it's accessible: ### Modal Component Pattern -**Date:** 2026-01-01 +**Date:** 2026-01-01 (Updated 2026-01-23) **Context:** Modal.tsx structure ```tsx @@ -190,6 +222,8 @@ When making a div clickable (like a card), you must ensure it's accessible: ``` +**Accessibility Update:** Added `role="dialog"` and `aria-modal="true"` to the inner content wrapper to ensure screen readers announce it correctly. + ### Toast Notification Pattern **Date:** 2026-01-01 diff --git a/.Jules/todo.md b/.Jules/todo.md index 4539a8a..3811403 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -77,12 +77,12 @@ - Size: ~35 lines - Added: 2026-01-01 -- [ ] **[ux]** Confirmation dialog for destructive actions - - Files: Create `web/components/ui/ConfirmDialog.tsx`, integrate - - Context: Confirm before deleting groups/expenses - - Impact: Prevents accidental data loss - - Size: ~70 lines - - Added: 2026-01-01 +- [x] **[ux]** Confirmation dialog for destructive actions + - Completed: 2026-01-23 + - Files: `web/components/ui/ConfirmDialog.tsx`, `web/contexts/ConfirmContext.tsx`, `web/pages/GroupDetails.tsx` + - Context: Replaced `window.confirm` with dual-theme accessible modal + - Impact: Prevents accidental data loss with native-feeling UI + - Size: ~100 lines ### Mobile diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/web/App.tsx b/web/App.tsx index 0a6d4c6..964388b 100644 --- a/web/App.tsx +++ b/web/App.tsx @@ -3,6 +3,7 @@ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; import { Layout } from './components/layout/Layout'; import { ThemeWrapper } from './components/layout/ThemeWrapper'; import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { ConfirmProvider } from './contexts/ConfirmContext'; import { ThemeProvider } from './contexts/ThemeContext'; import { ToastProvider } from './contexts/ToastContext'; import { ToastContainer } from './components/ui/Toast'; @@ -50,14 +51,16 @@ const App = () => { return ( - - - - - - - - + + + + + + + + + + ); diff --git a/web/components/ui/ConfirmDialog.tsx b/web/components/ui/ConfirmDialog.tsx new file mode 100644 index 0000000..9d23956 --- /dev/null +++ b/web/components/ui/ConfirmDialog.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Modal } from './Modal'; +import { Button } from './Button'; +import { AlertTriangle, Info } from 'lucide-react'; +import { THEMES } from '../../constants'; +import { useTheme } from '../../contexts/ThemeContext'; + +interface ConfirmDialogProps { + isOpen: boolean; + onConfirm: () => void; + onCancel: () => void; + title: string; + message: string; + variant?: 'danger' | 'info'; + confirmText?: string; + cancelText?: string; +} + +export const ConfirmDialog: React.FC = ({ + isOpen, + onConfirm, + onCancel, + title, + message, + variant = 'info', + confirmText = 'Confirm', + cancelText = 'Cancel' +}) => { + const { style } = useTheme(); + + return ( + + + + + } + > +
+
+
+ {variant === 'danger' ? : } +
+
+

+ {message} +

+
+
+
+
+ ); +}; diff --git a/web/components/ui/Modal.tsx b/web/components/ui/Modal.tsx index a70621c..072c8e6 100644 --- a/web/components/ui/Modal.tsx +++ b/web/components/ui/Modal.tsx @@ -53,6 +53,8 @@ export const Modal: React.FC = ({ isOpen, onClose, title, children, onClick={onClose} /> Promise; +} + +const ConfirmContext = createContext(undefined); + +export const ConfirmProvider = ({ children }: { children: ReactNode }) => { + const [options, setOptions] = useState({ title: '', message: '' }); + const [isOpen, setIsOpen] = useState(false); + const [resolveRef, setResolveRef] = useState<((value: boolean) => void) | null>(null); + + const confirm = useCallback((opts: ConfirmOptions) => { + setOptions(opts); + setIsOpen(true); + return new Promise((resolve) => { + setResolveRef(() => resolve); + }); + }, []); + + const handleConfirm = () => { + if (resolveRef) resolveRef(true); + setIsOpen(false); + }; + + const handleCancel = () => { + if (resolveRef) resolveRef(false); + setIsOpen(false); + }; + + return ( + + {children} + + + ); +}; + +export const useConfirm = () => { + const context = useContext(ConfirmContext); + if (!context) throw new Error('useConfirm must be used within ConfirmProvider'); + return context; +}; diff --git a/web/pages/GroupDetails.tsx b/web/pages/GroupDetails.tsx index d47ebc0..2c56d48 100644 --- a/web/pages/GroupDetails.tsx +++ b/web/pages/GroupDetails.tsx @@ -8,6 +8,7 @@ import { Modal } from '../components/ui/Modal'; import { Skeleton } from '../components/ui/Skeleton'; import { THEMES } from '../constants'; import { useAuth } from '../contexts/AuthContext'; +import { useConfirm } from '../contexts/ConfirmContext'; import { useTheme } from '../contexts/ThemeContext'; import { useToast } from '../contexts/ToastContext'; import { @@ -41,6 +42,7 @@ export const GroupDetails = () => { const { user } = useAuth(); const { style } = useTheme(); const { addToast } = useToast(); + const { confirm } = useConfirm(); const [group, setGroup] = useState(null); const [expenses, setExpenses] = useState([]); @@ -257,7 +259,13 @@ export const GroupDetails = () => { const handleDeleteExpense = async () => { if (!editingExpenseId || !id) return; - if (window.confirm("Are you sure you want to delete this expense?")) { + + if (await confirm({ + title: 'Delete Expense', + message: 'Are you sure you want to delete this expense?', + variant: 'danger', + confirmText: 'Delete' + })) { try { await deleteExpense(id, editingExpenseId); setIsExpenseModalOpen(false); @@ -313,7 +321,13 @@ export const GroupDetails = () => { const handleDeleteGroup = async () => { if (!id) return; - if (window.confirm("Are you sure? This cannot be undone.")) { + + if (await confirm({ + title: 'Delete Group', + message: 'Are you sure? This cannot be undone.', + variant: 'danger', + confirmText: 'Delete Group' + })) { try { await deleteGroup(id); navigate('/groups'); @@ -326,7 +340,13 @@ export const GroupDetails = () => { const handleLeaveGroup = async () => { if (!id) return; - if (window.confirm("You can only leave when your balances are settled. Continue?")) { + + if (await confirm({ + title: 'Leave Group', + message: 'You can only leave when your balances are settled. Continue?', + variant: 'danger', + confirmText: 'Leave Group' + })) { try { await leaveGroup(id); addToast('You have left the group', 'success'); @@ -341,7 +361,12 @@ export const GroupDetails = () => { if (!id || !isAdmin) return; if (memberId === user?._id) return; - if (window.confirm(`Are you sure you want to remove ${memberName} from the group?`)) { + if (await confirm({ + title: 'Remove Member', + message: `Are you sure you want to remove ${memberName} from the group?`, + variant: 'danger', + confirmText: 'Remove' + })) { try { const hasUnsettled = settlements.some( s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0