@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.
Files changed (173) hide show
  1. package/README.md +423 -0
  2. package/dist/features/index.d.ts +5 -0
  3. package/dist/features/index.d.ts.map +1 -0
  4. package/dist/features/index.js +3 -0
  5. package/dist/features/index.js.map +1 -0
  6. package/dist/features/registry.d.ts +10 -0
  7. package/dist/features/registry.d.ts.map +1 -0
  8. package/dist/features/registry.js +508 -0
  9. package/dist/features/registry.js.map +1 -0
  10. package/dist/features/types.d.ts +45 -0
  11. package/dist/features/types.d.ts.map +1 -0
  12. package/dist/features/types.js +5 -0
  13. package/dist/features/types.js.map +1 -0
  14. package/dist/features/versions.d.ts +16 -0
  15. package/dist/features/versions.d.ts.map +1 -0
  16. package/dist/features/versions.js +46 -0
  17. package/dist/features/versions.js.map +1 -0
  18. package/dist/features/versions.json +5 -0
  19. package/dist/index.d.ts +22 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +43 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/resources/docs.d.ts +29 -0
  24. package/dist/resources/docs.d.ts.map +1 -0
  25. package/dist/resources/docs.js +105 -0
  26. package/dist/resources/docs.js.map +1 -0
  27. package/dist/resources/index.d.ts +2 -0
  28. package/dist/resources/index.d.ts.map +1 -0
  29. package/dist/resources/index.js +2 -0
  30. package/dist/resources/index.js.map +1 -0
  31. package/dist/server.d.ts +12 -0
  32. package/dist/server.d.ts.map +1 -0
  33. package/dist/server.js +115 -0
  34. package/dist/server.js.map +1 -0
  35. package/dist/tools/get-example.d.ts +51 -0
  36. package/dist/tools/get-example.d.ts.map +1 -0
  37. package/dist/tools/get-example.js +90 -0
  38. package/dist/tools/get-example.js.map +1 -0
  39. package/dist/tools/get-features.d.ts +30 -0
  40. package/dist/tools/get-features.d.ts.map +1 -0
  41. package/dist/tools/get-features.js +46 -0
  42. package/dist/tools/get-features.js.map +1 -0
  43. package/dist/tools/get-scaffold.d.ts +77 -0
  44. package/dist/tools/get-scaffold.d.ts.map +1 -0
  45. package/dist/tools/get-scaffold.js +153 -0
  46. package/dist/tools/get-scaffold.js.map +1 -0
  47. package/dist/tools/index.d.ts +4 -0
  48. package/dist/tools/index.d.ts.map +1 -0
  49. package/dist/tools/index.js +4 -0
  50. package/dist/tools/index.js.map +1 -0
  51. package/dist/utils/docs.d.ts +14 -0
  52. package/dist/utils/docs.d.ts.map +1 -0
  53. package/dist/utils/docs.js +64 -0
  54. package/dist/utils/docs.js.map +1 -0
  55. package/dist/utils/examples.d.ts +27 -0
  56. package/dist/utils/examples.d.ts.map +1 -0
  57. package/dist/utils/examples.js +399 -0
  58. package/dist/utils/examples.js.map +1 -0
  59. package/dist/utils/index.d.ts +5 -0
  60. package/dist/utils/index.d.ts.map +1 -0
  61. package/dist/utils/index.js +5 -0
  62. package/dist/utils/index.js.map +1 -0
  63. package/dist/utils/paths.d.ts +28 -0
  64. package/dist/utils/paths.d.ts.map +1 -0
  65. package/dist/utils/paths.js +40 -0
  66. package/dist/utils/paths.js.map +1 -0
  67. package/dist/utils/scaffold.d.ts +50 -0
  68. package/dist/utils/scaffold.d.ts.map +1 -0
  69. package/dist/utils/scaffold.js +500 -0
  70. package/dist/utils/scaffold.js.map +1 -0
  71. package/dist/version.d.ts +5 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +19 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +63 -0
  76. package/templates/.bundled +0 -0
  77. package/templates/CLAUDE.md +145 -0
  78. package/templates/docs/API_REFERENCE.md +58 -0
  79. package/templates/docs/ARCHITECTURE.md +185 -0
  80. package/templates/docs/CODING_STANDARDS.md +53 -0
  81. package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
  82. package/templates/docs/E2E_TESTING.md +116 -0
  83. package/templates/docs/INTERNATIONALIZATION.md +67 -0
  84. package/templates/docs/TESTING.md +259 -0
  85. package/templates/docs/WORKFLOW.md +170 -0
  86. package/templates/src/App.tsx +42 -0
  87. package/templates/src/components/layout/Header.tsx +19 -0
  88. package/templates/src/components/layout/index.ts +1 -0
  89. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
  90. package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
  91. package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
  92. package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
  93. package/templates/src/components/shared/SEO/SEO.tsx +55 -0
  94. package/templates/src/components/shared/SEO/index.ts +1 -0
  95. package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
  96. package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
  97. package/templates/src/components/shared/index.ts +4 -0
  98. package/templates/src/components/ui/button.tsx +48 -0
  99. package/templates/src/components/ui/dropdown-menu.tsx +228 -0
  100. package/templates/src/components/ui/form-error.tsx +95 -0
  101. package/templates/src/components/ui/loading.tsx +58 -0
  102. package/templates/src/components/ui/skeleton.tsx +52 -0
  103. package/templates/src/components/ui/sonner.tsx +34 -0
  104. package/templates/src/components/ui/spinner.tsx +40 -0
  105. package/templates/src/components/ui/visually-hidden.tsx +51 -0
  106. package/templates/src/contexts/mobileContext.tsx +66 -0
  107. package/templates/src/contexts/queryContext.tsx +28 -0
  108. package/templates/src/hooks/index.ts +7 -0
  109. package/templates/src/hooks/useContactForm.ts +33 -0
  110. package/templates/src/hooks/useExampleQuery.ts +20 -0
  111. package/templates/src/hooks/useLanguage.ts +23 -0
  112. package/templates/src/hooks/useMediaQuery.ts +53 -0
  113. package/templates/src/hooks/useThemeEffect.ts +31 -0
  114. package/templates/src/hooks/useTouchSizes.ts +16 -0
  115. package/templates/src/i18n/config.ts +11 -0
  116. package/templates/src/i18n/detectLanguage.ts +57 -0
  117. package/templates/src/i18n/index.ts +20 -0
  118. package/templates/src/i18n/loadCatalog.ts +30 -0
  119. package/templates/src/index.css +98 -0
  120. package/templates/src/lib/api.ts +142 -0
  121. package/templates/src/lib/config.ts +15 -0
  122. package/templates/src/lib/constants.ts +8 -0
  123. package/templates/src/lib/env.ts +53 -0
  124. package/templates/src/lib/format.ts +119 -0
  125. package/templates/src/lib/index.ts +24 -0
  126. package/templates/src/lib/routes.ts +11 -0
  127. package/templates/src/lib/storage.ts +91 -0
  128. package/templates/src/lib/storageKeys.ts +10 -0
  129. package/templates/src/lib/utils.ts +6 -0
  130. package/templates/src/lib/validations.ts +39 -0
  131. package/templates/src/locales/de.po +65 -0
  132. package/templates/src/locales/en.po +65 -0
  133. package/templates/src/locales/es.po +65 -0
  134. package/templates/src/main.tsx +107 -0
  135. package/templates/src/mocks/fixtures/index.ts +1 -0
  136. package/templates/src/mocks/fixtures/todos.ts +40 -0
  137. package/templates/src/mocks/handlers/index.ts +7 -0
  138. package/templates/src/mocks/handlers/todos.ts +59 -0
  139. package/templates/src/mocks/index.ts +3 -0
  140. package/templates/src/mocks/node.ts +9 -0
  141. package/templates/src/pages/Home.tsx +27 -0
  142. package/templates/src/pages/NotFound.tsx +28 -0
  143. package/templates/src/pages/index.ts +2 -0
  144. package/templates/src/stores/index.ts +2 -0
  145. package/templates/src/stores/preferencesStore.ts +85 -0
  146. package/templates/src/test/index.ts +8 -0
  147. package/templates/src/test/mocks.ts +17 -0
  148. package/templates/src/test/providers.tsx +54 -0
  149. package/templates/src/test-setup.ts +54 -0
  150. package/templates/src/types/api.ts +31 -0
  151. package/templates/src/types/index.ts +2 -0
  152. package/templates/src/types/preferences.ts +5 -0
  153. package/templates/src/vite-env.d.ts +10 -0
  154. package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
  155. package/templates/tests/unit/components/Header.test.tsx +33 -0
  156. package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
  157. package/templates/tests/unit/components/Loading.test.tsx +76 -0
  158. package/templates/tests/unit/components/SEO.test.tsx +80 -0
  159. package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
  160. package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
  161. package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
  162. package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
  163. package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
  164. package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
  165. package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
  166. package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
  167. package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
  168. package/templates/tests/unit/lib/api.test.ts +142 -0
  169. package/templates/tests/unit/lib/format.test.ts +100 -0
  170. package/templates/tests/unit/lib/storage.test.ts +90 -0
  171. package/templates/tests/unit/lib/utils.test.ts +19 -0
  172. package/templates/tests/unit/lib/validations.test.ts +56 -0
  173. 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,7 @@
1
+ import { todosHandlers } from './todos';
2
+
3
+ /**
4
+ * All MSW request handlers.
5
+ * Add new feature handlers here as the application grows.
6
+ */
7
+ export const handlers = [...todosHandlers];
@@ -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,3 @@
1
+ export { handlers } from './handlers';
2
+ export { server } from './node';
3
+ export * from './fixtures';
@@ -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,2 @@
1
+ export { HomePage } from './Home';
2
+ export { NotFoundPage } from './NotFound';
@@ -0,0 +1,2 @@
1
+ export { usePreferencesStore, initPreferencesSync } from './preferencesStore';
2
+ export type { Theme, Preferences } from './preferencesStore';
@@ -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,8 @@
1
+ // Custom render with all providers
2
+ export { createTestQueryClient, render } from './providers';
3
+
4
+ // Mock utilities
5
+ export { mockMatchMedia } from './mocks';
6
+
7
+ // MSW server instance
8
+ export { server } from '@/mocks/node';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './preferences';
2
+ export * from './api';
@@ -0,0 +1,5 @@
1
+ export type Theme = 'light' | 'dark' | 'system';
2
+
3
+ export interface Preferences {
4
+ theme: Theme;
5
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_APP_NAME: string;
5
+ readonly VITE_APP_URL: string;
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv;
10
+ }