@react-spa-scaffold/mcp 0.4.1 → 1.1.1
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 +41 -19
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +59 -25
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.d.ts +1 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/features/versions.json +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/get-example.js +1 -1
- package/dist/tools/get-scaffold.d.ts +1 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +33 -17
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/utils/examples.d.ts.map +1 -1
- package/dist/utils/examples.js +19 -16
- package/dist/utils/examples.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +15 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/scaffold.d.ts +6 -1
- package/dist/utils/scaffold.d.ts.map +1 -1
- package/dist/utils/scaffold.js +86 -13
- package/dist/utils/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +4 -2
- package/templates/docs/API_REFERENCE.md +0 -1
- package/templates/docs/INTERNATIONALIZATION.md +26 -0
- package/templates/gitignore +33 -0
- package/templates/package.json +1 -1
- package/templates/src/components/shared/RegisterForm/RegisterForm.tsx +91 -0
- package/templates/src/components/shared/RegisterForm/index.ts +1 -0
- package/templates/src/components/shared/index.ts +1 -0
- package/templates/src/components/ui/card.tsx +70 -0
- package/templates/src/components/ui/input.tsx +19 -0
- package/templates/src/components/ui/label.tsx +19 -0
- package/templates/src/hooks/index.ts +1 -1
- package/templates/src/hooks/useRegisterForm.ts +36 -0
- package/templates/src/lib/index.ts +1 -11
- package/templates/src/lib/validations.ts +6 -13
- package/templates/src/pages/Home.tsx +29 -10
- package/templates/tests/unit/components/RegisterForm.test.tsx +105 -0
- package/templates/tests/unit/hooks/useRegisterForm.test.tsx +153 -0
- package/templates/tests/unit/lib/validations.test.ts +22 -33
- package/templates/tests/unit/stores/preferencesStore.test.ts +81 -0
- package/templates/vitest.config.ts +1 -1
- package/templates/src/hooks/useContactForm.ts +0 -33
- package/templates/src/lib/constants.ts +0 -8
- package/templates/src/lib/format.ts +0 -119
- package/templates/tests/unit/hooks/useContactForm.test.ts +0 -60
- package/templates/tests/unit/lib/format.test.ts +0 -100
|
@@ -1,119 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { useContactForm } from '@/hooks/useContactForm';
|
|
5
|
-
|
|
6
|
-
describe('useContactForm', () => {
|
|
7
|
-
describe('initial state', () => {
|
|
8
|
-
it('initializes with empty values and no errors', () => {
|
|
9
|
-
const { result } = renderHook(() => useContactForm());
|
|
10
|
-
|
|
11
|
-
expect(result.current.form.getValues()).toEqual({ name: '', email: '', message: '' });
|
|
12
|
-
expect(result.current.errors).toEqual({});
|
|
13
|
-
expect(result.current.isSubmitting).toBe(false);
|
|
14
|
-
expect(typeof result.current.onSubmit).toBe('function');
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('validation', () => {
|
|
19
|
-
it.each([
|
|
20
|
-
{ field: 'name', value: 'J', valid: { email: 'test@example.com', message: 'Valid message here' } },
|
|
21
|
-
{ field: 'email', value: 'invalid', valid: { name: 'John', message: 'Valid message here' } },
|
|
22
|
-
{ field: 'message', value: 'Short', valid: { name: 'John', email: 'test@example.com' } },
|
|
23
|
-
])('rejects invalid $field', async ({ field, value, valid }) => {
|
|
24
|
-
const { result } = renderHook(() => useContactForm());
|
|
25
|
-
|
|
26
|
-
act(() => {
|
|
27
|
-
result.current.form.setValue(field as 'name' | 'email' | 'message', value);
|
|
28
|
-
Object.entries(valid).forEach(([k, v]) => {
|
|
29
|
-
result.current.form.setValue(k as 'name' | 'email' | 'message', v);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await act(async () => {
|
|
34
|
-
await result.current.form.trigger();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
await waitFor(() => {
|
|
38
|
-
expect(result.current.errors[field as keyof typeof result.current.errors]).toBeDefined();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('passes with valid data', async () => {
|
|
43
|
-
const { result } = renderHook(() => useContactForm());
|
|
44
|
-
|
|
45
|
-
act(() => {
|
|
46
|
-
result.current.form.setValue('name', 'John Doe');
|
|
47
|
-
result.current.form.setValue('email', 'test@example.com');
|
|
48
|
-
result.current.form.setValue('message', 'This is a valid message that is long enough.');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
await act(async () => {
|
|
52
|
-
await result.current.form.trigger();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await waitFor(() => {
|
|
56
|
-
expect(result.current.errors).toEqual({});
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
formatBytes,
|
|
5
|
-
formatCurrency,
|
|
6
|
-
formatDate,
|
|
7
|
-
formatDateTime,
|
|
8
|
-
formatNumber,
|
|
9
|
-
formatPercent,
|
|
10
|
-
formatRelativeTime,
|
|
11
|
-
} from '@/lib/format';
|
|
12
|
-
|
|
13
|
-
describe('formatDate', () => {
|
|
14
|
-
it('formats date objects and strings', () => {
|
|
15
|
-
const date = new Date('2024-01-15T12:00:00Z');
|
|
16
|
-
expect(formatDate(date, {}, 'en-US')).toContain('2024');
|
|
17
|
-
expect(formatDate('2024-06-20', {}, 'en-US')).toContain('2024');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('returns "Invalid date" for invalid input', () => {
|
|
21
|
-
expect(formatDate('invalid')).toBe('Invalid date');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('respects custom options', () => {
|
|
25
|
-
const date = new Date('2024-01-15');
|
|
26
|
-
expect(formatDate(date, { weekday: 'long' }, 'en-US')).toContain('Monday');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('formatDateTime', () => {
|
|
31
|
-
it('includes time in output', () => {
|
|
32
|
-
const date = new Date('2024-01-15T14:30:00Z');
|
|
33
|
-
expect(formatDateTime(date, {}, 'en-US')).toMatch(/\d{1,2}:\d{2}/);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('formatRelativeTime', () => {
|
|
38
|
-
it.each([
|
|
39
|
-
{ offset: -30 * 1000, unit: 'seconds', pattern: /second|now/i },
|
|
40
|
-
{ offset: -5 * 60 * 1000, unit: 'minutes', pattern: /minute/i },
|
|
41
|
-
{ offset: -60 * 60 * 1000, unit: 'hours', pattern: /hour|ago/i },
|
|
42
|
-
{ offset: 24 * 60 * 60 * 1000, unit: 'days (future)', pattern: /day|tomorrow/i },
|
|
43
|
-
{ offset: -45 * 24 * 60 * 60 * 1000, unit: 'months', pattern: /month/i },
|
|
44
|
-
{ offset: -400 * 24 * 60 * 60 * 1000, unit: 'years', pattern: /year/i },
|
|
45
|
-
])('formats $unit correctly', ({ offset, pattern }) => {
|
|
46
|
-
const date = new Date(Date.now() + offset);
|
|
47
|
-
expect(formatRelativeTime(date, 'en-US')).toMatch(pattern);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('returns "Invalid date" for invalid input', () => {
|
|
51
|
-
expect(formatRelativeTime('invalid')).toBe('Invalid date');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('accepts timestamp numbers', () => {
|
|
55
|
-
const timestamp = Date.now() - 60 * 60 * 1000;
|
|
56
|
-
expect(formatRelativeTime(timestamp, 'en-US')).toMatch(/hour|ago/i);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('formatNumber', () => {
|
|
61
|
-
it.each([
|
|
62
|
-
{ value: 1234567.89, options: {}, expected: '1,234,567.89' },
|
|
63
|
-
{ value: 1234.5, options: { minimumFractionDigits: 2 }, expected: '1,234.50' },
|
|
64
|
-
])('formats $value with options', ({ value, options, expected }) => {
|
|
65
|
-
expect(formatNumber(value, options, 'en-US')).toBe(expected);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('formatCurrency', () => {
|
|
70
|
-
it.each([
|
|
71
|
-
{ value: 99.99, currency: 'USD', locale: 'en-US', expected: '$99.99' },
|
|
72
|
-
{ value: 99.99, currency: 'EUR', locale: 'de-DE', contains: '€' },
|
|
73
|
-
])('formats $currency correctly', ({ value, currency, locale, expected, contains }) => {
|
|
74
|
-
const result = formatCurrency(value, currency, locale);
|
|
75
|
-
if (expected) expect(result).toBe(expected);
|
|
76
|
-
if (contains) expect(result).toContain(contains);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('formatPercent', () => {
|
|
81
|
-
it.each([
|
|
82
|
-
{ value: 0.25, decimals: 0, expected: '25%' },
|
|
83
|
-
{ value: 0.2567, decimals: 2, expected: '25.67%' },
|
|
84
|
-
])('formats $value with $decimals decimals', ({ value, decimals, expected }) => {
|
|
85
|
-
expect(formatPercent(value, decimals, 'en-US')).toBe(expected);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('formatBytes', () => {
|
|
90
|
-
it.each([
|
|
91
|
-
{ bytes: 0, expected: '0 Bytes' },
|
|
92
|
-
{ bytes: 1024, expected: '1 KB' },
|
|
93
|
-
{ bytes: 1024 * 1024, expected: '1 MB' },
|
|
94
|
-
{ bytes: 1024 * 1024 * 1024, expected: '1 GB' },
|
|
95
|
-
{ bytes: 1536, decimals: 1, expected: '1.5 KB' },
|
|
96
|
-
{ bytes: 1536, decimals: 0, expected: '2 KB' },
|
|
97
|
-
])('formats $bytes bytes', ({ bytes, decimals, expected }) => {
|
|
98
|
-
expect(formatBytes(bytes, decimals)).toBe(expected);
|
|
99
|
-
});
|
|
100
|
-
});
|