generator-kodly-react-app 1.0.1 → 1.0.3

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.
@@ -1,2 +1,5 @@
1
- # API Base URL
1
+ # API Configuration
2
2
  VITE_API_BASE_URL=http://localhost:8181
3
+
4
+ # Server Configuration
5
+ PORT=3000
@@ -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 --port 3000",
8
- "start": "vite --port 3000",
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 "@tanstack/react-router";
2
- import { useAtomValue } from "jotai";
3
- import { Loader2 } from "lucide-react";
4
- import { useEffect } from "react";
5
- import "./index.css";
6
- import { AuthProvider } from "./modules/auth/auth-context";
7
- import {
8
- authTokenAtom,
9
- currentUserDetailsAtom,
10
- useValidateToken,
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 "@tanstack/react-router" {
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 <RouterProvider router={router} context={{ token, data }} />;
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 "en";
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="flex h-screen w-screen items-center justify-center">
52
- <Loader2 className="h-6 w-6 animate-spin" />
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 "axios";
1
+ import axios from 'axios';
2
+ import { handleBackendError } from '../utils/error-handler';
2
3
 
3
- const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8181";
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
- "Content-Type": "application/json",
9
+ 'Content-Type': 'application/json',
9
10
  },
10
11
  });
11
12
 
12
- export default client;
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
+ };
@@ -7,8 +7,12 @@
7
7
  "passwordPlaceholder": "Enter your password",
8
8
  "submit": "Sign In"
9
9
  },
10
+ "welcome": {
11
+ "title": "Welcome",
12
+ "message": "You have successfully logged in!",
13
+ "logout": "Logout"
14
+ },
10
15
  "notFound": {
11
16
  "message": "Page not found"
12
17
  }
13
18
  }
14
-
@@ -1,32 +1,27 @@
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";
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
- "authTokenAtom",
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["x-auth-token"] = token;
17
+ client.defaults.headers.common['x-auth-token'] = token;
23
18
  } else {
24
- delete client.defaults.headers.common["x-auth-token"];
19
+ delete client.defaults.headers.common['x-auth-token'];
25
20
  }
26
21
  };
27
22
 
28
- export const setLocaleInAxios = (locale: "en") => {
29
- client.defaults.headers.common["Accept-Language"] = locale;
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("/auth/login", data);
33
+ const response = await client.post('/auth/user/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
- console.error("Login failed:", err);
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("/auth/validate");
57
+ const response = await client.post('/auth/user/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("/auth/logout");
78
+ await client.post('/auth/user/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 "@tanstack/react-router";
2
- import { routeTree } from "./routeTree.gen";
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 "@tanstack/react-router";
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("/app/")({
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="container mx-auto p-8">
10
- <h1 className="text-3xl font-bold mb-4">Welcome</h1>
11
- <p className="text-muted-foreground">This is a protected route.</p>
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
-
@@ -2,9 +2,9 @@
2
2
 
3
3
  interface ImportMetaEnv {
4
4
  readonly VITE_API_BASE_URL: string;
5
+ readonly VITE_BASE: string;
5
6
  }
6
7
 
7
8
  interface ImportMeta {
8
9
  readonly env: ImportMetaEnv;
9
10
  }
10
-
@@ -1,13 +1,14 @@
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";
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: "react",
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
- "@": resolve(__dirname, "./src"),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-kodly-react-app",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A Yeoman generator for creating React.js applications with Vite, TanStack Router, authentication, and i18n",
5
5
  "main": "index.js",
6
6
  "type": "module",