generator-kodly-react-app 1.0.0 → 1.0.2
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/generators/app/index.js +1 -1
- package/generators/app/templates/.env.example +4 -1
- package/generators/app/templates/README.md +23 -1
- package/generators/app/templates/gitignore.template +10 -0
- package/generators/app/templates/package.json +3 -3
- package/generators/app/templates/src/app.tsx +21 -20
- package/generators/app/templates/src/lib/api/client.ts +16 -4
- package/generators/app/templates/src/lib/utils/error-handler.ts +62 -0
- package/generators/app/templates/src/locales/en.json +5 -1
- package/generators/app/templates/src/modules/auth/use-auth-hook.ts +22 -27
- package/generators/app/templates/src/router.tsx +3 -3
- package/generators/app/templates/src/routes/app/index.tsx +27 -6
- package/generators/app/templates/src/vite-env.d.ts +1 -1
- package/generators/app/templates/vite.config.js +11 -8
- package/package.json +1 -1
package/generators/app/index.js
CHANGED
|
@@ -25,11 +25,11 @@ export default class ReactAppGenerator extends BaseGenerator {
|
|
|
25
25
|
'postcss.config.js',
|
|
26
26
|
'index.html',
|
|
27
27
|
'types.d.ts',
|
|
28
|
-
'.gitignore',
|
|
29
28
|
'.env.example',
|
|
30
29
|
'README.md',
|
|
31
30
|
];
|
|
32
31
|
configFiles.forEach((file) => this._writeFile(file));
|
|
32
|
+
this._writeFileWithPaths('gitignore.template', '.gitignore');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
_writeSourceFiles() {
|
|
@@ -23,7 +23,29 @@ npm run dev
|
|
|
23
23
|
|
|
24
24
|
## Environment Variables
|
|
25
25
|
|
|
26
|
-
- `VITE_API_BASE_URL` - Base URL for your API backend
|
|
26
|
+
- `VITE_API_BASE_URL` - Base URL for your API backend (default: `http://localhost:8181`)
|
|
27
|
+
- `PORT` - Port for the development server (default: `3000`)
|
|
28
|
+
|
|
29
|
+
### Setting Environment Variables
|
|
30
|
+
|
|
31
|
+
You can set environment variables in several ways:
|
|
32
|
+
|
|
33
|
+
**Option 1: Using a `.env` file (recommended)**
|
|
34
|
+
```bash
|
|
35
|
+
# Create .env file
|
|
36
|
+
echo "VITE_API_BASE_URL=http://localhost:8080/api" > .env
|
|
37
|
+
echo "PORT=3001" >> .env
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Option 2: Using environment variables when running commands**
|
|
41
|
+
```bash
|
|
42
|
+
VITE_API_BASE_URL=http://localhost:8080/api PORT=3001 npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Option 3: Using cross-env (works on all platforms)**
|
|
46
|
+
```bash
|
|
47
|
+
cross-env VITE_API_BASE_URL=http://localhost:8080/api PORT=3001 npm run dev
|
|
48
|
+
```
|
|
27
49
|
|
|
28
50
|
## Project Structure
|
|
29
51
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"version": "0.0.1",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "vite
|
|
8
|
-
"start": "vite
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"start": "vite",
|
|
9
9
|
"build": "vite build && tsc",
|
|
10
10
|
"serve": "vite preview"
|
|
11
11
|
},
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"react": "<%= REACT_VERSION %>",
|
|
25
25
|
"react-dom": "<%= REACT_DOM_VERSION %>",
|
|
26
26
|
"react-i18next": "<%= REACT_I18NEXT_VERSION %>",
|
|
27
|
+
"sonner": "^2.0.7",
|
|
27
28
|
"tailwind-merge": "^3.4.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
@@ -39,4 +40,3 @@
|
|
|
39
40
|
"vite": "<%= VITE_VERSION %>"
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { RouterProvider } from
|
|
2
|
-
import { useAtomValue } from
|
|
3
|
-
import { Loader2 } from
|
|
4
|
-
import { useEffect } from
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
setLocaleInAxios,
|
|
12
|
-
} from "./modules/auth/use-auth-hook";
|
|
13
|
-
import { router } from "./router";
|
|
14
|
-
import { getLanguage } from "./lib/i18n";
|
|
1
|
+
import { RouterProvider } from '@tanstack/react-router';
|
|
2
|
+
import { useAtomValue } from 'jotai';
|
|
3
|
+
import { Loader2 } from 'lucide-react';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { Toaster } from 'sonner';
|
|
6
|
+
import './index.css';
|
|
7
|
+
import { AuthProvider } from './modules/auth/auth-context';
|
|
8
|
+
import { authTokenAtom, currentUserDetailsAtom, useValidateToken, setLocaleInAxios } from './modules/auth/use-auth-hook';
|
|
9
|
+
import { router } from './router';
|
|
10
|
+
import { getLanguage } from './lib/i18n';
|
|
15
11
|
|
|
16
|
-
declare module
|
|
12
|
+
declare module '@tanstack/react-router' {
|
|
17
13
|
interface Register {
|
|
18
14
|
router: typeof router;
|
|
19
15
|
}
|
|
@@ -22,7 +18,12 @@ declare module "@tanstack/react-router" {
|
|
|
22
18
|
function InnerApp() {
|
|
23
19
|
const token = useAtomValue(authTokenAtom);
|
|
24
20
|
const data = useAtomValue(currentUserDetailsAtom);
|
|
25
|
-
return
|
|
21
|
+
return (
|
|
22
|
+
<RouterProvider
|
|
23
|
+
router={router}
|
|
24
|
+
context={{ token, data }}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export default function App() {
|
|
@@ -30,7 +31,7 @@ export default function App() {
|
|
|
30
31
|
const token = useAtomValue(authTokenAtom);
|
|
31
32
|
|
|
32
33
|
useEffect(() => {
|
|
33
|
-
const currentLanguage = getLanguage() as
|
|
34
|
+
const currentLanguage = getLanguage() as 'en';
|
|
34
35
|
setLocaleInAxios(currentLanguage);
|
|
35
36
|
}, []);
|
|
36
37
|
|
|
@@ -43,14 +44,14 @@ export default function App() {
|
|
|
43
44
|
if (!token || (!isPending && (isError || isSuccess))) {
|
|
44
45
|
return (
|
|
45
46
|
<AuthProvider>
|
|
47
|
+
<Toaster position='top-right' />
|
|
46
48
|
<InnerApp />
|
|
47
49
|
</AuthProvider>
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
52
|
return (
|
|
51
|
-
<div className=
|
|
52
|
-
<Loader2 className=
|
|
53
|
+
<div className='flex h-screen w-screen items-center justify-center'>
|
|
54
|
+
<Loader2 className='h-6 w-6 animate-spin' />
|
|
53
55
|
</div>
|
|
54
56
|
);
|
|
55
57
|
}
|
|
56
|
-
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import axios from
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { handleBackendError } from '../utils/error-handler';
|
|
2
3
|
|
|
3
|
-
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ||
|
|
4
|
+
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8181';
|
|
4
5
|
|
|
5
6
|
export const client = axios.create({
|
|
6
7
|
baseURL: apiBaseUrl,
|
|
7
8
|
headers: {
|
|
8
|
-
|
|
9
|
+
'Content-Type': 'application/json',
|
|
9
10
|
},
|
|
10
11
|
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// Add response interceptor to handle non-2xx responses
|
|
14
|
+
client.interceptors.response.use(
|
|
15
|
+
(response) => response,
|
|
16
|
+
(error) => {
|
|
17
|
+
// Only handle non-2xx responses
|
|
18
|
+
if (error.response && error.response.status >= 300) {
|
|
19
|
+
handleBackendError(error);
|
|
20
|
+
}
|
|
21
|
+
return Promise.reject(error);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
13
24
|
|
|
25
|
+
export default client;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
import { AxiosError } from 'axios';
|
|
3
|
+
|
|
4
|
+
interface BackendErrorResponse {
|
|
5
|
+
message?: string;
|
|
6
|
+
status?: boolean;
|
|
7
|
+
data?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles backend errors and displays appropriate toast messages.
|
|
12
|
+
*
|
|
13
|
+
* Expected error response format:
|
|
14
|
+
* {
|
|
15
|
+
* "message": "exception.login.failed",
|
|
16
|
+
* "status": false,
|
|
17
|
+
* "data": null
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* If the response doesn't match this format, it will show the error message
|
|
21
|
+
* from the exception instead.
|
|
22
|
+
*/
|
|
23
|
+
export const handleBackendError = (error: unknown): void => {
|
|
24
|
+
if (error instanceof Error && 'response' in error) {
|
|
25
|
+
const axiosError = error as AxiosError<BackendErrorResponse>;
|
|
26
|
+
const response = axiosError.response;
|
|
27
|
+
|
|
28
|
+
if (response) {
|
|
29
|
+
const errorData = response.data;
|
|
30
|
+
|
|
31
|
+
// Check if the response matches the expected format
|
|
32
|
+
if (errorData && typeof errorData === 'object' && 'message' in errorData && 'status' in errorData) {
|
|
33
|
+
// Use the message from the backend response
|
|
34
|
+
const errorMessage = errorData.message || 'An error occurred';
|
|
35
|
+
toast.error(errorMessage);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If the response doesn't match the expected format,
|
|
40
|
+
// try to extract a message from the response data
|
|
41
|
+
if (errorData && typeof errorData === 'object') {
|
|
42
|
+
const message = (errorData as any).message || (errorData as any).error || response.statusText || 'An error occurred';
|
|
43
|
+
toast.error(message);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback to the error message from the exception
|
|
49
|
+
const errorMessage = axiosError.message || 'An error occurred';
|
|
50
|
+
toast.error(errorMessage);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If it's not an axios error, show a generic message
|
|
55
|
+
if (error instanceof Error) {
|
|
56
|
+
toast.error(error.message || 'An error occurred');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Final fallback
|
|
61
|
+
toast.error('An unexpected error occurred');
|
|
62
|
+
};
|
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
import { Route as LoginRoute } from
|
|
2
|
-
import { Route as AppRoute } from
|
|
3
|
-
import { useMutation } from
|
|
4
|
-
import { useNavigate } from
|
|
5
|
-
import { atom, useAtomValue, useSetAtom } from
|
|
6
|
-
import { atomWithStorage } from
|
|
7
|
-
import { client } from
|
|
1
|
+
import { Route as LoginRoute } from '@/routes/auth/login';
|
|
2
|
+
import { Route as AppRoute } from '@/routes/index';
|
|
3
|
+
import { useMutation } from '@tanstack/react-query';
|
|
4
|
+
import { useNavigate } from '@tanstack/react-router';
|
|
5
|
+
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
|
6
|
+
import { atomWithStorage } from 'jotai/utils';
|
|
7
|
+
import { client } from '@/lib/api/client';
|
|
8
8
|
|
|
9
|
-
export const authTokenAtom = atomWithStorage<string>(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
undefined,
|
|
13
|
-
{
|
|
14
|
-
getOnInit: true,
|
|
15
|
-
}
|
|
16
|
-
);
|
|
9
|
+
export const authTokenAtom = atomWithStorage<string>('authTokenAtom', '', undefined, {
|
|
10
|
+
getOnInit: true,
|
|
11
|
+
});
|
|
17
12
|
|
|
18
13
|
export const currentUserDetailsAtom = atom<any>();
|
|
19
14
|
|
|
20
15
|
export const setTokenInAxios = (token?: string | null) => {
|
|
21
16
|
if (token) {
|
|
22
|
-
client.defaults.headers.common[
|
|
17
|
+
client.defaults.headers.common['x-auth-token'] = token;
|
|
23
18
|
} else {
|
|
24
|
-
delete client.defaults.headers.common[
|
|
19
|
+
delete client.defaults.headers.common['x-auth-token'];
|
|
25
20
|
}
|
|
26
21
|
};
|
|
27
22
|
|
|
28
|
-
export const setLocaleInAxios = (locale:
|
|
29
|
-
client.defaults.headers.common[
|
|
23
|
+
export const setLocaleInAxios = (locale: 'en') => {
|
|
24
|
+
client.defaults.headers.common['Accept-Language'] = locale;
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
export const useLogin = () => {
|
|
@@ -35,18 +30,19 @@ export const useLogin = () => {
|
|
|
35
30
|
const navigate = useNavigate();
|
|
36
31
|
return useMutation({
|
|
37
32
|
mutationFn: async (data: { email: string; password: string }) => {
|
|
38
|
-
const response = await client.post(
|
|
33
|
+
const response = await client.post('/auth/login', data);
|
|
39
34
|
return response.data;
|
|
40
35
|
},
|
|
41
36
|
onSuccess: (authData) => {
|
|
42
|
-
const token = authData?.token ||
|
|
37
|
+
const token = authData?.token || '';
|
|
43
38
|
setAuthToken(token);
|
|
44
39
|
setCurrentUser(authData);
|
|
45
40
|
setTokenInAxios(token);
|
|
46
41
|
navigate({ to: AppRoute.to });
|
|
47
42
|
},
|
|
48
43
|
onError: (err: any) => {
|
|
49
|
-
|
|
44
|
+
// Error is already handled by axios interceptor
|
|
45
|
+
console.error('Login failed:', err);
|
|
50
46
|
},
|
|
51
47
|
});
|
|
52
48
|
};
|
|
@@ -58,7 +54,7 @@ export const useValidateToken = () => {
|
|
|
58
54
|
setTokenInAxios(authToken);
|
|
59
55
|
return useMutation({
|
|
60
56
|
mutationFn: async () => {
|
|
61
|
-
const response = await client.get(
|
|
57
|
+
const response = await client.get('/auth/validate');
|
|
62
58
|
return response.data;
|
|
63
59
|
},
|
|
64
60
|
retry: false,
|
|
@@ -67,7 +63,7 @@ export const useValidateToken = () => {
|
|
|
67
63
|
},
|
|
68
64
|
onError: () => {
|
|
69
65
|
setCurrentUser(undefined);
|
|
70
|
-
setAuthToken(
|
|
66
|
+
setAuthToken('');
|
|
71
67
|
setTokenInAxios();
|
|
72
68
|
},
|
|
73
69
|
});
|
|
@@ -79,14 +75,13 @@ export const useLogout = () => {
|
|
|
79
75
|
const navigate = useNavigate();
|
|
80
76
|
return useMutation({
|
|
81
77
|
mutationFn: async () => {
|
|
82
|
-
await client.post(
|
|
78
|
+
await client.post('/auth/logout');
|
|
83
79
|
},
|
|
84
80
|
onSuccess: () => {
|
|
85
|
-
setAuthToken(
|
|
81
|
+
setAuthToken('');
|
|
86
82
|
setCurrentUser(undefined);
|
|
87
83
|
setTokenInAxios();
|
|
88
84
|
navigate({ to: LoginRoute.to });
|
|
89
85
|
},
|
|
90
86
|
});
|
|
91
87
|
};
|
|
92
|
-
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createRouter } from
|
|
2
|
-
import { routeTree } from
|
|
1
|
+
import { createRouter } from '@tanstack/react-router';
|
|
2
|
+
import { routeTree } from './routeTree.gen';
|
|
3
3
|
|
|
4
4
|
export const router = createRouter({
|
|
5
|
+
basepath: import.meta.env.VITE_BASE || '/',
|
|
5
6
|
routeTree,
|
|
6
7
|
context: {
|
|
7
8
|
token: undefined!,
|
|
8
9
|
data: undefined!,
|
|
9
10
|
},
|
|
10
11
|
});
|
|
11
|
-
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
import { createFileRoute } from
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { useLogout } from '@/modules/auth/use-auth-hook';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
2
5
|
|
|
3
|
-
export const Route = createFileRoute(
|
|
6
|
+
export const Route = createFileRoute('/app/')({
|
|
4
7
|
component: AppIndex,
|
|
5
8
|
});
|
|
6
9
|
|
|
7
10
|
function AppIndex() {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const { mutate: logout, isPending } = useLogout();
|
|
13
|
+
|
|
14
|
+
const handleLogout = () => {
|
|
15
|
+
logout();
|
|
16
|
+
};
|
|
17
|
+
|
|
8
18
|
return (
|
|
9
|
-
<div className=
|
|
10
|
-
<
|
|
11
|
-
|
|
19
|
+
<div className='container mx-auto p-8'>
|
|
20
|
+
<div className='flex flex-col gap-6'>
|
|
21
|
+
<div>
|
|
22
|
+
<h1 className='text-3xl font-bold mb-4'>{t('welcome.title')}</h1>
|
|
23
|
+
<p className='text-muted-foreground'>{t('welcome.message')}</p>
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<Button
|
|
27
|
+
onClick={handleLogout}
|
|
28
|
+
disabled={isPending}
|
|
29
|
+
variant='outline'>
|
|
30
|
+
{t('welcome.logout')}
|
|
31
|
+
</Button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
12
34
|
</div>
|
|
13
35
|
);
|
|
14
36
|
}
|
|
15
|
-
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import viteReact from
|
|
3
|
-
import { tanstackRouter } from
|
|
4
|
-
import tailwindcss from
|
|
5
|
-
import { resolve } from
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import viteReact from '@vitejs/plugin-react';
|
|
3
|
+
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
|
4
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
6
|
|
|
7
7
|
export default defineConfig({
|
|
8
|
+
base: process.env.VITE_BASE || '/',
|
|
8
9
|
plugins: [
|
|
9
10
|
tanstackRouter({
|
|
10
|
-
target:
|
|
11
|
+
target: 'react',
|
|
11
12
|
autoCodeSplitting: true,
|
|
12
13
|
spa: {
|
|
13
14
|
enabled: true,
|
|
@@ -18,8 +19,10 @@ export default defineConfig({
|
|
|
18
19
|
],
|
|
19
20
|
resolve: {
|
|
20
21
|
alias: {
|
|
21
|
-
|
|
22
|
+
'@': resolve(__dirname, './src'),
|
|
22
23
|
},
|
|
23
24
|
},
|
|
25
|
+
server: {
|
|
26
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
27
|
+
},
|
|
24
28
|
});
|
|
25
|
-
|
package/package.json
CHANGED