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.
- package/dist/FirebaseOS.d.ts +15 -0
- package/dist/firebase-os.cjs.js +5 -20
- package/dist/firebase-os.es.js +95 -90
- package/dist/index.d.ts +3 -0
- package/dist/lib/ConfigContext.d.ts +12 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +86 -15
- package/src/App.css +184 -0
- package/src/App.tsx +214 -0
- package/src/FirebaseOS.tsx +81 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/AdminNotifications.test.tsx +98 -0
- package/src/components/AdminNotifications.tsx +194 -0
- package/src/components/Button.test.tsx +22 -0
- package/src/components/Button.tsx +53 -0
- package/src/components/ConfirmModal.test.tsx +98 -0
- package/src/components/ConfirmModal.tsx +73 -0
- package/src/components/ContactPopup.test.tsx +98 -0
- package/src/components/ContactPopup.tsx +437 -0
- package/src/components/CustomSelect.test.tsx +47 -0
- package/src/components/CustomSelect.tsx +89 -0
- package/src/components/DashboardNav.test.tsx +98 -0
- package/src/components/DashboardNav.tsx +281 -0
- package/src/components/Input.test.tsx +33 -0
- package/src/components/Input.tsx +61 -0
- package/src/components/JsonEditor.tsx +579 -0
- package/src/components/Navbar.test.tsx +98 -0
- package/src/components/Navbar.tsx +563 -0
- package/src/configs/forms/contactForm.config.ts +15 -0
- package/src/configs/forms/index.ts +29 -0
- package/src/configs/forms/pubForm.config.ts +11 -0
- package/src/configs/forms/supportForm.config.ts +14 -0
- package/src/configs/forms/userForm.config.ts +11 -0
- package/src/configs/pages/admin.config.ts +29 -0
- package/src/configs/pages/contact.config.ts +6 -0
- package/src/configs/pages/home.config.ts +18 -0
- package/src/configs/pages/mem.config.ts +2 -0
- package/src/configs/pages/menuOrders.config.ts +11 -0
- package/src/configs/pages/pub.config.ts +11 -0
- package/src/configs/pages/shared.config.ts +29 -0
- package/src/configs/pages/support.config.ts +7 -0
- package/src/configs/pages/tabOrders.config.ts +33 -0
- package/src/configs/pages/user.config.ts +29 -0
- package/src/configs/theme.config.ts +93 -0
- package/src/index.css +403 -0
- package/src/index.ts +22 -0
- package/src/lib/AuthContext.test.tsx +88 -0
- package/src/lib/AuthContext.tsx +191 -0
- package/src/lib/ConfigContext.tsx +45 -0
- package/src/lib/ThemeContext.tsx +233 -0
- package/src/lib/firebase.ts +91 -0
- package/src/main.tsx +22 -0
- package/src/microcomponents/AdminExampleContent.tsx +44 -0
- package/src/microcomponents/PrivateExampleContent.tsx +39 -0
- package/src/microcomponents/Public.tsx +126 -0
- package/src/microcomponents/SharedExampleContent.tsx +53 -0
- package/src/pages/Dashboard.test.tsx +98 -0
- package/src/pages/Dashboard.tsx +60 -0
- package/src/pages/DynamicPage.tsx +237 -0
- package/src/pages/FormsAdmin.test.tsx +98 -0
- package/src/pages/FormsAdmin.tsx +459 -0
- package/src/pages/Home.test.tsx +98 -0
- package/src/pages/Home.tsx +144 -0
- package/src/pages/Login.test.tsx +98 -0
- package/src/pages/Login.tsx +108 -0
- package/src/pages/PagesAdmin.test.tsx +98 -0
- package/src/pages/PagesAdmin.tsx +1022 -0
- package/src/pages/Profile.test.tsx +98 -0
- package/src/pages/Profile.tsx +319 -0
- package/src/pages/Register.test.tsx +98 -0
- package/src/pages/Register.tsx +116 -0
- package/src/pages/Requests.test.tsx +95 -0
- package/src/pages/Requests.tsx +422 -0
- package/src/pages/ResetPassword.test.tsx +98 -0
- package/src/pages/ResetPassword.tsx +92 -0
- package/src/pages/Settings.test.tsx +98 -0
- package/src/pages/Settings.tsx +393 -0
- package/src/pages/Setup.tsx +407 -0
- package/src/pages/StorageAdmin.test.tsx +150 -0
- package/src/pages/StorageAdmin.tsx +769 -0
- package/src/pages/Submissions.test.tsx +95 -0
- package/src/pages/Submissions.tsx +378 -0
- package/src/pages/Templates.test.tsx +98 -0
- package/src/pages/Templates.tsx +103 -0
- package/src/pages/ThemeAdmin.test.tsx +144 -0
- package/src/pages/ThemeAdmin.tsx +1000 -0
- package/src/pages/Users.test.tsx +95 -0
- package/src/pages/Users.tsx +334 -0
- package/src/pages/Verify.test.tsx +98 -0
- package/src/pages/Verify.tsx +95 -0
- package/src/prompts/index.ts +13 -0
- package/src/prompts/pages/publicPage.ts +44 -0
- package/src/prompts/sharedConstants.ts +12 -0
- package/src/prompts/tabs/board/adminboard.ts +32 -0
- package/src/prompts/tabs/board/privateboard.ts +36 -0
- package/src/prompts/tabs/board/publicboard.ts +36 -0
- package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
- package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
- package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
- package/src/prompts/tabs/crud/admin.ts +54 -0
- package/src/prompts/tabs/crud/private.ts +55 -0
- package/src/prompts/tabs/crud/shared.ts +53 -0
- package/src/prompts/tabs/table/admintable.ts +32 -0
- package/src/prompts/tabs/table/privatetable.ts +36 -0
- package/src/prompts/tabs/table/publictable.ts +36 -0
- package/src/setupTests.ts +1 -0
- package/src/templates/AdminPageTemplate.tsx +678 -0
- package/src/templates/PrivatePageTemplate.tsx +594 -0
- package/src/templates/PublicPageTemplate.tsx +92 -0
- package/src/templates/SharedPageTemplate.tsx +551 -0
- package/src/templates/TemplateBoard.test.tsx +106 -0
- package/src/templates/TemplateBoard.tsx +642 -0
- package/src/templates/TemplateCalendar.test.tsx +106 -0
- package/src/templates/TemplateCalendar.tsx +848 -0
- package/src/templates/TemplateConfirmation.test.tsx +106 -0
- package/src/templates/TemplateConfirmation.tsx +145 -0
- package/src/templates/TemplateInlineForm.test.tsx +106 -0
- package/src/templates/TemplateInlineForm.tsx +129 -0
- package/src/templates/TemplatePopupForm.test.tsx +106 -0
- package/src/templates/TemplatePopupForm.tsx +174 -0
- package/src/templates/TemplateTable.test.tsx +106 -0
- package/src/templates/TemplateTable.tsx +675 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { Zap } from 'lucide-react';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { ContactPopup } from '../components/ContactPopup';
|
|
6
|
+
import { AnimatePresence } from 'framer-motion';
|
|
7
|
+
import { doc, onSnapshot } from 'firebase/firestore';
|
|
8
|
+
import { db } from '../lib/firebase';
|
|
9
|
+
import { defaultHomeConfig } from '../configs/pages/home.config';
|
|
10
|
+
|
|
11
|
+
export function Home() {
|
|
12
|
+
const [showForm, setShowForm] = useState<string | null>(null);
|
|
13
|
+
const [homeConfig, setHomeConfig] = useState<any>(() => {
|
|
14
|
+
try {
|
|
15
|
+
const cached = localStorage.getItem('fbos_home_config');
|
|
16
|
+
if (cached) return JSON.parse(cached);
|
|
17
|
+
} catch(e) {}
|
|
18
|
+
return null;
|
|
19
|
+
});
|
|
20
|
+
const [isReady, setIsReady] = useState(() => !!localStorage.getItem('fbos_home_config'));
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const unsub = onSnapshot(doc(db, 'sys_pages', 'home'), docSnap => {
|
|
24
|
+
if (docSnap.exists()) {
|
|
25
|
+
const data = docSnap.data();
|
|
26
|
+
setHomeConfig(data);
|
|
27
|
+
localStorage.setItem('fbos_home_config', JSON.stringify(data));
|
|
28
|
+
} else {
|
|
29
|
+
setHomeConfig(defaultHomeConfig);
|
|
30
|
+
localStorage.setItem('fbos_home_config', JSON.stringify(defaultHomeConfig));
|
|
31
|
+
}
|
|
32
|
+
setIsReady(true);
|
|
33
|
+
});
|
|
34
|
+
return unsub;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (!isReady) return null;
|
|
38
|
+
|
|
39
|
+
const home = homeConfig || defaultHomeConfig;
|
|
40
|
+
|
|
41
|
+
// Safety fallback just in case data is being structured
|
|
42
|
+
const showSubtitle = home?.hero?.showSubtitle ?? true;
|
|
43
|
+
const subtitleEmoji = home?.hero?.subtitleEmoji ?? '⚡️';
|
|
44
|
+
const heroTitle = home?.hero?.title || home?.heroTitle || 'The Open-Source Firebase Starter';
|
|
45
|
+
const heroSubtitle = home?.hero?.subtitle || home?.heroSubtitle || 'The Open-Source Firebase Starter';
|
|
46
|
+
const heroText = home?.hero?.text || home?.heroText || 'A production-ready Firebase backend template with Auth, Firestore prefix rules, and a clean dashboard — ready for vibe-coded apps.';
|
|
47
|
+
|
|
48
|
+
const primaryText = home?.hero?.buttons?.primary?.text || home?.primaryBtnText || 'Start Building Free';
|
|
49
|
+
const primaryAction = home?.hero?.buttons?.primary?.action || home?.primaryBtnAction || 'register';
|
|
50
|
+
const primaryBtn = home?.hero?.buttons?.primary || {};
|
|
51
|
+
const secondaryBtn = home?.hero?.buttons?.secondary || {};
|
|
52
|
+
|
|
53
|
+
const renderButton = (btnData: any, defaultText: string, defaultAction: string, baseClassName: string) => {
|
|
54
|
+
const text = btnData.text || defaultText;
|
|
55
|
+
const action = btnData.action || defaultAction;
|
|
56
|
+
|
|
57
|
+
// Strip manual hardcoded rounded utilities and apply semantic radius
|
|
58
|
+
const cleanClassName = baseClassName.replace(/rounded-\[.*?\]|rounded-\w+/g, '').replace(/border /g, '').replace(/border-\[.*?\]/g, '');
|
|
59
|
+
const style = { borderRadius: 'var(--btn-radius, 0.75rem)' };
|
|
60
|
+
|
|
61
|
+
if (action === 'form') {
|
|
62
|
+
const targetForm = btnData.form || 'contact';
|
|
63
|
+
return (
|
|
64
|
+
<button onClick={() => setShowForm(targetForm)} className={cleanClassName} style={style}>
|
|
65
|
+
{text}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (action === 'redirect') {
|
|
71
|
+
const url = btnData.url || '/';
|
|
72
|
+
if (url.startsWith('http')) {
|
|
73
|
+
return (
|
|
74
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className={cleanClassName} style={style}>
|
|
75
|
+
{text}
|
|
76
|
+
</a>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
<Link to={url} className={cleanClassName} style={style}>
|
|
81
|
+
{text}
|
|
82
|
+
</Link>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pathMap: Record<string, string> = { sign_in: '/login', register: '/register' };
|
|
87
|
+
return (
|
|
88
|
+
<Link to={pathMap[action] || '/register'} className={cleanClassName} style={style}>
|
|
89
|
+
{text}
|
|
90
|
+
</Link>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<main className="flex-1 flex flex-col items-center justify-center w-full max-w-5xl mx-auto mt-4 md:mt-12 mb-16 md:mb-24 z-10 relative">
|
|
96
|
+
<motion.div
|
|
97
|
+
initial={{ opacity: 0, y: 30 }}
|
|
98
|
+
animate={{ opacity: 1, y: 0 }}
|
|
99
|
+
transition={{ duration: 0.8 }}
|
|
100
|
+
className="text-center mb-8 md:mb-12"
|
|
101
|
+
>
|
|
102
|
+
{showSubtitle && (
|
|
103
|
+
<motion.div
|
|
104
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
105
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
106
|
+
transition={{ delay: 0.2 }}
|
|
107
|
+
className="inline-flex items-center gap-2 md:gap-3 px-5 md:px-6 py-2 md:py-2.5 mb-8 md:mb-10 text-[13px] md:text-[15px] font-semibold tracking-wide uppercase rounded-full glass-panel text-foreground glow-hover cursor-pointer"
|
|
108
|
+
>
|
|
109
|
+
{heroSubtitle === defaultHomeConfig.hero.subtitle && <Zap className="w-4 h-4 text-accent" />} {heroSubtitle}
|
|
110
|
+
</motion.div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<motion.h1
|
|
114
|
+
className="text-5xl sm:text-6xl md:text-7xl lg:text-[5.5rem] font-extrabold tracking-tight mb-6 md:mb-8 text-gradient animate-float leading-[1.1] md:leading-[1.15] px-2"
|
|
115
|
+
>
|
|
116
|
+
{heroTitle}
|
|
117
|
+
</motion.h1>
|
|
118
|
+
|
|
119
|
+
<p className="text-[15px] sm:text-lg md:text-xl lg:text-[22px] text-foreground/60 max-w-3xl mx-auto font-medium leading-[1.6] md:leading-[1.7] mb-10 md:mb-14 px-6 md:px-4">
|
|
120
|
+
{heroText}
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
<motion.div
|
|
124
|
+
className="flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 w-full max-w-2xl mx-auto px-6"
|
|
125
|
+
initial={{ opacity: 0 }}
|
|
126
|
+
animate={{ opacity: 1 }}
|
|
127
|
+
transition={{ delay: 0.5 }}
|
|
128
|
+
>
|
|
129
|
+
{renderButton(primaryBtn, 'Start Building Free', 'register', "w-full sm:w-auto px-6 md:px-8 py-3 md:py-3.5 rounded-xl font-semibold tracking-wide btn-primary text-[15px] shadow-xl transition-all duration-300 hover:-translate-y-1 active:scale-95 flex items-center justify-center")}
|
|
130
|
+
{renderButton(secondaryBtn, 'Sign In', 'sign_in', "w-full sm:w-auto px-6 md:px-8 py-3 md:py-3.5 rounded-xl font-semibold tracking-wide btn-secondary text-[15px] transition-all duration-300 flex items-center justify-center hover:-translate-y-1")}
|
|
131
|
+
</motion.div>
|
|
132
|
+
</motion.div>
|
|
133
|
+
|
|
134
|
+
<AnimatePresence>
|
|
135
|
+
{showForm && (
|
|
136
|
+
<ContactPopup
|
|
137
|
+
onClose={() => setShowForm(null)}
|
|
138
|
+
formId={showForm}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</AnimatePresence>
|
|
142
|
+
</main>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -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 { Login } from './Login';
|
|
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('Login 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
|
+
<Login />
|
|
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,108 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import { ArrowRight } from 'lucide-react';
|
|
5
|
+
import { Input } from '../components/Input';
|
|
6
|
+
import { Button } from '../components/Button';
|
|
7
|
+
import { auth } from '../lib/firebase';
|
|
8
|
+
import { signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
|
9
|
+
|
|
10
|
+
export function Login() {
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState<React.ReactNode | null>(null);
|
|
13
|
+
const [searchParams] = useSearchParams();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const [emailValue] = useState(searchParams.get('email') || '');
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
const formData = new FormData(e.currentTarget);
|
|
23
|
+
const email = formData.get('email') as string;
|
|
24
|
+
const password = formData.get('password') as string;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await signInWithEmailAndPassword(auth, email, password);
|
|
28
|
+
// Wait to redirect and trigger reload only if necessary on the new path!
|
|
29
|
+
const intendedUrl = searchParams.get('from') || '/dashboard';
|
|
30
|
+
navigate(intendedUrl, { replace: true });
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
if (err?.code === 'auth/invalid-credential' || err?.code === 'auth/user-not-found' || err?.code === 'auth/wrong-password') {
|
|
33
|
+
setError(
|
|
34
|
+
<span>
|
|
35
|
+
Email or Password are incorrect.{' '}
|
|
36
|
+
<Link to="/reset-password" className="font-bold underline hover:text-red-400 transition-colors">Reset password?</Link>
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
setError(err?.message || 'Invalid credentials.');
|
|
41
|
+
}
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleGoogle = async () => {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
try {
|
|
49
|
+
const provider = new GoogleAuthProvider();
|
|
50
|
+
await signInWithPopup(auth, provider);
|
|
51
|
+
const intendedUrl = searchParams.get('from') || '/dashboard';
|
|
52
|
+
navigate(intendedUrl, { replace: true });
|
|
53
|
+
} catch (err: unknown) {
|
|
54
|
+
if (err instanceof Error) {
|
|
55
|
+
setError(err.message);
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex-1 flex items-center justify-center w-full z-10 px-4 mt-6 md:mt-8 mb-8">
|
|
64
|
+
<motion.div
|
|
65
|
+
initial={{ opacity: 0, y: 10 }}
|
|
66
|
+
animate={{ opacity: 1, y: 0 }}
|
|
67
|
+
transition={{ duration: 0.5 }}
|
|
68
|
+
className="w-full max-w-[480px] p-6 sm:p-8 lg:p-10 rounded-3xl md:rounded-3xl glass-panel relative glow-hover shadow-[0_8px_32px_rgba(0,0,0,0.08)]"
|
|
69
|
+
>
|
|
70
|
+
<div className="absolute -inset-[1px] bg-gradient-to-br from-[var(--primary-glow)] to-transparent opacity-30 blur-[20px] rounded-3xl md:rounded-3xl -z-10" />
|
|
71
|
+
|
|
72
|
+
<div className="mb-6 text-center">
|
|
73
|
+
<h3 className="text-2xl sm:text-3xl font-extrabold mb-2 text-foreground tracking-tight">Welcome back</h3>
|
|
74
|
+
<p className="text-[14px] md:text-[15px] font-medium text-foreground/60 tracking-wide">Sign in to Firebase OS.</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{error && <div className="p-3 mb-6 text-[14px] font-medium text-red-400 bg-red-500/10 rounded-xl border border-red-500/20">{error}</div>}
|
|
78
|
+
|
|
79
|
+
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
80
|
+
<Input name="email" label="Email" type="email" defaultValue={emailValue} placeholder="name@example.com" required />
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
<div className="flex justify-between items-center pr-1">
|
|
83
|
+
<label className="text-[14px] md:text-[15px] tracking-wide font-medium text-foreground/80 select-none pb-1">Password</label>
|
|
84
|
+
<Link to="/reset-password" className="text-[13px] md:text-[14px] text-accent hover:underline font-bold tracking-wide">Forgot Password?</Link>
|
|
85
|
+
</div>
|
|
86
|
+
<Input name="password" type="password" placeholder="••••••••" required />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="pt-2">
|
|
90
|
+
<Button type="submit" className="w-full py-3 text-base font-bold tracking-wide rounded-xl transition-all hover:-translate-y-0.5" isLoading={loading}>
|
|
91
|
+
Sign In <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform shrink-0" />
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
|
|
96
|
+
<div className="mt-6 pt-5 border-t border-[var(--panel-border)] space-y-4 text-center">
|
|
97
|
+
<Button variant="secondary" onClick={handleGoogle} className="w-full py-3 text-base font-bold tracking-wide rounded-xl transition-all hover:-translate-y-0.5" disabled={loading}>
|
|
98
|
+
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24"><path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" /><path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /><path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /><path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /></svg>
|
|
99
|
+
Sign in with Google
|
|
100
|
+
</Button>
|
|
101
|
+
<p className="text-[14px] text-foreground/70 font-medium pt-2 tracking-wide">
|
|
102
|
+
New here? <Link to="/register" className="text-accent hover:underline font-bold ml-1">Create account</Link>
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</motion.div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -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 { PagesAdmin } from './PagesAdmin';
|
|
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('PagesAdmin 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
|
+
<PagesAdmin />
|
|
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
|
+
});
|