robodev 0.12.0 → 0.13.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 (155) hide show
  1. package/package.json +1 -1
  2. package/src/emit-starter.test.ts +14 -0
  3. package/src/index.ts +12 -4
  4. package/templates/auth-chat/.config/local.spa.template.yml +27 -0
  5. package/templates/auth-chat/.oxlint-base.json +29 -0
  6. package/templates/auth-chat/.oxlintrc.json +78 -0
  7. package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +36 -0
  8. package/templates/auth-chat/.rulesync/rules/frontend-component-structure.md +38 -0
  9. package/templates/auth-chat/.rulesync/rules/frontend-forms.md +32 -0
  10. package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +24 -0
  11. package/templates/auth-chat/.rulesync/rules/frontend-povio-components.md +42 -0
  12. package/templates/auth-chat/.rulesync/rules/frontend-povio-ui.md +45 -0
  13. package/templates/auth-chat/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  14. package/templates/auth-chat/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  15. package/templates/auth-chat/.rulesync/rules/frontend-translations.md +26 -0
  16. package/templates/auth-chat/.rulesync/rules/project-overview.md +32 -0
  17. package/templates/auth-chat/.rulesync/rules/robodev-api.md +30 -0
  18. package/templates/auth-chat/.rulesync/rules/robodev-auth.md +40 -0
  19. package/templates/auth-chat/.rulesync/rules/robodev-database.md +22 -0
  20. package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +36 -0
  21. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +21 -0
  22. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  23. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  24. package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  25. package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +12 -0
  26. package/templates/auth-chat/README.md +26 -7
  27. package/templates/auth-chat/api/invite.ts +43 -0
  28. package/templates/auth-chat/apps/fe/index.html +24 -0
  29. package/templates/auth-chat/apps/fe/openapi-codegen.config.ts +37 -0
  30. package/templates/auth-chat/apps/fe/package.json +82 -0
  31. package/templates/auth-chat/apps/fe/public/apple-touch-icon.png +0 -0
  32. package/templates/auth-chat/apps/fe/public/favicon-96x96.png +0 -0
  33. package/templates/auth-chat/apps/fe/public/favicon.ico +0 -0
  34. package/templates/auth-chat/apps/fe/public/favicon.svg +3 -0
  35. package/templates/auth-chat/apps/fe/public/site.webmanifest +21 -0
  36. package/templates/auth-chat/apps/fe/public/web-app-manifest-192x192.png +0 -0
  37. package/templates/auth-chat/apps/fe/public/web-app-manifest-512x512.png +0 -0
  38. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  39. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  40. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  41. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  42. package/templates/auth-chat/apps/fe/src/assets/locales/en/translation.json +202 -0
  43. package/templates/auth-chat/apps/fe/src/assets/locales/sl/translation.json +202 -0
  44. package/templates/auth-chat/apps/fe/src/clients/app-rest-client.ts +15 -0
  45. package/templates/auth-chat/apps/fe/src/clients/auth-token-store.ts +101 -0
  46. package/templates/auth-chat/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  47. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  48. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  49. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  50. package/templates/auth-chat/apps/fe/src/components/404.tsx +18 -0
  51. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  52. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  53. package/templates/auth-chat/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  54. package/templates/auth-chat/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  55. package/templates/auth-chat/apps/fe/src/components/features/chat/ChatPage.tsx +105 -0
  56. package/templates/auth-chat/apps/fe/src/components/features/chat/InviteForm.tsx +84 -0
  57. package/templates/auth-chat/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  58. package/templates/auth-chat/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  59. package/templates/auth-chat/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  60. package/templates/auth-chat/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  61. package/templates/auth-chat/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  62. package/templates/auth-chat/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  63. package/templates/auth-chat/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  64. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  65. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  66. package/templates/auth-chat/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  67. package/templates/auth-chat/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  68. package/templates/auth-chat/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  69. package/templates/auth-chat/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  70. package/templates/auth-chat/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  71. package/templates/auth-chat/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  72. package/templates/auth-chat/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  73. package/templates/auth-chat/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  74. package/templates/auth-chat/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  75. package/templates/auth-chat/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  76. package/templates/auth-chat/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  77. package/templates/auth-chat/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  78. package/templates/auth-chat/apps/fe/src/config/app.config.ts +35 -0
  79. package/templates/auth-chat/apps/fe/src/config/i18n.ts +43 -0
  80. package/templates/auth-chat/apps/fe/src/config/inits/a11y.ts +14 -0
  81. package/templates/auth-chat/apps/fe/src/config/inits/logger.ts +7 -0
  82. package/templates/auth-chat/apps/fe/src/config/inits/sentry.ts +21 -0
  83. package/templates/auth-chat/apps/fe/src/config/jwt.config.ts +2 -0
  84. package/templates/auth-chat/apps/fe/src/config/query.config.ts +20 -0
  85. package/templates/auth-chat/apps/fe/src/hooks/useAuth.ts +5 -0
  86. package/templates/auth-chat/apps/fe/src/main.tsx +52 -0
  87. package/templates/auth-chat/apps/fe/src/pages/(guest)/login.tsx +23 -0
  88. package/templates/auth-chat/apps/fe/src/pages/(guest)/register.tsx +23 -0
  89. package/templates/auth-chat/apps/fe/src/pages/(guest)/route.tsx +18 -0
  90. package/templates/auth-chat/apps/fe/src/pages/(private)/index.tsx +23 -0
  91. package/templates/auth-chat/apps/fe/src/pages/(private)/profile.tsx +23 -0
  92. package/templates/auth-chat/apps/fe/src/pages/(private)/route.tsx +18 -0
  93. package/templates/auth-chat/apps/fe/src/pages/(public)/auth.tsx +37 -0
  94. package/templates/auth-chat/apps/fe/src/pages/(public)/route.tsx +9 -0
  95. package/templates/auth-chat/apps/fe/src/pages/__root.tsx +164 -0
  96. package/templates/auth-chat/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  97. package/templates/auth-chat/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  98. package/templates/auth-chat/apps/fe/src/providers/index.tsx +44 -0
  99. package/templates/auth-chat/apps/fe/src/providers/jwt.provider.tsx +95 -0
  100. package/templates/auth-chat/apps/fe/src/routeTree.gen.ts +225 -0
  101. package/templates/auth-chat/apps/fe/src/styles/base.css +103 -0
  102. package/templates/auth-chat/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  103. package/templates/auth-chat/apps/fe/src/styles/fonts/general-sans.css +31 -0
  104. package/templates/auth-chat/apps/fe/src/styles/globals.css +28 -0
  105. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  106. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  107. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  108. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  109. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  110. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  111. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  112. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  113. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  114. package/templates/auth-chat/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  115. package/templates/auth-chat/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  116. package/templates/auth-chat/apps/fe/src/styles/theme.css +2177 -0
  117. package/templates/auth-chat/apps/fe/src/types/i18next.d.ts +12 -0
  118. package/templates/auth-chat/apps/fe/src/types/table.d.ts +17 -0
  119. package/templates/auth-chat/apps/fe/src/types/ui.d.ts +26 -0
  120. package/templates/auth-chat/apps/fe/src/types/vite-env.d.ts +20 -0
  121. package/templates/auth-chat/apps/fe/src/utils/date.utils.ts +17 -0
  122. package/templates/auth-chat/apps/fe/src/utils/number.utils.ts +12 -0
  123. package/templates/auth-chat/apps/fe/src/utils/string.utils.ts +5 -0
  124. package/templates/auth-chat/apps/fe/src/vite-env.d.ts +1 -0
  125. package/templates/auth-chat/apps/fe/tsconfig.app.json +32 -0
  126. package/templates/auth-chat/apps/fe/tsconfig.json +9 -0
  127. package/templates/auth-chat/apps/fe/tsconfig.node.json +31 -0
  128. package/templates/auth-chat/apps/fe/vite.config.ts +94 -0
  129. package/templates/auth-chat/openapi.json +1016 -0
  130. package/templates/auth-chat/oxfmt.config.js +23 -0
  131. package/templates/auth-chat/package.json +6 -13
  132. package/templates/auth-chat/rulesync.jsonc +18 -0
  133. package/templates/auth-chat/tsconfig.json +2 -4
  134. package/templates/backend/README.md +2 -2
  135. package/templates/backend/api/_lib.ts +359 -0
  136. package/templates/backend/api/aliens/labels.ts +19 -0
  137. package/templates/backend/api/files/presigned-url.ts +48 -0
  138. package/templates/backend/api/files/upload.ts +29 -0
  139. package/templates/backend/api/planets/[id]/like.ts +36 -0
  140. package/templates/backend/api/planets/[id].ts +56 -13
  141. package/templates/backend/api/planets/paginate.ts +36 -0
  142. package/templates/backend/api/planets.ts +52 -44
  143. package/templates/backend/database.ts +33 -13
  144. package/templates/backend/package.json +1 -1
  145. package/templates/catalog.json +2 -2
  146. package/templates/empty/package.json +1 -1
  147. package/templates/space/package.json +1 -1
  148. package/templates/auth-chat/index.html +0 -201
  149. package/templates/auth-chat/src/main.tsx +0 -282
  150. package/templates/auth-chat/vite.config.ts +0 -7
  151. package/templates/backend/api/health.ts +0 -7
  152. package/templates/backend/api/launch.ts +0 -29
  153. package/templates/backend/api/me.ts +0 -14
  154. package/templates/backend/api/motto.ts +0 -9
  155. package/templates/backend/api/rockets.ts +0 -43
@@ -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,105 @@
1
+ import { Button, TextArea, Typography, useForm, useToast } from "@povio/ui/tanstack";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { LoadingState } from "@/components/shared/layout/LoadingState";
5
+ import { PageHeader } from "@/components/shared/page/PageHeader";
6
+ import { Card } from "@/components/shared/ui/Card";
7
+ import { MessageModels } from "@/openapi/message/message.models";
8
+ import { MessageQueries } from "@/openapi/message/message.queries";
9
+
10
+ import { InviteForm } from "./InviteForm";
11
+
12
+ export function ChatPage() {
13
+ const { t } = useTranslation();
14
+ const { errorToast } = useToast();
15
+ const { data: messages, isLoading } = MessageQueries.useGetAll({
16
+ refetchInterval: 2500,
17
+ });
18
+ const createMessage = MessageQueries.useCreate();
19
+
20
+ const form = useForm({
21
+ zodSchema: MessageModels.MessagesCreateRequestSchema,
22
+ defaultValues: {
23
+ body: "",
24
+ },
25
+ });
26
+
27
+ const onSubmit = async (data: MessageModels.MessagesCreateRequest) => {
28
+ try {
29
+ await createMessage.mutateAsync({ data });
30
+ form.reset({ body: "" });
31
+ } catch {
32
+ errorToast({ text: t(($) => $.chat.composer.error) });
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="flex flex-col gap-6">
38
+ <PageHeader title={t(($) => $.chat.title)} />
39
+
40
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
41
+ <Card title={t(($) => $.chat.roomTitle)}>
42
+ <div className="flex flex-col gap-4">
43
+ {isLoading ? (
44
+ <LoadingState />
45
+ ) : !messages || messages.length === 0 ? (
46
+ <Typography
47
+ size="body-3"
48
+ className="text-text-default-2"
49
+ >
50
+ {t(($) => $.chat.empty)}
51
+ </Typography>
52
+ ) : (
53
+ <div className="flex max-h-96 flex-col gap-3 overflow-y-auto">
54
+ {messages.map((message) => (
55
+ <div
56
+ key={message.id}
57
+ className="flex flex-col gap-1 rounded-sm border-elevation-outline-default-1 border-b pb-3"
58
+ >
59
+ <Typography
60
+ size="label-3"
61
+ variant="prominent-1"
62
+ className="text-text-default-1"
63
+ >
64
+ {message.authorName?.trim() || message.authorEmail}
65
+ </Typography>
66
+ <Typography
67
+ size="body-3"
68
+ className="whitespace-pre-wrap text-text-default-1"
69
+ >
70
+ {message.body}
71
+ </Typography>
72
+ </div>
73
+ ))}
74
+ </div>
75
+ )}
76
+
77
+ <form
78
+ onSubmit={form.handleSubmit(onSubmit)}
79
+ className="flex flex-col gap-3"
80
+ >
81
+ <TextArea
82
+ field={{ form, name: "body" }}
83
+ label={t(($) => $.chat.composer.label)}
84
+ placeholder={t(($) => $.chat.composer.placeholder)}
85
+ isRequired
86
+ />
87
+ <div className="flex justify-end">
88
+ <Button
89
+ type="submit"
90
+ isLoading={createMessage.isPending}
91
+ >
92
+ {createMessage.isPending
93
+ ? t(($) => $.chat.composer.submitLoading)
94
+ : t(($) => $.chat.composer.submit)}
95
+ </Button>
96
+ </div>
97
+ </form>
98
+ </div>
99
+ </Card>
100
+
101
+ <InviteForm />
102
+ </div>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,84 @@
1
+ import { Alert, Button, TextInput, Typography, useForm, useToast } from "@povio/ui/tanstack";
2
+ import { useState } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { Card } from "@/components/shared/ui/Card";
6
+ import { InviteModels } from "@/openapi/invite/invite.models";
7
+ import { InviteQueries } from "@/openapi/invite/invite.queries";
8
+
9
+ export function InviteForm() {
10
+ const { t } = useTranslation();
11
+ const { successToast, errorToast } = useToast();
12
+ const sendInvite = InviteQueries.useCreate();
13
+ const [success, setSuccess] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ const form = useForm({
17
+ zodSchema: InviteModels.InviteCreateRequestSchema,
18
+ defaultValues: {
19
+ to: "",
20
+ },
21
+ });
22
+
23
+ const onSubmit = async (data: InviteModels.InviteCreateRequest) => {
24
+ setSuccess(false);
25
+ setError(null);
26
+ try {
27
+ await sendInvite.mutateAsync({ data });
28
+ setSuccess(true);
29
+ successToast({ text: t(($) => $.invite.success) });
30
+ form.reset({ to: "" });
31
+ } catch {
32
+ const message = t(($) => $.invite.error);
33
+ setError(message);
34
+ errorToast({ text: message });
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Card title={t(($) => $.invite.title)}>
40
+ <div className="flex flex-col gap-4">
41
+ <Typography
42
+ size="body-3"
43
+ className="text-text-default-2"
44
+ >
45
+ {t(($) => $.invite.description)}
46
+ </Typography>
47
+
48
+ {success ? (
49
+ <Alert
50
+ color="success"
51
+ text={t(($) => $.invite.success)}
52
+ />
53
+ ) : null}
54
+ {error ? (
55
+ <Alert
56
+ color="error"
57
+ text={error}
58
+ />
59
+ ) : null}
60
+
61
+ <form
62
+ onSubmit={form.handleSubmit(onSubmit)}
63
+ className="flex flex-col gap-4"
64
+ >
65
+ <TextInput
66
+ field={{ form, name: "to" }}
67
+ label={t(($) => $.invite.email)}
68
+ placeholder={t(($) => $.invite.emailPlaceholder)}
69
+ type="email"
70
+ isRequired
71
+ />
72
+ <div className="flex justify-end">
73
+ <Button
74
+ type="submit"
75
+ isLoading={sendInvite.isPending}
76
+ >
77
+ {sendInvite.isPending ? t(($) => $.invite.submitLoading) : t(($) => $.invite.submit)}
78
+ </Button>
79
+ </div>
80
+ </form>
81
+ </div>
82
+ </Card>
83
+ );
84
+ }
@@ -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,35 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ import { AppConfig } from "@/config/app.config";
4
+
5
+ const { measurementId } = AppConfig.googleAnalytics;
6
+
7
+ const GoogleAnalytics = () => {
8
+ const isInitializedRef = useRef(false);
9
+
10
+ useEffect(() => {
11
+ if (isInitializedRef.current || !measurementId || (window as any).gtag) {
12
+ return;
13
+ }
14
+ isInitializedRef.current = true;
15
+
16
+ const script1 = document.createElement("script");
17
+ script1.async = true;
18
+ script1.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
19
+
20
+ const script2 = document.createElement("script");
21
+ script2.innerHTML = `
22
+ window.dataLayer = window.dataLayer || [];
23
+ function gtag(){dataLayer.push(arguments);}
24
+ gtag('js', new Date());
25
+ gtag('config', '${measurementId}');
26
+ `;
27
+
28
+ document.head.appendChild(script1);
29
+ document.head.appendChild(script2);
30
+ }, []);
31
+
32
+ return null;
33
+ };
34
+
35
+ export default GoogleAnalytics;
@@ -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
+ }