robodev 0.47.2 → 0.48.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 (172) hide show
  1. package/package.json +3 -3
  2. package/src/emit-starter.test.ts +6 -3
  3. package/templates/auth-chat/ROBODEV.md +7 -1
  4. package/templates/auth-chat/package.json +2 -2
  5. package/templates/backend/ROBODEV.md +7 -1
  6. package/templates/backend/package.json +2 -2
  7. package/templates/catalog.json +5 -0
  8. package/templates/empty/ROBODEV.md +7 -1
  9. package/templates/empty/package.json +2 -2
  10. package/templates/marketplace/ROBODEV.md +7 -1
  11. package/templates/marketplace/package.json +2 -2
  12. package/templates/povio/.config/local.spa.template.yml +27 -0
  13. package/templates/povio/.oxlint-base.json +29 -0
  14. package/templates/povio/.oxlintrc.json +78 -0
  15. package/templates/povio/.rulesync/rules/frontend-api-boundary.md +36 -0
  16. package/templates/povio/.rulesync/rules/frontend-component-structure.md +40 -0
  17. package/templates/povio/.rulesync/rules/frontend-forms.md +27 -0
  18. package/templates/povio/.rulesync/rules/frontend-notifications.md +24 -0
  19. package/templates/povio/.rulesync/rules/frontend-povio-components.md +42 -0
  20. package/templates/povio/.rulesync/rules/frontend-povio-ui.md +45 -0
  21. package/templates/povio/.rulesync/rules/frontend-query-autocomplete.md +24 -0
  22. package/templates/povio/.rulesync/rules/frontend-tables-and-lists.md +26 -0
  23. package/templates/povio/.rulesync/rules/frontend-translations.md +25 -0
  24. package/templates/povio/.rulesync/rules/project-overview.md +32 -0
  25. package/templates/povio/.rulesync/rules/robodev-api.md +30 -0
  26. package/templates/povio/.rulesync/rules/robodev-auth.md +40 -0
  27. package/templates/povio/.rulesync/rules/robodev-database.md +22 -0
  28. package/templates/povio/.rulesync/rules/role-based-app-structure.md +36 -0
  29. package/templates/povio/.rulesync/skills/media-feature/SKILL.md +21 -0
  30. package/templates/povio/.rulesync/skills/povio-ui-styling/SKILL.md +211 -0
  31. package/templates/povio/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  32. package/templates/povio/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  33. package/templates/povio/.rulesync/skills/robodev-table/SKILL.md +12 -0
  34. package/templates/povio/README.md +40 -0
  35. package/templates/povio/ROBODEV.md +84 -0
  36. package/templates/povio/apps/fe/index.html +24 -0
  37. package/templates/povio/apps/fe/openapi-codegen.config.ts +72 -0
  38. package/templates/povio/apps/fe/package.json +71 -0
  39. package/templates/povio/apps/fe/public/apple-touch-icon.png +0 -0
  40. package/templates/povio/apps/fe/public/favicon-96x96.png +0 -0
  41. package/templates/povio/apps/fe/public/favicon.ico +0 -0
  42. package/templates/povio/apps/fe/public/favicon.svg +3 -0
  43. package/templates/povio/apps/fe/public/site.webmanifest +21 -0
  44. package/templates/povio/apps/fe/public/web-app-manifest-192x192.png +0 -0
  45. package/templates/povio/apps/fe/public/web-app-manifest-512x512.png +0 -0
  46. package/templates/povio/apps/fe/src/assets/locales/en/translation.json +166 -0
  47. package/templates/povio/apps/fe/src/clients/app-rest-client.ts +15 -0
  48. package/templates/povio/apps/fe/src/clients/auth-token-store.ts +101 -0
  49. package/templates/povio/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  50. package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  51. package/templates/povio/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +45 -0
  52. package/templates/povio/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  53. package/templates/povio/apps/fe/src/components/404.tsx +18 -0
  54. package/templates/povio/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  55. package/templates/povio/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  56. package/templates/povio/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  57. package/templates/povio/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  58. package/templates/povio/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  59. package/templates/povio/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  60. package/templates/povio/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  61. package/templates/povio/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  62. package/templates/povio/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  63. package/templates/povio/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  64. package/templates/povio/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  65. package/templates/povio/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  66. package/templates/povio/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  67. package/templates/povio/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  68. package/templates/povio/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  69. package/templates/povio/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  70. package/templates/povio/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  71. package/templates/povio/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  72. package/templates/povio/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  73. package/templates/povio/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  74. package/templates/povio/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  75. package/templates/povio/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  76. package/templates/povio/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  77. package/templates/povio/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  78. package/templates/povio/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  79. package/templates/povio/apps/fe/src/config/app.config.ts +23 -0
  80. package/templates/povio/apps/fe/src/config/i18n.ts +37 -0
  81. package/templates/povio/apps/fe/src/config/inits/logger.ts +7 -0
  82. package/templates/povio/apps/fe/src/config/jwt.config.ts +2 -0
  83. package/templates/povio/apps/fe/src/config/query.config.ts +20 -0
  84. package/templates/povio/apps/fe/src/hooks/useAuth.ts +5 -0
  85. package/templates/povio/apps/fe/src/main.tsx +52 -0
  86. package/templates/povio/apps/fe/src/pages/(guest)/login.tsx +23 -0
  87. package/templates/povio/apps/fe/src/pages/(guest)/register.tsx +23 -0
  88. package/templates/povio/apps/fe/src/pages/(guest)/route.tsx +18 -0
  89. package/templates/povio/apps/fe/src/pages/(private)/index.tsx +23 -0
  90. package/templates/povio/apps/fe/src/pages/(private)/profile.tsx +23 -0
  91. package/templates/povio/apps/fe/src/pages/(private)/route.tsx +18 -0
  92. package/templates/povio/apps/fe/src/pages/(public)/auth.tsx +37 -0
  93. package/templates/povio/apps/fe/src/pages/(public)/route.tsx +9 -0
  94. package/templates/povio/apps/fe/src/pages/__root.tsx +133 -0
  95. package/templates/povio/apps/fe/src/providers/AppErrorBoundary.tsx +44 -0
  96. package/templates/povio/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  97. package/templates/povio/apps/fe/src/providers/index.tsx +44 -0
  98. package/templates/povio/apps/fe/src/providers/jwt.provider.tsx +95 -0
  99. package/templates/povio/apps/fe/src/routeTree.gen.ts +225 -0
  100. package/templates/povio/apps/fe/src/styles/base.css +84 -0
  101. package/templates/povio/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  102. package/templates/povio/apps/fe/src/styles/globals.css +26 -0
  103. package/templates/povio/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  104. package/templates/povio/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  105. package/templates/povio/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  106. package/templates/povio/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  107. package/templates/povio/apps/fe/src/styles/overrides/defaults/modal.override.ts +62 -0
  108. package/templates/povio/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  109. package/templates/povio/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  110. package/templates/povio/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  111. package/templates/povio/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  112. package/templates/povio/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  113. package/templates/povio/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  114. package/templates/povio/apps/fe/src/styles/theme.css +2177 -0
  115. package/templates/povio/apps/fe/src/types/i18next.d.ts +12 -0
  116. package/templates/povio/apps/fe/src/types/table.d.ts +17 -0
  117. package/templates/povio/apps/fe/src/types/ui.d.ts +26 -0
  118. package/templates/povio/apps/fe/src/types/vite-env.d.ts +13 -0
  119. package/templates/povio/apps/fe/src/utils/date.utils.ts +17 -0
  120. package/templates/povio/apps/fe/src/utils/image-fallback.ts +9 -0
  121. package/templates/povio/apps/fe/src/utils/number.utils.ts +12 -0
  122. package/templates/povio/apps/fe/src/utils/string.utils.ts +5 -0
  123. package/templates/povio/apps/fe/src/vite-env.d.ts +1 -0
  124. package/templates/povio/apps/fe/tsconfig.app.json +32 -0
  125. package/templates/povio/apps/fe/tsconfig.json +9 -0
  126. package/templates/povio/apps/fe/tsconfig.node.json +31 -0
  127. package/templates/povio/apps/fe/vite.config.ts +150 -0
  128. package/templates/povio/openapi.json +911 -0
  129. package/templates/povio/oxfmt.config.js +23 -0
  130. package/templates/povio/package.json +19 -0
  131. package/templates/povio/robodev/api/_lib.ts +99 -0
  132. package/templates/povio/robodev/api/health.ts +7 -0
  133. package/templates/povio/robodev/database.ts +28 -0
  134. package/templates/povio/robodev/package.json +4 -0
  135. package/templates/povio/rulesync.jsonc +18 -0
  136. package/templates/povio/tsconfig.json +11 -0
  137. package/templates/shadcn/.rulesync/rules/frontend-api-boundary.md +29 -0
  138. package/templates/shadcn/.rulesync/rules/frontend-component-structure.md +19 -0
  139. package/templates/shadcn/.rulesync/rules/frontend-hosted-ui.md +38 -0
  140. package/templates/shadcn/.rulesync/rules/project-overview.md +29 -0
  141. package/templates/shadcn/.rulesync/rules/robodev-api.md +22 -0
  142. package/templates/shadcn/.rulesync/rules/robodev-auth.md +47 -0
  143. package/templates/shadcn/.rulesync/rules/robodev-database.md +21 -0
  144. package/templates/shadcn/.rulesync/skills/robodev-crud-page/SKILL.md +181 -0
  145. package/templates/shadcn/README.md +9 -4
  146. package/templates/shadcn/ROBODEV.md +14 -6
  147. package/templates/shadcn/package.json +4 -6
  148. package/templates/shadcn/rulesync.jsonc +18 -0
  149. package/templates/shadcn/src/App.tsx +12 -3
  150. package/templates/shadcn/src/app.css +359 -0
  151. package/templates/shadcn/src/components/auth/AuthCard.tsx +29 -0
  152. package/templates/shadcn/src/components/ui/badge.tsx +17 -0
  153. package/templates/shadcn/src/components/ui/card.tsx +28 -0
  154. package/templates/shadcn/src/components/ui/dialog.tsx +38 -0
  155. package/templates/shadcn/src/components/ui/empty-state.tsx +17 -0
  156. package/templates/shadcn/src/components/ui/input.tsx +8 -0
  157. package/templates/shadcn/src/components/ui/label.tsx +8 -0
  158. package/templates/shadcn/src/components/ui/skeleton.tsx +6 -0
  159. package/templates/shadcn/src/components/ui/spinner.tsx +3 -0
  160. package/templates/shadcn/src/components/ui/table.tsx +30 -0
  161. package/templates/shadcn/src/components/ui/textarea.tsx +8 -0
  162. package/templates/shadcn/src/lib/auth.tsx +72 -0
  163. package/templates/shadcn/src/lib/client.ts +8 -0
  164. package/templates/shadcn/src/pages/Home.tsx +15 -0
  165. package/templates/shadcn/src/pages/Login.tsx +70 -0
  166. package/templates/shadcn/src/pages/NotFound.tsx +14 -0
  167. package/templates/shadcn/src/pages/Register.tsx +82 -0
  168. package/templates/space/ROBODEV.md +7 -1
  169. package/templates/space/package.json +2 -2
  170. package/templates/shadcn/components.json +0 -21
  171. package/templates/shadcn/postcss.config.js +0 -6
  172. package/templates/shadcn/tailwind.config.ts +0 -42
@@ -0,0 +1,111 @@
1
+ import { Alert, Button, PasswordInput, TextButton, TextInput, Typography, useForm } from "@povio/ui/tanstack";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { useAuth } from "@/hooks/useAuth";
7
+ import { UserAuthModels } from "@/openapi/userAuth/userAuth.models";
8
+ import { UserAuthQueries } from "@/openapi/userAuth/userAuth.queries";
9
+
10
+ export function LoginPage() {
11
+ const { t } = useTranslation();
12
+ const navigate = useNavigate();
13
+ const { updateTokens, userPromise } = useAuth();
14
+
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ const loginMutation = UserAuthQueries.useUserAuthLocalPasswordLogin();
18
+
19
+ const form = useForm({
20
+ zodSchema: UserAuthModels.UserAuthPasswordLoginRequestSchema,
21
+ defaultValues: {
22
+ email: "",
23
+ password: "",
24
+ },
25
+ });
26
+
27
+ const completeLogin = async (tokens: UserAuthModels.AuthnTokenResponse) => {
28
+ updateTokens?.(tokens.accessToken, tokens.refreshToken);
29
+ const user = await userPromise?.();
30
+ if (user === null) {
31
+ throw new Error(t(($) => $.auth.login.form.loginFailed));
32
+ }
33
+ await navigate({ to: "/" });
34
+ };
35
+
36
+ const onSubmit = async (data: UserAuthModels.UserAuthPasswordLoginRequest) => {
37
+ setError(null);
38
+ try {
39
+ const tokens = await loginMutation.mutateAsync({ data });
40
+ await completeLogin(tokens);
41
+ } catch (error) {
42
+ setError(error instanceof Error ? error.message : t(($) => $.auth.login.form.loginFailed));
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="flex flex-col gap-8">
48
+ <div className="flex flex-col gap-3">
49
+ <Typography
50
+ variant="prominent-1"
51
+ size="title-2"
52
+ as="h2"
53
+ className="text-text-default-1"
54
+ >
55
+ {t(($) => $.auth.login.form.title)}
56
+ </Typography>
57
+ </div>
58
+
59
+ {error && (
60
+ <Alert
61
+ color="error"
62
+ text={error}
63
+ />
64
+ )}
65
+
66
+ <form
67
+ onSubmit={form.handleSubmit(onSubmit)}
68
+ className="flex flex-col gap-8"
69
+ >
70
+ <div className="flex flex-col gap-4">
71
+ <TextInput
72
+ field={{ form, name: "email" }}
73
+ label={t(($) => $.auth.login.form.email)}
74
+ placeholder={t(($) => $.auth.login.form.emailPlaceholder)}
75
+ size="default"
76
+ />
77
+
78
+ <PasswordInput
79
+ field={{ form, name: "password" }}
80
+ label={t(($) => $.auth.login.form.password)}
81
+ placeholder={t(($) => $.auth.login.form.passwordPlaceholder)}
82
+ size="default"
83
+ />
84
+ </div>
85
+
86
+ <div className="flex flex-col gap-4">
87
+ <Button
88
+ width="fill"
89
+ type="submit"
90
+ isDisabled={loginMutation.isPending}
91
+ isLoading={loginMutation.isPending}
92
+ >
93
+ {t(($) => $.auth.login.form.submit)}
94
+ </Button>
95
+
96
+ </div>
97
+ </form>
98
+
99
+ <div className="flex justify-center gap-1 border-elevation-outline-default-1 border-t pt-6">
100
+ <Typography
101
+ size="label-2"
102
+ className="text-text-default-2"
103
+ >
104
+ {t(($) => $.auth.login.form.noAccount)}
105
+ </Typography>
106
+
107
+ <TextButton link={{ to: "/register" }}>{t(($) => $.auth.login.form.registerLink)}</TextButton>
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,143 @@
1
+ import { Alert, Button, PasswordInput, TextButton, TextInput, Typography, useForm } from "@povio/ui/tanstack";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { useMemo, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { z } from "zod";
6
+
7
+ import { useAuth } from "@/hooks/useAuth";
8
+ import { UserAuthModels } from "@/openapi/userAuth/userAuth.models";
9
+ import { UserAuthQueries } from "@/openapi/userAuth/userAuth.queries";
10
+
11
+ export function RegisterPage() {
12
+ const { t } = useTranslation();
13
+ const navigate = useNavigate();
14
+ const { updateTokens, userPromise } = useAuth();
15
+
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const registerMutation = UserAuthQueries.useRegister();
19
+ const registerFormSchema = useMemo(
20
+ () =>
21
+ UserAuthModels.UserAuthPasswordRegisterRequestSchema.extend({
22
+ confirmPassword: z.string().min(
23
+ 1,
24
+ t(($) => $.auth.register.form.confirmPasswordRequired),
25
+ ),
26
+ }).refine((data) => data.password === data.confirmPassword, {
27
+ path: ["confirmPassword"],
28
+ message: t(($) => $.auth.register.form.passwordsDoNotMatch),
29
+ }),
30
+ [t],
31
+ );
32
+
33
+ const form = useForm({
34
+ zodSchema: registerFormSchema,
35
+ defaultValues: {
36
+ email: "",
37
+ name: "",
38
+ password: "",
39
+ confirmPassword: "",
40
+ },
41
+ });
42
+
43
+ const onSubmit = async (formData: z.infer<typeof registerFormSchema>) => {
44
+ setError(null);
45
+ const data: UserAuthModels.UserAuthPasswordRegisterRequest = {
46
+ email: formData.email,
47
+ name: formData.name,
48
+ password: formData.password,
49
+ };
50
+
51
+ try {
52
+ const tokens = await registerMutation.mutateAsync({ data });
53
+ updateTokens?.(tokens.accessToken, tokens.refreshToken);
54
+ const user = await userPromise?.();
55
+ if (user === null) {
56
+ throw new Error(t(($) => $.auth.register.form.registrationFailed));
57
+ }
58
+ await navigate({ to: "/" });
59
+ } catch (error) {
60
+ setError(error instanceof Error ? error.message : t(($) => $.auth.register.form.registrationFailed));
61
+ }
62
+ };
63
+
64
+ return (
65
+ <div className="flex flex-col gap-8">
66
+ <Typography
67
+ variant="prominent-1"
68
+ size="title-2"
69
+ as="h2"
70
+ className="text-center text-text-default-1"
71
+ >
72
+ {t(($) => $.auth.register.form.title)}
73
+ </Typography>
74
+
75
+ {error && (
76
+ <Alert
77
+ color="error"
78
+ text={error}
79
+ />
80
+ )}
81
+
82
+ <form
83
+ onSubmit={form.handleSubmit(onSubmit)}
84
+ className="flex flex-col gap-8"
85
+ >
86
+ <div className="flex flex-col gap-4">
87
+ <TextInput
88
+ field={{ form, name: "email" }}
89
+ label={t(($) => $.auth.register.form.email)}
90
+ placeholder={t(($) => $.auth.register.form.emailPlaceholder)}
91
+ />
92
+
93
+ <TextInput
94
+ field={{ form, name: "name" }}
95
+ label={t(($) => $.auth.register.form.name)}
96
+ placeholder={t(($) => $.auth.register.form.namePlaceholder)}
97
+ />
98
+
99
+ <PasswordInput
100
+ field={{ form, name: "password" }}
101
+ label={t(($) => $.auth.register.form.password)}
102
+ placeholder={t(($) => $.auth.register.form.passwordPlaceholder)}
103
+ />
104
+
105
+ <div className="flex flex-col gap-2">
106
+ <PasswordInput
107
+ field={{ form, name: "confirmPassword" }}
108
+ label={t(($) => $.auth.register.form.confirmPassword)}
109
+ placeholder={t(($) => $.auth.register.form.confirmPasswordPlaceholder)}
110
+ />
111
+
112
+ <Typography
113
+ size="label-2"
114
+ className="text-text-default-2"
115
+ >
116
+ {t(($) => $.auth.register.form.passwordRequirements)}
117
+ </Typography>
118
+ </div>
119
+ </div>
120
+
121
+ <Button
122
+ width="fill"
123
+ type="submit"
124
+ isDisabled={registerMutation.isPending}
125
+ isLoading={registerMutation.isPending}
126
+ >
127
+ {t(($) => $.auth.register.form.submit)}
128
+ </Button>
129
+ </form>
130
+
131
+ <div className="flex justify-center gap-1">
132
+ <Typography
133
+ size="label-2"
134
+ className="text-text-default-2"
135
+ >
136
+ {t(($) => $.auth.register.form.hasAccount)}
137
+ </Typography>
138
+
139
+ <TextButton link={{ to: "/login" }}>{t(($) => $.auth.register.form.loginLink)}</TextButton>
140
+ </div>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,21 @@
1
+ import { Typography } from "@povio/ui";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { PageHeader } from "@/components/shared/page/PageHeader";
5
+
6
+ export function HomePage() {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <div className="flex flex-col gap-6">
11
+ <PageHeader title={t(($) => $.home.title)} />
12
+
13
+ <Typography
14
+ size="body-2"
15
+ as="p"
16
+ >
17
+ {t(($) => $.home.message)}
18
+ </Typography>
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,96 @@
1
+ import { Button, TextInput, Typography, useForm, useToast } from "@povio/ui/tanstack";
2
+ import { Check } from "lucide-react";
3
+ import { useEffect } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { LoadingState } from "@/components/shared/layout/LoadingState";
7
+ import { PageHeader } from "@/components/shared/page/PageHeader";
8
+ import { Card } from "@/components/shared/ui/Card";
9
+ import { useAuth } from "@/hooks/useAuth";
10
+ import { UserModels } from "@/openapi/user/user.models";
11
+ import { UserQueries } from "@/openapi/user/user.queries";
12
+
13
+ export function ProfilePage() {
14
+ const { t } = useTranslation();
15
+ const { isAuthenticated, user } = useAuth();
16
+ const { successToast, errorToast } = useToast();
17
+
18
+ const { data: fetchedUser, isLoading } = UserQueries.useGet({ enabled: isAuthenticated });
19
+ const fullUser = fetchedUser ?? user;
20
+ const updateProfile = UserQueries.useUpdate();
21
+
22
+ const form = useForm({
23
+ zodSchema: UserModels.UserMeUpdateRequestSchema,
24
+ defaultValues: {
25
+ name: "",
26
+ email: "",
27
+ },
28
+ });
29
+
30
+ useEffect(() => {
31
+ if (fullUser) {
32
+ form.reset({
33
+ name: fullUser.name ?? "",
34
+ email: fullUser.email ?? "",
35
+ });
36
+ }
37
+ }, [form, fullUser]);
38
+
39
+ const onSubmit = async (data: UserModels.UserMeUpdateRequest) => {
40
+ try {
41
+ await updateProfile.mutateAsync({ data });
42
+ successToast({ text: t(($) => $.profile.form.notifications.success) });
43
+ } catch {
44
+ errorToast({ text: t(($) => $.profile.form.notifications.error) });
45
+ }
46
+ };
47
+
48
+ if (isLoading && !fullUser) {
49
+ return <LoadingState />;
50
+ }
51
+
52
+ return (
53
+ <div className="flex flex-col gap-6">
54
+ <PageHeader title={t(($) => $.profile.form.title)} />
55
+
56
+ <Card>
57
+ <div className="flex flex-col gap-4">
58
+ <Typography
59
+ size="body-3"
60
+ className="text-text-default-2"
61
+ >
62
+ {t(($) => $.profile.form.description)}
63
+ </Typography>
64
+
65
+ <form
66
+ onSubmit={form.handleSubmit(onSubmit)}
67
+ className="flex flex-col gap-6"
68
+ >
69
+ <TextInput
70
+ field={{ form, name: "email" }}
71
+ label={t(($) => $.profile.form.email)}
72
+ placeholder={t(($) => $.profile.form.emailPlaceholder)}
73
+ />
74
+ <TextInput
75
+ field={{ form, name: "name" }}
76
+ label={t(($) => $.profile.form.name)}
77
+ placeholder={t(($) => $.profile.form.namePlaceholder)}
78
+ />
79
+
80
+ <div className="flex justify-end">
81
+ <Button
82
+ type="submit"
83
+ size="s"
84
+ icon={Check}
85
+ isDisabled={updateProfile.isPending}
86
+ isLoading={updateProfile.isPending}
87
+ >
88
+ {updateProfile.isPending ? t(($) => $.profile.form.submitLoading) : t(($) => $.profile.form.submit)}
89
+ </Button>
90
+ </div>
91
+ </form>
92
+ </div>
93
+ </Card>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { AppHeader } from "./app-header/AppHeader";
4
+
5
+ interface AppLayoutProps {
6
+ children: ReactNode;
7
+ }
8
+
9
+ export function AppLayout({ children }: AppLayoutProps) {
10
+ return (
11
+ <div className="flex min-h-dvh w-full flex-col">
12
+ <AppHeader />
13
+
14
+ <main className="flex w-full flex-1 flex-col">
15
+ <div className="w-full flex-1 px-4 py-4">{children}</div>
16
+ </main>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,91 @@
1
+ import { Button, IconButton, Link, Menu } from "@povio/ui";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import clsx from "clsx";
4
+ import { ChevronDown, MenuIcon, X } from "lucide-react";
5
+ import { type Key, useState } from "react";
6
+ import { useTranslation } from "react-i18next";
7
+
8
+ import { BrandLogo } from "@/components/shared/branding/BrandLogo";
9
+ import { useAuth } from "@/hooks/useAuth";
10
+
11
+ import { MobileNavigation } from "./MobileNavigation";
12
+ import { NavLink } from "./NavLink";
13
+
14
+ export function AppHeader() {
15
+ const { t } = useTranslation();
16
+ const navigate = useNavigate();
17
+ const { isAuthenticated, user, logout } = useAuth();
18
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
19
+
20
+ const menuItems = [
21
+ { label: t(($) => $.layout.header.profile), id: "profile" },
22
+ { label: t(($) => $.layout.header.logout), id: "logout" },
23
+ ];
24
+
25
+ const accountLabel = user?.name || user?.email || t(($) => $.layout.header.profile);
26
+
27
+ const handleMenuAction = (key: Key) => {
28
+ if (key === "profile") {
29
+ navigate({ to: "/profile" });
30
+ } else if (key === "logout") {
31
+ logout();
32
+ }
33
+ };
34
+
35
+ return (
36
+ <header
37
+ className={clsx(
38
+ "relative z-11 flex shrink-0 items-center gap-4 border-elevation-outline-default-1 border-b bg-elevation-fill-default-1 px-4",
39
+ isMobileMenuOpen && "flex-wrap",
40
+ )}
41
+ >
42
+ <Link
43
+ to="/"
44
+ aria-label={t(($) => $.layout.header.logo)}
45
+ className="flex shrink-0 items-center no-underline!"
46
+ >
47
+ <BrandLogo className="text-interactive-contained-primary-idle" />
48
+ </Link>
49
+
50
+ <div className="hidden min-w-0 lg:block">
51
+ <nav className="flex flex-row items-center gap-1 overflow-visible border-elevation-outline-default-1 px-0">
52
+ <NavLink to="/">{t(($) => $.layout.header.nav.home)}</NavLink>
53
+ </nav>
54
+ </div>
55
+
56
+ {isAuthenticated && (
57
+ <div className="ml-auto hidden shrink-0 items-center gap-2 lg:flex">
58
+ <Menu
59
+ trigger={
60
+ <div className="flex cursor-pointer items-center gap-1">
61
+ <Button
62
+ type="button"
63
+ color="secondary"
64
+ variant="text"
65
+ size="xs"
66
+ className="items-center gap-1 overflow-hidden whitespace-nowrap text-text-default-1 [&>span]:font-labels-default!"
67
+ >
68
+ {accountLabel}
69
+ </Button>
70
+ <ChevronDown className="size-5 shrink-0" />
71
+ </div>
72
+ }
73
+ items={menuItems}
74
+ onAction={handleMenuAction}
75
+ />
76
+ </div>
77
+ )}
78
+
79
+ <IconButton
80
+ className="ml-auto lg:hidden"
81
+ color="primary"
82
+ icon={isMobileMenuOpen ? X : MenuIcon}
83
+ label={t(($) => (isMobileMenuOpen ? $.layout.header.closeMenu : $.layout.header.openMenu))}
84
+ size="xs"
85
+ variant="ghost"
86
+ onPress={() => setIsMobileMenuOpen((isOpen) => !isOpen)}
87
+ />
88
+ {isMobileMenuOpen ? <MobileNavigation /> : null}
89
+ </header>
90
+ );
91
+ }
@@ -0,0 +1,51 @@
1
+ import { Menu, TextButton } from "@povio/ui";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { User } from "lucide-react";
4
+ import type { Key } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ import { useAuth } from "@/hooks/useAuth";
8
+
9
+ import { NavLink } from "./NavLink";
10
+
11
+ export function MobileNavigation() {
12
+ const { t } = useTranslation();
13
+ const navigate = useNavigate();
14
+ const { isAuthenticated, user, logout } = useAuth();
15
+ const accountLabel = user?.name || user?.email || t(($) => $.layout.header.profile);
16
+
17
+ const handleMenuAction = (key: Key) => {
18
+ if (key === "profile") {
19
+ navigate({ to: "/profile" });
20
+ } else if (key === "logout") {
21
+ logout();
22
+ }
23
+ };
24
+
25
+ return (
26
+ <div className="-mx-4 flex w-[calc(100%+2rem)] basis-full flex-col gap-2 border-elevation-outline-default-1 border-t px-4 py-3 lg:hidden">
27
+ <nav className="flex flex-col gap-1">
28
+ <NavLink to="/">{t(($) => $.layout.header.nav.home)}</NavLink>
29
+ </nav>
30
+
31
+ {isAuthenticated ? (
32
+ <Menu
33
+ items={[
34
+ { id: "profile", label: t(($) => $.layout.header.profile) },
35
+ { id: "logout", label: t(($) => $.layout.header.logout) },
36
+ ]}
37
+ trigger={
38
+ <TextButton
39
+ className="mr-auto"
40
+ color="secondary"
41
+ icon={User}
42
+ >
43
+ {accountLabel}
44
+ </TextButton>
45
+ }
46
+ onAction={handleMenuAction}
47
+ />
48
+ ) : null}
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,29 @@
1
+ import { Link, type LinkProps, Typography } from "@povio/ui";
2
+ import { useMatchRoute } from "@tanstack/react-router";
3
+ import clsx from "clsx";
4
+
5
+ export function NavLink({ to, children, ...props }: LinkProps) {
6
+ const matchRoute = useMatchRoute();
7
+ const isActive = Boolean(matchRoute({ to, fuzzy: true }));
8
+
9
+ return (
10
+ <Link
11
+ to={to}
12
+ className={clsx(
13
+ "no-underline! flex min-h-auto items-center rounded-xs px-button-side-xs py-button-height-xs font-medium transition-colors",
14
+ isActive
15
+ ? "bg-elevation-fill-default-4 font-semibold text-interactive-text-primary-idle"
16
+ : "text-text-default-2 hover:bg-elevation-fill-default-2 hover:text-text-default-1",
17
+ )}
18
+ {...props}
19
+ >
20
+ <Typography
21
+ className="relative whitespace-nowrap no-underline! after:invisible after:block after:h-0 after:overflow-hidden after:font-semibold after:content-[attr(data-text)]"
22
+ size="label-2"
23
+ data-text={children}
24
+ >
25
+ {children}
26
+ </Typography>
27
+ </Link>
28
+ );
29
+ }
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx";
2
+ import type { HTMLAttributes } from "react";
3
+
4
+ export function BrandLogo({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
5
+ return (
6
+ <span
7
+ className={clsx("inline-flex h-[34px] items-center font-semibold text-xl tracking-tight", className)}
8
+ {...props}
9
+ >
10
+ Tiny
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,43 @@
1
+ import { ErrorHandler } from "@povio/openapi-codegen-cli";
2
+ import { Button, Typography } from "@povio/ui";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { ThinPageWrapper } from "@/components/shared/layout/ThinPageWrapper";
6
+
7
+ export interface ErrorFallbackProps {
8
+ error: unknown;
9
+ resetError: () => void;
10
+ }
11
+
12
+ export const ErrorFallback = ({ error, resetError }: ErrorFallbackProps) => {
13
+ const { t } = useTranslation();
14
+
15
+ return (
16
+ <ThinPageWrapper>
17
+ <main className="flex flex-col items-center justify-center gap-2 p-8">
18
+ <Typography
19
+ size="headline-1"
20
+ variant="prominent-1"
21
+ as="h2"
22
+ >
23
+ {t(($) => $.shared.errorFallback.heading)}
24
+ </Typography>
25
+ <Typography
26
+ size="title-2"
27
+ className="text-center text-text-error-1"
28
+ >
29
+ {t(($) => $.shared.errorFallback.subheading, {
30
+ error: ErrorHandler.getErrorMessage(error),
31
+ })}
32
+ </Typography>
33
+ <Button
34
+ size="s"
35
+ type="button"
36
+ onPress={resetError}
37
+ >
38
+ {t(($) => $.shared.errorFallback.retryBtn)}
39
+ </Button>
40
+ </main>
41
+ </ThinPageWrapper>
42
+ );
43
+ };
@@ -0,0 +1,20 @@
1
+ import { Typography } from "@povio/ui";
2
+ import { clsx } from "clsx";
3
+
4
+ interface Props {
5
+ error?: string;
6
+ isDisabled?: boolean;
7
+ isHidden?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ export const ErrorText = ({ error, isDisabled, isHidden, className }: Props) => {
12
+ return (
13
+ <Typography
14
+ className={clsx("text-text-error-1", !error || isDisabled || isHidden ? "sr-only" : "mt-1-5", className)}
15
+ size="label-3"
16
+ >
17
+ {error}
18
+ </Typography>
19
+ );
20
+ };
@@ -0,0 +1,36 @@
1
+ import { Button, Typography } from "@povio/ui";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { ThinPageWrapper } from "@/components/shared/layout/ThinPageWrapper";
5
+
6
+ export const NotFound = () => {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <ThinPageWrapper>
11
+ <div className="flex flex-col items-center justify-center gap-2 p-8 text-center">
12
+ <Typography
13
+ size="title-1"
14
+ variant="prominent-1"
15
+ as="h2"
16
+ >
17
+ {t(($) => $.shared.notFound.heading)}
18
+ </Typography>
19
+
20
+ <Typography
21
+ size="body-2"
22
+ className="text-center text-text-error-1"
23
+ >
24
+ {t(($) => $.shared.notFound.subheading)}
25
+ </Typography>
26
+
27
+ <Button
28
+ size="s"
29
+ link={{ to: "/" }}
30
+ >
31
+ {t(($) => $.shared.notFound.homeBtn)}
32
+ </Button>
33
+ </div>
34
+ </ThinPageWrapper>
35
+ );
36
+ };