diff --git a/src/App.tsx b/src/App.tsx index e5f5916..abee26b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,9 @@ import Send from '@/pages/Send'; import Receive from '@/pages/Receive'; import Privacy from '@/pages/Privacy'; import { HelpButton } from '@/components/HelpButton'; +import { UndoToast } from '@/components/UndoToast'; +import Contacts from '@/pages/Contacts'; +import Notifications from '@/pages/Notifications'; import Vault from '@/pages/Vault'; import Schedule from '@/pages/Schedule'; @@ -23,10 +26,13 @@ export function App() { } /> } /> } /> + } /> + } /> } /> + ); } diff --git a/src/components/UndoToast.tsx b/src/components/UndoToast.tsx new file mode 100644 index 0000000..b0dd4ff --- /dev/null +++ b/src/components/UndoToast.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; +import { useUndoStore } from '@/stores/undoStore'; + +function ToastItem({ + id, + message, + onUndo, + expiresAt, +}: { + id: string; + message: string; + onUndo: () => void; + expiresAt: number; +}) { + const removeToast = useUndoStore((s) => s.removeToast); + const [remaining, setRemaining] = useState(5); + + useEffect(() => { + const interval = setInterval(() => { + const secs = Math.ceil((expiresAt - Date.now()) / 1000); + setRemaining(secs > 0 ? secs : 0); + }, 200); + return () => clearInterval(interval); + }, [expiresAt]); + + const handleUndo = () => { + onUndo(); + removeToast(id); + }; + + return ( +
+ {message} + + + {remaining}s + + +
+ ); +} + +export function UndoToast() { + const toasts = useUndoStore((s) => s.toasts); + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map((t) => ( + + ))} +
+ ); +} diff --git a/src/pages/Contacts.tsx b/src/pages/Contacts.tsx new file mode 100644 index 0000000..c1725c5 --- /dev/null +++ b/src/pages/Contacts.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { useUndoStore } from '@/stores/undoStore'; + +interface Contact { + id: string; + name: string; + address: string; +} + +const INITIAL_CONTACTS: Contact[] = [ + { id: '1', name: 'Alice', address: 'GBVR...XYZW' }, + { id: '2', name: 'Bob', address: 'GCBA...MNOP' }, + { id: '3', name: 'Carol', address: 'GDZT...QRST' }, +]; + +export default function Contacts() { + const [contacts, setContacts] = useState(INITIAL_CONTACTS); + const addToast = useUndoStore((s) => s.addToast); + + const deleteContact = (contact: Contact) => { + const previous = [...contacts]; + setContacts((c) => c.filter((x) => x.id !== contact.id)); + addToast(`Deleted "${contact.name}"`, () => { + setContacts(previous); + }); + }; + + return ( +
+
+

+ Contacts +

+

+ Manage your saved contacts. +

+
+
    + {contacts.length === 0 && ( +

    No contacts yet.

    + )} + {contacts.map((contact) => ( +
  • +
    + + {contact.name} + + {contact.address} +
    + +
  • + ))} +
+
+ ); +} diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx new file mode 100644 index 0000000..8928bb8 --- /dev/null +++ b/src/pages/Notifications.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { useUndoStore } from '@/stores/undoStore'; + +interface Notification { + id: string; + title: string; + body: string; + read: boolean; +} + +const INITIAL_NOTIFICATIONS: Notification[] = [ + { id: '1', title: 'Payment received', body: 'You received 10 XLM from Alice.', read: false }, + { id: '2', title: 'Contact added', body: 'Bob was added to your contacts.', read: false }, + { + id: '3', + title: 'Schedule executed', + body: 'Recurring payment to Carol completed.', + read: true, + }, +]; + +export default function Notifications() { + const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS); + const [filter, setFilter] = useState<'all' | 'unread'>('all'); + const addToast = useUndoStore((s) => s.addToast); + + const dismiss = (notification: Notification) => { + const previous = [...notifications]; + setNotifications((n) => n.filter((x) => x.id !== notification.id)); + addToast(`Dismissed "${notification.title}"`, () => { + setNotifications(previous); + }); + }; + + const clearFilter = () => { + const previousFilter = filter; + setFilter('all'); + addToast('Filter cleared', () => { + setFilter(previousFilter); + }); + }; + + const filtered = filter === 'unread' ? notifications.filter((n) => !n.read) : notifications; + + return ( +
+
+

+ Notifications +

+
+
+ {(['all', 'unread'] as const).map((f) => ( + + ))} +
+ {filter !== 'all' && ( + + )} +
+
+
    + {filtered.length === 0 && ( +

    No notifications.

    + )} + {filtered.map((n) => ( +
  • +
    + + {n.title} + + {n.body} +
    + +
  • + ))} +
+
+ ); +} diff --git a/src/stores/undoStore.ts b/src/stores/undoStore.ts new file mode 100644 index 0000000..d578fed --- /dev/null +++ b/src/stores/undoStore.ts @@ -0,0 +1,32 @@ +import { create } from 'zustand'; + +export interface UndoEntry { + id: string; + message: string; + onUndo: () => void; + expiresAt: number; +} + +interface UndoState { + toasts: UndoEntry[]; + addToast: (message: string, onUndo: () => void) => string; + removeToast: (id: string) => void; +} + +export const useUndoStore = create((set) => ({ + toasts: [], + addToast: (message, onUndo) => { + const id = Math.random().toString(36).substring(2, 9); + const expiresAt = Date.now() + 5000; + set((state) => ({ + toasts: [...state.toasts, { id, message, onUndo, expiresAt }], + })); + setTimeout(() => { + set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })); + }, 5000); + return id; + }, + removeToast: (id) => { + set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })); + }, +}));