@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,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,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,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,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"
|