@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,142 @@
1
+ /**
2
+ * API client abstraction.
3
+ * Provides a centralized, typed API layer with error handling.
4
+ */
5
+
6
+ import type { ApiError } from '@/types/api';
7
+
8
+ /**
9
+ * API configuration.
10
+ */
11
+ export const API_CONFIG = {
12
+ baseUrl: import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com',
13
+ timeout: 30000,
14
+ } as const;
15
+
16
+ /**
17
+ * Custom API error class
18
+ */
19
+ export class ApiClientError extends Error {
20
+ status: number;
21
+ code?: string;
22
+
23
+ constructor(message: string, status: number, code?: string) {
24
+ super(message);
25
+ this.name = 'ApiClientError';
26
+ this.status = status;
27
+ this.code = code;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Request options extending standard fetch options
33
+ */
34
+ interface RequestOptions extends Omit<RequestInit, 'body'> {
35
+ body?: unknown;
36
+ timeout?: number;
37
+ }
38
+
39
+ /**
40
+ * Create an AbortController with timeout
41
+ */
42
+ function createTimeoutController(timeout: number): { controller: AbortController; timeoutId: NodeJS.Timeout } {
43
+ const controller = new AbortController();
44
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
45
+ return { controller, timeoutId };
46
+ }
47
+
48
+ /**
49
+ * Parse API error response
50
+ */
51
+ async function parseErrorResponse(response: Response): Promise<ApiError> {
52
+ try {
53
+ const data = await response.json();
54
+ return {
55
+ message: data.message || data.error || response.statusText,
56
+ code: data.code,
57
+ status: response.status,
58
+ };
59
+ } catch {
60
+ return {
61
+ message: response.statusText || 'An error occurred',
62
+ status: response.status,
63
+ };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Core request function
69
+ */
70
+ async function request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
71
+ const { body, timeout = API_CONFIG.timeout, ...fetchOptions } = options;
72
+
73
+ const url = endpoint.startsWith('http') ? endpoint : `${API_CONFIG.baseUrl}${endpoint}`;
74
+
75
+ const { controller, timeoutId } = createTimeoutController(timeout);
76
+
77
+ try {
78
+ const response = await fetch(url, {
79
+ ...fetchOptions,
80
+ body: body ? JSON.stringify(body) : undefined,
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ ...fetchOptions.headers,
84
+ },
85
+ signal: controller.signal,
86
+ });
87
+
88
+ clearTimeout(timeoutId);
89
+
90
+ if (!response.ok) {
91
+ const error = await parseErrorResponse(response);
92
+ throw new ApiClientError(error.message, response.status, error.code);
93
+ }
94
+
95
+ // Handle 204 No Content
96
+ if (response.status === 204) {
97
+ return undefined as T;
98
+ }
99
+
100
+ return response.json();
101
+ } catch (error) {
102
+ clearTimeout(timeoutId);
103
+
104
+ if (error instanceof ApiClientError) {
105
+ throw error;
106
+ }
107
+
108
+ if (error instanceof Error) {
109
+ if (error.name === 'AbortError') {
110
+ throw new ApiClientError('Request timeout', 408, 'TIMEOUT');
111
+ }
112
+ throw new ApiClientError(error.message, 0, 'NETWORK_ERROR');
113
+ }
114
+
115
+ throw new ApiClientError('An unexpected error occurred', 0, 'UNKNOWN');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * API client with HTTP method helpers
121
+ */
122
+ export const api = {
123
+ get<T>(endpoint: string, options?: RequestOptions): Promise<T> {
124
+ return request<T>(endpoint, { ...options, method: 'GET' });
125
+ },
126
+
127
+ post<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<T> {
128
+ return request<T>(endpoint, { ...options, method: 'POST', body });
129
+ },
130
+
131
+ put<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<T> {
132
+ return request<T>(endpoint, { ...options, method: 'PUT', body });
133
+ },
134
+
135
+ patch<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<T> {
136
+ return request<T>(endpoint, { ...options, method: 'PATCH', body });
137
+ },
138
+
139
+ delete<T>(endpoint: string, options?: RequestOptions): Promise<T> {
140
+ return request<T>(endpoint, { ...options, method: 'DELETE' });
141
+ },
142
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Application configuration.
3
+ * Centralized config for feature flags, etc.
4
+ */
5
+
6
+ export const APP_CONFIG = {
7
+ name: import.meta.env.VITE_APP_NAME || 'My App',
8
+ url: import.meta.env.VITE_APP_URL || 'http://localhost:5173',
9
+ } as const;
10
+
11
+ export const SENTRY_CONFIG = {
12
+ enabled: import.meta.env.VITE_SENTRY_ENABLED !== 'false',
13
+ dsn: import.meta.env.VITE_SENTRY_DSN,
14
+ tracesSampleRate: 0.1,
15
+ } as const;
@@ -0,0 +1,8 @@
1
+ export const TIMING = {
2
+ DEBOUNCE_MS: 300,
3
+ TOAST_DURATION_MS: 5000,
4
+ } as const;
5
+
6
+ export const UI = {
7
+ MAX_WIDTH: 1280,
8
+ } as const;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Environment variable validation using Zod.
3
+ * Validates at runtime to catch missing/invalid env vars early.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ const envSchema = z.object({
9
+ VITE_APP_NAME: z.string().min(1).optional(),
10
+ VITE_APP_URL: z.string().url().optional(),
11
+ VITE_API_URL: z.string().url().optional(),
12
+ VITE_SENTRY_DSN: z.string().url().optional(),
13
+ MODE: z.enum(['development', 'production', 'test']).default('development'),
14
+ DEV: z.boolean().default(false),
15
+ PROD: z.boolean().default(false),
16
+ });
17
+
18
+ export type Env = z.infer<typeof envSchema>;
19
+
20
+ /**
21
+ * Validate environment variables and return typed env object.
22
+ * Throws if validation fails in production.
23
+ */
24
+ export function validateEnv(): Env {
25
+ const env = {
26
+ VITE_APP_NAME: import.meta.env.VITE_APP_NAME,
27
+ VITE_APP_URL: import.meta.env.VITE_APP_URL,
28
+ VITE_API_URL: import.meta.env.VITE_API_URL,
29
+ VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN,
30
+ MODE: import.meta.env.MODE,
31
+ DEV: import.meta.env.DEV,
32
+ PROD: import.meta.env.PROD,
33
+ };
34
+
35
+ const result = envSchema.safeParse(env);
36
+
37
+ if (!result.success) {
38
+ const errors = result.error.format();
39
+ console.error('Environment validation failed:', errors);
40
+
41
+ if (import.meta.env.PROD) {
42
+ throw new Error('Invalid environment configuration');
43
+ }
44
+ }
45
+
46
+ return result.success ? result.data : (env as Env);
47
+ }
48
+
49
+ /**
50
+ * Validated environment variables.
51
+ * Access this instead of import.meta.env for type safety.
52
+ */
53
+ export const env = validateEnv();
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Formatting utilities for dates, numbers, and currencies.
3
+ * All formatters are locale-aware.
4
+ */
5
+
6
+ /**
7
+ * Format a date with locale support
8
+ */
9
+ export function formatDate(
10
+ date: Date | string | number,
11
+ options: Intl.DateTimeFormatOptions = {},
12
+ locale?: string,
13
+ ): string {
14
+ const dateObj = date instanceof Date ? date : new Date(date);
15
+
16
+ if (isNaN(dateObj.getTime())) {
17
+ return 'Invalid date';
18
+ }
19
+
20
+ const defaultOptions: Intl.DateTimeFormatOptions = {
21
+ year: 'numeric',
22
+ month: 'short',
23
+ day: 'numeric',
24
+ ...options,
25
+ };
26
+
27
+ return new Intl.DateTimeFormat(locale, defaultOptions).format(dateObj);
28
+ }
29
+
30
+ /**
31
+ * Format a date with time
32
+ */
33
+ export function formatDateTime(
34
+ date: Date | string | number,
35
+ options: Intl.DateTimeFormatOptions = {},
36
+ locale?: string,
37
+ ): string {
38
+ return formatDate(
39
+ date,
40
+ {
41
+ hour: '2-digit',
42
+ minute: '2-digit',
43
+ ...options,
44
+ },
45
+ locale,
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Format relative time (e.g., "2 hours ago", "in 3 days")
51
+ */
52
+ export function formatRelativeTime(date: Date | string | number, locale?: string): string {
53
+ const dateObj = date instanceof Date ? date : new Date(date);
54
+
55
+ if (isNaN(dateObj.getTime())) {
56
+ return 'Invalid date';
57
+ }
58
+
59
+ const now = new Date();
60
+ const diffInSeconds = Math.floor((dateObj.getTime() - now.getTime()) / 1000);
61
+ const absoluteDiff = Math.abs(diffInSeconds);
62
+
63
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
64
+
65
+ if (absoluteDiff < 60) {
66
+ return rtf.format(diffInSeconds, 'second');
67
+ } else if (absoluteDiff < 3600) {
68
+ return rtf.format(Math.floor(diffInSeconds / 60), 'minute');
69
+ } else if (absoluteDiff < 86400) {
70
+ return rtf.format(Math.floor(diffInSeconds / 3600), 'hour');
71
+ } else if (absoluteDiff < 2592000) {
72
+ return rtf.format(Math.floor(diffInSeconds / 86400), 'day');
73
+ } else if (absoluteDiff < 31536000) {
74
+ return rtf.format(Math.floor(diffInSeconds / 2592000), 'month');
75
+ } else {
76
+ return rtf.format(Math.floor(diffInSeconds / 31536000), 'year');
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Format a number with locale support
82
+ */
83
+ export function formatNumber(value: number, options: Intl.NumberFormatOptions = {}, locale?: string): string {
84
+ return new Intl.NumberFormat(locale, options).format(value);
85
+ }
86
+
87
+ /**
88
+ * Format a number as currency
89
+ */
90
+ export function formatCurrency(value: number, currency = 'USD', locale?: string): string {
91
+ return new Intl.NumberFormat(locale, {
92
+ style: 'currency',
93
+ currency,
94
+ }).format(value);
95
+ }
96
+
97
+ /**
98
+ * Format a number as percentage
99
+ */
100
+ export function formatPercent(value: number, decimals = 0, locale?: string): string {
101
+ return new Intl.NumberFormat(locale, {
102
+ style: 'percent',
103
+ minimumFractionDigits: decimals,
104
+ maximumFractionDigits: decimals,
105
+ }).format(value);
106
+ }
107
+
108
+ /**
109
+ * Format bytes to human readable string
110
+ */
111
+ export function formatBytes(bytes: number, decimals = 2): string {
112
+ if (bytes === 0) return '0 Bytes';
113
+
114
+ const k = 1024;
115
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
116
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
117
+
118
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
119
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Central lib exports.
3
+ * Import from '@/lib' instead of individual files.
4
+ */
5
+
6
+ export { cn } from './utils';
7
+ export { TIMING, UI } from './constants';
8
+ export { STORAGE_KEYS, isAppKey } from './storageKeys';
9
+ export { APP_CONFIG, SENTRY_CONFIG } from './config';
10
+ export { API_CONFIG } from './api';
11
+ export { ROUTES, type AppRoute } from './routes';
12
+ export { env, validateEnv, type Env } from './env';
13
+ export { api, ApiClientError } from './api';
14
+ export { getStorageItem, setStorageItem, removeStorageItem, clearAppStorage } from './storage';
15
+ export {
16
+ formatDate,
17
+ formatDateTime,
18
+ formatRelativeTime,
19
+ formatNumber,
20
+ formatCurrency,
21
+ formatPercent,
22
+ formatBytes,
23
+ } from './format';
24
+ export { contactFormSchema, registerFormSchema, type ContactFormData, type RegisterFormData } from './validations';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Typed route constants.
3
+ * Use these instead of hardcoded strings for type-safe navigation.
4
+ */
5
+
6
+ export const ROUTES = {
7
+ HOME: '/',
8
+ NOT_FOUND: '*',
9
+ } as const;
10
+
11
+ export type AppRoute = (typeof ROUTES)[keyof typeof ROUTES];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Type-safe localStorage abstraction with SSR safety and error handling.
3
+ */
4
+
5
+ import { STORAGE_KEYS } from './storageKeys';
6
+
7
+ type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
8
+
9
+ /**
10
+ * Check if we're in a browser environment
11
+ */
12
+ function isBrowser(): boolean {
13
+ return typeof window !== 'undefined' && typeof localStorage !== 'undefined';
14
+ }
15
+
16
+ /**
17
+ * Get a value from localStorage with type safety
18
+ */
19
+ export function getStorageItem<T>(key: StorageKey, defaultValue: T): T {
20
+ if (!isBrowser()) {
21
+ return defaultValue;
22
+ }
23
+
24
+ try {
25
+ const item = localStorage.getItem(key);
26
+ if (item === null) {
27
+ return defaultValue;
28
+ }
29
+ return JSON.parse(item) as T;
30
+ } catch {
31
+ // If parsing fails, try returning the raw string if T is string
32
+ const item = localStorage.getItem(key);
33
+ if (item !== null) {
34
+ return item as unknown as T;
35
+ }
36
+ return defaultValue;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Set a value in localStorage with error handling
42
+ */
43
+ export function setStorageItem<T>(key: StorageKey, value: T): boolean {
44
+ if (!isBrowser()) {
45
+ return false;
46
+ }
47
+
48
+ try {
49
+ localStorage.setItem(key, JSON.stringify(value));
50
+ return true;
51
+ } catch (error) {
52
+ console.error(`Failed to set localStorage key "${key}":`, error);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Remove a value from localStorage
59
+ */
60
+ export function removeStorageItem(key: StorageKey): boolean {
61
+ if (!isBrowser()) {
62
+ return false;
63
+ }
64
+
65
+ try {
66
+ localStorage.removeItem(key);
67
+ return true;
68
+ } catch (error) {
69
+ console.error(`Failed to remove localStorage key "${key}":`, error);
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Clear all app-related storage keys
76
+ */
77
+ export function clearAppStorage(): boolean {
78
+ if (!isBrowser()) {
79
+ return false;
80
+ }
81
+
82
+ try {
83
+ Object.values(STORAGE_KEYS).forEach((key) => {
84
+ localStorage.removeItem(key);
85
+ });
86
+ return true;
87
+ } catch (error) {
88
+ console.error('Failed to clear app storage:', error);
89
+ return false;
90
+ }
91
+ }
@@ -0,0 +1,10 @@
1
+ const PREFIX = 'myapp';
2
+
3
+ export const STORAGE_KEYS = {
4
+ preferences: `${PREFIX}-preferences`,
5
+ locale: `${PREFIX}-locale`,
6
+ } as const;
7
+
8
+ export function isAppKey(key: string): boolean {
9
+ return key.startsWith(`${PREFIX}-`);
10
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Example validation schema for a contact form.
5
+ * Extend or replace with your own schemas.
6
+ */
7
+ export const contactFormSchema = z.object({
8
+ name: z.string().min(2, 'Name must be at least 2 characters'),
9
+ email: z.string().email('Please enter a valid email address'),
10
+ message: z.string().min(10, 'Message must be at least 10 characters'),
11
+ });
12
+
13
+ export type ContactFormData = z.infer<typeof contactFormSchema>;
14
+
15
+ /**
16
+ * Example validation schema for user registration.
17
+ */
18
+ export const registerFormSchema = z
19
+ .object({
20
+ username: z
21
+ .string()
22
+ .min(3, 'Username must be at least 3 characters')
23
+ .max(20, 'Username must be at most 20 characters')
24
+ .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
25
+ email: z.string().email('Please enter a valid email address'),
26
+ password: z
27
+ .string()
28
+ .min(8, 'Password must be at least 8 characters')
29
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
30
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
31
+ .regex(/[0-9]/, 'Password must contain at least one number'),
32
+ confirmPassword: z.string(),
33
+ })
34
+ .refine((data) => data.password === data.confirmPassword, {
35
+ message: "Passwords don't match",
36
+ path: ['confirmPassword'],
37
+ });
38
+
39
+ export type RegisterFormData = z.infer<typeof registerFormSchema>;
@@ -0,0 +1,65 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Language: de\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 "Sprache ändern"
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 "Beginne mit der Bearbeitung von <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 "Meine 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 "Zum Dunkelmodus wechseln"
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 "Zum Hellmodus wechseln"
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 "Willkommen bei Meine App"
@@ -0,0 +1,65 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Language: en\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 "Back to Home"
18
+
19
+ #. Accessibility label for the language selector dropdown button
20
+ #: src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx:24
21
+ msgid "Change language"
22
+ msgstr "Change language"
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 "Get started by editing <0>src/App.tsx</0>"
28
+
29
+ #: src/components/ui/loading.tsx:54
30
+ msgid "Loading..."
31
+ msgstr "Loading..."
32
+
33
+ #. Application name displayed in the header navigation
34
+ #: src/components/layout/Header.tsx:10
35
+ msgid "My App"
36
+ msgstr "My 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 "Page Not Found"
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 "Switch to dark mode"
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 "Switch to light mode"
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 "The page you're looking for doesn't exist or has been moved."
61
+
62
+ #. Main heading on the home page
63
+ #: src/pages/Home.tsx:7
64
+ msgid "Welcome to My App"
65
+ msgstr "Welcome to My App"