@crmy/web 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/index.html +23 -0
  2. package/package.json +76 -0
  3. package/postcss.config.js +6 -0
  4. package/public/android-chrome-192x192.png +0 -0
  5. package/public/android-chrome-512x512.png +0 -0
  6. package/public/apple-touch-icon.png +0 -0
  7. package/public/favicon-16x16.png +0 -0
  8. package/public/favicon-32x32.png +0 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/favicon.svg +13 -0
  11. package/public/site.webmanifest +1 -0
  12. package/src/App.tsx +158 -0
  13. package/src/api/client.ts +82 -0
  14. package/src/api/hooks.ts +689 -0
  15. package/src/assets/crmy-logo.png +0 -0
  16. package/src/components/CustomFields.tsx +240 -0
  17. package/src/components/NavLink.tsx +28 -0
  18. package/src/components/crm/AIFab.tsx +37 -0
  19. package/src/components/crm/AccountDrawer.tsx +372 -0
  20. package/src/components/crm/ActivityTimeline.tsx +115 -0
  21. package/src/components/crm/AssignmentDrawer.tsx +396 -0
  22. package/src/components/crm/BriefingPanel.tsx +217 -0
  23. package/src/components/crm/CommandPalette.tsx +254 -0
  24. package/src/components/crm/ContactAvatar.tsx +49 -0
  25. package/src/components/crm/ContactDrawer.tsx +438 -0
  26. package/src/components/crm/ContextPanel.tsx +200 -0
  27. package/src/components/crm/CrmWidgets.tsx +417 -0
  28. package/src/components/crm/DrawerShell.tsx +77 -0
  29. package/src/components/crm/ListToolbar.tsx +252 -0
  30. package/src/components/crm/OpportunityDrawer.tsx +372 -0
  31. package/src/components/crm/PaginationBar.tsx +111 -0
  32. package/src/components/crm/QuickAddDrawer.tsx +652 -0
  33. package/src/components/crm/ShortcutsOverlay.tsx +65 -0
  34. package/src/components/crm/UseCaseDrawer.tsx +454 -0
  35. package/src/components/layout/MobileNav.tsx +49 -0
  36. package/src/components/layout/Sidebar.tsx +157 -0
  37. package/src/components/layout/TopBar.tsx +54 -0
  38. package/src/components/settings/ActorsSettings.tsx +1190 -0
  39. package/src/components/ui/accordion.tsx +52 -0
  40. package/src/components/ui/alert-dialog.tsx +104 -0
  41. package/src/components/ui/alert.tsx +43 -0
  42. package/src/components/ui/aspect-ratio.tsx +5 -0
  43. package/src/components/ui/avatar.tsx +38 -0
  44. package/src/components/ui/badge.tsx +29 -0
  45. package/src/components/ui/breadcrumb.tsx +90 -0
  46. package/src/components/ui/button.tsx +47 -0
  47. package/src/components/ui/calendar.tsx +54 -0
  48. package/src/components/ui/card.tsx +43 -0
  49. package/src/components/ui/carousel.tsx +224 -0
  50. package/src/components/ui/chart.tsx +303 -0
  51. package/src/components/ui/checkbox.tsx +26 -0
  52. package/src/components/ui/collapsible.tsx +9 -0
  53. package/src/components/ui/command.tsx +132 -0
  54. package/src/components/ui/context-menu.tsx +178 -0
  55. package/src/components/ui/date-picker.tsx +313 -0
  56. package/src/components/ui/dialog.tsx +95 -0
  57. package/src/components/ui/drawer.tsx +87 -0
  58. package/src/components/ui/dropdown-menu.tsx +179 -0
  59. package/src/components/ui/form.tsx +129 -0
  60. package/src/components/ui/hover-card.tsx +27 -0
  61. package/src/components/ui/input-otp.tsx +61 -0
  62. package/src/components/ui/input.tsx +22 -0
  63. package/src/components/ui/label.tsx +17 -0
  64. package/src/components/ui/menubar.tsx +207 -0
  65. package/src/components/ui/navigation-menu.tsx +120 -0
  66. package/src/components/ui/pagination.tsx +81 -0
  67. package/src/components/ui/popover.tsx +29 -0
  68. package/src/components/ui/progress.tsx +23 -0
  69. package/src/components/ui/radio-group.tsx +36 -0
  70. package/src/components/ui/resizable.tsx +37 -0
  71. package/src/components/ui/scroll-area.tsx +38 -0
  72. package/src/components/ui/select.tsx +143 -0
  73. package/src/components/ui/separator.tsx +20 -0
  74. package/src/components/ui/sheet.tsx +107 -0
  75. package/src/components/ui/sidebar.tsx +637 -0
  76. package/src/components/ui/skeleton.tsx +7 -0
  77. package/src/components/ui/slider.tsx +23 -0
  78. package/src/components/ui/sonner.tsx +24 -0
  79. package/src/components/ui/switch.tsx +27 -0
  80. package/src/components/ui/table.tsx +72 -0
  81. package/src/components/ui/tabs.tsx +53 -0
  82. package/src/components/ui/textarea.tsx +21 -0
  83. package/src/components/ui/toast.tsx +111 -0
  84. package/src/components/ui/toaster.tsx +24 -0
  85. package/src/components/ui/toggle-group.tsx +49 -0
  86. package/src/components/ui/toggle.tsx +37 -0
  87. package/src/components/ui/tooltip.tsx +28 -0
  88. package/src/components/ui/use-toast.ts +1 -0
  89. package/src/components/ui/utils.ts +9 -0
  90. package/src/contexts/AgentSettingsContext.tsx +24 -0
  91. package/src/hooks/use-mobile.tsx +19 -0
  92. package/src/hooks/use-toast.ts +186 -0
  93. package/src/hooks/useKeyboardShortcuts.ts +95 -0
  94. package/src/hooks/useTheme.ts +24 -0
  95. package/src/index.css +245 -0
  96. package/src/lib/entityColors.ts +18 -0
  97. package/src/lib/stageConfig.ts +32 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/main.tsx +25 -0
  100. package/src/pages/Accounts.tsx +205 -0
  101. package/src/pages/Activities.tsx +251 -0
  102. package/src/pages/Agent.tsx +237 -0
  103. package/src/pages/AgentSettings.tsx +544 -0
  104. package/src/pages/Assignments.tsx +750 -0
  105. package/src/pages/Contacts.tsx +200 -0
  106. package/src/pages/Dashboard.tsx +143 -0
  107. package/src/pages/Inbox.tsx +615 -0
  108. package/src/pages/NotFound.tsx +24 -0
  109. package/src/pages/Opportunities.tsx +386 -0
  110. package/src/pages/SearchResults.tsx +49 -0
  111. package/src/pages/Settings.tsx +1884 -0
  112. package/src/pages/UseCases.tsx +396 -0
  113. package/src/pages/auth/Login.tsx +261 -0
  114. package/src/pages/hitl/HITL.tsx +101 -0
  115. package/src/store/appStore.ts +103 -0
  116. package/src/vite-env.d.ts +14 -0
  117. package/tailwind.config.js +121 -0
  118. package/tsconfig.json +24 -0
  119. package/vite.config.ts +27 -0
@@ -0,0 +1,200 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { useState, useMemo, useEffect } from 'react';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import { ContactAvatar } from '@/components/crm/ContactAvatar';
7
+ import { TopBar } from '@/components/layout/TopBar';
8
+ import { useContacts } from '@/api/hooks';
9
+ import { useAppStore } from '@/store/appStore';
10
+ import { useAgentSettings } from '@/contexts/AgentSettingsContext';
11
+ import { StageBadge } from '@/components/crm/CrmWidgets';
12
+ import { ListToolbar, type FilterConfig, type SortOption } from '@/components/crm/ListToolbar';
13
+ import { motion } from 'framer-motion';
14
+ import { LayoutGrid, List, Sparkles, ChevronUp, ChevronDown } from 'lucide-react';
15
+ import { PaginationBar } from '@/components/crm/PaginationBar';
16
+ import { useIsMobile } from '@/hooks/use-mobile';
17
+ import { stageConfig } from '@/lib/stageConfig';
18
+
19
+ type ViewMode = 'table' | 'cards';
20
+
21
+ const filterConfigs: FilterConfig[] = [
22
+ { key: 'lifecycle_stage', label: 'Stage', options: Object.entries(stageConfig).map(([k, v]) => ({ value: k, label: v.label })) },
23
+ ];
24
+
25
+ const sortOptions: SortOption[] = [
26
+ { key: 'name', label: 'Name' },
27
+ { key: 'company', label: 'Company' },
28
+ { key: 'created_at', label: 'Created' },
29
+ { key: 'lifecycle_stage', label: 'Stage' },
30
+ ];
31
+
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ type Contact = any;
34
+
35
+ function displayName(c: Contact): string {
36
+ const parts = [c.first_name, c.last_name].filter(Boolean);
37
+ if (parts.length > 0) return parts.join(' ');
38
+ return c.email || c.company_name || 'Unknown';
39
+ }
40
+
41
+ export default function Contacts() {
42
+ const navigate = useNavigate();
43
+ const isMobile = useIsMobile();
44
+ const [view, setView] = useState<ViewMode>('table');
45
+ const effectiveView = isMobile ? 'cards' : view;
46
+ const { openDrawer, openQuickAdd, openAIWithContext } = useAppStore();
47
+ const { enabled: agentEnabled } = useAgentSettings();
48
+ const [search, setSearch] = useState('');
49
+ const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
50
+ const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(null);
51
+ const [page, setPage] = useState(1);
52
+ const [pageSize, setPageSize] = useState(25);
53
+
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ const { data, isLoading } = useContacts({ q: search || undefined, limit: 200 }) as any;
56
+ const allContacts: Contact[] = data?.data ?? [];
57
+
58
+ const handleFilterChange = (key: string, values: string[]) => {
59
+ setActiveFilters(prev => { const next = { ...prev }; if (values.length === 0) delete next[key]; else next[key] = values; return next; });
60
+ };
61
+ const handleSortChange = (key: string) => {
62
+ setSort(prev => prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' });
63
+ };
64
+
65
+ const filtered = useMemo(() => {
66
+ let result = [...allContacts];
67
+ if (activeFilters.lifecycle_stage?.length) result = result.filter(c => activeFilters.lifecycle_stage.includes(c.lifecycle_stage as string));
68
+ if (sort) {
69
+ result.sort((a, b) => {
70
+ const aVal = (a[sort.key] ?? '') as string | number;
71
+ const bVal = (b[sort.key] ?? '') as string | number;
72
+ if (typeof aVal === 'number' && typeof bVal === 'number') return sort.dir === 'asc' ? aVal - bVal : bVal - aVal;
73
+ return sort.dir === 'asc' ? String(aVal).localeCompare(String(bVal)) : String(bVal).localeCompare(String(aVal));
74
+ });
75
+ }
76
+ return result;
77
+ }, [allContacts, activeFilters, sort]);
78
+
79
+ useEffect(() => { setPage(1); }, [search, activeFilters, sort]);
80
+ const paginated = filtered.slice((page - 1) * pageSize, page * pageSize);
81
+
82
+ const SortHeader = ({ label, sortKey }: { label: string; sortKey: string }) => (
83
+ <th onClick={() => handleSortChange(sortKey)}
84
+ className="text-left px-4 py-3 text-xs font-display font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors select-none">
85
+ <span className="inline-flex items-center gap-1">
86
+ {label}
87
+ {sort?.key === sortKey ? (sort.dir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />) : null}
88
+ </span>
89
+ </th>
90
+ );
91
+
92
+ return (
93
+ <div className="flex flex-col h-full">
94
+ <TopBar title="Contacts">
95
+ <div className="hidden md:flex items-center gap-1 bg-muted rounded-xl p-0.5">
96
+ <button onClick={() => setView('table')} className={`p-1.5 rounded-lg text-sm transition-all ${view === 'table' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground'}`}>
97
+ <List className="w-4 h-4" />
98
+ </button>
99
+ <button onClick={() => setView('cards')} className={`p-1.5 rounded-lg text-sm transition-all ${view === 'cards' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground'}`}>
100
+ <LayoutGrid className="w-4 h-4" />
101
+ </button>
102
+ </div>
103
+ </TopBar>
104
+
105
+ <ListToolbar
106
+ searchValue={search} onSearchChange={setSearch} searchPlaceholder="Search contacts..."
107
+ filters={filterConfigs} activeFilters={activeFilters} onFilterChange={handleFilterChange}
108
+ onClearFilters={() => setActiveFilters({})} sortOptions={sortOptions} currentSort={sort}
109
+ onSortChange={handleSortChange} onAdd={() => openQuickAdd('contact')} addLabel="New Contact" entityType="contacts"
110
+ />
111
+
112
+ <div className="flex-1 overflow-y-auto px-4 md:px-6 pb-24 md:pb-6">
113
+ {isLoading ? (
114
+ <div className="space-y-2 pt-2">
115
+ {[...Array(5)].map((_, i) => <div key={i} className="h-14 bg-muted/50 rounded-xl animate-pulse" />)}
116
+ </div>
117
+ ) : filtered.length === 0 ? (
118
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
119
+ <p className="text-sm">No contacts found.</p>
120
+ <button onClick={() => { setSearch(''); setActiveFilters({}); }} className="mt-2 text-xs text-primary font-semibold hover:underline">
121
+ Clear all filters
122
+ </button>
123
+ </div>
124
+ ) : effectiveView === 'table' ? (
125
+ <div className="bg-card border border-border rounded-2xl overflow-hidden shadow-sm">
126
+ <div className="overflow-x-auto">
127
+ <table className="w-full text-sm">
128
+ <thead>
129
+ <tr className="border-b border-border bg-surface-sunken/50">
130
+ <SortHeader label="Name" sortKey="name" />
131
+ <SortHeader label="Company" sortKey="company" />
132
+ <th className="text-left px-4 py-3 text-xs font-display font-semibold text-muted-foreground">Phone</th>
133
+ <SortHeader label="Stage" sortKey="lifecycle_stage" />
134
+ {agentEnabled && <th className="px-2 py-3 w-8"></th>}
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ {paginated.map((c, i) => (
139
+ <tr key={c.id as string} onClick={() => openDrawer('contact', c.id as string)}
140
+ className={`border-b border-border last:border-0 hover:bg-primary/5 cursor-pointer group transition-colors ${i % 2 === 1 ? 'bg-surface-sunken/30' : ''}`}>
141
+ <td className="px-4 py-3">
142
+ <div className="flex items-center gap-3">
143
+ <ContactAvatar name={displayName(c)} className="w-8 h-8 text-xs" />
144
+ <span className="font-semibold text-foreground">{displayName(c)}</span>
145
+ </div>
146
+ </td>
147
+ <td className="px-4 py-3 text-muted-foreground">{(c.company_name as string) || '—'}</td>
148
+ <td className="px-4 py-3 text-muted-foreground text-xs">{(c.phone as string) || '—'}</td>
149
+ <td className="px-4 py-3">{c.lifecycle_stage ? <StageBadge stage={c.lifecycle_stage as string} /> : '—'}</td>
150
+ {agentEnabled && (
151
+ <td className="px-2 py-3">
152
+ <button onClick={(e) => { e.stopPropagation(); openAIWithContext({ type: 'contact', id: c.id as string, name: displayName(c), detail: c.company_name as string }); navigate('/agent'); }}
153
+ className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-accent/10 transition-all">
154
+ <Sparkles className="w-3.5 h-3.5 text-accent" />
155
+ </button>
156
+ </td>
157
+ )}
158
+ </tr>
159
+ ))}
160
+ </tbody>
161
+ </table>
162
+ </div>
163
+ <div className="px-4">
164
+ <PaginationBar page={page} pageSize={pageSize} total={filtered.length} onPageChange={setPage} onPageSizeChange={setPageSize} />
165
+ </div>
166
+ </div>
167
+ ) : (
168
+ <>
169
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
170
+ {paginated.map((c, i) => (
171
+ <motion.div key={c.id as string} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.02 }}
172
+ onClick={() => openDrawer('contact', c.id as string)}
173
+ className="bg-card border border-border rounded-2xl p-4 cursor-pointer hover:shadow-lg hover:border-primary/20 transition-all press-scale group relative">
174
+ {agentEnabled && (
175
+ <button onClick={(e) => { e.stopPropagation(); openAIWithContext({ type: 'contact', id: c.id as string, name: displayName(c), detail: c.company_name as string }); navigate('/agent'); }}
176
+ className="absolute top-3 right-3 p-1.5 rounded-lg hover:bg-accent/10 transition-all md:opacity-0 md:group-hover:opacity-100">
177
+ <Sparkles className="w-3.5 h-3.5 text-accent" />
178
+ </button>
179
+ )}
180
+ <div className="flex items-center gap-3 mb-3">
181
+ <ContactAvatar name={displayName(c)} className="w-11 h-11 rounded-2xl text-sm" />
182
+ <div>
183
+ <p className="font-display font-bold text-foreground">{displayName(c)}</p>
184
+ <p className="text-xs text-muted-foreground">{(c.company_name as string) || 'Individual'}</p>
185
+ </div>
186
+ </div>
187
+ <div className="flex items-center gap-2 mb-2">
188
+ {c.lifecycle_stage && <StageBadge stage={c.lifecycle_stage as string} />}
189
+ </div>
190
+ {c.email && <p className="text-xs text-muted-foreground">{c.email as string}</p>}
191
+ </motion.div>
192
+ ))}
193
+ </div>
194
+ <PaginationBar page={page} pageSize={pageSize} total={filtered.length} onPageChange={setPage} onPageSizeChange={setPageSize} />
195
+ </>
196
+ )}
197
+ </div>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,143 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { useState } from 'react';
5
+ import { TopBar } from '@/components/layout/TopBar';
6
+ import { PipelineSnapshot, ActivityFeed, AccountHealth } from '@/components/crm/CrmWidgets';
7
+ import { useAppStore } from '@/store/appStore';
8
+ import { useOpportunities } from '@/api/hooks';
9
+ import { motion } from 'framer-motion';
10
+ import { ArrowRight, TrendingUp, UserPlus, FolderKanban, Activity, Briefcase } from 'lucide-react';
11
+
12
+ function greeting() {
13
+ const h = new Date().getHours();
14
+ if (h < 12) return 'Good morning';
15
+ if (h < 17) return 'Good afternoon';
16
+ return 'Good evening';
17
+ }
18
+
19
+ export default function Dashboard() {
20
+ const { openDrawer, openQuickAdd } = useAppStore();
21
+ const [activityWindow, setActivityWindow] = useState<'today' | 'week'>('today');
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ const { data: oppsData } = useOpportunities({ limit: 10 }) as any;
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const opps: any[] = oppsData?.data ?? [];
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ const hotDeals: any[] = opps
29
+ .filter((d: any) => d.stage === 'negotiation' || (d.stage === 'proposal' && d.probability > 50))
30
+ .slice(0, 3);
31
+
32
+ return (
33
+ <div className="flex flex-col h-full">
34
+ <TopBar title="Dashboard" />
35
+ <div className="flex-1 overflow-y-auto p-4 md:p-6 pb-24 md:pb-6">
36
+ {/* Greeting */}
37
+ <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} className="mb-6">
38
+ <h1 className="text-2xl md:text-3xl font-display font-extrabold">
39
+ <span className="gradient-text">{greeting()}</span>
40
+ </h1>
41
+ <p className="text-sm text-muted-foreground mt-1">Here's what needs your attention today.</p>
42
+ </motion.div>
43
+
44
+ {/* Quick Actions */}
45
+ <motion.div
46
+ initial={{ opacity: 0, y: 12 }}
47
+ animate={{ opacity: 1, y: 0 }}
48
+ transition={{ delay: 0.05 }}
49
+ className="grid grid-cols-2 md:grid-cols-4 gap-2 md:gap-3 mb-6"
50
+ >
51
+ {[
52
+ { icon: UserPlus, label: 'New Contact', gradient: 'from-primary/15 to-primary/5', color: 'text-primary', action: () => openQuickAdd('contact') },
53
+ { icon: TrendingUp, label: 'New Opportunity', gradient: 'from-accent/15 to-accent/5', color: 'text-accent', action: () => openQuickAdd('opportunity') },
54
+ { icon: FolderKanban, label: 'New Use Case', gradient: 'from-success/15 to-success/5', color: 'text-success', action: () => openQuickAdd('use-case') },
55
+ { icon: Activity, label: 'Log Activity', gradient: 'from-warning/15 to-warning/5', color: 'text-warning', action: () => openQuickAdd('activity') },
56
+ ].map((action) => (
57
+ <button
58
+ key={action.label}
59
+ onClick={action.action}
60
+ className={`flex items-center gap-3 p-3 md:p-4 rounded-2xl bg-gradient-to-br ${action.gradient} border border-border/50 hover:shadow-md transition-all press-scale`}
61
+ >
62
+ <action.icon className={`w-5 h-5 ${action.color}`} />
63
+ <span className="text-sm font-display font-semibold text-foreground">{action.label}</span>
64
+ </button>
65
+ ))}
66
+ </motion.div>
67
+
68
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
69
+ {/* Left column */}
70
+ <div className="lg:col-span-2 space-y-4 md:space-y-5">
71
+ {hotDeals.length > 0 && (
72
+ <motion.div
73
+ initial={{ opacity: 0, y: 12 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ transition={{ delay: 0.1 }}
76
+ className="bg-card border border-border rounded-2xl p-5 shadow-sm"
77
+ >
78
+ <h2 className="font-display font-bold text-foreground mb-4">Today's focus</h2>
79
+ <div className="space-y-2">
80
+ {hotDeals.map((deal: Record<string, unknown>) => {
81
+ const contactName = (deal.contact_name ?? deal.contactName ?? '') as string;
82
+ const amount = (deal.amount as number) ?? 0;
83
+ return (
84
+ <div
85
+ key={deal.id as string}
86
+ onClick={() => openDrawer('opportunity', deal.id as string)}
87
+ className="flex items-center gap-3 p-3 rounded-xl bg-surface hover:bg-surface-sunken cursor-pointer transition-all press-scale"
88
+ >
89
+ <div className="w-8 h-8 rounded-xl bg-accent/10 flex items-center justify-center flex-shrink-0">
90
+ <Briefcase className="w-4 h-4 text-accent" />
91
+ </div>
92
+ <div className="flex-1 min-w-0">
93
+ <p className="text-sm font-semibold text-foreground truncate">{deal.name as string}</p>
94
+ <p className="text-xs text-muted-foreground">
95
+ ${amount >= 1000 ? `${(amount / 1000).toFixed(0)}K` : amount} · {contactName}
96
+ </p>
97
+ </div>
98
+ <ArrowRight className="w-4 h-4 text-muted-foreground" />
99
+ </div>
100
+ );
101
+ })}
102
+ </div>
103
+ </motion.div>
104
+ )}
105
+
106
+ <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }}>
107
+ <div className="bg-card border border-border rounded-2xl p-5 shadow-sm">
108
+ <div className="flex items-center justify-between mb-3">
109
+ <h3 className="font-display font-bold text-foreground">Recent activity</h3>
110
+ <div className="flex items-center gap-0.5 bg-muted rounded-lg p-0.5">
111
+ <button
112
+ onClick={() => setActivityWindow('today')}
113
+ className={`px-2.5 py-1 rounded-md text-[11px] font-semibold transition-all ${activityWindow === 'today' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
114
+ >
115
+ Today
116
+ </button>
117
+ <button
118
+ onClick={() => setActivityWindow('week')}
119
+ className={`px-2.5 py-1 rounded-md text-[11px] font-semibold transition-all ${activityWindow === 'week' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
120
+ >
121
+ This Week
122
+ </button>
123
+ </div>
124
+ </div>
125
+ <ActivityFeed limit={8} filterWindow={activityWindow} />
126
+ </div>
127
+ </motion.div>
128
+ </div>
129
+
130
+ {/* Right column */}
131
+ <div className="space-y-4 md:space-y-5">
132
+ <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.08 }}>
133
+ <PipelineSnapshot />
134
+ </motion.div>
135
+ <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.11 }}>
136
+ <AccountHealth />
137
+ </motion.div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ );
143
+ }