robodev 0.47.3 → 0.49.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 (175) hide show
  1. package/package.json +3 -3
  2. package/src/dev-inspect.test.ts +15 -0
  3. package/src/dev-inspect.ts +10 -3
  4. package/src/dev-server.ts +6 -0
  5. package/src/emit-starter.test.ts +6 -3
  6. package/templates/auth-chat/ROBODEV.md +7 -1
  7. package/templates/auth-chat/package.json +2 -2
  8. package/templates/backend/ROBODEV.md +7 -1
  9. package/templates/backend/package.json +2 -2
  10. package/templates/catalog.json +5 -0
  11. package/templates/empty/ROBODEV.md +7 -1
  12. package/templates/empty/package.json +2 -2
  13. package/templates/marketplace/ROBODEV.md +7 -1
  14. package/templates/marketplace/package.json +2 -2
  15. package/templates/povio/.config/local.spa.template.yml +27 -0
  16. package/templates/povio/.oxlint-base.json +29 -0
  17. package/templates/povio/.oxlintrc.json +78 -0
  18. package/templates/povio/.rulesync/rules/frontend-api-boundary.md +36 -0
  19. package/templates/povio/.rulesync/rules/frontend-component-structure.md +40 -0
  20. package/templates/povio/.rulesync/rules/frontend-forms.md +27 -0
  21. package/templates/povio/.rulesync/rules/frontend-notifications.md +24 -0
  22. package/templates/povio/.rulesync/rules/frontend-povio-components.md +42 -0
  23. package/templates/povio/.rulesync/rules/frontend-povio-ui.md +45 -0
  24. package/templates/povio/.rulesync/rules/frontend-query-autocomplete.md +24 -0
  25. package/templates/povio/.rulesync/rules/frontend-tables-and-lists.md +26 -0
  26. package/templates/povio/.rulesync/rules/frontend-translations.md +25 -0
  27. package/templates/povio/.rulesync/rules/project-overview.md +32 -0
  28. package/templates/povio/.rulesync/rules/robodev-api.md +30 -0
  29. package/templates/povio/.rulesync/rules/robodev-auth.md +40 -0
  30. package/templates/povio/.rulesync/rules/robodev-database.md +22 -0
  31. package/templates/povio/.rulesync/rules/role-based-app-structure.md +36 -0
  32. package/templates/povio/.rulesync/skills/media-feature/SKILL.md +21 -0
  33. package/templates/povio/.rulesync/skills/povio-ui-styling/SKILL.md +211 -0
  34. package/templates/povio/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  35. package/templates/povio/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  36. package/templates/povio/.rulesync/skills/robodev-table/SKILL.md +12 -0
  37. package/templates/povio/README.md +40 -0
  38. package/templates/povio/ROBODEV.md +84 -0
  39. package/templates/povio/apps/fe/index.html +24 -0
  40. package/templates/povio/apps/fe/openapi-codegen.config.ts +72 -0
  41. package/templates/povio/apps/fe/package.json +71 -0
  42. package/templates/povio/apps/fe/public/apple-touch-icon.png +0 -0
  43. package/templates/povio/apps/fe/public/favicon-96x96.png +0 -0
  44. package/templates/povio/apps/fe/public/favicon.ico +0 -0
  45. package/templates/povio/apps/fe/public/favicon.svg +3 -0
  46. package/templates/povio/apps/fe/public/site.webmanifest +21 -0
  47. package/templates/povio/apps/fe/public/web-app-manifest-192x192.png +0 -0
  48. package/templates/povio/apps/fe/public/web-app-manifest-512x512.png +0 -0
  49. package/templates/povio/apps/fe/src/assets/locales/en/translation.json +166 -0
  50. package/templates/povio/apps/fe/src/clients/app-rest-client.ts +15 -0
  51. package/templates/povio/apps/fe/src/clients/auth-token-store.ts +101 -0
  52. package/templates/povio/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  53. package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  54. package/templates/povio/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +45 -0
  55. package/templates/povio/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  56. package/templates/povio/apps/fe/src/components/404.tsx +18 -0
  57. package/templates/povio/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  58. package/templates/povio/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  59. package/templates/povio/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  60. package/templates/povio/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  61. package/templates/povio/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  62. package/templates/povio/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  63. package/templates/povio/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  64. package/templates/povio/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  65. package/templates/povio/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  66. package/templates/povio/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  67. package/templates/povio/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  68. package/templates/povio/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  69. package/templates/povio/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  70. package/templates/povio/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  71. package/templates/povio/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  72. package/templates/povio/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  73. package/templates/povio/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  74. package/templates/povio/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  75. package/templates/povio/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  76. package/templates/povio/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  77. package/templates/povio/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  78. package/templates/povio/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  79. package/templates/povio/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  80. package/templates/povio/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  81. package/templates/povio/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  82. package/templates/povio/apps/fe/src/config/app.config.ts +23 -0
  83. package/templates/povio/apps/fe/src/config/i18n.ts +37 -0
  84. package/templates/povio/apps/fe/src/config/inits/logger.ts +7 -0
  85. package/templates/povio/apps/fe/src/config/jwt.config.ts +2 -0
  86. package/templates/povio/apps/fe/src/config/query.config.ts +20 -0
  87. package/templates/povio/apps/fe/src/hooks/useAuth.ts +5 -0
  88. package/templates/povio/apps/fe/src/main.tsx +52 -0
  89. package/templates/povio/apps/fe/src/pages/(guest)/login.tsx +23 -0
  90. package/templates/povio/apps/fe/src/pages/(guest)/register.tsx +23 -0
  91. package/templates/povio/apps/fe/src/pages/(guest)/route.tsx +18 -0
  92. package/templates/povio/apps/fe/src/pages/(private)/index.tsx +23 -0
  93. package/templates/povio/apps/fe/src/pages/(private)/profile.tsx +23 -0
  94. package/templates/povio/apps/fe/src/pages/(private)/route.tsx +18 -0
  95. package/templates/povio/apps/fe/src/pages/(public)/auth.tsx +37 -0
  96. package/templates/povio/apps/fe/src/pages/(public)/route.tsx +9 -0
  97. package/templates/povio/apps/fe/src/pages/__root.tsx +133 -0
  98. package/templates/povio/apps/fe/src/providers/AppErrorBoundary.tsx +44 -0
  99. package/templates/povio/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  100. package/templates/povio/apps/fe/src/providers/index.tsx +44 -0
  101. package/templates/povio/apps/fe/src/providers/jwt.provider.tsx +95 -0
  102. package/templates/povio/apps/fe/src/routeTree.gen.ts +225 -0
  103. package/templates/povio/apps/fe/src/styles/base.css +84 -0
  104. package/templates/povio/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  105. package/templates/povio/apps/fe/src/styles/globals.css +26 -0
  106. package/templates/povio/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  107. package/templates/povio/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  108. package/templates/povio/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  109. package/templates/povio/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  110. package/templates/povio/apps/fe/src/styles/overrides/defaults/modal.override.ts +62 -0
  111. package/templates/povio/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  112. package/templates/povio/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  113. package/templates/povio/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  114. package/templates/povio/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  115. package/templates/povio/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  116. package/templates/povio/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  117. package/templates/povio/apps/fe/src/styles/theme.css +2177 -0
  118. package/templates/povio/apps/fe/src/types/i18next.d.ts +12 -0
  119. package/templates/povio/apps/fe/src/types/table.d.ts +17 -0
  120. package/templates/povio/apps/fe/src/types/ui.d.ts +26 -0
  121. package/templates/povio/apps/fe/src/types/vite-env.d.ts +13 -0
  122. package/templates/povio/apps/fe/src/utils/date.utils.ts +17 -0
  123. package/templates/povio/apps/fe/src/utils/image-fallback.ts +9 -0
  124. package/templates/povio/apps/fe/src/utils/number.utils.ts +12 -0
  125. package/templates/povio/apps/fe/src/utils/string.utils.ts +5 -0
  126. package/templates/povio/apps/fe/src/vite-env.d.ts +1 -0
  127. package/templates/povio/apps/fe/tsconfig.app.json +32 -0
  128. package/templates/povio/apps/fe/tsconfig.json +9 -0
  129. package/templates/povio/apps/fe/tsconfig.node.json +31 -0
  130. package/templates/povio/apps/fe/vite.config.ts +150 -0
  131. package/templates/povio/openapi.json +911 -0
  132. package/templates/povio/oxfmt.config.js +23 -0
  133. package/templates/povio/package.json +19 -0
  134. package/templates/povio/robodev/api/_lib.ts +99 -0
  135. package/templates/povio/robodev/api/health.ts +7 -0
  136. package/templates/povio/robodev/database.ts +28 -0
  137. package/templates/povio/robodev/package.json +4 -0
  138. package/templates/povio/rulesync.jsonc +18 -0
  139. package/templates/povio/tsconfig.json +11 -0
  140. package/templates/shadcn/.rulesync/rules/frontend-api-boundary.md +29 -0
  141. package/templates/shadcn/.rulesync/rules/frontend-component-structure.md +19 -0
  142. package/templates/shadcn/.rulesync/rules/frontend-hosted-ui.md +38 -0
  143. package/templates/shadcn/.rulesync/rules/project-overview.md +29 -0
  144. package/templates/shadcn/.rulesync/rules/robodev-api.md +22 -0
  145. package/templates/shadcn/.rulesync/rules/robodev-auth.md +47 -0
  146. package/templates/shadcn/.rulesync/rules/robodev-database.md +21 -0
  147. package/templates/shadcn/.rulesync/skills/robodev-crud-page/SKILL.md +181 -0
  148. package/templates/shadcn/README.md +9 -4
  149. package/templates/shadcn/ROBODEV.md +14 -6
  150. package/templates/shadcn/package.json +4 -6
  151. package/templates/shadcn/rulesync.jsonc +18 -0
  152. package/templates/shadcn/src/App.tsx +12 -3
  153. package/templates/shadcn/src/app.css +359 -0
  154. package/templates/shadcn/src/components/auth/AuthCard.tsx +29 -0
  155. package/templates/shadcn/src/components/ui/badge.tsx +17 -0
  156. package/templates/shadcn/src/components/ui/card.tsx +28 -0
  157. package/templates/shadcn/src/components/ui/dialog.tsx +38 -0
  158. package/templates/shadcn/src/components/ui/empty-state.tsx +17 -0
  159. package/templates/shadcn/src/components/ui/input.tsx +8 -0
  160. package/templates/shadcn/src/components/ui/label.tsx +8 -0
  161. package/templates/shadcn/src/components/ui/skeleton.tsx +6 -0
  162. package/templates/shadcn/src/components/ui/spinner.tsx +3 -0
  163. package/templates/shadcn/src/components/ui/table.tsx +30 -0
  164. package/templates/shadcn/src/components/ui/textarea.tsx +8 -0
  165. package/templates/shadcn/src/lib/auth.tsx +72 -0
  166. package/templates/shadcn/src/lib/client.ts +8 -0
  167. package/templates/shadcn/src/pages/Home.tsx +15 -0
  168. package/templates/shadcn/src/pages/Login.tsx +70 -0
  169. package/templates/shadcn/src/pages/NotFound.tsx +14 -0
  170. package/templates/shadcn/src/pages/Register.tsx +82 -0
  171. package/templates/space/ROBODEV.md +7 -1
  172. package/templates/space/package.json +2 -2
  173. package/templates/shadcn/components.json +0 -21
  174. package/templates/shadcn/postcss.config.js +0 -6
  175. package/templates/shadcn/tailwind.config.ts +0 -42
@@ -0,0 +1,17 @@
1
+ import { RestInterceptor } from "@povio/openapi-codegen-cli";
2
+ import type { AxiosError, AxiosResponse } from "axios";
3
+
4
+ export const ResponseInterceptor = new RestInterceptor((client) => {
5
+ return client.interceptors.response.use(
6
+ async (response: AxiosResponse) => {
7
+ return {
8
+ ...response,
9
+ data: response.data === "" ? undefined : response.data,
10
+ };
11
+ },
12
+ // oxlint-disable-next-line promise/prefer-await-to-callbacks
13
+ async (error: AxiosError) => {
14
+ throw error;
15
+ },
16
+ );
17
+ });
@@ -0,0 +1,18 @@
1
+ import { useTranslation } from "react-i18next";
2
+
3
+ import { NotFound } from "@/components/shared/error/NotFound";
4
+ import { AppHead } from "@/components/shared/head/AppHead";
5
+
6
+ export const NotFoundPage = () => {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <>
11
+ <AppHead
12
+ title={t(($) => $.shared.notFound.head.title)}
13
+ description={t(($) => $.shared.notFound.head.description)}
14
+ />
15
+ <NotFound />
16
+ </>
17
+ );
18
+ };
@@ -0,0 +1,60 @@
1
+ import { Typography } from "@povio/ui";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { BrandLogo } from "@/components/shared/branding/BrandLogo";
5
+
6
+ export function AuthBrandPanel() {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <section className="relative hidden flex-1 overflow-hidden bg-elevation-fill-default-2 lg:flex lg:flex-col lg:justify-between">
11
+ <div className="relative z-raised flex flex-col gap-1 px-16 pt-20 xl:px-24 xl:pt-24">
12
+ <Typography
13
+ as="p"
14
+ className="text-text-default-2"
15
+ size="title-3"
16
+ >
17
+ {t(($) => $.auth.wrapper.title)}
18
+ </Typography>
19
+ <Typography
20
+ as="h1"
21
+ className="text-text-default-2"
22
+ size="title-1"
23
+ variant="prominent-1"
24
+ >
25
+ {t(($) => $.auth.wrapper.description)}
26
+ </Typography>
27
+ </div>
28
+
29
+ <BrandLogo className="absolute -right-12 top-10 text-[10rem] leading-none text-[#e1e9ee]" />
30
+
31
+ <svg
32
+ aria-hidden="true"
33
+ className="absolute bottom-0 left-0 h-auto w-full opacity-20"
34
+ viewBox="0 0 760 390"
35
+ >
36
+ <path
37
+ d="M0 245 80 202l62 22 98-85 92 77 68-45 76 53 84-113 74 95 126-50v234H0Z"
38
+ fill="#006ab6"
39
+ />
40
+ <path
41
+ d="m52 390 132-120 78 61 106-130 91 108 69-59 128 140Z"
42
+ fill="#006ab6"
43
+ opacity=".4"
44
+ />
45
+ </svg>
46
+
47
+ <div className="relative z-raised flex flex-col items-center gap-3 pb-8">
48
+ <BrandLogo className="text-interactive-contained-primary-idle" />
49
+ <Typography
50
+ as="span"
51
+ className="text-text-default-2"
52
+ size="body-4"
53
+ variant="prominent-1"
54
+ >
55
+ {t(($) => $.auth.wrapper.watermark, { year: new Date().getFullYear() })}
56
+ </Typography>
57
+ </div>
58
+ </section>
59
+ );
60
+ }
@@ -0,0 +1,23 @@
1
+ import type { PropsWithChildren } from "react";
2
+
3
+ import { BrandLogo } from "@/components/shared/branding/BrandLogo";
4
+
5
+ import { AuthBrandPanel } from "./AuthBrandPanel";
6
+
7
+ export function AuthLayout({ children }: PropsWithChildren) {
8
+ return (
9
+ <div className="flex min-h-dvh w-full overflow-hidden bg-elevation-fill-default-2">
10
+ <AuthBrandPanel />
11
+
12
+ <section className="m-4 flex w-full flex-1 flex-col rounded-sm bg-elevation-fill-default-1 lg:m-0 lg:rounded-none">
13
+ <div className="flex flex-1 items-center justify-center px-4 py-10 sm:px-8">
14
+ <div className="w-full max-w-[440px]">{children}</div>
15
+ </div>
16
+
17
+ <div className="flex justify-center pb-6 lg:hidden">
18
+ <BrandLogo className="text-interactive-contained-primary-idle" />
19
+ </div>
20
+ </section>
21
+ </div>
22
+ );
23
+ }
@@ -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
+ }