generator-kodly-react-app 1.0.0

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.
Files changed (40) hide show
  1. package/generators/app/index.js +96 -0
  2. package/generators/app/templates/.env.example +2 -0
  3. package/generators/app/templates/README.md +35 -0
  4. package/generators/app/templates/index.html +18 -0
  5. package/generators/app/templates/package.json +42 -0
  6. package/generators/app/templates/postcss.config.js +6 -0
  7. package/generators/app/templates/src/app.tsx +56 -0
  8. package/generators/app/templates/src/components/theme/theme-provider.tsx +73 -0
  9. package/generators/app/templates/src/components/ui/button.tsx +55 -0
  10. package/generators/app/templates/src/components/ui/input.tsx +24 -0
  11. package/generators/app/templates/src/index.css +79 -0
  12. package/generators/app/templates/src/lib/api/client.ts +13 -0
  13. package/generators/app/templates/src/lib/i18n.ts +46 -0
  14. package/generators/app/templates/src/lib/utils.ts +7 -0
  15. package/generators/app/templates/src/locales/en.json +14 -0
  16. package/generators/app/templates/src/main.tsx +23 -0
  17. package/generators/app/templates/src/modules/auth/auth-context.tsx +13 -0
  18. package/generators/app/templates/src/modules/auth/login/login-form.tsx +49 -0
  19. package/generators/app/templates/src/modules/auth/login/login-page.tsx +12 -0
  20. package/generators/app/templates/src/modules/auth/use-auth-hook.ts +92 -0
  21. package/generators/app/templates/src/routeTree.gen.ts +10 -0
  22. package/generators/app/templates/src/router.tsx +11 -0
  23. package/generators/app/templates/src/routes/$.tsx +19 -0
  24. package/generators/app/templates/src/routes/__root.tsx +15 -0
  25. package/generators/app/templates/src/routes/app/index.tsx +15 -0
  26. package/generators/app/templates/src/routes/app/route.tsx +18 -0
  27. package/generators/app/templates/src/routes/auth/login.tsx +11 -0
  28. package/generators/app/templates/src/routes/auth/route.tsx +18 -0
  29. package/generators/app/templates/src/routes/index.tsx +12 -0
  30. package/generators/app/templates/src/types/i18n.d.ts +8 -0
  31. package/generators/app/templates/src/vite-env.d.ts +10 -0
  32. package/generators/app/templates/tailwind.config.js +9 -0
  33. package/generators/app/templates/tsconfig.app.json +9 -0
  34. package/generators/app/templates/tsconfig.json +29 -0
  35. package/generators/app/templates/types.d.ts +3 -0
  36. package/generators/app/templates/vite.config.js +25 -0
  37. package/generators/base-generator.js +62 -0
  38. package/generators/constants.js +20 -0
  39. package/index.js +36 -0
  40. package/package.json +45 -0
@@ -0,0 +1,96 @@
1
+ import BaseGenerator from '../base-generator.js';
2
+
3
+ export default class ReactAppGenerator extends BaseGenerator {
4
+ constructor(args, opts) {
5
+ super(args, opts);
6
+ }
7
+
8
+ writing() {
9
+ this._writeConfigFiles();
10
+ this._writeSourceFiles();
11
+ this._writeRouteFiles();
12
+ this._writeAuthFiles();
13
+ this._writeComponentFiles();
14
+ this._writeLibFiles();
15
+ this._writeLocaleFiles();
16
+ }
17
+
18
+ _writeConfigFiles() {
19
+ const configFiles = [
20
+ 'package.json',
21
+ 'vite.config.js',
22
+ 'tsconfig.json',
23
+ 'tsconfig.app.json',
24
+ 'tailwind.config.js',
25
+ 'postcss.config.js',
26
+ 'index.html',
27
+ 'types.d.ts',
28
+ '.gitignore',
29
+ '.env.example',
30
+ 'README.md',
31
+ ];
32
+ configFiles.forEach((file) => this._writeFile(file));
33
+ }
34
+
35
+ _writeSourceFiles() {
36
+ const srcFiles = [
37
+ 'src/main.tsx',
38
+ 'src/app.tsx',
39
+ 'src/router.tsx',
40
+ 'src/routeTree.gen.ts',
41
+ 'src/index.css',
42
+ 'src/vite-env.d.ts',
43
+ 'src/types/i18n.d.ts',
44
+ ];
45
+ srcFiles.forEach((file) => this._writeFile(file));
46
+ }
47
+
48
+ _writeRouteFiles() {
49
+ const routeFiles = [
50
+ 'src/routes/__root.tsx',
51
+ 'src/routes/index.tsx',
52
+ 'src/routes/$.tsx',
53
+ 'src/routes/auth/route.tsx',
54
+ 'src/routes/auth/login.tsx',
55
+ 'src/routes/app/route.tsx',
56
+ 'src/routes/app/index.tsx',
57
+ ];
58
+ routeFiles.forEach((file) => this._writeFile(file));
59
+ }
60
+
61
+ _writeAuthFiles() {
62
+ const authFiles = [
63
+ 'src/modules/auth/auth-context.tsx',
64
+ 'src/modules/auth/use-auth-hook.ts',
65
+ 'src/modules/auth/login/login-page.tsx',
66
+ 'src/modules/auth/login/login-form.tsx',
67
+ ];
68
+ authFiles.forEach((file) => this._writeFile(file));
69
+ }
70
+
71
+ _writeComponentFiles() {
72
+ const componentFiles = [
73
+ 'src/components/ui/button.tsx',
74
+ 'src/components/ui/input.tsx',
75
+ 'src/components/theme/theme-provider.tsx',
76
+ ];
77
+ componentFiles.forEach((file) => this._writeFile(file));
78
+ }
79
+
80
+ _writeLibFiles() {
81
+ const libFiles = [
82
+ 'src/lib/utils.ts',
83
+ 'src/lib/i18n.ts',
84
+ 'src/lib/api/client.ts',
85
+ ];
86
+ libFiles.forEach((file) => this._writeFile(file));
87
+ }
88
+
89
+ _writeLocaleFiles() {
90
+ const localeFiles = [
91
+ 'src/locales/en.json',
92
+ ];
93
+ localeFiles.forEach((file) => this._writeFile(file));
94
+ }
95
+ }
96
+
@@ -0,0 +1,2 @@
1
+ # API Base URL
2
+ VITE_API_BASE_URL=http://localhost:8181
@@ -0,0 +1,35 @@
1
+ # <%= appNameTitleCase %>
2
+
3
+ A React application built with Vite, TanStack Router, and TypeScript.
4
+
5
+ ## Getting Started
6
+
7
+ 1. Install dependencies:
8
+ ```bash
9
+ npm install
10
+ ```
11
+
12
+ 2. Create a `.env` file from `.env.example`:
13
+ ```bash
14
+ cp .env.example .env
15
+ ```
16
+
17
+ 3. Update the `.env` file with your API base URL.
18
+
19
+ 4. Start the development server:
20
+ ```bash
21
+ npm run dev
22
+ ```
23
+
24
+ ## Environment Variables
25
+
26
+ - `VITE_API_BASE_URL` - Base URL for your API backend
27
+
28
+ ## Project Structure
29
+
30
+ - `src/routes/` - File-based routing with TanStack Router
31
+ - `src/modules/auth/` - Authentication module
32
+ - `src/components/` - Reusable UI components
33
+ - `src/lib/` - Utility functions and configurations
34
+ - `src/locales/` - i18n translation files
35
+
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
8
+ />
9
+ <meta name="theme-color" content="#000000" />
10
+ <meta name="description" content="<%= appNameTitleCase %> Application" />
11
+ <title><%= appNameTitleCase %></title>
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ <script type="module" src="/src/main.tsx"></script>
16
+ </body>
17
+ </html>
18
+
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "<%= appNameSlug %>",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "scripts": {
7
+ "dev": "vite --port 3000",
8
+ "start": "vite --port 3000",
9
+ "build": "vite build && tsc",
10
+ "serve": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@radix-ui/react-slot": "^1.2.4",
14
+ "@tanstack/react-query": "<%= TANSTACK_REACT_QUERY_VERSION %>",
15
+ "@tanstack/react-router": "<%= TANSTACK_ROUTER_VERSION %>",
16
+ "@tanstack/router-plugin": "<%= TANSTACK_ROUTER_PLUGIN_VERSION %>",
17
+ "axios": "<%= AXIOS_VERSION %>",
18
+ "class-variance-authority": "^0.7.1",
19
+ "clsx": "^2.1.1",
20
+ "dotenv": "<%= DOTENV_VERSION %>",
21
+ "i18next": "<%= I18NEXT_VERSION %>",
22
+ "jotai": "<%= JOTAI_VERSION %>",
23
+ "lucide-react": "^0.554.0",
24
+ "react": "<%= REACT_VERSION %>",
25
+ "react-dom": "<%= REACT_DOM_VERSION %>",
26
+ "react-i18next": "<%= REACT_I18NEXT_VERSION %>",
27
+ "tailwind-merge": "^3.4.0"
28
+ },
29
+ "devDependencies": {
30
+ "@tailwindcss/postcss": "<%= TAILWIND_VERSION %>",
31
+ "@tailwindcss/vite": "<%= TAILWIND_VERSION %>",
32
+ "@types/node": "^24.10.1",
33
+ "@types/react": "^19.2.6",
34
+ "@types/react-dom": "^19.2.3",
35
+ "@vitejs/plugin-react": "^5.1.1",
36
+ "cross-env": "<%= CROSS_ENV_VERSION %>",
37
+ "tailwindcss": "<%= TAILWIND_VERSION %>",
38
+ "typescript": "<%= TYPESCRIPT_VERSION %>",
39
+ "vite": "<%= VITE_VERSION %>"
40
+ }
41
+ }
42
+
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
@@ -0,0 +1,56 @@
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";
15
+
16
+ declare module "@tanstack/react-router" {
17
+ interface Register {
18
+ router: typeof router;
19
+ }
20
+ }
21
+
22
+ function InnerApp() {
23
+ const token = useAtomValue(authTokenAtom);
24
+ const data = useAtomValue(currentUserDetailsAtom);
25
+ return <RouterProvider router={router} context={{ token, data }} />;
26
+ }
27
+
28
+ export default function App() {
29
+ const { isPending, mutate, isSuccess, isError } = useValidateToken();
30
+ const token = useAtomValue(authTokenAtom);
31
+
32
+ useEffect(() => {
33
+ const currentLanguage = getLanguage() as "en";
34
+ setLocaleInAxios(currentLanguage);
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (token) {
39
+ mutate();
40
+ }
41
+ }, [token, mutate]);
42
+
43
+ if (!token || (!isPending && (isError || isSuccess))) {
44
+ return (
45
+ <AuthProvider>
46
+ <InnerApp />
47
+ </AuthProvider>
48
+ );
49
+ }
50
+ return (
51
+ <div className="flex h-screen w-screen items-center justify-center">
52
+ <Loader2 className="h-6 w-6 animate-spin" />
53
+ </div>
54
+ );
55
+ }
56
+
@@ -0,0 +1,73 @@
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ type Theme = "dark" | "light" | "system";
4
+
5
+ type ThemeProviderProps = {
6
+ children: React.ReactNode;
7
+ defaultTheme?: Theme;
8
+ storageKey?: string;
9
+ };
10
+
11
+ type ThemeProviderState = {
12
+ theme: Theme;
13
+ setTheme: (theme: Theme) => void;
14
+ };
15
+
16
+ const initialState: ThemeProviderState = {
17
+ theme: "system",
18
+ setTheme: () => null,
19
+ };
20
+
21
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22
+
23
+ export function ThemeProvider({
24
+ children,
25
+ defaultTheme = "system",
26
+ storageKey = "vite-ui-theme",
27
+ ...props
28
+ }: ThemeProviderProps) {
29
+ const [theme, setTheme] = useState<Theme>(
30
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31
+ );
32
+
33
+ useEffect(() => {
34
+ const root = window.document.documentElement;
35
+
36
+ root.classList.remove("light", "dark");
37
+
38
+ if (theme === "system") {
39
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40
+ .matches
41
+ ? "dark"
42
+ : "light";
43
+ root.classList.add(systemTheme);
44
+ return;
45
+ }
46
+
47
+ root.classList.add(theme);
48
+ }, [theme]);
49
+
50
+ const value = {
51
+ theme,
52
+ setTheme: (theme: Theme) => {
53
+ localStorage.setItem(storageKey, theme);
54
+ setTheme(theme);
55
+ },
56
+ };
57
+
58
+ return (
59
+ <ThemeProviderContext.Provider {...props} value={value}>
60
+ {children}
61
+ </ThemeProviderContext.Provider>
62
+ );
63
+ }
64
+
65
+ export const useTheme = () => {
66
+ const context = useContext(ThemeProviderContext);
67
+
68
+ if (context === undefined)
69
+ throw new Error("useTheme must be used within a ThemeProvider");
70
+
71
+ return context;
72
+ };
73
+
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:cursor-not-allowed disabled:opacity-50 hover:cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
+ outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
14
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ ghost: "hover:bg-accent hover:text-accent-foreground",
16
+ link: "text-primary underline-offset-4 hover:underline",
17
+ },
18
+ size: {
19
+ default: "h-9 px-4 py-2",
20
+ sm: "h-8 rounded-md px-3",
21
+ lg: "h-10 rounded-md px-8",
22
+ icon: "h-9 w-9",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: "default",
27
+ size: "default",
28
+ },
29
+ }
30
+ );
31
+
32
+ function Button({
33
+ className,
34
+ variant,
35
+ size,
36
+ asChild = false,
37
+ type,
38
+ ...props
39
+ }: React.ComponentProps<"button"> &
40
+ VariantProps<typeof buttonVariants> & {
41
+ asChild?: boolean;
42
+ }) {
43
+ const Comp = asChild ? Slot : "button";
44
+
45
+ return (
46
+ <Comp
47
+ type={type ?? "button"}
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export { Button, buttonVariants };
55
+
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const Input = React.forwardRef<
5
+ HTMLInputElement,
6
+ React.ComponentProps<"input">
7
+ >(({ className, type, ...props }, ref) => {
8
+ return (
9
+ <input
10
+ type={type}
11
+ className={cn(
12
+ "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
13
+ className
14
+ )}
15
+ ref={ref}
16
+ {...props}
17
+ />
18
+ );
19
+ });
20
+
21
+ Input.displayName = "Input";
22
+
23
+ export { Input };
24
+
@@ -0,0 +1,79 @@
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ body {
6
+ @apply m-0;
7
+ font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
8
+ -webkit-font-smoothing: antialiased;
9
+ -moz-osx-font-smoothing: grayscale;
10
+ }
11
+
12
+ :root {
13
+ --background: oklch(1 0 0);
14
+ --foreground: oklch(0.141 0.005 285.823);
15
+ --primary: oklch(0.21 0.006 285.885);
16
+ --primary-foreground: oklch(0.985 0 0);
17
+ --secondary: oklch(0.967 0.001 286.375);
18
+ --secondary-foreground: oklch(0.21 0.006 285.885);
19
+ --muted: oklch(0.967 0.001 286.375);
20
+ --muted-foreground: oklch(59.56% 0.038 257.87);
21
+ --accent: oklch(0.967 0.001 286.375);
22
+ --accent-foreground: oklch(0.21 0.006 285.885);
23
+ --destructive: oklch(0.577 0.245 27.325);
24
+ --destructive-foreground: oklch(0.985 0 0);
25
+ --border: oklch(0.92 0.004 286.32);
26
+ --input: oklch(0.92 0.004 286.32);
27
+ --ring: oklch(78.54% 0.124 238.13);
28
+ --radius: 0.625rem;
29
+ }
30
+
31
+ .dark {
32
+ --background: oklch(0.141 0.005 285.823);
33
+ --foreground: oklch(0.985 0 0);
34
+ --primary: oklch(0.985 0 0);
35
+ --primary-foreground: oklch(0.141 0.005 285.823);
36
+ --secondary: oklch(0.274 0.006 286.033);
37
+ --secondary-foreground: oklch(0.985 0 0);
38
+ --muted: oklch(0.274 0.006 286.033);
39
+ --muted-foreground: oklch(0.705 0.015 286.067);
40
+ --accent: oklch(0.274 0.006 286.033);
41
+ --accent-foreground: oklch(0.985 0 0);
42
+ --destructive: oklch(0.396 0.141 25.723);
43
+ --destructive-foreground: oklch(0.637 0.237 25.331);
44
+ --border: oklch(0.274 0.006 286.033);
45
+ --input: oklch(0.274 0.006 286.033);
46
+ --ring: oklch(0.442 0.017 285.786);
47
+ }
48
+
49
+ @theme inline {
50
+ --color-background: var(--background);
51
+ --color-foreground: var(--foreground);
52
+ --color-primary: var(--primary);
53
+ --color-primary-foreground: var(--primary-foreground);
54
+ --color-secondary: var(--secondary);
55
+ --color-secondary-foreground: var(--secondary-foreground);
56
+ --color-muted: var(--muted);
57
+ --color-muted-foreground: var(--muted-foreground);
58
+ --color-accent: var(--accent);
59
+ --color-accent-foreground: var(--accent-foreground);
60
+ --color-destructive: var(--destructive);
61
+ --color-destructive-foreground: var(--destructive-foreground);
62
+ --color-border: var(--border);
63
+ --color-input: var(--input);
64
+ --color-ring: var(--ring);
65
+ --radius-sm: calc(var(--radius) - 4px);
66
+ --radius-md: calc(var(--radius) - 2px);
67
+ --radius-lg: var(--radius);
68
+ --radius-xl: calc(var(--radius) + 4px);
69
+ }
70
+
71
+ @layer base {
72
+ * {
73
+ @apply border-border outline-ring/50;
74
+ }
75
+ body {
76
+ @apply bg-background text-foreground;
77
+ }
78
+ }
79
+
@@ -0,0 +1,13 @@
1
+ import axios from "axios";
2
+
3
+ const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8181";
4
+
5
+ export const client = axios.create({
6
+ baseURL: apiBaseUrl,
7
+ headers: {
8
+ "Content-Type": "application/json",
9
+ },
10
+ });
11
+
12
+ export default client;
13
+
@@ -0,0 +1,46 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+ import en from "../locales/en.json";
4
+
5
+ const getStoredLanguage = (): "en" => {
6
+ try {
7
+ const stored = localStorage.getItem("app-language");
8
+ if (!stored) {
9
+ localStorage.setItem("app-language", "en");
10
+ return "en";
11
+ }
12
+ return "en";
13
+ } catch {
14
+ return "en";
15
+ }
16
+ };
17
+
18
+ if (!i18n.isInitialized) {
19
+ i18n.use(initReactI18next).init({
20
+ resources: {
21
+ en: { translation: en },
22
+ },
23
+ supportedLngs: ["en"],
24
+ lng: getStoredLanguage(),
25
+ fallbackLng: "en",
26
+ interpolation: {
27
+ escapeValue: false,
28
+ },
29
+ });
30
+ }
31
+
32
+ export const getLanguage = () => {
33
+ return i18n.language;
34
+ };
35
+
36
+ export const changeLanguage = (language: "en") => {
37
+ try {
38
+ localStorage.setItem("app-language", language);
39
+ } catch {
40
+ // Fallback if localStorage is not available
41
+ }
42
+ i18n.changeLanguage(language);
43
+ };
44
+
45
+ export default i18n;
46
+
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
@@ -0,0 +1,14 @@
1
+ {
2
+ "login": {
3
+ "title": "Login",
4
+ "email": "Email",
5
+ "emailPlaceholder": "Enter your email",
6
+ "password": "Password",
7
+ "passwordPlaceholder": "Enter your password",
8
+ "submit": "Sign In"
9
+ },
10
+ "notFound": {
11
+ "message": "Page not found"
12
+ }
13
+ }
14
+
@@ -0,0 +1,23 @@
1
+ import ReactDOM from "react-dom/client";
2
+ import { I18nextProvider } from "react-i18next";
3
+ import i18n from "./lib/i18n";
4
+ import { ThemeProvider } from "./components/theme/theme-provider";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+ import App from "./app";
7
+
8
+ const queryClient = new QueryClient();
9
+
10
+ const rootElement = document.getElementById("app");
11
+ if (rootElement && !rootElement.innerHTML) {
12
+ const root = ReactDOM.createRoot(rootElement);
13
+ root.render(
14
+ <I18nextProvider i18n={i18n}>
15
+ <ThemeProvider defaultTheme="light" storageKey="vite-ui-theme">
16
+ <QueryClientProvider client={queryClient}>
17
+ <App />
18
+ </QueryClientProvider>
19
+ </ThemeProvider>
20
+ </I18nextProvider>
21
+ );
22
+ }
23
+
@@ -0,0 +1,13 @@
1
+ import { createContext } from "react";
2
+ import { currentUserDetailsAtom } from "./use-auth-hook";
3
+ import { useAtomValue } from "jotai";
4
+
5
+ export const AuthContext = createContext<any | undefined>(undefined);
6
+
7
+ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
8
+ const authData = useAtomValue(currentUserDetailsAtom);
9
+ return (
10
+ <AuthContext.Provider value={authData}>{children}</AuthContext.Provider>
11
+ );
12
+ };
13
+
@@ -0,0 +1,49 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { Input } from "@/components/ui/input";
3
+ import { useLogin } from "../use-auth-hook";
4
+ import { useState } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ export function LoginForm() {
8
+ const { t } = useTranslation();
9
+ const { mutate, isPending } = useLogin();
10
+ const [email, setEmail] = useState("");
11
+ const [password, setPassword] = useState("");
12
+
13
+ const handleSubmit = (e: React.FormEvent) => {
14
+ e.preventDefault();
15
+ mutate({ email, password });
16
+ };
17
+
18
+ return (
19
+ <div className="flex flex-col gap-6">
20
+ <h1 className="text-2xl font-semibold">{t("login.title")}</h1>
21
+ <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
22
+ <div className="flex flex-col gap-2">
23
+ <label className="text-sm font-medium">{t("login.email")}</label>
24
+ <Input
25
+ type="email"
26
+ value={email}
27
+ onChange={(e) => setEmail(e.target.value)}
28
+ placeholder={t("login.emailPlaceholder")}
29
+ required
30
+ />
31
+ </div>
32
+ <div className="flex flex-col gap-2">
33
+ <label className="text-sm font-medium">{t("login.password")}</label>
34
+ <Input
35
+ type="password"
36
+ value={password}
37
+ onChange={(e) => setPassword(e.target.value)}
38
+ placeholder={t("login.passwordPlaceholder")}
39
+ required
40
+ />
41
+ </div>
42
+ <Button type="submit" disabled={isPending}>
43
+ {t("login.submit")}
44
+ </Button>
45
+ </form>
46
+ </div>
47
+ );
48
+ }
49
+
@@ -0,0 +1,12 @@
1
+ import { LoginForm } from "./login-form";
2
+
3
+ export function LoginPage() {
4
+ return (
5
+ <div className="flex h-screen items-center justify-center bg-background">
6
+ <div className="w-full max-w-md p-8">
7
+ <LoginForm />
8
+ </div>
9
+ </div>
10
+ );
11
+ }
12
+
@@ -0,0 +1,92 @@
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
+
9
+ export const authTokenAtom = atomWithStorage<string>(
10
+ "authTokenAtom",
11
+ "",
12
+ undefined,
13
+ {
14
+ getOnInit: true,
15
+ }
16
+ );
17
+
18
+ export const currentUserDetailsAtom = atom<any>();
19
+
20
+ export const setTokenInAxios = (token?: string | null) => {
21
+ if (token) {
22
+ client.defaults.headers.common["x-auth-token"] = token;
23
+ } else {
24
+ delete client.defaults.headers.common["x-auth-token"];
25
+ }
26
+ };
27
+
28
+ export const setLocaleInAxios = (locale: "en") => {
29
+ client.defaults.headers.common["Accept-Language"] = locale;
30
+ };
31
+
32
+ export const useLogin = () => {
33
+ const setAuthToken = useSetAtom(authTokenAtom);
34
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
35
+ const navigate = useNavigate();
36
+ return useMutation({
37
+ mutationFn: async (data: { email: string; password: string }) => {
38
+ const response = await client.post("/auth/login", data);
39
+ return response.data;
40
+ },
41
+ onSuccess: (authData) => {
42
+ const token = authData?.token || "";
43
+ setAuthToken(token);
44
+ setCurrentUser(authData);
45
+ setTokenInAxios(token);
46
+ navigate({ to: AppRoute.to });
47
+ },
48
+ onError: (err: any) => {
49
+ console.error("Login failed:", err);
50
+ },
51
+ });
52
+ };
53
+
54
+ export const useValidateToken = () => {
55
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
56
+ const setAuthToken = useSetAtom(authTokenAtom);
57
+ const authToken = useAtomValue(authTokenAtom);
58
+ setTokenInAxios(authToken);
59
+ return useMutation({
60
+ mutationFn: async () => {
61
+ const response = await client.get("/auth/validate");
62
+ return response.data;
63
+ },
64
+ retry: false,
65
+ onSuccess: (data) => {
66
+ setCurrentUser(data);
67
+ },
68
+ onError: () => {
69
+ setCurrentUser(undefined);
70
+ setAuthToken("");
71
+ setTokenInAxios();
72
+ },
73
+ });
74
+ };
75
+
76
+ export const useLogout = () => {
77
+ const setAuthToken = useSetAtom(authTokenAtom);
78
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
79
+ const navigate = useNavigate();
80
+ return useMutation({
81
+ mutationFn: async () => {
82
+ await client.post("/auth/logout");
83
+ },
84
+ onSuccess: () => {
85
+ setAuthToken("");
86
+ setCurrentUser(undefined);
87
+ setTokenInAxios();
88
+ navigate({ to: LoginRoute.to });
89
+ },
90
+ });
91
+ };
92
+
@@ -0,0 +1,10 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+
4
+ // This file is auto-generated by TanStack Router
5
+ // It will be overwritten when the router plugin runs
6
+
7
+ import { Route as rootRouteImport } from './routes/__root'
8
+
9
+ export const routeTree = rootRouteImport
10
+
@@ -0,0 +1,11 @@
1
+ import { createRouter } from "@tanstack/react-router";
2
+ import { routeTree } from "./routeTree.gen";
3
+
4
+ export const router = createRouter({
5
+ routeTree,
6
+ context: {
7
+ token: undefined!,
8
+ data: undefined!,
9
+ },
10
+ });
11
+
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ export const Route = createFileRoute("/$")({
5
+ component: NotFound,
6
+ });
7
+
8
+ function NotFound() {
9
+ const { t } = useTranslation();
10
+ return (
11
+ <div className="flex h-screen w-screen items-center justify-center">
12
+ <div className="text-center">
13
+ <h1 className="text-4xl font-bold mb-4">404</h1>
14
+ <p className="text-muted-foreground">{t("notFound.message")}</p>
15
+ </div>
16
+ </div>
17
+ );
18
+ }
19
+
@@ -0,0 +1,15 @@
1
+ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
2
+
3
+ interface RouterContext {
4
+ token: string;
5
+ data: any;
6
+ }
7
+
8
+ export const Route = createRootRouteWithContext<RouterContext>()({
9
+ component: App,
10
+ });
11
+
12
+ function App() {
13
+ return <Outlet />;
14
+ }
15
+
@@ -0,0 +1,15 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ export const Route = createFileRoute("/app/")({
4
+ component: AppIndex,
5
+ });
6
+
7
+ function AppIndex() {
8
+ 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>
12
+ </div>
13
+ );
14
+ }
15
+
@@ -0,0 +1,18 @@
1
+ import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
2
+ import { Route as LoginRoute } from "../auth/login";
3
+
4
+ export const Route = createFileRoute("/app")({
5
+ beforeLoad: ({ context }) => {
6
+ if (!context.token) {
7
+ throw redirect({
8
+ to: LoginRoute.to,
9
+ });
10
+ }
11
+ },
12
+ component: App,
13
+ });
14
+
15
+ function App() {
16
+ return <Outlet />;
17
+ }
18
+
@@ -0,0 +1,11 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { LoginPage } from "@/modules/auth/login/login-page";
3
+
4
+ export const Route = createFileRoute("/auth/login")({
5
+ component: App,
6
+ });
7
+
8
+ function App() {
9
+ return <LoginPage />;
10
+ }
11
+
@@ -0,0 +1,18 @@
1
+ import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
2
+ import { Route as AppRoute } from "../app";
3
+
4
+ export const Route = createFileRoute("/auth")({
5
+ component: RouteComponent,
6
+ beforeLoad: ({ context }) => {
7
+ if (context.token) {
8
+ throw redirect({
9
+ to: AppRoute.to,
10
+ });
11
+ }
12
+ },
13
+ });
14
+
15
+ function RouteComponent() {
16
+ return <Outlet />;
17
+ }
18
+
@@ -0,0 +1,12 @@
1
+ import { createFileRoute, redirect } from "@tanstack/react-router";
2
+ import { Route as AppRoute } from "./app";
3
+
4
+ export const Route = createFileRoute("/")({
5
+ beforeLoad: () => {
6
+ throw redirect({
7
+ to: AppRoute.to,
8
+ replace: true,
9
+ });
10
+ },
11
+ });
12
+
@@ -0,0 +1,8 @@
1
+ import "i18next";
2
+
3
+ declare module "i18next" {
4
+ interface CustomTypeOptions {
5
+ returnNull: false;
6
+ }
7
+ }
8
+
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_BASE_URL: string;
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv;
9
+ }
10
+
@@ -0,0 +1,9 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ };
9
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ }
7
+ }
8
+ }
9
+
@@ -0,0 +1,29 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "jsx": "react-jsx",
6
+ "module": "ESNext",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "types": ["vite/client", "./types"],
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": false,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "skipLibCheck": true,
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "baseUrl": ".",
24
+ "paths": {
25
+ "@/*": ["./src/*"]
26
+ }
27
+ }
28
+ }
29
+
@@ -0,0 +1,3 @@
1
+ // Global type definitions
2
+ // Add your custom type definitions here
3
+
@@ -0,0 +1,25 @@
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
+
7
+ export default defineConfig({
8
+ plugins: [
9
+ tanstackRouter({
10
+ target: "react",
11
+ autoCodeSplitting: true,
12
+ spa: {
13
+ enabled: true,
14
+ },
15
+ }),
16
+ viteReact(),
17
+ tailwindcss(),
18
+ ],
19
+ resolve: {
20
+ alias: {
21
+ "@": resolve(__dirname, "./src"),
22
+ },
23
+ },
24
+ });
25
+
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+ import Generator from 'yeoman-generator';
3
+ import _ from 'lodash';
4
+ import constants from './constants.js';
5
+ import chalk from 'chalk';
6
+ const log = console.log;
7
+
8
+ export default class BaseGenerator extends Generator {
9
+ constructor(args, opts) {
10
+ super(args, opts);
11
+
12
+ this.option('appName', { type: String, required: true });
13
+
14
+ this.configData = {
15
+ appNameTitleCase: _.startCase(this.options['appName']).replace(/ /g, ''),
16
+ appNameSlug: _.kebabCase(this.options['appName']),
17
+ };
18
+
19
+ this.options.force = true;
20
+ log('this.options)', this.options);
21
+ Object.assign(this.configData, this.config.getAll(), constants, this.options);
22
+ }
23
+
24
+ logSuccess(msg) {
25
+ log(chalk.green(msg));
26
+ }
27
+
28
+ logWarn(msg) {
29
+ log(chalk.yellow(msg));
30
+ }
31
+
32
+ logError(msg) {
33
+ log(chalk.red(msg));
34
+ }
35
+
36
+ logEmphasis(msg) {
37
+ log(chalk.cyan(msg));
38
+ }
39
+
40
+ _destinationPrefix() {
41
+ return this.options['appName'];
42
+ }
43
+
44
+ _writeFileWithPaths(srcFilePath, destinationFilePath) {
45
+ this.fs.copyTpl(this.templatePath(srcFilePath), this.destinationPath(this._destinationPrefix(), destinationFilePath), { ...this.configData });
46
+ }
47
+
48
+ _writeFile(fileName) {
49
+ this._writeFileWithPaths(fileName, fileName);
50
+ }
51
+
52
+ _writeFiles(files, src, dest) {
53
+ files.forEach((fileName) => {
54
+ this._writeFileWithPaths(`${src}/${fileName}`, `${dest}/${fileName}`);
55
+ });
56
+ }
57
+
58
+ _writeDir(dirName) {
59
+ this.fs.copy(this.templatePath(dirName), this.destinationPath(this._destinationPrefix(), dirName));
60
+ }
61
+ }
62
+
@@ -0,0 +1,20 @@
1
+ const constants = {
2
+ REACT_VERSION: '^19.2.0',
3
+ REACT_DOM_VERSION: '^19.2.0',
4
+ VITE_VERSION: '^7.2.4',
5
+ TYPESCRIPT_VERSION: '^5.9.3',
6
+ TANSTACK_ROUTER_VERSION: '^1.139.3',
7
+ TANSTACK_ROUTER_PLUGIN_VERSION: '^1.139.3',
8
+ TANSTACK_REACT_QUERY_VERSION: '^5.90.10',
9
+ I18NEXT_VERSION: '^25.6.3',
10
+ REACT_I18NEXT_VERSION: '^16.3.5',
11
+ AXIOS_VERSION: '^1.13.2',
12
+ JOTAI_VERSION: '^2.15.1',
13
+ TAILWIND_VERSION: '^4.1.17',
14
+ RADIX_UI_VERSION: '^1.4.3',
15
+ DOTENV_VERSION: '^17.2.3',
16
+ CROSS_ENV_VERSION: '^10.1.0',
17
+ };
18
+
19
+ export default constants;
20
+
package/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import Generator from 'yeoman-generator';
2
+
3
+ export default class extends Generator {
4
+ constructor(args, opts) {
5
+ super(args, opts);
6
+
7
+ this.option('appName', {
8
+ type: String,
9
+ required: false,
10
+ description: 'Application name',
11
+ });
12
+ }
13
+
14
+ async prompting() {
15
+ const prompts = [
16
+ {
17
+ type: 'input',
18
+ name: 'appName',
19
+ message: 'What is your application name?',
20
+ default: 'myapp',
21
+ when: !this.options.appName,
22
+ },
23
+ ];
24
+
25
+ const answers = await this.prompt(prompts);
26
+ this.options = { ...this.options, ...answers };
27
+ }
28
+
29
+ writing() {
30
+ // Import and use the app generator
31
+ const AppGenerator = require('./generators/app/index.js').default;
32
+ const appGen = new AppGenerator(this.args, this.options);
33
+ appGen.writing();
34
+ }
35
+ }
36
+
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "generator-kodly-react-app",
3
+ "version": "1.0.0",
4
+ "description": "A Yeoman generator for creating React.js applications with Vite, TanStack Router, authentication, and i18n",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "generators",
9
+ "index.js"
10
+ ],
11
+ "keywords": [
12
+ "yeoman-generator",
13
+ "react",
14
+ "vite",
15
+ "typescript",
16
+ "tanstack-router",
17
+ "scaffolding",
18
+ "generator"
19
+ ],
20
+ "scripts": {
21
+ "test": "echo \"Error: no test specified\" && exit 1"
22
+ },
23
+ "author": "The Panther <info@thepanther.io>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git@github.com:thepanther-io/kodly-react-yo-generator.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/thepanther-io/kodly-react-yo-generator/issues"
31
+ },
32
+ "homepage": "https://github.com/thepanther-io/kodly-react-yo-generator#readme",
33
+ "engines": {
34
+ "node": ">=14.0.0"
35
+ },
36
+ "dependencies": {
37
+ "yeoman-generator": "^7.5.1",
38
+ "lodash": "^4.17.21",
39
+ "mem-fs": "^4.1.2",
40
+ "mem-fs-editor": "^11.1.4"
41
+ },
42
+ "devDependencies": {
43
+ "chalk": "^5.4.1"
44
+ }
45
+ }