@react-spa-scaffold/mcp 0.3.0
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/README.md +423 -0
- package/dist/features/index.d.ts +5 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +3 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +10 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +508 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/types.d.ts +45 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/types.js +5 -0
- package/dist/features/types.js.map +1 -0
- package/dist/features/versions.d.ts +16 -0
- package/dist/features/versions.d.ts.map +1 -0
- package/dist/features/versions.js +46 -0
- package/dist/features/versions.js.map +1 -0
- package/dist/features/versions.json +5 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/docs.d.ts +29 -0
- package/dist/resources/docs.d.ts.map +1 -0
- package/dist/resources/docs.js +105 -0
- package/dist/resources/docs.js.map +1 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +115 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/get-example.d.ts +51 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +90 -0
- package/dist/tools/get-example.js.map +1 -0
- package/dist/tools/get-features.d.ts +30 -0
- package/dist/tools/get-features.d.ts.map +1 -0
- package/dist/tools/get-features.js +46 -0
- package/dist/tools/get-features.js.map +1 -0
- package/dist/tools/get-scaffold.d.ts +77 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -0
- package/dist/tools/get-scaffold.js +153 -0
- package/dist/tools/get-scaffold.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/utils/docs.d.ts +14 -0
- package/dist/utils/docs.d.ts.map +1 -0
- package/dist/utils/docs.js +64 -0
- package/dist/utils/docs.js.map +1 -0
- package/dist/utils/examples.d.ts +27 -0
- package/dist/utils/examples.d.ts.map +1 -0
- package/dist/utils/examples.js +399 -0
- package/dist/utils/examples.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/paths.d.ts +28 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +40 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/scaffold.d.ts +50 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +500 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -0
- package/templates/.bundled +0 -0
- package/templates/CLAUDE.md +145 -0
- package/templates/docs/API_REFERENCE.md +58 -0
- package/templates/docs/ARCHITECTURE.md +185 -0
- package/templates/docs/CODING_STANDARDS.md +53 -0
- package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
- package/templates/docs/E2E_TESTING.md +116 -0
- package/templates/docs/INTERNATIONALIZATION.md +67 -0
- package/templates/docs/TESTING.md +259 -0
- package/templates/docs/WORKFLOW.md +170 -0
- package/templates/src/App.tsx +42 -0
- package/templates/src/components/layout/Header.tsx +19 -0
- package/templates/src/components/layout/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
- package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
- package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
- package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
- package/templates/src/components/shared/SEO/SEO.tsx +55 -0
- package/templates/src/components/shared/SEO/index.ts +1 -0
- package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
- package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
- package/templates/src/components/shared/index.ts +4 -0
- package/templates/src/components/ui/button.tsx +48 -0
- package/templates/src/components/ui/dropdown-menu.tsx +228 -0
- package/templates/src/components/ui/form-error.tsx +95 -0
- package/templates/src/components/ui/loading.tsx +58 -0
- package/templates/src/components/ui/skeleton.tsx +52 -0
- package/templates/src/components/ui/sonner.tsx +34 -0
- package/templates/src/components/ui/spinner.tsx +40 -0
- package/templates/src/components/ui/visually-hidden.tsx +51 -0
- package/templates/src/contexts/mobileContext.tsx +66 -0
- package/templates/src/contexts/queryContext.tsx +28 -0
- package/templates/src/hooks/index.ts +7 -0
- package/templates/src/hooks/useContactForm.ts +33 -0
- package/templates/src/hooks/useExampleQuery.ts +20 -0
- package/templates/src/hooks/useLanguage.ts +23 -0
- package/templates/src/hooks/useMediaQuery.ts +53 -0
- package/templates/src/hooks/useThemeEffect.ts +31 -0
- package/templates/src/hooks/useTouchSizes.ts +16 -0
- package/templates/src/i18n/config.ts +11 -0
- package/templates/src/i18n/detectLanguage.ts +57 -0
- package/templates/src/i18n/index.ts +20 -0
- package/templates/src/i18n/loadCatalog.ts +30 -0
- package/templates/src/index.css +98 -0
- package/templates/src/lib/api.ts +142 -0
- package/templates/src/lib/config.ts +15 -0
- package/templates/src/lib/constants.ts +8 -0
- package/templates/src/lib/env.ts +53 -0
- package/templates/src/lib/format.ts +119 -0
- package/templates/src/lib/index.ts +24 -0
- package/templates/src/lib/routes.ts +11 -0
- package/templates/src/lib/storage.ts +91 -0
- package/templates/src/lib/storageKeys.ts +10 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/lib/validations.ts +39 -0
- package/templates/src/locales/de.po +65 -0
- package/templates/src/locales/en.po +65 -0
- package/templates/src/locales/es.po +65 -0
- package/templates/src/main.tsx +107 -0
- package/templates/src/mocks/fixtures/index.ts +1 -0
- package/templates/src/mocks/fixtures/todos.ts +40 -0
- package/templates/src/mocks/handlers/index.ts +7 -0
- package/templates/src/mocks/handlers/todos.ts +59 -0
- package/templates/src/mocks/index.ts +3 -0
- package/templates/src/mocks/node.ts +9 -0
- package/templates/src/pages/Home.tsx +27 -0
- package/templates/src/pages/NotFound.tsx +28 -0
- package/templates/src/pages/index.ts +2 -0
- package/templates/src/stores/index.ts +2 -0
- package/templates/src/stores/preferencesStore.ts +85 -0
- package/templates/src/test/index.ts +8 -0
- package/templates/src/test/mocks.ts +17 -0
- package/templates/src/test/providers.tsx +54 -0
- package/templates/src/test-setup.ts +54 -0
- package/templates/src/types/api.ts +31 -0
- package/templates/src/types/index.ts +2 -0
- package/templates/src/types/preferences.ts +5 -0
- package/templates/src/vite-env.d.ts +10 -0
- package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
- package/templates/tests/unit/components/Header.test.tsx +33 -0
- package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
- package/templates/tests/unit/components/Loading.test.tsx +76 -0
- package/templates/tests/unit/components/SEO.test.tsx +80 -0
- package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
- package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
- package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
- package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
- package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
- package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
- package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
- package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
- package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
- package/templates/tests/unit/lib/api.test.ts +142 -0
- package/templates/tests/unit/lib/format.test.ts +100 -0
- package/templates/tests/unit/lib/storage.test.ts +90 -0
- package/templates/tests/unit/lib/utils.test.ts +19 -0
- package/templates/tests/unit/lib/validations.test.ts +56 -0
- package/templates/tests/unit/stores/preferencesStore.test.ts +75 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
msgid ""
|
|
2
|
+
msgstr ""
|
|
3
|
+
"Language: es\n"
|
|
4
|
+
"Project-Id-Version: \n"
|
|
5
|
+
"Report-Msgid-Bugs-To: \n"
|
|
6
|
+
"POT-Creation-Date: \n"
|
|
7
|
+
"PO-Revision-Date: \n"
|
|
8
|
+
"Last-Translator: \n"
|
|
9
|
+
"Language-Team: \n"
|
|
10
|
+
"Content-Type: \n"
|
|
11
|
+
"Content-Transfer-Encoding: \n"
|
|
12
|
+
"Plural-Forms: \n"
|
|
13
|
+
|
|
14
|
+
#. Button label to navigate back to the home page from 404 error
|
|
15
|
+
#: src/pages/NotFound.tsx:23
|
|
16
|
+
msgid "Back to Home"
|
|
17
|
+
msgstr ""
|
|
18
|
+
|
|
19
|
+
#. Accessibility label for the language selector dropdown button
|
|
20
|
+
#: src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx:24
|
|
21
|
+
msgid "Change language"
|
|
22
|
+
msgstr "Cambiar idioma"
|
|
23
|
+
|
|
24
|
+
#. Instructions for developers on how to start customizing the app
|
|
25
|
+
#: src/pages/Home.tsx:10
|
|
26
|
+
msgid "Get started by editing <0>src/App.tsx</0>"
|
|
27
|
+
msgstr "Comienza editando <0>src/App.tsx</0>"
|
|
28
|
+
|
|
29
|
+
#: src/components/ui/loading.tsx:54
|
|
30
|
+
msgid "Loading..."
|
|
31
|
+
msgstr ""
|
|
32
|
+
|
|
33
|
+
#. Application name displayed in the header navigation
|
|
34
|
+
#: src/components/layout/Header.tsx:10
|
|
35
|
+
msgid "My App"
|
|
36
|
+
msgstr "Mi App"
|
|
37
|
+
|
|
38
|
+
#. Heading shown on 404 error page when URL doesn't exist
|
|
39
|
+
#: src/pages/NotFound.tsx:13
|
|
40
|
+
msgid "Page Not Found"
|
|
41
|
+
msgstr ""
|
|
42
|
+
|
|
43
|
+
#. Accessibility label when clicking will switch to dark theme
|
|
44
|
+
#. Accessibility label when clicking will switch to dark theme
|
|
45
|
+
#: src/components/shared/ThemeToggle/ThemeToggle.tsx:26
|
|
46
|
+
#: src/components/shared/ThemeToggle/ThemeToggle.tsx:29
|
|
47
|
+
msgid "Switch to dark mode"
|
|
48
|
+
msgstr "Cambiar a modo oscuro"
|
|
49
|
+
|
|
50
|
+
#. Accessibility label when clicking will switch to light theme
|
|
51
|
+
#. Accessibility label when clicking will switch to light theme
|
|
52
|
+
#: src/components/shared/ThemeToggle/ThemeToggle.tsx:22
|
|
53
|
+
#: src/components/shared/ThemeToggle/ThemeToggle.tsx:30
|
|
54
|
+
msgid "Switch to light mode"
|
|
55
|
+
msgstr "Cambiar a modo claro"
|
|
56
|
+
|
|
57
|
+
#. Explanation message on 404 error page
|
|
58
|
+
#: src/pages/NotFound.tsx:16
|
|
59
|
+
msgid "The page you're looking for doesn't exist or has been moved."
|
|
60
|
+
msgstr ""
|
|
61
|
+
|
|
62
|
+
#. Main heading on the home page
|
|
63
|
+
#: src/pages/Home.tsx:7
|
|
64
|
+
msgid "Welcome to My App"
|
|
65
|
+
msgstr "Bienvenido a Mi App"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { I18nProvider } from '@lingui/react';
|
|
2
|
+
import { StrictMode } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import './index.css';
|
|
7
|
+
import { ErrorBoundary } from '@/components/shared';
|
|
8
|
+
import { Toaster } from '@/components/ui/sonner';
|
|
9
|
+
import { MobileProvider } from '@/contexts/mobileContext';
|
|
10
|
+
import { QueryProvider } from '@/contexts/queryContext';
|
|
11
|
+
import { i18n, initI18n } from '@/i18n';
|
|
12
|
+
import { SENTRY_CONFIG } from '@/lib/config';
|
|
13
|
+
import { initPreferencesSync } from '@/stores/preferencesStore';
|
|
14
|
+
|
|
15
|
+
import App from './App';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lazy load Sentry after initial render to avoid blocking web vitals.
|
|
19
|
+
* Returns the Sentry module for use in global error handlers.
|
|
20
|
+
*/
|
|
21
|
+
async function initSentry() {
|
|
22
|
+
if (import.meta.env.PROD && SENTRY_CONFIG.enabled && SENTRY_CONFIG.dsn) {
|
|
23
|
+
try {
|
|
24
|
+
const Sentry = await import('@sentry/react');
|
|
25
|
+
Sentry.init({
|
|
26
|
+
dsn: SENTRY_CONFIG.dsn,
|
|
27
|
+
sendDefaultPii: true,
|
|
28
|
+
integrations: [Sentry.browserTracingIntegration()],
|
|
29
|
+
tracesSampleRate: SENTRY_CONFIG.tracesSampleRate,
|
|
30
|
+
});
|
|
31
|
+
return Sentry;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to initialize Sentry:', error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Setup global error handlers for uncaught errors and promise rejections.
|
|
41
|
+
* @param Sentry - The Sentry module or null if not available
|
|
42
|
+
*/
|
|
43
|
+
function setupGlobalErrorHandlers(Sentry: Awaited<ReturnType<typeof initSentry>>) {
|
|
44
|
+
// Handle uncaught errors
|
|
45
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
46
|
+
console.error('Uncaught error:', { message, source, lineno, colno, error });
|
|
47
|
+
|
|
48
|
+
if (Sentry && error) {
|
|
49
|
+
Sentry.captureException(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Return false to allow default browser handling
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Handle unhandled promise rejections
|
|
57
|
+
window.onunhandledrejection = (event) => {
|
|
58
|
+
console.error('Unhandled promise rejection:', event.reason);
|
|
59
|
+
|
|
60
|
+
if (Sentry) {
|
|
61
|
+
Sentry.captureException(event.reason);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize i18n before rendering
|
|
67
|
+
initI18n().then(() => {
|
|
68
|
+
// Initialize multi-tab sync for preferences
|
|
69
|
+
const cleanupPreferencesSync = initPreferencesSync();
|
|
70
|
+
|
|
71
|
+
createRoot(document.getElementById('root')!).render(
|
|
72
|
+
<StrictMode>
|
|
73
|
+
<QueryProvider>
|
|
74
|
+
<I18nProvider i18n={i18n}>
|
|
75
|
+
<BrowserRouter>
|
|
76
|
+
<MobileProvider>
|
|
77
|
+
<ErrorBoundary>
|
|
78
|
+
<App />
|
|
79
|
+
<Toaster />
|
|
80
|
+
</ErrorBoundary>
|
|
81
|
+
</MobileProvider>
|
|
82
|
+
</BrowserRouter>
|
|
83
|
+
</I18nProvider>
|
|
84
|
+
</QueryProvider>
|
|
85
|
+
</StrictMode>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Initialize Sentry after render, using idle callback for best web vitals
|
|
89
|
+
const initSentryAndHandlers = () => {
|
|
90
|
+
initSentry().then((Sentry) => {
|
|
91
|
+
setupGlobalErrorHandlers(Sentry);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if ('requestIdleCallback' in window) {
|
|
96
|
+
requestIdleCallback(initSentryAndHandlers);
|
|
97
|
+
} else {
|
|
98
|
+
setTimeout(initSentryAndHandlers, 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cleanup on HMR (development only)
|
|
102
|
+
if (import.meta.hot) {
|
|
103
|
+
import.meta.hot.dispose(() => {
|
|
104
|
+
cleanupPreferencesSync();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { mockTodos, createTodo, createTodos, type Todo } from './todos';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock data and factory functions for todos.
|
|
3
|
+
* Matches the JSONPlaceholder API structure used by useExampleQuery.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Todo } from '@/types/api';
|
|
7
|
+
|
|
8
|
+
export type { Todo };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sample todos for testing.
|
|
12
|
+
* These match the structure from https://jsonplaceholder.typicode.com/todos
|
|
13
|
+
*/
|
|
14
|
+
export const mockTodos: Todo[] = [
|
|
15
|
+
{ userId: 1, id: 1, title: 'Setup MSW for testing', completed: true },
|
|
16
|
+
{ userId: 1, id: 2, title: 'Write integration tests', completed: false },
|
|
17
|
+
{ userId: 1, id: 3, title: 'Add error handling', completed: false },
|
|
18
|
+
{ userId: 2, id: 4, title: 'Review PR', completed: false },
|
|
19
|
+
{ userId: 2, id: 5, title: 'Deploy to staging', completed: false },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Factory function to create a single todo with optional overrides.
|
|
24
|
+
*/
|
|
25
|
+
export function createTodo(overrides: Partial<Todo> = {}): Todo {
|
|
26
|
+
return {
|
|
27
|
+
userId: 1,
|
|
28
|
+
id: Date.now(),
|
|
29
|
+
title: 'New Todo',
|
|
30
|
+
completed: false,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Factory function to create multiple todos.
|
|
37
|
+
*/
|
|
38
|
+
export function createTodos(count: number, overrides: Partial<Todo> = {}): Todo[] {
|
|
39
|
+
return Array.from({ length: count }, (_, i) => createTodo({ id: i + 1, title: `Todo ${i + 1}`, ...overrides }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW handlers for JSONPlaceholder todos API.
|
|
3
|
+
* These intercept requests to the API used by useExampleQuery.
|
|
4
|
+
*/
|
|
5
|
+
import { delay, http, HttpResponse } from 'msw';
|
|
6
|
+
|
|
7
|
+
import { API_CONFIG } from '@/lib/api';
|
|
8
|
+
import type { Todo } from '@/types/api';
|
|
9
|
+
|
|
10
|
+
import { mockTodos } from '../fixtures/todos';
|
|
11
|
+
|
|
12
|
+
const BASE_URL = API_CONFIG.baseUrl;
|
|
13
|
+
|
|
14
|
+
export const todosHandlers = [
|
|
15
|
+
// GET /todos - List todos with optional limit
|
|
16
|
+
http.get(`${BASE_URL}/todos`, async ({ request }) => {
|
|
17
|
+
const url = new URL(request.url);
|
|
18
|
+
const limit = url.searchParams.get('_limit');
|
|
19
|
+
|
|
20
|
+
// Small delay to simulate network latency
|
|
21
|
+
await delay(50);
|
|
22
|
+
|
|
23
|
+
const todos = limit ? mockTodos.slice(0, parseInt(limit, 10)) : mockTodos;
|
|
24
|
+
|
|
25
|
+
return HttpResponse.json(todos);
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
// GET /todos/:id - Get single todo
|
|
29
|
+
http.get(`${BASE_URL}/todos/:id`, async ({ params }) => {
|
|
30
|
+
const { id } = params;
|
|
31
|
+
const todo = mockTodos.find((t) => t.id === Number(id));
|
|
32
|
+
|
|
33
|
+
await delay(50);
|
|
34
|
+
|
|
35
|
+
if (!todo) {
|
|
36
|
+
return new HttpResponse(null, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return HttpResponse.json(todo);
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
// POST /todos - Create todo
|
|
43
|
+
http.post(`${BASE_URL}/todos`, async ({ request }) => {
|
|
44
|
+
const body = (await request.json()) as Partial<Todo>;
|
|
45
|
+
|
|
46
|
+
await delay(50);
|
|
47
|
+
|
|
48
|
+
return HttpResponse.json(
|
|
49
|
+
{
|
|
50
|
+
id: mockTodos.length + 1,
|
|
51
|
+
userId: 1,
|
|
52
|
+
title: '',
|
|
53
|
+
completed: false,
|
|
54
|
+
...body,
|
|
55
|
+
},
|
|
56
|
+
{ status: 201 },
|
|
57
|
+
);
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW server for Node.js (testing environment).
|
|
3
|
+
* This is used by Vitest to intercept network requests during tests.
|
|
4
|
+
*/
|
|
5
|
+
import { setupServer } from 'msw/node';
|
|
6
|
+
|
|
7
|
+
import { handlers } from './handlers';
|
|
8
|
+
|
|
9
|
+
export const server = setupServer(...handlers);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Trans, useLingui } from '@lingui/react/macro';
|
|
2
|
+
|
|
3
|
+
import { SEO } from '@/components/shared';
|
|
4
|
+
|
|
5
|
+
export function HomePage() {
|
|
6
|
+
const { t } = useLingui();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="container mx-auto px-4 py-8">
|
|
10
|
+
<SEO
|
|
11
|
+
title={t({ message: 'Home', comment: 'Home page title for SEO' })}
|
|
12
|
+
description={t({
|
|
13
|
+
message: 'Welcome to our modern React application',
|
|
14
|
+
comment: 'Home page meta description for SEO',
|
|
15
|
+
})}
|
|
16
|
+
/>
|
|
17
|
+
<h1 className="text-3xl font-bold">
|
|
18
|
+
<Trans comment="Main heading on the home page">Welcome to My App</Trans>
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-muted-foreground mt-2">
|
|
21
|
+
<Trans comment="Instructions for developers on how to start customizing the app">
|
|
22
|
+
Get started by editing <code className="bg-muted rounded px-1">src/App.tsx</code>
|
|
23
|
+
</Trans>
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro';
|
|
2
|
+
import { Home } from 'lucide-react';
|
|
3
|
+
import { Link } from 'react-router';
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { ROUTES } from '@/lib/routes';
|
|
7
|
+
|
|
8
|
+
export function NotFoundPage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="container mx-auto flex min-h-[60vh] flex-col items-center justify-center px-4 py-16 text-center">
|
|
11
|
+
<h1 className="text-foreground text-6xl font-bold">404</h1>
|
|
12
|
+
<h2 className="text-muted-foreground mt-4 text-2xl">
|
|
13
|
+
<Trans comment="Heading shown on 404 error page when URL doesn't exist">Page Not Found</Trans>
|
|
14
|
+
</h2>
|
|
15
|
+
<p className="text-muted-foreground mt-2 max-w-md">
|
|
16
|
+
<Trans comment="Explanation message on 404 error page">
|
|
17
|
+
The page you're looking for doesn't exist or has been moved.
|
|
18
|
+
</Trans>
|
|
19
|
+
</p>
|
|
20
|
+
<Button asChild className="mt-8">
|
|
21
|
+
<Link to={ROUTES.HOME}>
|
|
22
|
+
<Home className="mr-2 size-4" />
|
|
23
|
+
<Trans comment="Button label to navigate back to the home page from 404 error">Back to Home</Trans>
|
|
24
|
+
</Link>
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { devtools, persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
import { STORAGE_KEYS } from '@/lib/storageKeys';
|
|
5
|
+
|
|
6
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
7
|
+
|
|
8
|
+
export interface Preferences {
|
|
9
|
+
theme: Theme;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PreferencesState extends Preferences {
|
|
13
|
+
setTheme: (theme: Theme) => void;
|
|
14
|
+
toggleTheme: () => void;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Get the resolved theme (light/dark) based on system preference when theme is 'system'
|
|
18
|
+
*/
|
|
19
|
+
getResolvedTheme: () => 'light' | 'dark';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const initialState: Preferences = {
|
|
23
|
+
theme: 'system',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the system's preferred color scheme
|
|
28
|
+
*/
|
|
29
|
+
function getSystemTheme(): 'light' | 'dark' {
|
|
30
|
+
if (typeof window === 'undefined') return 'light';
|
|
31
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const usePreferencesStore = create<PreferencesState>()(
|
|
35
|
+
devtools(
|
|
36
|
+
persist(
|
|
37
|
+
(set, get) => ({
|
|
38
|
+
...initialState,
|
|
39
|
+
setTheme: (theme) => set({ theme }),
|
|
40
|
+
toggleTheme: () =>
|
|
41
|
+
set((state) => {
|
|
42
|
+
const resolved = state.theme === 'system' ? getSystemTheme() : state.theme;
|
|
43
|
+
return { theme: resolved === 'light' ? 'dark' : 'light' };
|
|
44
|
+
}),
|
|
45
|
+
reset: () => set(initialState),
|
|
46
|
+
getResolvedTheme: () => {
|
|
47
|
+
const { theme } = get();
|
|
48
|
+
return theme === 'system' ? getSystemTheme() : theme;
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
{
|
|
52
|
+
name: STORAGE_KEYS.preferences,
|
|
53
|
+
partialize: (state) => ({ theme: state.theme }),
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
{ name: 'preferences' },
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize multi-tab sync for preferences.
|
|
62
|
+
* Call this once at app startup.
|
|
63
|
+
*/
|
|
64
|
+
export function initPreferencesSync(): () => void {
|
|
65
|
+
if (typeof window === 'undefined') return () => {};
|
|
66
|
+
|
|
67
|
+
const handleStorage = (e: StorageEvent) => {
|
|
68
|
+
if (e.key === STORAGE_KEYS.preferences && e.newValue) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(e.newValue);
|
|
71
|
+
if (parsed.state) {
|
|
72
|
+
usePreferencesStore.setState(parsed.state);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore parse errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
window.addEventListener('storage', handleStorage);
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
window.removeEventListener('storage', handleStorage);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock for window.matchMedia.
|
|
5
|
+
* Usage: window.matchMedia = mockMatchMedia(true) // matches
|
|
6
|
+
*/
|
|
7
|
+
export const mockMatchMedia = (matches: boolean) =>
|
|
8
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
9
|
+
matches,
|
|
10
|
+
media: query,
|
|
11
|
+
onchange: null,
|
|
12
|
+
addEventListener: vi.fn(),
|
|
13
|
+
removeEventListener: vi.fn(),
|
|
14
|
+
addListener: vi.fn(),
|
|
15
|
+
removeListener: vi.fn(),
|
|
16
|
+
dispatchEvent: vi.fn(),
|
|
17
|
+
}));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { i18n } from '@lingui/core';
|
|
2
|
+
import { I18nProvider } from '@lingui/react';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { render, type RenderOptions } from '@testing-library/react';
|
|
5
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
6
|
+
import { MemoryRouter } from 'react-router';
|
|
7
|
+
|
|
8
|
+
import { MobileProvider } from '@/contexts/mobileContext';
|
|
9
|
+
|
|
10
|
+
// Setup empty English catalog for tests
|
|
11
|
+
i18n.loadAndActivate({ locale: 'en', messages: {} });
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a fresh QueryClient for each test with test-optimized settings.
|
|
15
|
+
* Best practices from TanStack Query docs:
|
|
16
|
+
* - retry: false - Faster failure tests, no retry delays
|
|
17
|
+
* - gcTime: 0 - Prevents caching between tests
|
|
18
|
+
* - staleTime: 0 - Data always considered stale in tests
|
|
19
|
+
*/
|
|
20
|
+
export function createTestQueryClient() {
|
|
21
|
+
return new QueryClient({
|
|
22
|
+
defaultOptions: {
|
|
23
|
+
queries: {
|
|
24
|
+
retry: false,
|
|
25
|
+
gcTime: 0,
|
|
26
|
+
staleTime: 0,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface WrapperProps {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function AllProviders({ children }: WrapperProps) {
|
|
37
|
+
const queryClient = createTestQueryClient();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<QueryClientProvider client={queryClient}>
|
|
41
|
+
<I18nProvider i18n={i18n}>
|
|
42
|
+
<MemoryRouter>
|
|
43
|
+
<MobileProvider>{children}</MobileProvider>
|
|
44
|
+
</MemoryRouter>
|
|
45
|
+
</I18nProvider>
|
|
46
|
+
</QueryClientProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function customRender(ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) {
|
|
51
|
+
return render(ui, { wrapper: AllProviders, ...options });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { customRender as render };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
import { afterAll, afterEach, beforeAll } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { server } from '@/mocks/node';
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// MSW Server Setup
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
// Start MSW server before all tests
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
server.listen({
|
|
13
|
+
onUnhandledRequest: 'warn', // Warn about unhandled requests during tests
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Reset handlers after each test to ensure test isolation
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
server.resetHandlers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Close MSW server after all tests complete
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
server.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Browser API Mocks
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
// Mock ResizeObserver
|
|
32
|
+
global.ResizeObserver = class ResizeObserver {
|
|
33
|
+
observe() {}
|
|
34
|
+
unobserve() {}
|
|
35
|
+
disconnect() {}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Mock matchMedia
|
|
39
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
40
|
+
writable: true,
|
|
41
|
+
value: (query: string) => ({
|
|
42
|
+
matches: false,
|
|
43
|
+
media: query,
|
|
44
|
+
onchange: null,
|
|
45
|
+
addListener: () => {},
|
|
46
|
+
removeListener: () => {},
|
|
47
|
+
addEventListener: () => {},
|
|
48
|
+
removeEventListener: () => {},
|
|
49
|
+
dispatchEvent: () => false,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mock scrollTo
|
|
54
|
+
window.scrollTo = () => {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API response types.
|
|
3
|
+
* Shared between hooks, mocks, and components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Todo {
|
|
7
|
+
userId: number;
|
|
8
|
+
id: number;
|
|
9
|
+
title: string;
|
|
10
|
+
completed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generic API error response
|
|
15
|
+
*/
|
|
16
|
+
export interface ApiError {
|
|
17
|
+
message: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
status?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Paginated response wrapper
|
|
24
|
+
*/
|
|
25
|
+
export interface PaginatedResponse<T> {
|
|
26
|
+
data: T[];
|
|
27
|
+
total: number;
|
|
28
|
+
page: number;
|
|
29
|
+
pageSize: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
}
|