generator-kodly-react-app 1.0.6 → 1.0.10

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 (101) hide show
  1. package/generators/app/index.js +63 -11
  2. package/generators/app/templates/.env.dev +4 -0
  3. package/generators/app/templates/.env.sandbox +4 -0
  4. package/generators/app/templates/STRUCTURE.md +661 -0
  5. package/generators/app/templates/components.json +22 -0
  6. package/generators/app/templates/index.html +14 -6
  7. package/generators/app/templates/openapi-ts.config.ts +29 -0
  8. package/generators/app/templates/package.json +46 -26
  9. package/generators/app/templates/public/favicon.svg +4 -0
  10. package/generators/app/templates/src/app.tsx +8 -8
  11. package/generators/app/templates/src/components/layout/language-switcher.tsx +40 -0
  12. package/generators/app/templates/src/components/layout/theme-switcher.tsx +37 -0
  13. package/generators/app/templates/src/components/theme/theme-provider.tsx +22 -28
  14. package/generators/app/templates/src/components/ui/button.tsx +34 -32
  15. package/generators/app/templates/src/components/ui/card.tsx +76 -0
  16. package/generators/app/templates/src/components/ui/field.tsx +242 -0
  17. package/generators/app/templates/src/components/ui/form.tsx +129 -0
  18. package/generators/app/templates/src/components/ui/input-group.tsx +170 -0
  19. package/generators/app/templates/src/components/ui/input.tsx +19 -21
  20. package/generators/app/templates/src/components/ui/label.tsx +24 -0
  21. package/generators/app/templates/src/components/ui/select.tsx +184 -0
  22. package/generators/app/templates/src/components/ui/separator.tsx +31 -0
  23. package/generators/app/templates/src/components/ui/textarea.tsx +22 -0
  24. package/generators/app/templates/src/index.css +83 -26
  25. package/generators/app/templates/src/lib/api/client.ts +4 -5
  26. package/generators/app/templates/src/lib/i18n.ts +31 -16
  27. package/generators/app/templates/src/lib/routes.ts +14 -0
  28. package/generators/app/templates/src/lib/utils/error-handler.ts +7 -2
  29. package/generators/app/templates/src/lib/utils/init-form-schema.ts +12 -0
  30. package/generators/app/templates/src/lib/utils.ts +3 -4
  31. package/generators/app/templates/src/locales/en/common.json +5 -0
  32. package/generators/app/templates/src/locales/en/theme.json +5 -0
  33. package/generators/app/templates/src/locales/index.ts +31 -0
  34. package/generators/app/templates/src/locales/ja/common.json +5 -0
  35. package/generators/app/templates/src/locales/ja/theme.json +6 -0
  36. package/generators/app/templates/src/main.tsx +19 -15
  37. package/generators/app/templates/src/modules/app/layouts/app-layout.tsx +37 -0
  38. package/generators/app/templates/src/modules/app/locales/app-en.json +9 -0
  39. package/generators/app/templates/src/modules/app/locales/app-ja.json +9 -0
  40. package/generators/app/templates/src/modules/auth/components/forgot-password-form.tsx +74 -0
  41. package/generators/app/templates/src/modules/auth/components/login-form.tsx +95 -0
  42. package/generators/app/templates/src/modules/auth/components/reset-password-form.tsx +112 -0
  43. package/generators/app/templates/src/modules/auth/components/signup-form.tsx +92 -0
  44. package/generators/app/templates/src/modules/auth/contexts/auth-context.tsx +11 -0
  45. package/generators/app/templates/src/modules/auth/hooks/use-auth-hook.ts +175 -0
  46. package/generators/app/templates/src/modules/auth/layouts/auth-layout.tsx +28 -0
  47. package/generators/app/templates/src/modules/auth/locales/auth-en.json +105 -0
  48. package/generators/app/templates/src/modules/auth/locales/auth-ja.json +105 -0
  49. package/generators/app/templates/src/modules/landing/components/auth-hero.tsx +34 -0
  50. package/generators/app/templates/src/modules/landing/components/welcome-hero.tsx +24 -0
  51. package/generators/app/templates/src/modules/landing/landing-page-layout.tsx +24 -0
  52. package/generators/app/templates/src/modules/landing/landing-page.tsx +17 -0
  53. package/generators/app/templates/src/modules/landing/layouts/landing-page-layout.tsx +24 -0
  54. package/generators/app/templates/src/modules/landing/locales/landing-en.json +12 -0
  55. package/generators/app/templates/src/modules/landing/locales/landing-ja.json +11 -0
  56. package/generators/app/templates/src/openapi-client-config.ts +6 -0
  57. package/generators/app/templates/src/routeTree.gen.ts +268 -3
  58. package/generators/app/templates/src/router.tsx +2 -2
  59. package/generators/app/templates/src/routes/__root.tsx +3 -3
  60. package/generators/app/templates/src/routes/_landing/index.tsx +10 -0
  61. package/generators/app/templates/src/routes/_landing/route.tsx +14 -0
  62. package/generators/app/templates/src/routes/app/index.tsx +4 -21
  63. package/generators/app/templates/src/routes/app/route.tsx +12 -8
  64. package/generators/app/templates/src/routes/auth/forgot-password.tsx +10 -0
  65. package/generators/app/templates/src/routes/auth/login.tsx +6 -7
  66. package/generators/app/templates/src/routes/auth/reset-password.tsx +15 -0
  67. package/generators/app/templates/src/routes/auth/route.tsx +23 -6
  68. package/generators/app/templates/src/routes/auth/signup.tsx +11 -0
  69. package/generators/app/templates/src/sdk/@tanstack/react-query.gen.ts +91 -0
  70. package/generators/app/templates/src/sdk/client/client.gen.ts +167 -0
  71. package/generators/app/templates/src/sdk/client/index.ts +23 -0
  72. package/generators/app/templates/src/sdk/client/types.gen.ts +197 -0
  73. package/generators/app/templates/src/sdk/client/utils.gen.ts +213 -0
  74. package/generators/app/templates/src/sdk/client.gen.ts +18 -0
  75. package/generators/app/templates/src/sdk/core/auth.gen.ts +42 -0
  76. package/generators/app/templates/src/sdk/core/bodySerializer.gen.ts +100 -0
  77. package/generators/app/templates/src/sdk/core/params.gen.ts +176 -0
  78. package/generators/app/templates/src/sdk/core/pathSerializer.gen.ts +181 -0
  79. package/generators/app/templates/src/sdk/core/queryKeySerializer.gen.ts +136 -0
  80. package/generators/app/templates/src/sdk/core/serverSentEvents.gen.ts +266 -0
  81. package/generators/app/templates/src/sdk/core/types.gen.ts +118 -0
  82. package/generators/app/templates/src/sdk/core/utils.gen.ts +143 -0
  83. package/generators/app/templates/src/sdk/index.ts +4 -0
  84. package/generators/app/templates/src/sdk/schemas.gen.ts +195 -0
  85. package/generators/app/templates/src/sdk/sdk.gen.ts +80 -0
  86. package/generators/app/templates/src/sdk/types.gen.ts +158 -0
  87. package/generators/app/templates/src/sdk/zod.gen.ts +148 -0
  88. package/generators/app/templates/src/vite-env.d.ts +2 -1
  89. package/generators/app/templates/tsconfig.json +1 -1
  90. package/generators/app/templates/vite.config.js +35 -21
  91. package/generators/constants.js +9 -9
  92. package/package.json +3 -2
  93. package/generators/app/templates/.env.example +0 -5
  94. package/generators/app/templates/README.md +0 -57
  95. package/generators/app/templates/src/locales/en.json +0 -18
  96. package/generators/app/templates/src/modules/auth/auth-context.tsx +0 -13
  97. package/generators/app/templates/src/modules/auth/login/login-form.tsx +0 -49
  98. package/generators/app/templates/src/modules/auth/login/login-page.tsx +0 -12
  99. package/generators/app/templates/src/modules/auth/use-auth-hook.ts +0 -87
  100. package/generators/app/templates/src/routes/index.tsx +0 -12
  101. package/generators/app/templates/types.d.ts +0 -3
@@ -0,0 +1,37 @@
1
+ import { LanguageSwitcher } from '@/components/layout/language-switcher';
2
+ import { ThemeSwitcher } from '@/components/layout/theme-switcher';
3
+ import { Button } from '@/components/ui/button';
4
+ import { useLogout } from '@/modules/auth/hooks/use-auth-hook';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ export function AppLayout({ children }: { children: React.ReactNode }) {
8
+ const { t } = useTranslation();
9
+ const { mutate: logout, isPending } = useLogout();
10
+
11
+ const handleLogout = () => {
12
+ logout({});
13
+ };
14
+
15
+ return (
16
+ <div className='min-h-screen bg-background'>
17
+ <header className='border-b'>
18
+ <div className='container mx-auto flex h-16 items-center justify-between px-4'>
19
+ <div className='flex items-center gap-4'>
20
+ <h1 className='text-xl font-semibold'>{t('app.title')}</h1>
21
+ </div>
22
+ <div className='flex items-center gap-4'>
23
+ <ThemeSwitcher />
24
+ <LanguageSwitcher />
25
+ <Button
26
+ onClick={handleLogout}
27
+ disabled={isPending}
28
+ variant='outline'>
29
+ {t('app.welcome.logout')}
30
+ </Button>
31
+ </div>
32
+ </div>
33
+ </header>
34
+ <main className='container mx-auto p-8'>{children}</main>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "App",
3
+ "welcome": {
4
+ "title": "Welcome",
5
+ "message": "You have successfully logged in!",
6
+ "logout": "Logout"
7
+ }
8
+ }
9
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "アプリ",
3
+ "welcome": {
4
+ "title": "ようこそ",
5
+ "message": "ログインに成功しました!",
6
+ "logout": "ログアウト"
7
+ }
8
+ }
9
+
@@ -0,0 +1,74 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Input } from '@/components/ui/input';
3
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
4
+ import { useSendOtp } from '@/modules/auth/hooks/use-auth-hook';
5
+ import { z } from 'zod';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import { useForm } from 'react-hook-form';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { init } from 'zod-empty';
10
+ import { Link } from '@tanstack/react-router';
11
+ import { Route as LoginRoute } from '@/routes/auth/login';
12
+
13
+ const zForgotPasswordDto = z.object({
14
+ email: z.email().min(1).max(256),
15
+ });
16
+
17
+ type ForgotPasswordFormData = z.infer<typeof zForgotPasswordDto>;
18
+
19
+ export function ForgotPasswordForm() {
20
+ const { t } = useTranslation();
21
+ const { mutate, isPending } = useSendOtp();
22
+ const form = useForm<ForgotPasswordFormData>({
23
+ resolver: zodResolver(zForgotPasswordDto),
24
+ defaultValues: init(zForgotPasswordDto),
25
+ });
26
+
27
+ const onSubmit = (data: ForgotPasswordFormData) => {
28
+ mutate({ query: { authId: data.email } });
29
+ };
30
+
31
+ return (
32
+ <div className='flex flex-col gap-6'>
33
+ <h1 className='text-2xl font-semibold'>{t('auth.forgotPassword.title')}</h1>
34
+ <Form {...form}>
35
+ <form
36
+ noValidate
37
+ className='flex flex-col gap-4'
38
+ onSubmit={form.handleSubmit(onSubmit)}>
39
+ <FormField
40
+ control={form.control}
41
+ name='email'
42
+ render={({ field }) => (
43
+ <FormItem>
44
+ <FormLabel>{t('auth.forgotPassword.email')} *</FormLabel>
45
+ <FormControl>
46
+ <Input
47
+ type='email'
48
+ placeholder={t('auth.forgotPassword.emailPlaceholder')}
49
+ autoComplete='email'
50
+ autoFocus
51
+ {...field}
52
+ />
53
+ </FormControl>
54
+ <FormMessage />
55
+ </FormItem>
56
+ )}
57
+ />
58
+ <Button
59
+ type='submit'
60
+ disabled={isPending}>
61
+ {t('auth.forgotPassword.submit')}
62
+ </Button>
63
+ </form>
64
+ </Form>
65
+ <div className='text-sm text-center'>
66
+ <Link
67
+ to={LoginRoute.to}
68
+ className='text-primary hover:underline'>
69
+ {t('auth.forgotPassword.loginLink')}
70
+ </Link>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,95 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Input } from '@/components/ui/input';
3
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
4
+ import { useLogin } from '@/modules/auth/hooks/use-auth-hook';
5
+ import { zUserLoginDto } from '@/sdk/zod.gen';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import { useForm } from 'react-hook-form';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { init } from 'zod-empty';
10
+ import type { z } from 'zod';
11
+ import { Link } from '@tanstack/react-router';
12
+ import { Route as SignupRoute } from '@/routes/auth/signup';
13
+ import { Route as ForgotPasswordRoute } from '@/routes/auth/forgot-password';
14
+
15
+ type LoginFormData = z.infer<typeof zUserLoginDto>;
16
+
17
+ export function LoginForm() {
18
+ const { t } = useTranslation();
19
+ const { mutate, isPending } = useLogin();
20
+ const form = useForm<LoginFormData>({
21
+ resolver: zodResolver(zUserLoginDto),
22
+ defaultValues: init(zUserLoginDto),
23
+ });
24
+
25
+ const onSubmit = (data: LoginFormData) => {
26
+ mutate({ body: data });
27
+ };
28
+
29
+ return (
30
+ <div className='flex flex-col gap-6'>
31
+ <h1 className='text-2xl font-semibold'>{t('auth.login.title')}</h1>
32
+ <Form {...form}>
33
+ <form
34
+ noValidate
35
+ className='flex flex-col gap-4'
36
+ onSubmit={form.handleSubmit(onSubmit)}>
37
+ <FormField
38
+ control={form.control}
39
+ name='email'
40
+ render={({ field }) => (
41
+ <FormItem>
42
+ <FormLabel>{t('auth.login.email')} *</FormLabel>
43
+ <FormControl>
44
+ <Input
45
+ type='email'
46
+ placeholder={t('auth.login.emailPlaceholder')}
47
+ autoComplete='email'
48
+ autoFocus
49
+ {...field}
50
+ />
51
+ </FormControl>
52
+ <FormMessage />
53
+ </FormItem>
54
+ )}
55
+ />
56
+ <FormField
57
+ control={form.control}
58
+ name='password'
59
+ render={({ field }) => (
60
+ <FormItem>
61
+ <FormLabel>{t('auth.login.password')} *</FormLabel>
62
+ <FormControl>
63
+ <Input
64
+ type='password'
65
+ placeholder={t('auth.login.passwordPlaceholder')}
66
+ autoComplete='current-password'
67
+ {...field}
68
+ />
69
+ </FormControl>
70
+ <FormMessage />
71
+ </FormItem>
72
+ )}
73
+ />
74
+ <Button
75
+ type='submit'
76
+ disabled={isPending}>
77
+ {t('auth.login.submit')}
78
+ </Button>
79
+ </form>
80
+ </Form>
81
+ <div className='flex flex-col gap-2 text-sm text-center'>
82
+ <Link
83
+ to={ForgotPasswordRoute.to}
84
+ className='text-primary hover:underline'>
85
+ {t('auth.login.forgotPasswordLink')}
86
+ </Link>
87
+ <Link
88
+ to={SignupRoute.to}
89
+ className='text-primary hover:underline'>
90
+ {t('auth.login.signupLink')}
91
+ </Link>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,112 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Input } from '@/components/ui/input';
3
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
4
+ import { useResetPassword } from '@/modules/auth/hooks/use-auth-hook';
5
+ import { zUserResetPasswordDto } from '@/sdk/zod.gen';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import { useForm } from 'react-hook-form';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { init } from 'zod-empty';
10
+ import type { z } from 'zod';
11
+ import { Link, useSearch } from '@tanstack/react-router';
12
+ import { Route as LoginRoute } from '@/routes/auth/login';
13
+
14
+ type ResetPasswordFormData = z.infer<typeof zUserResetPasswordDto>;
15
+
16
+ export function ResetPasswordForm() {
17
+ const { t } = useTranslation();
18
+ const { mutate, isPending } = useResetPassword();
19
+ const search = useSearch({ from: '/auth/reset-password' });
20
+ const form = useForm<ResetPasswordFormData>({
21
+ resolver: zodResolver(zUserResetPasswordDto),
22
+ defaultValues: {
23
+ ...init(zUserResetPasswordDto),
24
+ email: search.email || '',
25
+ },
26
+ });
27
+
28
+ const onSubmit = (data: ResetPasswordFormData) => {
29
+ mutate({ body: data });
30
+ };
31
+
32
+ return (
33
+ <div className='flex flex-col gap-6'>
34
+ <h1 className='text-2xl font-semibold'>{t('auth.resetPassword.title')}</h1>
35
+ <Form {...form}>
36
+ <form
37
+ noValidate
38
+ className='flex flex-col gap-4'
39
+ onSubmit={form.handleSubmit(onSubmit)}>
40
+ <FormField
41
+ control={form.control}
42
+ name='email'
43
+ render={({ field }) => (
44
+ <FormItem>
45
+ <FormLabel>{t('auth.resetPassword.email')} *</FormLabel>
46
+ <FormControl>
47
+ <Input
48
+ type='email'
49
+ placeholder={t('auth.resetPassword.emailPlaceholder')}
50
+ autoComplete='email'
51
+ readOnly
52
+ {...field}
53
+ />
54
+ </FormControl>
55
+ <FormMessage />
56
+ </FormItem>
57
+ )}
58
+ />
59
+ <FormField
60
+ control={form.control}
61
+ name='otp'
62
+ render={({ field }) => (
63
+ <FormItem>
64
+ <FormLabel>{t('auth.resetPassword.otp')} *</FormLabel>
65
+ <FormControl>
66
+ <Input
67
+ type='text'
68
+ placeholder={t('auth.resetPassword.otpPlaceholder')}
69
+ autoComplete='one-time-code'
70
+ autoFocus
71
+ {...field}
72
+ />
73
+ </FormControl>
74
+ <FormMessage />
75
+ </FormItem>
76
+ )}
77
+ />
78
+ <FormField
79
+ control={form.control}
80
+ name='password'
81
+ render={({ field }) => (
82
+ <FormItem>
83
+ <FormLabel>{t('auth.resetPassword.password')} *</FormLabel>
84
+ <FormControl>
85
+ <Input
86
+ type='password'
87
+ placeholder={t('auth.resetPassword.passwordPlaceholder')}
88
+ autoComplete='new-password'
89
+ {...field}
90
+ />
91
+ </FormControl>
92
+ <FormMessage />
93
+ </FormItem>
94
+ )}
95
+ />
96
+ <Button
97
+ type='submit'
98
+ disabled={isPending}>
99
+ {t('auth.resetPassword.submit')}
100
+ </Button>
101
+ </form>
102
+ </Form>
103
+ <div className='text-sm text-center'>
104
+ <Link
105
+ to={LoginRoute.to}
106
+ className='text-primary hover:underline'>
107
+ {t('auth.forgotPassword.loginLink')}
108
+ </Link>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,92 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Input } from '@/components/ui/input';
3
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
4
+ import { useSignup } from '@/modules/auth/hooks/use-auth-hook';
5
+ import { zUserSignupDto } from '@/sdk/zod.gen';
6
+ import { zodResolver } from '@hookform/resolvers/zod';
7
+ import { useForm } from 'react-hook-form';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { init } from 'zod-empty';
10
+ import { Link } from '@tanstack/react-router';
11
+ import { Route as LoginRoute } from '@/routes/auth/login';
12
+ import { z } from 'zod';
13
+
14
+ type SignupFormData = z.infer<typeof zUserSignupDto>;
15
+
16
+ export function SignupForm() {
17
+ const { t } = useTranslation();
18
+ const { mutate, isPending } = useSignup();
19
+ const extendedSchema = zUserSignupDto.extend({
20
+ password: z.string().min(6).max(32),
21
+ });
22
+ const form = useForm<SignupFormData>({
23
+ resolver: zodResolver(extendedSchema),
24
+ defaultValues: init(zUserSignupDto),
25
+ });
26
+
27
+ const onSubmit = (data: SignupFormData) => {
28
+ mutate({ body: data });
29
+ };
30
+
31
+ return (
32
+ <div className='flex flex-col gap-6'>
33
+ <h1 className='text-2xl font-semibold'>{t('auth.signup.title')}</h1>
34
+ <Form {...form}>
35
+ <form
36
+ noValidate
37
+ className='flex flex-col gap-4'
38
+ onSubmit={form.handleSubmit(onSubmit)}>
39
+ <FormField
40
+ control={form.control}
41
+ name='email'
42
+ render={({ field }) => (
43
+ <FormItem>
44
+ <FormLabel>{t('auth.signup.email')} *</FormLabel>
45
+ <FormControl>
46
+ <Input
47
+ type='email'
48
+ placeholder={t('auth.signup.emailPlaceholder')}
49
+ autoComplete='email'
50
+ autoFocus
51
+ {...field}
52
+ />
53
+ </FormControl>
54
+ <FormMessage />
55
+ </FormItem>
56
+ )}
57
+ />
58
+ <FormField
59
+ control={form.control}
60
+ name='password'
61
+ render={({ field }) => (
62
+ <FormItem>
63
+ <FormLabel>{t('auth.signup.password')} *</FormLabel>
64
+ <FormControl>
65
+ <Input
66
+ type='password'
67
+ placeholder={t('auth.signup.passwordPlaceholder')}
68
+ autoComplete='new-password'
69
+ {...field}
70
+ />
71
+ </FormControl>
72
+ <FormMessage />
73
+ </FormItem>
74
+ )}
75
+ />
76
+ <Button
77
+ type='submit'
78
+ disabled={isPending}>
79
+ {t('auth.signup.submit')}
80
+ </Button>
81
+ </form>
82
+ </Form>
83
+ <div className='text-sm text-center'>
84
+ <Link
85
+ to={LoginRoute.to}
86
+ className='text-primary hover:underline'>
87
+ {t('auth.signup.loginLink')}
88
+ </Link>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,11 @@
1
+ import { createContext } from 'react';
2
+ import { currentUserDetailsAtom } from '@/modules/auth/hooks/use-auth-hook';
3
+ import type { AuthResponseDtoUserDetailDto } from '@/sdk';
4
+ import { useAtomValue } from 'jotai';
5
+
6
+ export const AuthContext = createContext<AuthResponseDtoUserDetailDto | undefined>(undefined);
7
+
8
+ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
9
+ const authData = useAtomValue(currentUserDetailsAtom);
10
+ return <AuthContext.Provider value={authData}>{children}</AuthContext.Provider>;
11
+ };
@@ -0,0 +1,175 @@
1
+ import { Route as LoginRoute } from '@/routes/auth/login';
2
+ import type {
3
+ AuthResponseDtoUserDetailDto,
4
+ LoginUserAuthResponse,
5
+ RegisterUserAuthResponse,
6
+ ResetPasswordUserAuthResponse,
7
+ SendOtpUserAuthResponse,
8
+ ValidateTokenUserAuthResponse,
9
+ } from '@/sdk';
10
+ import {
11
+ loginUserAuthMutation,
12
+ logoutUserAuthMutation,
13
+ registerUserAuthMutation,
14
+ resetPasswordUserAuthMutation,
15
+ sendOtpUserAuthMutation,
16
+ validateTokenUserAuthMutation,
17
+ } from '@/sdk/@tanstack/react-query.gen';
18
+ import { client } from '@/sdk/client.gen';
19
+ import { useMutation } from '@tanstack/react-query';
20
+ import { useNavigate } from '@tanstack/react-router';
21
+ import { atom, useAtomValue, useSetAtom } from 'jotai';
22
+ import { atomWithStorage } from 'jotai/utils';
23
+ import { useTranslation } from 'react-i18next';
24
+ import { toast } from 'sonner';
25
+ import { SupportedLanguages } from '@/lib/i18n';
26
+ import { DEFAULT_NON_LOGGED_IN_ROUTE } from '@/lib/routes';
27
+
28
+ export const HEADER_KEYS = {
29
+ AUTH_TOKEN: 'x-auth-token',
30
+ ACCEPT_LANGUAGE: 'accept-language',
31
+ };
32
+
33
+ export const LOCAL_STORAGE_KEYS = {
34
+ AUTH_TOKEN: 'authTokenAtom',
35
+ };
36
+
37
+ export const authTokenAtom = atomWithStorage<string>(LOCAL_STORAGE_KEYS.AUTH_TOKEN, '', undefined, {
38
+ getOnInit: true,
39
+ });
40
+
41
+ export const currentUserDetailsAtom = atom<AuthResponseDtoUserDetailDto | undefined>();
42
+
43
+ export const setTokenInClient = (token?: string | null) => {
44
+ if (token) {
45
+ client.setConfig({
46
+ headers: {
47
+ [HEADER_KEYS.AUTH_TOKEN]: token,
48
+ },
49
+ });
50
+ } else {
51
+ client.setConfig({
52
+ headers: {
53
+ [HEADER_KEYS.AUTH_TOKEN]: undefined,
54
+ },
55
+ });
56
+ }
57
+ };
58
+
59
+ export const setLocaleInClient = (locale: SupportedLanguages) => {
60
+ client.setConfig({
61
+ headers: {
62
+ [HEADER_KEYS.ACCEPT_LANGUAGE]: locale,
63
+ },
64
+ });
65
+ };
66
+
67
+ export const useLogin = () => {
68
+ const { t } = useTranslation();
69
+ const setAuthToken = useSetAtom(authTokenAtom);
70
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
71
+ return useMutation({
72
+ ...loginUserAuthMutation({}),
73
+ onSuccess: (response: LoginUserAuthResponse) => {
74
+ const authData = response.data;
75
+ setAuthToken(authData?.token || '');
76
+ setCurrentUser(authData);
77
+ setTokenInClient(authData?.token);
78
+ },
79
+ onError: (err) => {
80
+ toast.error(err.response?.data.message || t('auth.messages.loginFailed'));
81
+ },
82
+ });
83
+ };
84
+
85
+ export const useSignup = () => {
86
+ const { t } = useTranslation();
87
+ const setAuthToken = useSetAtom(authTokenAtom);
88
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
89
+ return useMutation({
90
+ ...registerUserAuthMutation({}),
91
+ onSuccess: (response: RegisterUserAuthResponse) => {
92
+ const authData = response.data;
93
+ setAuthToken(authData?.token || '');
94
+ setCurrentUser(authData);
95
+ setTokenInClient(authData?.token);
96
+ },
97
+ onError: (err) => {
98
+ toast.error(err.response?.data.message || t('auth.messages.signupFailed'));
99
+ },
100
+ });
101
+ };
102
+
103
+ export const useValidateToken = () => {
104
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
105
+ const setAuthToken = useSetAtom(authTokenAtom);
106
+ const authToken = useAtomValue(authTokenAtom);
107
+ setTokenInClient(authToken);
108
+ return useMutation({
109
+ ...validateTokenUserAuthMutation({}),
110
+ retry: false,
111
+ onSuccess: (response: ValidateTokenUserAuthResponse) => {
112
+ const authData = response.data;
113
+ setCurrentUser(authData);
114
+ setAuthToken(authData?.token || '');
115
+ setTokenInClient(authData?.token);
116
+ },
117
+ onError: () => {
118
+ setCurrentUser(undefined);
119
+ setAuthToken('');
120
+ setTokenInClient();
121
+ },
122
+ });
123
+ };
124
+
125
+ export const useLogout = () => {
126
+ const setAuthToken = useSetAtom(authTokenAtom);
127
+ const setCurrentUser = useSetAtom(currentUserDetailsAtom);
128
+ const navigate = useNavigate();
129
+ return useMutation({
130
+ ...logoutUserAuthMutation({}),
131
+ onSuccess: () => {
132
+ setAuthToken('');
133
+ setCurrentUser(undefined);
134
+ setTokenInClient();
135
+ navigate({ to: DEFAULT_NON_LOGGED_IN_ROUTE });
136
+ },
137
+ });
138
+ };
139
+
140
+ export const useSendOtp = () => {
141
+ const { t } = useTranslation();
142
+ const navigate = useNavigate();
143
+ return useMutation({
144
+ ...sendOtpUserAuthMutation({}),
145
+ onSuccess: (response: SendOtpUserAuthResponse, variables) => {
146
+ toast.success(response.message || t('auth.messages.otpSentSuccess'));
147
+ // Extract email from the mutation variables and navigate to reset-password
148
+ const email = variables.query?.authId;
149
+ if (email) {
150
+ navigate({
151
+ to: '/auth/reset-password',
152
+ search: { email },
153
+ });
154
+ }
155
+ },
156
+ onError: (err) => {
157
+ toast.error(err.response?.data.message || t('auth.messages.otpSendFailed'));
158
+ },
159
+ });
160
+ };
161
+
162
+ export const useResetPassword = () => {
163
+ const { t } = useTranslation();
164
+ const navigate = useNavigate();
165
+ return useMutation({
166
+ ...resetPasswordUserAuthMutation({}),
167
+ onSuccess: (response: ResetPasswordUserAuthResponse) => {
168
+ toast.success(response.message || t('auth.messages.passwordResetSuccess'));
169
+ navigate({ to: LoginRoute.to });
170
+ },
171
+ onError: (err) => {
172
+ toast.error(err.response?.data.message || t('auth.messages.passwordResetFailed'));
173
+ },
174
+ });
175
+ };
@@ -0,0 +1,28 @@
1
+ import { LanguageSwitcher } from '@/components/layout/language-switcher';
2
+ import { ThemeSwitcher } from '@/components/layout/theme-switcher';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Home } from 'lucide-react';
5
+ import { Link } from '@tanstack/react-router';
6
+ import { DEFAULT_NON_LOGGED_IN_ROUTE } from '@/lib/routes';
7
+
8
+ export function AuthLayout({ children }: { children: React.ReactNode }) {
9
+ return (
10
+ <div className='flex h-screen items-center justify-center bg-background relative'>
11
+ <div className='absolute top-4 left-4'>
12
+ <Link to={DEFAULT_NON_LOGGED_IN_ROUTE}>
13
+ <Button
14
+ variant='outline'
15
+ size='icon'
16
+ aria-label='Go to home'>
17
+ <Home className='h-4 w-4' />
18
+ </Button>
19
+ </Link>
20
+ </div>
21
+ <div className='absolute top-4 right-4 flex gap-2'>
22
+ <ThemeSwitcher />
23
+ <LanguageSwitcher />
24
+ </div>
25
+ <div className='w-full max-w-md p-8'>{children}</div>
26
+ </div>
27
+ );
28
+ }