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,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"baseUrl": ".",
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["./src/*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{PROJECT_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
14
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runtime-vite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vite runtime with React SPA setup following bulletproof-react patterns",
|
|
5
|
+
"compatibleWith": ["base"],
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"react": "^18.2.0",
|
|
8
|
+
"react-dom": "^18.2.0",
|
|
9
|
+
"react-router-dom": "^6.22.0",
|
|
10
|
+
"react-error-boundary": "^4.0.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"vite": "^5.4.0",
|
|
14
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
15
|
+
"@types/react": "^18.2.0",
|
|
16
|
+
"@types/react-dom": "^18.2.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "tsc -b && vite build",
|
|
21
|
+
"preview": "vite preview"
|
|
22
|
+
},
|
|
23
|
+
"filePatterns": {
|
|
24
|
+
"include": ["**/*"],
|
|
25
|
+
"exclude": ["manifest.json"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFBD4F"></stop><stop offset="100%" stop-color="#FF980E"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
2
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ErrorFallback } from '@/components/errors/ErrorFallback';
|
|
2
|
+
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
5
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
6
|
+
|
|
7
|
+
type AppProviderProps = {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AppProvider({ children }: AppProviderProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
14
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
15
|
+
<BrowserRouter>{children}</BrowserRouter>
|
|
16
|
+
</ErrorBoundary>
|
|
17
|
+
</Suspense>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Landing } from '@/features/misc/routes/Landing';
|
|
2
|
+
import { NotFound } from '@/features/misc/routes/NotFound';
|
|
3
|
+
import { useRoutes } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
export function AppRouter() {
|
|
6
|
+
const routes = useRoutes([
|
|
7
|
+
{
|
|
8
|
+
path: '/',
|
|
9
|
+
element: <Landing />,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: '*',
|
|
13
|
+
element: <NotFound />,
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return routes;
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FallbackProps } from 'react-error-boundary';
|
|
2
|
+
|
|
3
|
+
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen flex-col items-center justify-center" role="alert">
|
|
6
|
+
<div className="text-center">
|
|
7
|
+
<h1 className="text-2xl font-bold text-red-600">Something went wrong</h1>
|
|
8
|
+
<p className="mt-4 text-gray-600">
|
|
9
|
+
{error.message || 'An unexpected error occurred'}
|
|
10
|
+
</p>
|
|
11
|
+
<button
|
|
12
|
+
onClick={resetErrorBoundary}
|
|
13
|
+
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"
|
|
14
|
+
>
|
|
15
|
+
Try again
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type LoadingSpinnerProps = {
|
|
2
|
+
size?: 'sm' | 'md' | 'lg';
|
|
3
|
+
className?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const sizes = {
|
|
7
|
+
sm: 'h-4 w-4',
|
|
8
|
+
md: 'h-8 w-8',
|
|
9
|
+
lg: 'h-16 w-16',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex items-center justify-center">
|
|
15
|
+
<div
|
|
16
|
+
className={`animate-spin rounded-full border-2 border-gray-300 border-t-indigo-600 ${sizes[size]} ${className}`}
|
|
17
|
+
role="status"
|
|
18
|
+
aria-label="Loading"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
|
|
3
|
+
export function Landing() {
|
|
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 React application scaffolded with create-react-forge.
|
|
12
|
+
</p>
|
|
13
|
+
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
14
|
+
<Link
|
|
15
|
+
to="/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,26 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
|
|
3
|
+
export 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
|
+
to="/"
|
|
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,11 @@
|
|
|
1
|
+
import { App } from '@/app/App';
|
|
2
|
+
import '@/styles/globals.css';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import ReactDOM from 'react-dom/client';
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<App />
|
|
9
|
+
</React.StrictMode>
|
|
10
|
+
);
|
|
11
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
51
|
+
#root {
|
|
52
|
+
isolation: isolate;
|
|
53
|
+
height: 100%;
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
|
|
23
|
+
/* Path aliases */
|
|
24
|
+
"baseUrl": ".",
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": ["src/*"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"include": ["src"],
|
|
30
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
|
|
8
|
+
/* Bundler mode */
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
|
|
15
|
+
/* Linting */
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["vite.config.ts"]
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
// https://vitejs.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@': path.resolve(__dirname, './src'),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
server: {
|
|
14
|
+
port: 3000,
|
|
15
|
+
open: true,
|
|
16
|
+
},
|
|
17
|
+
build: {
|
|
18
|
+
outDir: 'dist',
|
|
19
|
+
sourcemap: true,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "state-redux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Redux Toolkit state management with typed hooks",
|
|
5
|
+
"compatibleWith": ["runtime-vite", "runtime-nextjs"],
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@reduxjs/toolkit": "^2.2.0",
|
|
8
|
+
"react-redux": "^9.1.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {},
|
|
11
|
+
"scripts": {},
|
|
12
|
+
"filePatterns": {
|
|
13
|
+
"include": ["**/*"],
|
|
14
|
+
"exclude": ["manifest.json"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { store } from './store';
|
|
5
|
+
|
|
6
|
+
type StoreProviderProps = {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Redux Provider component
|
|
12
|
+
* Wrap your app with this to enable Redux state management
|
|
13
|
+
*/
|
|
14
|
+
export function StoreProvider({ children }: StoreProviderProps) {
|
|
15
|
+
return <Provider store={store}>{children}</Provider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
|
2
|
+
import type { AppDispatch, RootState } from './store';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed hooks for Redux following bulletproof-react patterns
|
|
6
|
+
* Use these instead of plain `useDispatch` and `useSelector`
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
10
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
11
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { useAppDispatch, useAppSelector } from './hooks';
|
|
2
|
+
export { store, type AppDispatch, type RootState } from './store';
|
|
3
|
+
|
|
4
|
+
// Notifications
|
|
5
|
+
export {
|
|
6
|
+
addNotification, clearNotifications, removeNotification, selectNotifications,
|
|
7
|
+
type Notification,
|
|
8
|
+
type NotificationType
|
|
9
|
+
} from './slices/notifications';
|
|
10
|
+
|
|
11
|
+
// Auth
|
|
12
|
+
export {
|
|
13
|
+
logout, selectCurrentUser,
|
|
14
|
+
selectIsAuthenticated,
|
|
15
|
+
selectToken, setCredentials, updateUser
|
|
16
|
+
} from './slices/auth';
|
|
17
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { User } from '@/types/api';
|
|
2
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Auth slice following bulletproof-react patterns
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type AuthState = {
|
|
9
|
+
user: User | null;
|
|
10
|
+
token: string | null;
|
|
11
|
+
isAuthenticated: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const initialState: AuthState = {
|
|
15
|
+
user: null,
|
|
16
|
+
token: null,
|
|
17
|
+
isAuthenticated: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const authSlice = createSlice({
|
|
21
|
+
name: 'auth',
|
|
22
|
+
initialState,
|
|
23
|
+
reducers: {
|
|
24
|
+
setCredentials: (
|
|
25
|
+
state,
|
|
26
|
+
action: PayloadAction<{ user: User; token: string }>
|
|
27
|
+
) => {
|
|
28
|
+
state.user = action.payload.user;
|
|
29
|
+
state.token = action.payload.token;
|
|
30
|
+
state.isAuthenticated = true;
|
|
31
|
+
},
|
|
32
|
+
logout: (state) => {
|
|
33
|
+
state.user = null;
|
|
34
|
+
state.token = null;
|
|
35
|
+
state.isAuthenticated = false;
|
|
36
|
+
},
|
|
37
|
+
updateUser: (state, action: PayloadAction<Partial<User>>) => {
|
|
38
|
+
if (state.user) {
|
|
39
|
+
state.user = { ...state.user, ...action.payload };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const { setCredentials, logout, updateUser } = authSlice.actions;
|
|
46
|
+
|
|
47
|
+
export const authReducer = authSlice.reducer;
|
|
48
|
+
|
|
49
|
+
// Selectors
|
|
50
|
+
export const selectCurrentUser = (state: { auth: AuthState }) => state.auth.user;
|
|
51
|
+
export const selectIsAuthenticated = (state: { auth: AuthState }) =>
|
|
52
|
+
state.auth.isAuthenticated;
|
|
53
|
+
export const selectToken = (state: { auth: AuthState }) => state.auth.token;
|
|
54
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { generateId } from '@/lib/utils';
|
|
2
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notifications slice following bulletproof-react patterns
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type NotificationType = 'info' | 'success' | 'warning' | 'error';
|
|
9
|
+
|
|
10
|
+
export type Notification = {
|
|
11
|
+
id: string;
|
|
12
|
+
type: NotificationType;
|
|
13
|
+
title: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type NotificationsState = {
|
|
18
|
+
notifications: Notification[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const initialState: NotificationsState = {
|
|
22
|
+
notifications: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const notificationsSlice = createSlice({
|
|
26
|
+
name: 'notifications',
|
|
27
|
+
initialState,
|
|
28
|
+
reducers: {
|
|
29
|
+
addNotification: (
|
|
30
|
+
state,
|
|
31
|
+
action: PayloadAction<Omit<Notification, 'id'>>
|
|
32
|
+
) => {
|
|
33
|
+
const notification = {
|
|
34
|
+
...action.payload,
|
|
35
|
+
id: generateId(),
|
|
36
|
+
};
|
|
37
|
+
state.notifications.push(notification);
|
|
38
|
+
},
|
|
39
|
+
removeNotification: (state, action: PayloadAction<string>) => {
|
|
40
|
+
state.notifications = state.notifications.filter(
|
|
41
|
+
(n) => n.id !== action.payload
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
clearNotifications: (state) => {
|
|
45
|
+
state.notifications = [];
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const { addNotification, removeNotification, clearNotifications } =
|
|
51
|
+
notificationsSlice.actions;
|
|
52
|
+
|
|
53
|
+
export const notificationsReducer = notificationsSlice.reducer;
|
|
54
|
+
|
|
55
|
+
// Selectors
|
|
56
|
+
export const selectNotifications = (state: { notifications: NotificationsState }) =>
|
|
57
|
+
state.notifications.notifications;
|
|
58
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
+
import { authReducer } from './slices/auth';
|
|
3
|
+
import { notificationsReducer } from './slices/notifications';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redux store configuration following bulletproof-react patterns
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const store = configureStore({
|
|
10
|
+
reducer: {
|
|
11
|
+
notifications: notificationsReducer,
|
|
12
|
+
auth: authReducer,
|
|
13
|
+
},
|
|
14
|
+
middleware: (getDefaultMiddleware) =>
|
|
15
|
+
getDefaultMiddleware({
|
|
16
|
+
serializableCheck: {
|
|
17
|
+
// Ignore these action types for serialization check
|
|
18
|
+
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
devTools: process.env.NODE_ENV !== 'production',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Infer the `RootState` and `AppDispatch` types from the store itself
|
|
25
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
26
|
+
export type AppDispatch = typeof store.dispatch;
|
|
27
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "state-zustand",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zustand state management following bulletproof-react patterns",
|
|
5
|
+
"compatibleWith": ["runtime-vite", "runtime-nextjs"],
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"zustand": "^4.5.0"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {},
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"filePatterns": {
|
|
12
|
+
"include": ["**/*"],
|
|
13
|
+
"exclude": ["manifest.json"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { User } from '@/types/api';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { persist } from 'zustand/middleware';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth store with persistence following bulletproof-react patterns
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type AuthState = {
|
|
10
|
+
user: User | null;
|
|
11
|
+
token: string | null;
|
|
12
|
+
isAuthenticated: boolean;
|
|
13
|
+
setAuth: (user: User, token: string) => void;
|
|
14
|
+
logout: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useAuth = create<AuthState>()(
|
|
18
|
+
persist(
|
|
19
|
+
(set) => ({
|
|
20
|
+
user: null,
|
|
21
|
+
token: null,
|
|
22
|
+
isAuthenticated: false,
|
|
23
|
+
|
|
24
|
+
setAuth: (user, token) =>
|
|
25
|
+
set({
|
|
26
|
+
user,
|
|
27
|
+
token,
|
|
28
|
+
isAuthenticated: true,
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
logout: () =>
|
|
32
|
+
set({
|
|
33
|
+
user: null,
|
|
34
|
+
token: null,
|
|
35
|
+
isAuthenticated: false,
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
{
|
|
39
|
+
name: 'auth-storage',
|
|
40
|
+
partialize: (state) => ({
|
|
41
|
+
user: state.user,
|
|
42
|
+
token: state.token,
|
|
43
|
+
isAuthenticated: state.isAuthenticated,
|
|
44
|
+
}),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
|