generator-kodly-react-app 1.0.15 → 1.0.16

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 (90) hide show
  1. package/generators/app/index.js +4 -0
  2. package/generators/app/templates/I18N_MIGRATION.md +13 -13
  3. package/generators/app/templates/STRUCTURE.md +26 -26
  4. package/generators/app/templates/index.html +6 -14
  5. package/generators/app/templates/openapi-ts.config.ts +12 -12
  6. package/generators/app/templates/postcss.config.js +0 -1
  7. package/generators/app/templates/src/app.tsx +20 -20
  8. package/generators/app/templates/src/components/layout/language-switcher.tsx +20 -16
  9. package/generators/app/templates/src/components/layout/page-layout.tsx +31 -0
  10. package/generators/app/templates/src/components/layout/theme-switcher.tsx +15 -14
  11. package/generators/app/templates/src/components/theme/theme-provider.tsx +20 -13
  12. package/generators/app/templates/src/components/ui/button.tsx +15 -14
  13. package/generators/app/templates/src/components/ui/card.tsx +23 -16
  14. package/generators/app/templates/src/components/ui/field.tsx +35 -35
  15. package/generators/app/templates/src/components/ui/form.tsx +66 -30
  16. package/generators/app/templates/src/components/ui/input-group.tsx +24 -24
  17. package/generators/app/templates/src/components/ui/input.tsx +8 -8
  18. package/generators/app/templates/src/components/ui/label.tsx +9 -9
  19. package/generators/app/templates/src/components/ui/select.tsx +0 -1
  20. package/generators/app/templates/src/components/ui/separator.tsx +10 -10
  21. package/generators/app/templates/src/components/ui/textarea.tsx +7 -7
  22. package/generators/app/templates/src/index.css +12 -2
  23. package/generators/app/templates/src/lib/i18n.ts +16 -14
  24. package/generators/app/templates/src/lib/routes.ts +2 -3
  25. package/generators/app/templates/src/lib/utils/error-handler.ts +21 -14
  26. package/generators/app/templates/src/lib/utils.ts +3 -3
  27. package/generators/app/templates/src/locales/index.ts +12 -12
  28. package/generators/app/templates/src/locales/ja/theme.json +0 -1
  29. package/generators/app/templates/src/main.tsx +10 -12
  30. package/generators/app/templates/src/modules/app/layouts/app-layout.tsx +14 -25
  31. package/generators/app/templates/src/modules/app/locales/app-en.json +0 -1
  32. package/generators/app/templates/src/modules/app/locales/app-ja.json +0 -1
  33. package/generators/app/templates/src/modules/auth/components/auth-form.tsx +83 -0
  34. package/generators/app/templates/src/modules/auth/components/forgot-password-form.tsx +27 -52
  35. package/generators/app/templates/src/modules/auth/components/login-form.tsx +42 -72
  36. package/generators/app/templates/src/modules/auth/components/login-link.tsx +12 -0
  37. package/generators/app/templates/src/modules/auth/components/reset-password-form.tsx +46 -92
  38. package/generators/app/templates/src/modules/auth/components/signup-form.tsx +35 -71
  39. package/generators/app/templates/src/modules/auth/contexts/auth-context.tsx +10 -6
  40. package/generators/app/templates/src/modules/auth/hooks/use-auth-hook.ts +51 -36
  41. package/generators/app/templates/src/modules/auth/layouts/auth-layout.tsx +12 -15
  42. package/generators/app/templates/src/modules/auth/locales/auth-en.json +0 -1
  43. package/generators/app/templates/src/modules/auth/locales/auth-ja.json +0 -1
  44. package/generators/app/templates/src/modules/auth/schemas/form-schemas.ts +14 -10
  45. package/generators/app/templates/src/modules/landing/components/auth-hero.tsx +20 -18
  46. package/generators/app/templates/src/modules/landing/components/welcome-hero.tsx +13 -11
  47. package/generators/app/templates/src/modules/landing/landing-page-layout.tsx +1 -24
  48. package/generators/app/templates/src/modules/landing/landing-page.tsx +11 -6
  49. package/generators/app/templates/src/modules/landing/layouts/landing-page-layout.tsx +2 -21
  50. package/generators/app/templates/src/modules/landing/locales/landing-en.json +0 -1
  51. package/generators/app/templates/src/openapi-client-config.ts +1 -1
  52. package/generators/app/templates/src/routeTree.gen.ts +188 -188
  53. package/generators/app/templates/src/router.tsx +2 -2
  54. package/generators/app/templates/src/routes/$.tsx +0 -1
  55. package/generators/app/templates/src/routes/__root.tsx +2 -2
  56. package/generators/app/templates/src/routes/_landing/index.tsx +3 -3
  57. package/generators/app/templates/src/routes/_landing/route.tsx +3 -3
  58. package/generators/app/templates/src/routes/app/index.tsx +6 -6
  59. package/generators/app/templates/src/routes/app/route.tsx +4 -4
  60. package/generators/app/templates/src/routes/auth/forgot-password.tsx +1 -1
  61. package/generators/app/templates/src/routes/auth/login.tsx +3 -3
  62. package/generators/app/templates/src/routes/auth/reset-password.tsx +3 -3
  63. package/generators/app/templates/src/routes/auth/route.tsx +12 -7
  64. package/generators/app/templates/src/routes/auth/signup.tsx +1 -2
  65. package/generators/app/templates/src/sdk/@tanstack/react-query.gen.ts +157 -76
  66. package/generators/app/templates/src/sdk/client/client.gen.ts +35 -37
  67. package/generators/app/templates/src/sdk/client/index.ts +8 -8
  68. package/generators/app/templates/src/sdk/client/types.gen.ts +22 -19
  69. package/generators/app/templates/src/sdk/client/utils.gen.ts +53 -45
  70. package/generators/app/templates/src/sdk/client.gen.ts +18 -5
  71. package/generators/app/templates/src/sdk/core/auth.gen.ts +6 -6
  72. package/generators/app/templates/src/sdk/core/bodySerializer.gen.ts +25 -27
  73. package/generators/app/templates/src/sdk/core/params.gen.ts +12 -12
  74. package/generators/app/templates/src/sdk/core/pathSerializer.gen.ts +40 -41
  75. package/generators/app/templates/src/sdk/core/queryKeySerializer.gen.ts +12 -29
  76. package/generators/app/templates/src/sdk/core/serverSentEvents.gen.ts +26 -24
  77. package/generators/app/templates/src/sdk/core/types.gen.ts +17 -15
  78. package/generators/app/templates/src/sdk/core/utils.gen.ts +18 -18
  79. package/generators/app/templates/src/sdk/index.ts +40 -2
  80. package/generators/app/templates/src/sdk/schemas.gen.ts +108 -166
  81. package/generators/app/templates/src/sdk/sdk.gen.ts +145 -63
  82. package/generators/app/templates/src/sdk/types.gen.ts +90 -84
  83. package/generators/app/templates/src/sdk/zod.gen.ts +57 -74
  84. package/generators/app/templates/src/types/i18n.d.ts +0 -1
  85. package/generators/app/templates/tailwind.config.js +0 -1
  86. package/generators/app/templates/tsconfig.app.json +0 -1
  87. package/generators/app/templates/tsconfig.json +0 -1
  88. package/generators/app/templates/tsr.config.json +4 -0
  89. package/generators/app/templates/vite.config.js +12 -12
  90. package/package.json +2 -4
@@ -24,6 +24,7 @@ export default class ReactAppGenerator extends BaseGenerator {
24
24
  'vite.config.js',
25
25
  'tsconfig.json',
26
26
  'tsconfig.app.json',
27
+ 'tsr.config.json',
27
28
  'tailwind.config.js',
28
29
  'postcss.config.js',
29
30
  'index.html',
@@ -70,6 +71,8 @@ export default class ReactAppGenerator extends BaseGenerator {
70
71
  _writeAuthFiles() {
71
72
  const authFiles = [
72
73
  'src/modules/auth/components/signup-form.tsx',
74
+ 'src/modules/auth/components/auth-form.tsx',
75
+ 'src/modules/auth/components/login-link.tsx',
73
76
  'src/modules/auth/components/forgot-password-form.tsx',
74
77
  'src/modules/auth/components/reset-password-form.tsx',
75
78
  'src/modules/auth/components/login-form.tsx',
@@ -86,6 +89,7 @@ export default class ReactAppGenerator extends BaseGenerator {
86
89
  _writeComponentFiles() {
87
90
  const componentFiles = [
88
91
  'src/components/layout/language-switcher.tsx',
92
+ 'src/components/layout/page-layout.tsx',
89
93
  'src/components/layout/theme-switcher.tsx',
90
94
  'src/components/theme/theme-provider.tsx',
91
95
  'src/components/ui/button.tsx',
@@ -6,10 +6,10 @@ This document describes the i18n configuration changes that enable **both** flat
6
6
 
7
7
  The i18n setup now supports two usage patterns interchangeably:
8
8
 
9
- | Pattern | Example | Use case |
10
- |---------|---------|----------|
11
- | **Flat (no namespace)** | `useTranslation()` + `t('namespace.section.key')` | Quick access, explicit full path |
12
- | **Namespaced** | `useTranslation('namespace')` + `t('section.key')` | Modular components, shorter keys |
9
+ | Pattern | Example | Use case |
10
+ | ----------------------- | -------------------------------------------------- | -------------------------------- |
11
+ | **Flat (no namespace)** | `useTranslation()` + `t('namespace.section.key')` | Quick access, explicit full path |
12
+ | **Namespaced** | `useTranslation('namespace')` + `t('section.key')` | Modular components, shorter keys |
13
13
 
14
14
  Previously, only the flat pattern worked because all translations lived under a single `translation` namespace. The namespaced pattern failed because i18next couldn't find the individual namespaces (the top-level keys in your merged translation object).
15
15
 
@@ -24,8 +24,8 @@ if (!i18n.isInitialized) {
24
24
  const lng = getStoredLanguage();
25
25
  i18n.use(initReactI18next).init({
26
26
  resources: {
27
- en: { translation: mergeTranslations('en') },
28
- ja: { translation: mergeTranslations('ja') },
27
+ en: { translation: mergeTranslations("en") },
28
+ ja: { translation: mergeTranslations("ja") },
29
29
  },
30
30
  supportedLngs: supportedLanguages,
31
31
  lng,
@@ -43,8 +43,8 @@ if (!i18n.isInitialized) {
43
43
  ```typescript
44
44
  if (!i18n.isInitialized) {
45
45
  const lng = getStoredLanguage();
46
- const enResources = mergeTranslations('en');
47
- const jaResources = mergeTranslations('ja');
46
+ const enResources = mergeTranslations("en");
47
+ const jaResources = mergeTranslations("ja");
48
48
  i18n.use(initReactI18next).init({
49
49
  resources: {
50
50
  en: {
@@ -56,8 +56,8 @@ if (!i18n.isInitialized) {
56
56
  ...jaResources,
57
57
  },
58
58
  },
59
- defaultNS: 'translation',
60
- ns: ['translation', ...Object.keys(enResources)],
59
+ defaultNS: "translation",
60
+ ns: ["translation", ...Object.keys(enResources)],
61
61
  supportedLngs: supportedLanguages,
62
62
  lng,
63
63
  fallbackLng: defaultLanguage,
@@ -103,11 +103,11 @@ You can migrate components to the namespaced pattern for shorter keys:
103
103
  ```tsx
104
104
  // Before (flat)
105
105
  const { t } = useTranslation();
106
- return <h1>{t('namespace.section.title')}</h1>;
106
+ return <h1>{t("namespace.section.title")}</h1>;
107
107
 
108
108
  // After (namespaced) – optional
109
- const { t } = useTranslation('namespace');
110
- return <h1>{t('section.title')}</h1>;
109
+ const { t } = useTranslation("namespace");
110
+ return <h1>{t("section.title")}</h1>;
111
111
  ```
112
112
 
113
113
  Both forms work. Choose what fits your component structure.
@@ -279,10 +279,10 @@ routes/
279
279
 
280
280
  ```tsx
281
281
  // src/routes/{module}/route.tsx (Parent route)
282
- import { createFileRoute, Outlet } from '@tanstack/react-router';
283
- import { ModuleLayout } from '@/modules/{module}/layouts/{module}-layout';
282
+ import { createFileRoute, Outlet } from "@tanstack/react-router";
283
+ import { ModuleLayout } from "@/modules/{module}/layouts/{module}-layout";
284
284
 
285
- export const Route = createFileRoute('/{module}')({
285
+ export const Route = createFileRoute("/{module}")({
286
286
  beforeLoad: ({ context }) => {
287
287
  // Guards, redirects, etc.
288
288
  },
@@ -300,10 +300,10 @@ function RouteComponent() {
300
300
 
301
301
  ```tsx
302
302
  // src/routes/{module}/{page}.tsx (Child route)
303
- import { createFileRoute } from '@tanstack/react-router';
304
- import { PageComponent } from '@/modules/{module}/components/{page}-component';
303
+ import { createFileRoute } from "@tanstack/react-router";
304
+ import { PageComponent } from "@/modules/{module}/components/{page}-component";
305
305
 
306
- export const Route = createFileRoute('/{module}/{page}')({
306
+ export const Route = createFileRoute("/{module}/{page}")({
307
307
  component: RouteComponent,
308
308
  });
309
309
 
@@ -363,7 +363,7 @@ Translations are merged in `src/locales/index.ts`:
363
363
 
364
364
  ```typescript
365
365
  export const mergeTranslations = (lang: SupportedLanguages) => {
366
- if (lang === 'en') {
366
+ if (lang === "en") {
367
367
  return {
368
368
  auth: { ...enAuth },
369
369
  app: { ...enApp },
@@ -379,15 +379,15 @@ export const mergeTranslations = (lang: SupportedLanguages) => {
379
379
  ### Using Translations in Components
380
380
 
381
381
  ```tsx
382
- import { useTranslation } from 'react-i18next';
382
+ import { useTranslation } from "react-i18next";
383
383
 
384
384
  function MyComponent() {
385
385
  const { t } = useTranslation();
386
386
 
387
387
  return (
388
388
  <div>
389
- <h1>{t('auth.login.title')}</h1>
390
- <p>{t('common.welcome')}</p>
389
+ <h1>{t("auth.login.title")}</h1>
390
+ <p>{t("common.welcome")}</p>
391
391
  </div>
392
392
  );
393
393
  }
@@ -447,11 +447,11 @@ src/modules/dashboard/
447
447
  Add module translations to `src/locales/index.ts`:
448
448
 
449
449
  ```typescript
450
- import enDashboard from '@/modules/dashboard/locales/dashboard-en.json';
451
- import jaDashboard from '@/modules/dashboard/locales/dashboard-ja.json';
450
+ import enDashboard from "@/modules/dashboard/locales/dashboard-en.json";
451
+ import jaDashboard from "@/modules/dashboard/locales/dashboard-ja.json";
452
452
 
453
453
  export const mergeTranslations = (lang: SupportedLanguages) => {
454
- if (lang === 'en') {
454
+ if (lang === "en") {
455
455
  return {
456
456
  // ... existing modules
457
457
  dashboard: { ...enDashboard },
@@ -472,7 +472,7 @@ export const mergeTranslations = (lang: SupportedLanguages) => {
472
472
  ```tsx
473
473
  export function DashboardLayout({ children }: { children: React.ReactNode }) {
474
474
  return (
475
- <div className='dashboard-container'>
475
+ <div className="dashboard-container">
476
476
  {/* Dashboard-specific layout */}
477
477
  {children}
478
478
  </div>
@@ -485,10 +485,10 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
485
485
  **`src/routes/dashboard/route.tsx`**:
486
486
 
487
487
  ```tsx
488
- import { createFileRoute, Outlet } from '@tanstack/react-router';
489
- import { DashboardLayout } from '@/modules/dashboard/layouts/dashboard-layout';
488
+ import { createFileRoute, Outlet } from "@tanstack/react-router";
489
+ import { DashboardLayout } from "@/modules/dashboard/layouts/dashboard-layout";
490
490
 
491
- export const Route = createFileRoute('/dashboard')({
491
+ export const Route = createFileRoute("/dashboard")({
492
492
  component: RouteComponent,
493
493
  });
494
494
 
@@ -504,10 +504,10 @@ function RouteComponent() {
504
504
  **`src/routes/dashboard/index.tsx`**:
505
505
 
506
506
  ```tsx
507
- import { createFileRoute } from '@tanstack/react-router';
508
- import { DashboardPage } from '@/modules/dashboard/components/dashboard-page';
507
+ import { createFileRoute } from "@tanstack/react-router";
508
+ import { DashboardPage } from "@/modules/dashboard/components/dashboard-page";
509
509
 
510
- export const Route = createFileRoute('/dashboard/')({
510
+ export const Route = createFileRoute("/dashboard/")({
511
511
  component: RouteComponent,
512
512
  });
513
513
 
@@ -564,7 +564,7 @@ _writeDashboardFiles() {
564
564
 
565
565
  ```tsx
566
566
  // src/modules/{module}/components/{component-name}.tsx
567
- import { useTranslation } from 'react-i18next';
567
+ import { useTranslation } from "react-i18next";
568
568
 
569
569
  export function ComponentName() {
570
570
  const { t } = useTranslation();
@@ -577,7 +577,7 @@ export function ComponentName() {
577
577
 
578
578
  ```tsx
579
579
  // src/modules/{module}/hooks/use-{hook-name}.ts
580
- import { useAtom } from 'jotai';
580
+ import { useAtom } from "jotai";
581
581
 
582
582
  export function useHookName() {
583
583
  // Hook logic
@@ -593,7 +593,7 @@ export function useHookName() {
593
593
  // src/modules/{module}/layouts/{module}-layout.tsx
594
594
  export function ModuleLayout({ children }: { children: React.ReactNode }) {
595
595
  return (
596
- <div className='module-container'>
596
+ <div className="module-container">
597
597
  {/* Layout structure */}
598
598
  {children}
599
599
  </div>
@@ -605,10 +605,10 @@ export function ModuleLayout({ children }: { children: React.ReactNode }) {
605
605
 
606
606
  ```tsx
607
607
  // src/routes/{module}/{page}.tsx
608
- import { createFileRoute } from '@tanstack/react-router';
609
- import { ComponentName } from '@/modules/{module}/components/component-name';
608
+ import { createFileRoute } from "@tanstack/react-router";
609
+ import { ComponentName } from "@/modules/{module}/components/component-name";
610
610
 
611
- export const Route = createFileRoute('/{module}/{page}')({
611
+ export const Route = createFileRoute("/{module}/{page}")({
612
612
  component: RouteComponent,
613
613
  });
614
614
 
@@ -4,23 +4,15 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta
6
6
  name="viewport"
7
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
8
- <meta
9
- name="theme-color"
10
- content="#000000" />
11
- <meta
12
- name="description"
13
- content="<%= appNameTitleCase %>" />
14
- <link
15
- rel="icon"
16
- type="image/svg+xml"
17
- href="/favicon.svg" />
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 %>" />
11
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
18
12
  <title><%= appNameTitleCase %></title>
19
13
  </head>
20
14
  <body>
21
15
  <div id="app"></div>
22
- <script
23
- type="module"
24
- src="/src/main.tsx"></script>
16
+ <script type="module" src="/src/main.tsx"></script>
25
17
  </body>
26
18
  </html>
@@ -1,29 +1,29 @@
1
- import { defineConfig } from '@hey-api/openapi-ts';
2
- import { loadEnv } from 'vite';
1
+ import { defineConfig } from "@hey-api/openapi-ts";
2
+ import { loadEnv } from "vite";
3
3
 
4
4
  // Get the mode from environment variable (set by pnpm scripts)
5
- const mode = process.env.MODE || 'development';
5
+ const mode = process.env.MODE || "development";
6
6
 
7
7
  // Load environment variables from .env files
8
- const env = loadEnv(mode, process.cwd(), '');
8
+ const env = loadEnv(mode, process.cwd(), "");
9
9
 
10
10
  export default defineConfig({
11
11
  input: `${env.VITE_API_BASE_URL}/v3/api-docs`,
12
- output: 'src/sdk',
12
+ output: "src/sdk",
13
13
  plugins: [
14
14
  {
15
- name: '@hey-api/client-axios',
16
- runtimeConfigPath: '@/openapi-client-config.ts',
15
+ name: "@hey-api/client-axios",
16
+ runtimeConfigPath: "@/openapi-client-config.ts",
17
17
  },
18
- '@tanstack/react-query',
19
- 'zod',
18
+ "@tanstack/react-query",
19
+ "zod",
20
20
  {
21
- name: '@hey-api/sdk',
21
+ name: "@hey-api/sdk",
22
22
  validator: true,
23
23
  },
24
24
  {
25
- name: '@hey-api/schemas',
26
- type: 'json',
25
+ name: "@hey-api/schemas",
26
+ type: "json",
27
27
  },
28
28
  ],
29
29
  });
@@ -3,4 +3,3 @@ export default {
3
3
  "@tailwindcss/postcss": {},
4
4
  },
5
5
  };
6
-
@@ -1,15 +1,20 @@
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/contexts/auth-context';
8
- import { authTokenAtom, currentUserDetailsAtom, useValidateToken, setLocaleInClient } from '@/modules/auth/hooks/use-auth-hook';
9
- import { router } from '@/router';
10
- import { getLanguage, SupportedLanguages } 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/contexts/auth-context";
8
+ import {
9
+ authTokenAtom,
10
+ currentUserDetailsAtom,
11
+ useValidateToken,
12
+ setLocaleInClient,
13
+ } from "@/modules/auth/hooks/use-auth-hook";
14
+ import { router } from "@/router";
15
+ import { getLanguage, SupportedLanguages } from "@/lib/i18n";
11
16
 
12
- declare module '@tanstack/react-router' {
17
+ declare module "@tanstack/react-router" {
13
18
  interface Register {
14
19
  router: typeof router;
15
20
  }
@@ -18,12 +23,7 @@ declare module '@tanstack/react-router' {
18
23
  function InnerApp() {
19
24
  const token = useAtomValue(authTokenAtom);
20
25
  const data = useAtomValue(currentUserDetailsAtom);
21
- return (
22
- <RouterProvider
23
- router={router}
24
- context={{ token, data }}
25
- />
26
- );
26
+ return <RouterProvider router={router} context={{ token, data }} />;
27
27
  }
28
28
 
29
29
  export default function App() {
@@ -44,14 +44,14 @@ export default function App() {
44
44
  if (!token || (!isPending && (isError || isSuccess))) {
45
45
  return (
46
46
  <AuthProvider>
47
- <Toaster position='top-right' />
47
+ <Toaster position="top-right" />
48
48
  <InnerApp />
49
49
  </AuthProvider>
50
50
  );
51
51
  }
52
52
  return (
53
- <div className='flex h-screen w-screen items-center justify-center'>
54
- <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" />
55
55
  </div>
56
56
  );
57
57
  }
@@ -1,39 +1,43 @@
1
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
2
- import { getLanguage, changeLanguage } from '@/lib/i18n';
3
- import { useTranslation } from 'react-i18next';
4
- import { useEffect, useState } from 'react';
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "@/components/ui/select";
8
+ import { getLanguage, changeLanguage } from "@/lib/i18n";
9
+ import { useTranslation } from "react-i18next";
10
+ import { useEffect, useState } from "react";
5
11
 
6
12
  export function LanguageSwitcher() {
7
13
  const { i18n } = useTranslation();
8
- const [currentLang, setCurrentLang] = useState<'en' | 'ja'>(() => {
9
- return (getLanguage() as 'en' | 'ja') || 'en';
14
+ const [currentLang, setCurrentLang] = useState<"en" | "ja">(() => {
15
+ return (getLanguage() as "en" | "ja") || "en";
10
16
  });
11
17
 
12
18
  useEffect(() => {
13
19
  const handleLanguageChange = (lng: string) => {
14
- setCurrentLang(lng as 'en' | 'ja');
20
+ setCurrentLang(lng as "en" | "ja");
15
21
  };
16
22
 
17
- i18n.on('languageChanged', handleLanguageChange);
23
+ i18n.on("languageChanged", handleLanguageChange);
18
24
  return () => {
19
- i18n.off('languageChanged', handleLanguageChange);
25
+ i18n.off("languageChanged", handleLanguageChange);
20
26
  };
21
27
  }, [i18n]);
22
28
 
23
29
  const handleLanguageChange = (value: string) => {
24
- changeLanguage(value as 'en' | 'ja');
30
+ changeLanguage(value as "en" | "ja");
25
31
  };
26
32
 
27
33
  return (
28
- <Select
29
- value={currentLang}
30
- onValueChange={handleLanguageChange}>
31
- <SelectTrigger className='min-w-[120px]'>
34
+ <Select value={currentLang} onValueChange={handleLanguageChange}>
35
+ <SelectTrigger className="min-w-[120px]">
32
36
  <SelectValue />
33
37
  </SelectTrigger>
34
38
  <SelectContent>
35
- <SelectItem value='en'>English</SelectItem>
36
- <SelectItem value='ja'>日本語</SelectItem>
39
+ <SelectItem value="en">English</SelectItem>
40
+ <SelectItem value="ja">日本語</SelectItem>
37
41
  </SelectContent>
38
42
  </Select>
39
43
  );
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import { LanguageSwitcher } from "./language-switcher";
4
+ import { ThemeSwitcher } from "./theme-switcher";
5
+
6
+ export function PageLayout({
7
+ children,
8
+ actions,
9
+ mainClassName,
10
+ }: {
11
+ children: ReactNode;
12
+ actions?: ReactNode;
13
+ mainClassName?: string;
14
+ }) {
15
+ const { t } = useTranslation();
16
+ return (
17
+ <div className="min-h-screen bg-background">
18
+ <header className="border-b">
19
+ <div className="container mx-auto flex h-16 items-center justify-between px-4">
20
+ <h1 className="text-xl font-semibold">{t("app.title")}</h1>
21
+ <div className="flex items-center gap-4">
22
+ <ThemeSwitcher />
23
+ <LanguageSwitcher />
24
+ {actions}
25
+ </div>
26
+ </div>
27
+ </header>
28
+ <main className={mainClassName}>{children}</main>
29
+ </div>
30
+ );
31
+ }
@@ -1,12 +1,12 @@
1
- import { Button } from '@/components/ui/button';
2
- import { ThemeTypes, useTheme } from '@/components/theme/theme-provider';
3
- import { Moon, Sun, Monitor } from 'lucide-react';
1
+ import { Button } from "@/components/ui/button";
2
+ import { ThemeTypes, useTheme } from "@/components/theme/theme-provider";
3
+ import { Moon, Sun, Monitor } from "lucide-react";
4
4
 
5
5
  export function ThemeSwitcher() {
6
6
  const { theme, setTheme } = useTheme();
7
7
 
8
8
  const cycleTheme = () => {
9
- const themes: ThemeTypes[] = ['light', 'dark', 'system'];
9
+ const themes: ThemeTypes[] = ["light", "dark", "system"];
10
10
  const currentIndex = themes.indexOf(theme);
11
11
  const nextIndex = (currentIndex + 1) % themes.length;
12
12
  setTheme(themes[nextIndex]);
@@ -14,23 +14,24 @@ export function ThemeSwitcher() {
14
14
 
15
15
  const getIcon = () => {
16
16
  switch (theme) {
17
- case 'light':
18
- return <Sun className='h-4 w-4' />;
19
- case 'dark':
20
- return <Moon className='h-4 w-4' />;
21
- case 'system':
22
- return <Monitor className='h-4 w-4' />;
17
+ case "light":
18
+ return <Sun className="h-4 w-4" />;
19
+ case "dark":
20
+ return <Moon className="h-4 w-4" />;
21
+ case "system":
22
+ return <Monitor className="h-4 w-4" />;
23
23
  default:
24
- return <Monitor className='h-4 w-4' />;
24
+ return <Monitor className="h-4 w-4" />;
25
25
  }
26
26
  };
27
27
 
28
28
  return (
29
29
  <Button
30
- variant='outline'
31
- size='icon'
30
+ variant="outline"
31
+ size="icon"
32
32
  onClick={cycleTheme}
33
- aria-label='Toggle theme'>
33
+ aria-label="Toggle theme"
34
+ >
34
35
  {getIcon()}
35
36
  </Button>
36
37
  );
@@ -1,6 +1,6 @@
1
- import { createContext, useContext, useEffect, useState } from 'react';
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
2
 
3
- export type ThemeTypes = 'dark' | 'light' | 'system';
3
+ export type ThemeTypes = "dark" | "light" | "system";
4
4
 
5
5
  type ThemeProviderProps = {
6
6
  children: React.ReactNode;
@@ -14,28 +14,36 @@ type ThemeProviderState = {
14
14
  };
15
15
 
16
16
  const initialState: ThemeProviderState = {
17
- theme: 'system' as ThemeTypes,
17
+ theme: "system" as ThemeTypes,
18
18
  setTheme: () => null,
19
19
  };
20
20
 
21
21
  const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22
22
 
23
- export function ThemeProvider({ children, defaultTheme = 'system', storageKey = 'vite-ui-theme', ...props }: ThemeProviderProps) {
23
+ export function ThemeProvider({
24
+ children,
25
+ defaultTheme = "system",
26
+ storageKey = "vite-ui-theme",
27
+ ...props
28
+ }: ThemeProviderProps) {
24
29
  const [theme, setTheme] = useState<ThemeTypes>(() => {
25
30
  const stored = localStorage.getItem(storageKey);
26
- if (stored && ['dark', 'light', 'system'].includes(stored)) {
31
+ if (stored && ["dark", "light", "system"].includes(stored)) {
27
32
  return stored as ThemeTypes;
28
33
  }
29
- return 'system';
34
+ return defaultTheme;
30
35
  });
31
36
 
32
37
  useEffect(() => {
33
38
  const root = window.document.documentElement;
34
39
 
35
- root.classList.remove('light', 'dark');
40
+ root.classList.remove("light", "dark");
36
41
 
37
- if (theme === 'system') {
38
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
42
+ if (theme === "system") {
43
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
44
+ .matches
45
+ ? "dark"
46
+ : "light";
39
47
  root.classList.add(systemTheme);
40
48
  return;
41
49
  }
@@ -52,9 +60,7 @@ export function ThemeProvider({ children, defaultTheme = 'system', storageKey =
52
60
  };
53
61
 
54
62
  return (
55
- <ThemeProviderContext.Provider
56
- {...props}
57
- value={value}>
63
+ <ThemeProviderContext.Provider {...props} value={value}>
58
64
  {children}
59
65
  </ThemeProviderContext.Provider>
60
66
  );
@@ -62,6 +68,7 @@ export function ThemeProvider({ children, defaultTheme = 'system', storageKey =
62
68
 
63
69
  export const useTheme = () => {
64
70
  const context = useContext(ThemeProviderContext);
65
- if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider');
71
+ if (context === undefined)
72
+ throw new Error("useTheme must be used within a ThemeProvider");
66
73
  return context;
67
74
  };
@@ -1,8 +1,8 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { cva, type VariantProps } from "class-variance-authority"
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
4
 
5
- import { cn } from "@/lib/utils"
5
+ import { cn } from "@/lib/utils";
6
6
 
7
7
  const buttonVariants = cva(
8
8
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -31,27 +31,28 @@ const buttonVariants = cva(
31
31
  variant: "default",
32
32
  size: "default",
33
33
  },
34
- }
35
- )
34
+ },
35
+ );
36
36
 
37
37
  export interface ButtonProps
38
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ extends
39
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
39
40
  VariantProps<typeof buttonVariants> {
40
- asChild?: boolean
41
+ asChild?: boolean;
41
42
  }
42
43
 
43
44
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
45
  ({ className, variant, size, asChild = false, ...props }, ref) => {
45
- const Comp = asChild ? Slot : "button"
46
+ const Comp = asChild ? Slot : "button";
46
47
  return (
47
48
  <Comp
48
49
  className={cn(buttonVariants({ variant, size, className }))}
49
50
  ref={ref}
50
51
  {...props}
51
52
  />
52
- )
53
- }
54
- )
55
- Button.displayName = "Button"
53
+ );
54
+ },
55
+ );
56
+ Button.displayName = "Button";
56
57
 
57
- export { Button, buttonVariants }
58
+ export { Button, buttonVariants };