create-react-forge 1.0.2 → 1.0.4
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 +91 -61
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/parser.js +1 -1
- package/dist/cli/parser.js.map +1 -1
- package/dist/docs/architecture-generator.js +2 -2
- package/dist/docs/architecture-generator.js.map +1 -1
- package/dist/generator/index.js +1 -1
- package/dist/generator/index.js.map +1 -1
- package/package.json +2 -1
- package/src/templates/overlays/base/manifest.json +16 -0
- package/src/templates/overlays/base/src/components/ui/Button.tsx +66 -0
- package/src/templates/overlays/base/src/components/ui/Input.tsx +51 -0
- package/src/templates/overlays/base/src/components/ui/index.ts +3 -0
- package/src/templates/overlays/base/src/hooks/use-disclosure.ts +21 -0
- package/src/templates/overlays/base/src/hooks/use-local-storage.ts +80 -0
- package/src/templates/overlays/base/src/lib/api-client.ts +101 -0
- package/src/templates/overlays/base/src/lib/utils.ts +34 -0
- package/src/templates/overlays/base/src/types/api.ts +47 -0
- package/src/templates/overlays/features/tanstack-query/manifest.json +17 -0
- package/src/templates/overlays/features/tanstack-query/src/features/users/api/create-user.ts +41 -0
- package/src/templates/overlays/features/tanstack-query/src/features/users/api/get-user.ts +27 -0
- package/src/templates/overlays/features/tanstack-query/src/features/users/api/get-users.ts +39 -0
- package/src/templates/overlays/features/tanstack-query/src/hooks/use-query-config.ts +42 -0
- package/src/templates/overlays/features/tanstack-query/src/lib/QueryProvider.tsx +28 -0
- package/src/templates/overlays/features/tanstack-query/src/lib/react-query.ts +46 -0
- package/src/templates/overlays/runtime/nextjs/manifest.json +27 -0
- package/src/templates/overlays/runtime/nextjs/next-env.d.ts +6 -0
- package/src/templates/overlays/runtime/nextjs/next.config.js +12 -0
- package/src/templates/overlays/runtime/nextjs/src/app/error.tsx +34 -0
- package/src/templates/overlays/runtime/nextjs/src/app/layout.tsx +23 -0
- package/src/templates/overlays/runtime/nextjs/src/app/loading.tsx +12 -0
- package/src/templates/overlays/runtime/nextjs/src/app/not-found.tsx +26 -0
- package/src/templates/overlays/runtime/nextjs/src/app/page.tsx +33 -0
- package/src/templates/overlays/runtime/nextjs/src/app/providers.tsx +14 -0
- package/src/templates/overlays/runtime/nextjs/src/styles/globals.css +50 -0
- package/src/templates/overlays/runtime/nextjs/tsconfig.json +29 -0
- package/src/templates/overlays/runtime/vite/index.html +14 -0
- package/src/templates/overlays/runtime/vite/manifest.json +28 -0
- package/src/templates/overlays/runtime/vite/public/vite.svg +2 -0
- package/src/templates/overlays/runtime/vite/src/app/App.tsx +11 -0
- package/src/templates/overlays/runtime/vite/src/app/provider.tsx +20 -0
- package/src/templates/overlays/runtime/vite/src/app/router.tsx +19 -0
- package/src/templates/overlays/runtime/vite/src/components/errors/ErrorFallback.tsx +21 -0
- package/src/templates/overlays/runtime/vite/src/components/ui/LoadingSpinner.tsx +23 -0
- package/src/templates/overlays/runtime/vite/src/features/misc/routes/Landing.tsx +33 -0
- package/src/templates/overlays/runtime/vite/src/features/misc/routes/NotFound.tsx +26 -0
- package/src/templates/overlays/runtime/vite/src/main.tsx +11 -0
- package/src/templates/overlays/runtime/vite/src/styles/globals.css +55 -0
- package/src/templates/overlays/runtime/vite/tsconfig.json +32 -0
- package/src/templates/overlays/runtime/vite/tsconfig.node.json +23 -0
- package/src/templates/overlays/runtime/vite/vite.config.ts +22 -0
- package/src/templates/overlays/state/redux/manifest.json +17 -0
- package/src/templates/overlays/state/redux/src/stores/Provider.tsx +17 -0
- package/src/templates/overlays/state/redux/src/stores/hooks.ts +11 -0
- package/src/templates/overlays/state/redux/src/stores/index.ts +17 -0
- package/src/templates/overlays/state/redux/src/stores/slices/auth.ts +54 -0
- package/src/templates/overlays/state/redux/src/stores/slices/notifications.ts +58 -0
- package/src/templates/overlays/state/redux/src/stores/store.ts +27 -0
- package/src/templates/overlays/state/zustand/manifest.json +16 -0
- package/src/templates/overlays/state/zustand/src/stores/auth.ts +48 -0
- package/src/templates/overlays/state/zustand/src/stores/index.ts +3 -0
- package/src/templates/overlays/state/zustand/src/stores/notifications.ts +54 -0
- package/src/templates/overlays/styling/css-modules/manifest.json +14 -0
- package/src/templates/overlays/styling/css-modules/src/components/ui/Button.module.css +87 -0
- package/src/templates/overlays/styling/css-modules/src/styles/globals.css +91 -0
- package/src/templates/overlays/styling/tailwind/manifest.json +18 -0
- package/src/templates/overlays/styling/tailwind/postcss.config.js +7 -0
- package/src/templates/overlays/styling/tailwind/src/styles/globals.css +54 -0
- package/src/templates/overlays/styling/tailwind/tailwind.config.js +62 -0
- package/src/templates/overlays/testing/jest/jest.config.js +24 -0
- package/src/templates/overlays/testing/jest/manifest.json +27 -0
- package/src/templates/overlays/testing/jest/src/components/ui/__tests__/Button.test.tsx +33 -0
- package/src/templates/overlays/testing/jest/src/testing/mocks/browser.ts +17 -0
- package/src/templates/overlays/testing/jest/src/testing/mocks/handlers.ts +42 -0
- package/src/templates/overlays/testing/jest/src/testing/mocks/server.ts +8 -0
- package/src/templates/overlays/testing/jest/src/testing/setup.ts +20 -0
- package/src/templates/overlays/testing/jest/src/testing/test-utils.tsx +39 -0
- package/src/templates/overlays/testing/playwright/manifest.json +21 -0
- package/src/templates/overlays/testing/playwright/playwright.config.ts +53 -0
- package/src/templates/overlays/testing/playwright/tests/e2e/accessibility.spec.ts +41 -0
- package/src/templates/overlays/testing/playwright/tests/e2e/home.spec.ts +41 -0
- package/src/templates/overlays/testing/vitest/manifest.json +28 -0
- package/src/templates/overlays/testing/vitest/src/components/ui/__tests__/Button.test.tsx +34 -0
- package/src/templates/overlays/testing/vitest/src/testing/mocks/browser.ts +17 -0
- package/src/templates/overlays/testing/vitest/src/testing/mocks/handlers.ts +42 -0
- package/src/templates/overlays/testing/vitest/src/testing/mocks/server.ts +8 -0
- package/src/templates/overlays/testing/vitest/src/testing/setup.ts +21 -0
- package/src/templates/overlays/testing/vitest/src/testing/test-utils.tsx +39 -0
- package/src/templates/overlays/testing/vitest/vitest.config.ts +31 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client - Centralized HTTP client following bulletproof-react patterns
|
|
3
|
+
* Replace with your actual API configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RequestConfig = {
|
|
7
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
params?: Record<string, string | number | boolean>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ApiResponse<T> = {
|
|
14
|
+
data: T;
|
|
15
|
+
status: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build URL with query parameters
|
|
22
|
+
*/
|
|
23
|
+
function buildUrl(endpoint: string, params?: Record<string, string | number | boolean>): string {
|
|
24
|
+
const url = new URL(endpoint, API_URL);
|
|
25
|
+
|
|
26
|
+
if (params) {
|
|
27
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
28
|
+
url.searchParams.append(key, String(value));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return url.toString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get authorization headers (customize based on your auth strategy)
|
|
37
|
+
*/
|
|
38
|
+
function getAuthHeaders(): Record<string, string> {
|
|
39
|
+
const token = localStorage.getItem('token');
|
|
40
|
+
|
|
41
|
+
if (token) {
|
|
42
|
+
return { Authorization: `Bearer ${token}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Main API client function
|
|
50
|
+
*/
|
|
51
|
+
export async function apiClient<T>(
|
|
52
|
+
endpoint: string,
|
|
53
|
+
config: RequestConfig = {}
|
|
54
|
+
): Promise<ApiResponse<T>> {
|
|
55
|
+
const { method = 'GET', headers = {}, body, params } = config;
|
|
56
|
+
|
|
57
|
+
const url = buildUrl(endpoint, params);
|
|
58
|
+
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
method,
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
...getAuthHeaders(),
|
|
64
|
+
...headers,
|
|
65
|
+
},
|
|
66
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = await response.json().catch(() => ({}));
|
|
71
|
+
throw new Error(error.message || `HTTP error! status: ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
data,
|
|
78
|
+
status: response.status,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convenience methods
|
|
84
|
+
*/
|
|
85
|
+
export const api = {
|
|
86
|
+
get: <T>(endpoint: string, params?: Record<string, string | number | boolean>) =>
|
|
87
|
+
apiClient<T>(endpoint, { method: 'GET', params }),
|
|
88
|
+
|
|
89
|
+
post: <T>(endpoint: string, body: unknown) =>
|
|
90
|
+
apiClient<T>(endpoint, { method: 'POST', body }),
|
|
91
|
+
|
|
92
|
+
put: <T>(endpoint: string, body: unknown) =>
|
|
93
|
+
apiClient<T>(endpoint, { method: 'PUT', body }),
|
|
94
|
+
|
|
95
|
+
patch: <T>(endpoint: string, body: unknown) =>
|
|
96
|
+
apiClient<T>(endpoint, { method: 'PATCH', body }),
|
|
97
|
+
|
|
98
|
+
delete: <T>(endpoint: string) =>
|
|
99
|
+
apiClient<T>(endpoint, { method: 'DELETE' }),
|
|
100
|
+
};
|
|
101
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility function for constructing className strings conditionally
|
|
5
|
+
*/
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return clsx(inputs);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a date to a human-readable string
|
|
12
|
+
*/
|
|
13
|
+
export function formatDate(date: Date | string): string {
|
|
14
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
15
|
+
month: 'long',
|
|
16
|
+
day: 'numeric',
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
}).format(new Date(date));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Delay execution for a specified number of milliseconds
|
|
23
|
+
*/
|
|
24
|
+
export function sleep(ms: number): Promise<void> {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a unique ID
|
|
30
|
+
*/
|
|
31
|
+
export function generateId(): string {
|
|
32
|
+
return Math.random().toString(36).substring(2, 9);
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common API types following bulletproof-react patterns
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type BaseEntity = {
|
|
6
|
+
id: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PaginatedResponse<T> = {
|
|
12
|
+
data: T[];
|
|
13
|
+
meta: {
|
|
14
|
+
page: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
total: number;
|
|
17
|
+
totalPages: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ApiError = {
|
|
22
|
+
message: string;
|
|
23
|
+
statusCode: number;
|
|
24
|
+
errors?: Record<string, string[]>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type User = BaseEntity & {
|
|
28
|
+
email: string;
|
|
29
|
+
name: string;
|
|
30
|
+
avatar?: string;
|
|
31
|
+
role: 'admin' | 'user';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AuthResponse = {
|
|
35
|
+
user: User;
|
|
36
|
+
token: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type LoginCredentials = {
|
|
40
|
+
email: string;
|
|
41
|
+
password: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type RegisterCredentials = LoginCredentials & {
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feature-tanstack-query",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TanStack Query (React Query) data fetching setup",
|
|
5
|
+
"compatibleWith": ["runtime-vite", "runtime-nextjs"],
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@tanstack/react-query": "^5.60.0",
|
|
8
|
+
"@tanstack/react-query-devtools": "^5.60.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {},
|
|
11
|
+
"scripts": {},
|
|
12
|
+
"filePatterns": {
|
|
13
|
+
"include": ["**/*"],
|
|
14
|
+
"exclude": ["manifest.json"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { api } from '@/lib/api-client';
|
|
2
|
+
import type { User } from '@/types/api';
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { usersKeys } from './get-users';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mutation hook for creating a user
|
|
8
|
+
* Following bulletproof-react patterns with optimistic updates
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type CreateUserData = {
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function createUser(data: CreateUserData) {
|
|
17
|
+
const response = await api.post<User>('/api/users', data);
|
|
18
|
+
return response.data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type UseCreateUserOptions = {
|
|
22
|
+
onSuccess?: (user: User) => void;
|
|
23
|
+
onError?: (error: Error) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function useCreateUser(options: UseCreateUserOptions = {}) {
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
|
|
29
|
+
return useMutation({
|
|
30
|
+
mutationFn: createUser,
|
|
31
|
+
onSuccess: (data) => {
|
|
32
|
+
// Invalidate and refetch users list
|
|
33
|
+
queryClient.invalidateQueries({ queryKey: usersKeys.lists() });
|
|
34
|
+
options.onSuccess?.(data);
|
|
35
|
+
},
|
|
36
|
+
onError: (error) => {
|
|
37
|
+
options.onError?.(error as Error);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { api } from '@/lib/api-client';
|
|
3
|
+
import type { User } from '@/types/api';
|
|
4
|
+
import { usersKeys } from './get-users';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Query hook for fetching a single user
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
async function getUser(userId: string) {
|
|
11
|
+
const response = await api.get<User>(`/api/users/${userId}`);
|
|
12
|
+
return response.data;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type UseUserOptions = {
|
|
16
|
+
userId: string;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function useUser({ userId, enabled = true }: UseUserOptions) {
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: usersKeys.detail(userId),
|
|
23
|
+
queryFn: () => getUser(userId),
|
|
24
|
+
enabled,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { api } from '@/lib/api-client';
|
|
3
|
+
import type { User, PaginatedResponse } from '@/types/api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Example query hook following bulletproof-react patterns
|
|
7
|
+
* Each feature has its own API folder with typed query hooks
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type GetUsersParams = {
|
|
11
|
+
page?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function getUsers(params: GetUsersParams = {}) {
|
|
16
|
+
const { page = 1, limit = 10 } = params;
|
|
17
|
+
const response = await api.get<PaginatedResponse<User>>('/api/users', {
|
|
18
|
+
page,
|
|
19
|
+
limit,
|
|
20
|
+
});
|
|
21
|
+
return response.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useUsers(params: GetUsersParams = {}) {
|
|
25
|
+
return useQuery({
|
|
26
|
+
queryKey: ['users', params],
|
|
27
|
+
queryFn: () => getUsers(params),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Query key factory for better organization
|
|
32
|
+
export const usersKeys = {
|
|
33
|
+
all: ['users'] as const,
|
|
34
|
+
lists: () => [...usersKeys.all, 'list'] as const,
|
|
35
|
+
list: (params: GetUsersParams) => [...usersKeys.lists(), params] as const,
|
|
36
|
+
details: () => [...usersKeys.all, 'detail'] as const,
|
|
37
|
+
detail: (id: string) => [...usersKeys.details(), id] as const,
|
|
38
|
+
};
|
|
39
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to access and manage the query client
|
|
5
|
+
* Following bulletproof-react patterns
|
|
6
|
+
*/
|
|
7
|
+
export function useQueryConfig() {
|
|
8
|
+
const queryClient = useQueryClient();
|
|
9
|
+
|
|
10
|
+
const invalidateQueries = (queryKey: string[]) => {
|
|
11
|
+
return queryClient.invalidateQueries({ queryKey });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const prefetchQuery = <T>(
|
|
15
|
+
queryKey: string[],
|
|
16
|
+
queryFn: () => Promise<T>
|
|
17
|
+
) => {
|
|
18
|
+
return queryClient.prefetchQuery({ queryKey, queryFn });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const setQueryData = <T>(queryKey: string[], data: T) => {
|
|
22
|
+
return queryClient.setQueryData(queryKey, data);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getQueryData = <T>(queryKey: string[]): T | undefined => {
|
|
26
|
+
return queryClient.getQueryData(queryKey);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const removeQueries = (queryKey: string[]) => {
|
|
30
|
+
return queryClient.removeQueries({ queryKey });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
queryClient,
|
|
35
|
+
invalidateQueries,
|
|
36
|
+
prefetchQuery,
|
|
37
|
+
setQueryData,
|
|
38
|
+
getQueryData,
|
|
39
|
+
removeQueries,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
6
|
+
import { createQueryClient } from './react-query';
|
|
7
|
+
|
|
8
|
+
type QueryProviderProps = {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* React Query Provider with DevTools
|
|
14
|
+
* Wrap your app with this component to enable data fetching
|
|
15
|
+
*/
|
|
16
|
+
export function QueryProvider({ children }: QueryProviderProps) {
|
|
17
|
+
// Create a new QueryClient instance for each request (for SSR)
|
|
18
|
+
// but reuse the same instance on client-side navigation
|
|
19
|
+
const [queryClient] = useState(() => createQueryClient());
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<QueryClientProvider client={queryClient}>
|
|
23
|
+
{children}
|
|
24
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
25
|
+
</QueryClientProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DefaultOptions, QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React Query configuration following bulletproof-react patterns
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const queryConfig: DefaultOptions = {
|
|
8
|
+
queries: {
|
|
9
|
+
// Stale time: 1 minute
|
|
10
|
+
staleTime: 1000 * 60,
|
|
11
|
+
// Don't refetch on window focus by default
|
|
12
|
+
refetchOnWindowFocus: false,
|
|
13
|
+
// Retry failed requests once
|
|
14
|
+
retry: 1,
|
|
15
|
+
// Retry delay
|
|
16
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
17
|
+
},
|
|
18
|
+
mutations: {
|
|
19
|
+
// Don't retry mutations by default
|
|
20
|
+
retry: false,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createQueryClient(): QueryClient {
|
|
25
|
+
return new QueryClient({
|
|
26
|
+
defaultOptions: queryConfig,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Singleton query client for client-side usage
|
|
31
|
+
let browserQueryClient: QueryClient | undefined = undefined;
|
|
32
|
+
|
|
33
|
+
export function getQueryClient(): QueryClient {
|
|
34
|
+
if (typeof window === 'undefined') {
|
|
35
|
+
// Server: always create a new query client
|
|
36
|
+
return createQueryClient();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Browser: create once and reuse
|
|
40
|
+
if (!browserQueryClient) {
|
|
41
|
+
browserQueryClient = createQueryClient();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return browserQueryClient;
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runtime-nextjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Next.js App Router runtime following bulletproof-react patterns",
|
|
5
|
+
"compatibleWith": ["base"],
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"react": "^18.2.0",
|
|
8
|
+
"react-dom": "^18.2.0",
|
|
9
|
+
"next": "^14.2.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/react": "^18.2.0",
|
|
13
|
+
"@types/react-dom": "^18.2.0",
|
|
14
|
+
"@types/node": "^20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "next dev",
|
|
18
|
+
"build": "next build",
|
|
19
|
+
"start": "next start",
|
|
20
|
+
"lint": "next lint"
|
|
21
|
+
},
|
|
22
|
+
"filePatterns": {
|
|
23
|
+
"include": ["**/*"],
|
|
24
|
+
"exclude": ["manifest.json"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function Error({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string };
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Log the error to an error reporting service
|
|
14
|
+
console.error(error);
|
|
15
|
+
}, [error]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex min-h-screen flex-col items-center justify-center" role="alert">
|
|
19
|
+
<div className="text-center">
|
|
20
|
+
<h1 className="text-2xl font-bold text-red-600">Something went wrong</h1>
|
|
21
|
+
<p className="mt-4 text-gray-600">
|
|
22
|
+
{error.message || 'An unexpected error occurred'}
|
|
23
|
+
</p>
|
|
24
|
+
<button
|
|
25
|
+
onClick={reset}
|
|
26
|
+
className="mt-6 rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
|
27
|
+
>
|
|
28
|
+
Try again
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '@/styles/globals.css';
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import { Providers } from './providers';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: '{{PROJECT_NAME}}',
|
|
7
|
+
description: 'A production-ready Next.js application',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<body>
|
|
18
|
+
<Providers>{children}</Providers>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
4
|
+
<div
|
|
5
|
+
className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-600"
|
|
6
|
+
role="status"
|
|
7
|
+
aria-label="Loading"
|
|
8
|
+
/>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen flex-col items-center justify-center">
|
|
6
|
+
<div className="text-center">
|
|
7
|
+
<p className="text-base font-semibold text-indigo-600">404</p>
|
|
8
|
+
<h1 className="mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
|
|
9
|
+
Page not found
|
|
10
|
+
</h1>
|
|
11
|
+
<p className="mt-6 text-base leading-7 text-gray-600">
|
|
12
|
+
Sorry, we couldn't find the page you're looking for.
|
|
13
|
+
</p>
|
|
14
|
+
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
15
|
+
<Link
|
|
16
|
+
href="/"
|
|
17
|
+
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
18
|
+
>
|
|
19
|
+
Go back home
|
|
20
|
+
</Link>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen flex-col items-center justify-center">
|
|
6
|
+
<div className="text-center">
|
|
7
|
+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
|
8
|
+
Welcome to Your App
|
|
9
|
+
</h1>
|
|
10
|
+
<p className="mt-6 text-lg leading-8 text-gray-600">
|
|
11
|
+
A production-ready Next.js application scaffolded with create-react-forge.
|
|
12
|
+
</p>
|
|
13
|
+
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
14
|
+
<Link
|
|
15
|
+
href="/dashboard"
|
|
16
|
+
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
17
|
+
>
|
|
18
|
+
Get started
|
|
19
|
+
</Link>
|
|
20
|
+
<a
|
|
21
|
+
href="https://github.com/alan2207/bulletproof-react"
|
|
22
|
+
className="text-sm font-semibold leading-6"
|
|
23
|
+
target="_blank"
|
|
24
|
+
rel="noopener noreferrer"
|
|
25
|
+
>
|
|
26
|
+
Learn more <span aria-hidden="true">→</span>
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type ProvidersProps = {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function Providers({ children }: ProvidersProps) {
|
|
10
|
+
// Add providers here (React Query, Theme, etc.)
|
|
11
|
+
// They will be added by feature overlays
|
|
12
|
+
return <>{children}</>;
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* Base styles - will be enhanced by styling overlay (tailwind/css-modules) */
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html,
|
|
14
|
+
body {
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
22
|
+
'Helvetica Neue', Arial, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
img,
|
|
26
|
+
picture,
|
|
27
|
+
video,
|
|
28
|
+
canvas,
|
|
29
|
+
svg {
|
|
30
|
+
display: block;
|
|
31
|
+
max-width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
input,
|
|
35
|
+
button,
|
|
36
|
+
textarea,
|
|
37
|
+
select {
|
|
38
|
+
font: inherit;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
p,
|
|
42
|
+
h1,
|
|
43
|
+
h2,
|
|
44
|
+
h3,
|
|
45
|
+
h4,
|
|
46
|
+
h5,
|
|
47
|
+
h6 {
|
|
48
|
+
overflow-wrap: break-word;
|
|
49
|
+
}
|
|
50
|
+
|