firebase-os 1.1.4 → 1.1.6

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 (124) hide show
  1. package/dist/FirebaseOS.d.ts +15 -0
  2. package/dist/firebase-os.cjs.js +5 -20
  3. package/dist/firebase-os.es.js +95 -90
  4. package/dist/index.d.ts +3 -0
  5. package/dist/lib/ConfigContext.d.ts +12 -0
  6. package/package.json +3 -2
  7. package/scripts/postinstall.js +86 -15
  8. package/src/App.css +184 -0
  9. package/src/App.tsx +214 -0
  10. package/src/FirebaseOS.tsx +81 -0
  11. package/src/assets/hero.png +0 -0
  12. package/src/assets/react.svg +1 -0
  13. package/src/assets/vite.svg +1 -0
  14. package/src/components/AdminNotifications.test.tsx +98 -0
  15. package/src/components/AdminNotifications.tsx +194 -0
  16. package/src/components/Button.test.tsx +22 -0
  17. package/src/components/Button.tsx +53 -0
  18. package/src/components/ConfirmModal.test.tsx +98 -0
  19. package/src/components/ConfirmModal.tsx +73 -0
  20. package/src/components/ContactPopup.test.tsx +98 -0
  21. package/src/components/ContactPopup.tsx +437 -0
  22. package/src/components/CustomSelect.test.tsx +47 -0
  23. package/src/components/CustomSelect.tsx +89 -0
  24. package/src/components/DashboardNav.test.tsx +98 -0
  25. package/src/components/DashboardNav.tsx +281 -0
  26. package/src/components/Input.test.tsx +33 -0
  27. package/src/components/Input.tsx +61 -0
  28. package/src/components/JsonEditor.tsx +579 -0
  29. package/src/components/Navbar.test.tsx +98 -0
  30. package/src/components/Navbar.tsx +563 -0
  31. package/src/configs/forms/contactForm.config.ts +15 -0
  32. package/src/configs/forms/index.ts +29 -0
  33. package/src/configs/forms/pubForm.config.ts +11 -0
  34. package/src/configs/forms/supportForm.config.ts +14 -0
  35. package/src/configs/forms/userForm.config.ts +11 -0
  36. package/src/configs/pages/admin.config.ts +29 -0
  37. package/src/configs/pages/contact.config.ts +6 -0
  38. package/src/configs/pages/home.config.ts +18 -0
  39. package/src/configs/pages/mem.config.ts +2 -0
  40. package/src/configs/pages/menuOrders.config.ts +11 -0
  41. package/src/configs/pages/pub.config.ts +11 -0
  42. package/src/configs/pages/shared.config.ts +29 -0
  43. package/src/configs/pages/support.config.ts +7 -0
  44. package/src/configs/pages/tabOrders.config.ts +33 -0
  45. package/src/configs/pages/user.config.ts +29 -0
  46. package/src/configs/theme.config.ts +93 -0
  47. package/src/index.css +403 -0
  48. package/src/index.ts +22 -0
  49. package/src/lib/AuthContext.test.tsx +88 -0
  50. package/src/lib/AuthContext.tsx +191 -0
  51. package/src/lib/ConfigContext.tsx +45 -0
  52. package/src/lib/ThemeContext.tsx +233 -0
  53. package/src/lib/firebase.ts +91 -0
  54. package/src/main.tsx +22 -0
  55. package/src/microcomponents/AdminExampleContent.tsx +44 -0
  56. package/src/microcomponents/PrivateExampleContent.tsx +39 -0
  57. package/src/microcomponents/Public.tsx +126 -0
  58. package/src/microcomponents/SharedExampleContent.tsx +53 -0
  59. package/src/pages/Dashboard.test.tsx +98 -0
  60. package/src/pages/Dashboard.tsx +60 -0
  61. package/src/pages/DynamicPage.tsx +237 -0
  62. package/src/pages/FormsAdmin.test.tsx +98 -0
  63. package/src/pages/FormsAdmin.tsx +459 -0
  64. package/src/pages/Home.test.tsx +98 -0
  65. package/src/pages/Home.tsx +144 -0
  66. package/src/pages/Login.test.tsx +98 -0
  67. package/src/pages/Login.tsx +108 -0
  68. package/src/pages/PagesAdmin.test.tsx +98 -0
  69. package/src/pages/PagesAdmin.tsx +1022 -0
  70. package/src/pages/Profile.test.tsx +98 -0
  71. package/src/pages/Profile.tsx +319 -0
  72. package/src/pages/Register.test.tsx +98 -0
  73. package/src/pages/Register.tsx +116 -0
  74. package/src/pages/Requests.test.tsx +95 -0
  75. package/src/pages/Requests.tsx +422 -0
  76. package/src/pages/ResetPassword.test.tsx +98 -0
  77. package/src/pages/ResetPassword.tsx +92 -0
  78. package/src/pages/Settings.test.tsx +98 -0
  79. package/src/pages/Settings.tsx +393 -0
  80. package/src/pages/Setup.tsx +407 -0
  81. package/src/pages/StorageAdmin.test.tsx +150 -0
  82. package/src/pages/StorageAdmin.tsx +769 -0
  83. package/src/pages/Submissions.test.tsx +95 -0
  84. package/src/pages/Submissions.tsx +378 -0
  85. package/src/pages/Templates.test.tsx +98 -0
  86. package/src/pages/Templates.tsx +103 -0
  87. package/src/pages/ThemeAdmin.test.tsx +144 -0
  88. package/src/pages/ThemeAdmin.tsx +1000 -0
  89. package/src/pages/Users.test.tsx +95 -0
  90. package/src/pages/Users.tsx +334 -0
  91. package/src/pages/Verify.test.tsx +98 -0
  92. package/src/pages/Verify.tsx +95 -0
  93. package/src/prompts/index.ts +13 -0
  94. package/src/prompts/pages/publicPage.ts +44 -0
  95. package/src/prompts/sharedConstants.ts +12 -0
  96. package/src/prompts/tabs/board/adminboard.ts +32 -0
  97. package/src/prompts/tabs/board/privateboard.ts +36 -0
  98. package/src/prompts/tabs/board/publicboard.ts +36 -0
  99. package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
  100. package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
  101. package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
  102. package/src/prompts/tabs/crud/admin.ts +54 -0
  103. package/src/prompts/tabs/crud/private.ts +55 -0
  104. package/src/prompts/tabs/crud/shared.ts +53 -0
  105. package/src/prompts/tabs/table/admintable.ts +32 -0
  106. package/src/prompts/tabs/table/privatetable.ts +36 -0
  107. package/src/prompts/tabs/table/publictable.ts +36 -0
  108. package/src/setupTests.ts +1 -0
  109. package/src/templates/AdminPageTemplate.tsx +678 -0
  110. package/src/templates/PrivatePageTemplate.tsx +594 -0
  111. package/src/templates/PublicPageTemplate.tsx +92 -0
  112. package/src/templates/SharedPageTemplate.tsx +551 -0
  113. package/src/templates/TemplateBoard.test.tsx +106 -0
  114. package/src/templates/TemplateBoard.tsx +642 -0
  115. package/src/templates/TemplateCalendar.test.tsx +106 -0
  116. package/src/templates/TemplateCalendar.tsx +848 -0
  117. package/src/templates/TemplateConfirmation.test.tsx +106 -0
  118. package/src/templates/TemplateConfirmation.tsx +145 -0
  119. package/src/templates/TemplateInlineForm.test.tsx +106 -0
  120. package/src/templates/TemplateInlineForm.tsx +129 -0
  121. package/src/templates/TemplatePopupForm.test.tsx +106 -0
  122. package/src/templates/TemplatePopupForm.tsx +174 -0
  123. package/src/templates/TemplateTable.test.tsx +106 -0
  124. package/src/templates/TemplateTable.tsx +675 -0
@@ -0,0 +1,194 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Bell, User, Mail, HelpCircle } from 'lucide-react';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+ import { db } from '../lib/firebase';
5
+ import { collection, query, orderBy, limit, onSnapshot } from 'firebase/firestore';
6
+ import { useNavigate } from 'react-router-dom';
7
+
8
+ interface NotificationItem {
9
+ id: string;
10
+ type: 'user' | 'submission' | 'request';
11
+ title: string;
12
+ subtitle: string;
13
+ time: any;
14
+ }
15
+
16
+ export function AdminNotifications() {
17
+ const [open, setOpen] = useState(false);
18
+ const [notifications, setNotifications] = useState<NotificationItem[]>([]);
19
+ const dropdownRef = useRef<HTMLDivElement>(null);
20
+ const navigate = useNavigate();
21
+
22
+ useEffect(() => {
23
+ function handleClickOutside(event: MouseEvent) {
24
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
25
+ setOpen(false);
26
+ }
27
+ }
28
+ document.addEventListener('mousedown', handleClickOutside);
29
+ return () => document.removeEventListener('mousedown', handleClickOutside);
30
+ }, []);
31
+
32
+ // Use a ref to combine data reliably on client side
33
+ const dataRef = useRef({ users: [] as NotificationItem[], submissions: [] as NotificationItem[], requests: [] as NotificationItem[] });
34
+
35
+ const updateNotifications = (type: 'users' | 'submissions' | 'requests', data: NotificationItem[]) => {
36
+ dataRef.current[type] = data;
37
+ const combined = [...dataRef.current.users, ...dataRef.current.submissions, ...dataRef.current.requests];
38
+ combined.sort((a, b) => {
39
+ const tA = a.time?.toDate ? a.time.toDate().getTime() : new Date(a.time).getTime();
40
+ const tB = b.time?.toDate ? b.time.toDate().getTime() : new Date(b.time).getTime();
41
+ return (tB || 0) - (tA || 0);
42
+ });
43
+ setNotifications(combined.slice(0, 8)); // keep top 8
44
+ };
45
+
46
+ useEffect(() => {
47
+ // Listen to recent users
48
+ const usersQ = query(collection(db, 'user_profiles'), orderBy('createdAt', 'desc'), limit(5));
49
+ const unsubUsers = onSnapshot(usersQ, (snap) => {
50
+ const usersData = snap.docs.map(doc => {
51
+ const data = doc.data();
52
+ return {
53
+ id: `u_${doc.id}`,
54
+ type: 'user' as const,
55
+ title: 'New user registered',
56
+ subtitle: data.email || 'Unknown',
57
+ time: data.createdAt
58
+ };
59
+ });
60
+ updateNotifications('users', usersData);
61
+ });
62
+
63
+ // Listen to recent submissions
64
+ const subsQ = query(collection(db, 'user_submissions'), orderBy('submittedAt', 'desc'), limit(5));
65
+ const unsubSubs = onSnapshot(subsQ, (snap) => {
66
+ const subsData = snap.docs.map(doc => {
67
+ const data = doc.data();
68
+ return {
69
+ id: `s_${doc.id}`,
70
+ type: 'submission' as const,
71
+ title: 'New form submission',
72
+ subtitle: data.name || data.email || 'Anonymous',
73
+ time: data.submittedAt
74
+ };
75
+ });
76
+ updateNotifications('submissions', subsData);
77
+ });
78
+
79
+ // Listen to recent user requests
80
+ const reqQ = query(collection(db, 'user_requests'), orderBy('submittedAt', 'desc'), limit(5));
81
+ const unsubReq = onSnapshot(reqQ, (snap) => {
82
+ const reqData = snap.docs.map(doc => {
83
+ const data = doc.data();
84
+ return {
85
+ id: `r_${doc.id}`,
86
+ type: 'request' as const,
87
+ title: 'New support request',
88
+ subtitle: data.name || data.submitterName || data.contactEmail || data.email || 'Anonymous',
89
+ time: data.submittedAt
90
+ };
91
+ });
92
+ updateNotifications('requests', reqData);
93
+ });
94
+
95
+ return () => {
96
+ unsubUsers();
97
+ unsubSubs();
98
+ unsubReq();
99
+ };
100
+ }, []);
101
+
102
+
103
+
104
+ const [lastReadTime, setLastReadTime] = useState(() => {
105
+ if (typeof window !== 'undefined') {
106
+ return parseInt(localStorage.getItem('admin_notifications_last_read') || '0', 10);
107
+ }
108
+ return 0;
109
+ });
110
+
111
+ const handleToggleOpen = () => {
112
+ const nextState = !open;
113
+ setOpen(nextState);
114
+ if (nextState) {
115
+ const now = Date.now();
116
+ setLastReadTime(now);
117
+ if (typeof window !== 'undefined') {
118
+ localStorage.setItem('admin_notifications_last_read', now.toString());
119
+ }
120
+ }
121
+ };
122
+
123
+ const hasUnread = notifications.some(n => {
124
+ if (!n.time) return true;
125
+ const t = n.time?.toDate ? n.time.toDate().getTime() : new Date(n.time).getTime();
126
+ return t > lastReadTime || isNaN(t);
127
+ });
128
+
129
+ const handleNavigate = (type: 'user' | 'submission' | 'request') => {
130
+ setOpen(false);
131
+ if (type === 'user') navigate('/users');
132
+ else if (type === 'submission') navigate('/submissions');
133
+ else navigate('/requests');
134
+ };
135
+
136
+ const formatTime = (ts: any) => {
137
+ if (!ts) return '';
138
+ const date = ts.toDate ? ts.toDate() : new Date(ts);
139
+ return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }).format(date);
140
+ };
141
+
142
+ return (
143
+ <div className="relative z-50 flex-shrink-0 mr-1 md:mr-2" ref={dropdownRef}>
144
+ <button
145
+ onClick={handleToggleOpen}
146
+ className="p-2 rounded-xl glass-panel transition-all border border-transparent relative text-notification-icon hover:bg-foreground/5 hover:border-[var(--panel-border)]"
147
+ aria-label="Admin Notifications"
148
+ >
149
+ <Bell className="w-4 h-4 md:w-5 md:h-5" />
150
+ {hasUnread && (
151
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full animate-pulse" />
152
+ )}
153
+ </button>
154
+
155
+ <AnimatePresence>
156
+ {open && (
157
+ <motion.div
158
+ initial={{ opacity: 0, y: -5, scale: 0.98 }}
159
+ animate={{ opacity: 1, y: 0, scale: 1 }}
160
+ exit={{ opacity: 0, scale: 0.98, y: -5 }}
161
+ className="absolute top-full mt-2 right-[-60px] md:right-0 w-[320px] md:w-[400px] glass-panel border border-[var(--panel-border)] rounded-2xl shadow-2xl z-50 flex flex-col origin-top-right overflow-hidden bg-[var(--panel-bg)]"
162
+ >
163
+ <div className="px-4 border-b border-[var(--panel-border)]/50 pb-3 pt-4 flex items-center justify-between">
164
+ <span className="font-bold text-[14px] text-foreground tracking-wide">Notifications</span>
165
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent bg-accent/10 px-2 py-0.5 rounded-md">Admin</span>
166
+ </div>
167
+ <div className="max-h-[350px] overflow-y-auto w-full p-2 flex flex-col gap-1">
168
+ {notifications.length === 0 ? (
169
+ <div className="p-6 text-center text-foreground/40 text-[13px] font-medium">No recent activity.</div>
170
+ ) : (
171
+ notifications.map((n) => (
172
+ <button
173
+ key={n.id}
174
+ onClick={() => handleNavigate(n.type)}
175
+ className="w-full flex items-start gap-3 p-3 rounded-xl hover:bg-foreground/5 transition-all text-left group border border-transparent hover:border-[var(--panel-border)]"
176
+ >
177
+ <div className="mt-0.5 w-8 h-8 rounded-full flex items-center justify-center shrink-0 bg-foreground/5 text-notification-icon">
178
+ {n.type === 'user' ? <User className="w-4 h-4" /> : n.type === 'submission' ? <Mail className="w-4 h-4" /> : <HelpCircle className="w-4 h-4" />}
179
+ </div>
180
+ <div className="flex flex-col flex-1 min-w-0 pr-2">
181
+ <span className="text-[13px] font-semibold text-foreground group-hover:text-accent transition-colors truncate">{n.title}</span>
182
+ <span className="text-[12px] text-foreground/60 truncate">{n.subtitle}</span>
183
+ <span className="text-[10px] font-mono tracking-wide text-foreground/40 mt-1">{formatTime(n.time)}</span>
184
+ </div>
185
+ </button>
186
+ ))
187
+ )}
188
+ </div>
189
+ </motion.div>
190
+ )}
191
+ </AnimatePresence>
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Button } from '../components/Button';
4
+
5
+ describe('Button component', () => {
6
+ it('renders correctly with given text', () => {
7
+ render(<Button>Click me</Button>);
8
+ expect(screen.getByText('Click me')).toBeInTheDocument();
9
+ });
10
+
11
+ it('triggers onClick when properly clicked', () => {
12
+ const handleClick = vi.fn();
13
+ render(<Button onClick={handleClick}>Submit</Button>);
14
+ fireEvent.click(screen.getByText('Submit'));
15
+ expect(handleClick).toHaveBeenCalledTimes(1);
16
+ });
17
+
18
+ it('shows loading state properly', () => {
19
+ render(<Button isLoading={true}>Submit</Button>);
20
+ expect(screen.queryByText('Submit')?.closest('span')).toHaveClass('opacity-0');
21
+ });
22
+ });
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { motion, type HTMLMotionProps } from 'framer-motion';
3
+ import { clsx, type ClassValue } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+
6
+ function cn(...inputs: ClassValue[]) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+
10
+ interface ButtonProps extends HTMLMotionProps<"button"> {
11
+ variant?: 'primary' | 'secondary' | 'ghost';
12
+ isLoading?: boolean;
13
+ }
14
+
15
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
16
+ ({ className, variant = 'primary', isLoading, children, ...props }, ref) => {
17
+
18
+ const variants = {
19
+ primary: 'btn-primary !border-none',
20
+ secondary: 'btn-secondary !border-none',
21
+ ghost: 'bg-transparent hover:bg-black/5 dark:hover:bg-white/5 text-foreground/60 hover:text-foreground transition-all duration-300 !border-none',
22
+ };
23
+
24
+ return (
25
+ <motion.button
26
+ ref={ref}
27
+ whileHover={{ scale: 1.02, y: -2 }}
28
+ whileTap={{ scale: 0.98 }}
29
+ className={cn(
30
+ 'relative inline-flex items-center justify-center px-5 py-2 text-sm font-medium transition-all duration-300 overflow-hidden group',
31
+ variants[variant],
32
+ className
33
+ )}
34
+ style={{ borderRadius: 'var(--btn-radius, 0.75rem)' }}
35
+ disabled={isLoading || props.disabled}
36
+ {...props}
37
+ >
38
+ {isLoading ? (
39
+ <span className="absolute inset-0 flex items-center justify-center">
40
+ <svg className="w-5 h-5 animate-spin text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
41
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
42
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
43
+ </svg>
44
+ </span>
45
+ ) : null}
46
+ <span className={cn("inline-flex items-center justify-center gap-2", isLoading && 'opacity-0')}>
47
+ {children as React.ReactNode}
48
+ </span>
49
+ </motion.button>
50
+ );
51
+ }
52
+ );
53
+ Button.displayName = 'Button';
@@ -0,0 +1,98 @@
1
+ // @generated-test
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen, act } from '@testing-library/react';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import { AuthProvider } from '../lib/AuthContext';
6
+ import { ThemeProvider } from '../lib/ThemeContext';
7
+ import { ConfirmModal } from './ConfirmModal';
8
+
9
+ // Global mocks
10
+ Object.defineProperty(window, 'matchMedia', {
11
+ writable: true,
12
+ value: vi.fn().mockImplementation(query => ({
13
+ matches: false,
14
+ media: query,
15
+ onchange: null,
16
+ addListener: vi.fn(),
17
+ removeListener: vi.fn(),
18
+ addEventListener: vi.fn(),
19
+ removeEventListener: vi.fn(),
20
+ dispatchEvent: vi.fn(),
21
+ })),
22
+ });
23
+
24
+ vi.mock('../lib/AuthContext', () => ({
25
+ AuthProvider: ({ children }: any) => <>{children}</>,
26
+ useAuth: () => ({
27
+ user: { uid: 'mock-user-123', email: 'test@example.com' },
28
+ userWorkspaces: [],
29
+ activeWorkspace: null,
30
+ activeOrg: null,
31
+ loading: false
32
+ })
33
+ }));
34
+
35
+ vi.mock('../lib/ThemeContext', () => ({
36
+ ThemeProvider: ({ children }: any) => <>{children}</>,
37
+ useTheme: () => ({ themeMode: 'light', setThemeMode: vi.fn(), activeConfig: {} })
38
+ }));
39
+
40
+ vi.mock('firebase/auth', () => ({
41
+ getAuth: vi.fn(() => ({})),
42
+ onAuthStateChanged: vi.fn((auth, cb) => { cb({ uid: 'mock-user-123', email: 'test@example.com', getIdToken: vi.fn(() => Promise.resolve('mock-token')) }); return () => {}; })
43
+ }));
44
+ vi.mock('firebase/firestore', () => ({
45
+ getFirestore: vi.fn(() => ({})),
46
+ collection: vi.fn(),
47
+ doc: vi.fn(),
48
+ setDoc: vi.fn(() => Promise.resolve()),
49
+ addDoc: vi.fn(() => Promise.resolve()),
50
+ updateDoc: vi.fn(() => Promise.resolve()),
51
+ deleteDoc: vi.fn(() => Promise.resolve()),
52
+ query: vi.fn(),
53
+ where: vi.fn(),
54
+ orderBy: vi.fn(),
55
+ limit: vi.fn(),
56
+ getDoc: vi.fn(() => Promise.resolve({ exists: () => true, data: () => ({ role: 'super_admin' }) })),
57
+ getDocs: vi.fn(() => Promise.resolve({ docs: [], forEach: vi.fn() })),
58
+ onSnapshot: vi.fn((...args: any[]) => {
59
+ let cb = args[1];
60
+ if (typeof args[2] === 'function') {
61
+ cb = args[2];
62
+ }
63
+ if (typeof cb === 'function') {
64
+ cb({docs: [], forEach: vi.fn(), data: () => ({}), exists: () => true});
65
+ }
66
+ return () => {};
67
+ })
68
+ }));
69
+ vi.mock('firebase/storage', () => ({
70
+ getStorage: vi.fn(() => ({})),
71
+ ref: vi.fn(),
72
+ listAll: vi.fn(() => Promise.resolve({ items: [], prefixes: [] })),
73
+ getDownloadURL: vi.fn(() => Promise.resolve('mock-url')),
74
+ getMetadata: vi.fn(() => Promise.resolve({ size: 1024, timeCreated: new Date().toISOString() }))
75
+ }));
76
+
77
+ describe('ConfirmModal Component', () => {
78
+ it('renders without crashing', async () => {
79
+ // Wrap in standard application providers inside act to process async side effects and prevent warnings
80
+ await act(async () => {
81
+ render(
82
+ <BrowserRouter>
83
+ <AuthProvider>
84
+ <ThemeProvider>
85
+ {/* @ts-ignore */}
86
+ <ConfirmModal />
87
+ </ThemeProvider>
88
+ </AuthProvider>
89
+ </BrowserRouter>
90
+ );
91
+ // Wait a tick to flush background state updates
92
+ await new Promise(resolve => setTimeout(resolve, 0));
93
+ });
94
+
95
+ // Check if the document has anything rendered without throwing
96
+ expect(document.body).toBeDefined();
97
+ });
98
+ });
@@ -0,0 +1,73 @@
1
+ import { motion, AnimatePresence } from 'framer-motion';
2
+ import { AlertCircle, Loader2 } from 'lucide-react';
3
+ import { Button } from './Button';
4
+
5
+ export interface ConfirmModalProps {
6
+ isOpen: boolean;
7
+ title: string;
8
+ message: React.ReactNode;
9
+ confirmText?: string;
10
+ cancelText?: string;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ isProcessing?: boolean;
14
+ disableConfirm?: boolean;
15
+ }
16
+
17
+ export function ConfirmModal({
18
+ isOpen,
19
+ title,
20
+ message,
21
+ confirmText = 'Delete',
22
+ cancelText = 'Cancel',
23
+ onConfirm,
24
+ onCancel,
25
+ isProcessing = false,
26
+ disableConfirm = false
27
+ }: ConfirmModalProps) {
28
+ return (
29
+ <AnimatePresence>
30
+ {isOpen && (
31
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
32
+ <motion.div
33
+ onClick={() => !isProcessing && onCancel()}
34
+ initial={{ opacity: 0 }}
35
+ animate={{ opacity: 1 }}
36
+ exit={{ opacity: 0 }}
37
+ className="absolute inset-0 bg-background/80 backdrop-blur-xl cursor-pointer"
38
+ />
39
+ <motion.div
40
+ initial={{ opacity: 0, scale: 0.95, y: 10 }}
41
+ animate={{ opacity: 1, scale: 1, y: 0 }}
42
+ exit={{ opacity: 0, scale: 0.95, y: 10 }}
43
+ className="w-full max-w-[400px] p-6 rounded-3xl glass-panel border border-[var(--panel-border)] shadow-2xl relative z-10 text-center"
44
+ >
45
+ <div className="w-full flex flex-col items-center">
46
+ <h3 className="text-xl font-extrabold mb-2 text-foreground tracking-tight">{title}</h3>
47
+ <p className="text-[14px] font-medium text-foreground/60 leading-relaxed mb-6">
48
+ {message}
49
+ </p>
50
+ <div className="flex items-center gap-3 w-full">
51
+ <Button
52
+ variant="secondary"
53
+ onClick={onCancel}
54
+ disabled={isProcessing}
55
+ className="flex-1 py-2.5 text-[13px] font-bold rounded-xl"
56
+ >
57
+ {cancelText}
58
+ </Button>
59
+ <Button
60
+ onClick={onConfirm}
61
+ disabled={isProcessing || disableConfirm}
62
+ className="flex-1 py-2.5 text-[13px] font-bold rounded-xl bg-red-500 hover:bg-red-600 border-none text-white shadow-xl hover:shadow-red-500/20 glow-hover"
63
+ >
64
+ {isProcessing ? <Loader2 className="w-5 h-5 animate-spin mx-auto" /> : confirmText}
65
+ </Button>
66
+ </div>
67
+ </div>
68
+ </motion.div>
69
+ </div>
70
+ )}
71
+ </AnimatePresence>
72
+ );
73
+ }
@@ -0,0 +1,98 @@
1
+ // @generated-test
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen, act } from '@testing-library/react';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import { AuthProvider } from '../lib/AuthContext';
6
+ import { ThemeProvider } from '../lib/ThemeContext';
7
+ import { ContactPopup } from './ContactPopup';
8
+
9
+ // Global mocks
10
+ Object.defineProperty(window, 'matchMedia', {
11
+ writable: true,
12
+ value: vi.fn().mockImplementation(query => ({
13
+ matches: false,
14
+ media: query,
15
+ onchange: null,
16
+ addListener: vi.fn(),
17
+ removeListener: vi.fn(),
18
+ addEventListener: vi.fn(),
19
+ removeEventListener: vi.fn(),
20
+ dispatchEvent: vi.fn(),
21
+ })),
22
+ });
23
+
24
+ vi.mock('../lib/AuthContext', () => ({
25
+ AuthProvider: ({ children }: any) => <>{children}</>,
26
+ useAuth: () => ({
27
+ user: { uid: 'mock-user-123', email: 'test@example.com' },
28
+ userWorkspaces: [],
29
+ activeWorkspace: null,
30
+ activeOrg: null,
31
+ loading: false
32
+ })
33
+ }));
34
+
35
+ vi.mock('../lib/ThemeContext', () => ({
36
+ ThemeProvider: ({ children }: any) => <>{children}</>,
37
+ useTheme: () => ({ themeMode: 'light', setThemeMode: vi.fn(), activeConfig: {} })
38
+ }));
39
+
40
+ vi.mock('firebase/auth', () => ({
41
+ getAuth: vi.fn(() => ({})),
42
+ onAuthStateChanged: vi.fn((auth, cb) => { cb({ uid: 'mock-user-123', email: 'test@example.com', getIdToken: vi.fn(() => Promise.resolve('mock-token')) }); return () => {}; })
43
+ }));
44
+ vi.mock('firebase/firestore', () => ({
45
+ getFirestore: vi.fn(() => ({})),
46
+ collection: vi.fn(),
47
+ doc: vi.fn(),
48
+ setDoc: vi.fn(() => Promise.resolve()),
49
+ addDoc: vi.fn(() => Promise.resolve()),
50
+ updateDoc: vi.fn(() => Promise.resolve()),
51
+ deleteDoc: vi.fn(() => Promise.resolve()),
52
+ query: vi.fn(),
53
+ where: vi.fn(),
54
+ orderBy: vi.fn(),
55
+ limit: vi.fn(),
56
+ getDoc: vi.fn(() => Promise.resolve({ exists: () => true, data: () => ({ role: 'super_admin' }) })),
57
+ getDocs: vi.fn(() => Promise.resolve({ docs: [], forEach: vi.fn() })),
58
+ onSnapshot: vi.fn((...args: any[]) => {
59
+ let cb = args[1];
60
+ if (typeof args[2] === 'function') {
61
+ cb = args[2];
62
+ }
63
+ if (typeof cb === 'function') {
64
+ cb({docs: [], forEach: vi.fn(), data: () => ({}), exists: () => true});
65
+ }
66
+ return () => {};
67
+ })
68
+ }));
69
+ vi.mock('firebase/storage', () => ({
70
+ getStorage: vi.fn(() => ({})),
71
+ ref: vi.fn(),
72
+ listAll: vi.fn(() => Promise.resolve({ items: [], prefixes: [] })),
73
+ getDownloadURL: vi.fn(() => Promise.resolve('mock-url')),
74
+ getMetadata: vi.fn(() => Promise.resolve({ size: 1024, timeCreated: new Date().toISOString() }))
75
+ }));
76
+
77
+ describe('ContactPopup Component', () => {
78
+ it('renders without crashing', async () => {
79
+ // Wrap in standard application providers inside act to process async side effects and prevent warnings
80
+ await act(async () => {
81
+ render(
82
+ <BrowserRouter>
83
+ <AuthProvider>
84
+ <ThemeProvider>
85
+ {/* @ts-ignore */}
86
+ <ContactPopup />
87
+ </ThemeProvider>
88
+ </AuthProvider>
89
+ </BrowserRouter>
90
+ );
91
+ // Wait a tick to flush background state updates
92
+ await new Promise(resolve => setTimeout(resolve, 0));
93
+ });
94
+
95
+ // Check if the document has anything rendered without throwing
96
+ expect(document.body).toBeDefined();
97
+ });
98
+ });