robodev 0.12.0 → 0.14.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 (157) hide show
  1. package/package.json +1 -1
  2. package/src/emit-starter.test.ts +14 -0
  3. package/src/index.ts +54 -45
  4. package/src/parse-create-args.test.ts +26 -0
  5. package/src/parse-create-args.ts +63 -0
  6. package/templates/auth-chat/.config/local.spa.template.yml +27 -0
  7. package/templates/auth-chat/.oxlint-base.json +29 -0
  8. package/templates/auth-chat/.oxlintrc.json +78 -0
  9. package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +36 -0
  10. package/templates/auth-chat/.rulesync/rules/frontend-component-structure.md +38 -0
  11. package/templates/auth-chat/.rulesync/rules/frontend-forms.md +32 -0
  12. package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +24 -0
  13. package/templates/auth-chat/.rulesync/rules/frontend-povio-components.md +42 -0
  14. package/templates/auth-chat/.rulesync/rules/frontend-povio-ui.md +45 -0
  15. package/templates/auth-chat/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  16. package/templates/auth-chat/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  17. package/templates/auth-chat/.rulesync/rules/frontend-translations.md +26 -0
  18. package/templates/auth-chat/.rulesync/rules/project-overview.md +32 -0
  19. package/templates/auth-chat/.rulesync/rules/robodev-api.md +30 -0
  20. package/templates/auth-chat/.rulesync/rules/robodev-auth.md +40 -0
  21. package/templates/auth-chat/.rulesync/rules/robodev-database.md +22 -0
  22. package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +36 -0
  23. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +21 -0
  24. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  25. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  26. package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  27. package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +12 -0
  28. package/templates/auth-chat/README.md +26 -7
  29. package/templates/auth-chat/api/invite.ts +43 -0
  30. package/templates/auth-chat/apps/fe/index.html +24 -0
  31. package/templates/auth-chat/apps/fe/openapi-codegen.config.ts +37 -0
  32. package/templates/auth-chat/apps/fe/package.json +82 -0
  33. package/templates/auth-chat/apps/fe/public/apple-touch-icon.png +0 -0
  34. package/templates/auth-chat/apps/fe/public/favicon-96x96.png +0 -0
  35. package/templates/auth-chat/apps/fe/public/favicon.ico +0 -0
  36. package/templates/auth-chat/apps/fe/public/favicon.svg +3 -0
  37. package/templates/auth-chat/apps/fe/public/site.webmanifest +21 -0
  38. package/templates/auth-chat/apps/fe/public/web-app-manifest-192x192.png +0 -0
  39. package/templates/auth-chat/apps/fe/public/web-app-manifest-512x512.png +0 -0
  40. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  41. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  42. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  43. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  44. package/templates/auth-chat/apps/fe/src/assets/locales/en/translation.json +202 -0
  45. package/templates/auth-chat/apps/fe/src/assets/locales/sl/translation.json +202 -0
  46. package/templates/auth-chat/apps/fe/src/clients/app-rest-client.ts +15 -0
  47. package/templates/auth-chat/apps/fe/src/clients/auth-token-store.ts +101 -0
  48. package/templates/auth-chat/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  49. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  50. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  51. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  52. package/templates/auth-chat/apps/fe/src/components/404.tsx +18 -0
  53. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  54. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  55. package/templates/auth-chat/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  56. package/templates/auth-chat/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  57. package/templates/auth-chat/apps/fe/src/components/features/chat/ChatPage.tsx +105 -0
  58. package/templates/auth-chat/apps/fe/src/components/features/chat/InviteForm.tsx +84 -0
  59. package/templates/auth-chat/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  60. package/templates/auth-chat/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  61. package/templates/auth-chat/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  62. package/templates/auth-chat/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  63. package/templates/auth-chat/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  64. package/templates/auth-chat/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  65. package/templates/auth-chat/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  66. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  67. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  68. package/templates/auth-chat/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  69. package/templates/auth-chat/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  70. package/templates/auth-chat/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  71. package/templates/auth-chat/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  72. package/templates/auth-chat/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  73. package/templates/auth-chat/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  74. package/templates/auth-chat/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  75. package/templates/auth-chat/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  76. package/templates/auth-chat/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  77. package/templates/auth-chat/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  78. package/templates/auth-chat/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  79. package/templates/auth-chat/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  80. package/templates/auth-chat/apps/fe/src/config/app.config.ts +35 -0
  81. package/templates/auth-chat/apps/fe/src/config/i18n.ts +43 -0
  82. package/templates/auth-chat/apps/fe/src/config/inits/a11y.ts +14 -0
  83. package/templates/auth-chat/apps/fe/src/config/inits/logger.ts +7 -0
  84. package/templates/auth-chat/apps/fe/src/config/inits/sentry.ts +21 -0
  85. package/templates/auth-chat/apps/fe/src/config/jwt.config.ts +2 -0
  86. package/templates/auth-chat/apps/fe/src/config/query.config.ts +20 -0
  87. package/templates/auth-chat/apps/fe/src/hooks/useAuth.ts +5 -0
  88. package/templates/auth-chat/apps/fe/src/main.tsx +52 -0
  89. package/templates/auth-chat/apps/fe/src/pages/(guest)/login.tsx +23 -0
  90. package/templates/auth-chat/apps/fe/src/pages/(guest)/register.tsx +23 -0
  91. package/templates/auth-chat/apps/fe/src/pages/(guest)/route.tsx +18 -0
  92. package/templates/auth-chat/apps/fe/src/pages/(private)/index.tsx +23 -0
  93. package/templates/auth-chat/apps/fe/src/pages/(private)/profile.tsx +23 -0
  94. package/templates/auth-chat/apps/fe/src/pages/(private)/route.tsx +18 -0
  95. package/templates/auth-chat/apps/fe/src/pages/(public)/auth.tsx +37 -0
  96. package/templates/auth-chat/apps/fe/src/pages/(public)/route.tsx +9 -0
  97. package/templates/auth-chat/apps/fe/src/pages/__root.tsx +164 -0
  98. package/templates/auth-chat/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  99. package/templates/auth-chat/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  100. package/templates/auth-chat/apps/fe/src/providers/index.tsx +44 -0
  101. package/templates/auth-chat/apps/fe/src/providers/jwt.provider.tsx +95 -0
  102. package/templates/auth-chat/apps/fe/src/routeTree.gen.ts +225 -0
  103. package/templates/auth-chat/apps/fe/src/styles/base.css +103 -0
  104. package/templates/auth-chat/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  105. package/templates/auth-chat/apps/fe/src/styles/fonts/general-sans.css +31 -0
  106. package/templates/auth-chat/apps/fe/src/styles/globals.css +28 -0
  107. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  108. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  109. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  110. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  111. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  112. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  113. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  114. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  115. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  116. package/templates/auth-chat/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  117. package/templates/auth-chat/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  118. package/templates/auth-chat/apps/fe/src/styles/theme.css +2177 -0
  119. package/templates/auth-chat/apps/fe/src/types/i18next.d.ts +12 -0
  120. package/templates/auth-chat/apps/fe/src/types/table.d.ts +17 -0
  121. package/templates/auth-chat/apps/fe/src/types/ui.d.ts +26 -0
  122. package/templates/auth-chat/apps/fe/src/types/vite-env.d.ts +20 -0
  123. package/templates/auth-chat/apps/fe/src/utils/date.utils.ts +17 -0
  124. package/templates/auth-chat/apps/fe/src/utils/number.utils.ts +12 -0
  125. package/templates/auth-chat/apps/fe/src/utils/string.utils.ts +5 -0
  126. package/templates/auth-chat/apps/fe/src/vite-env.d.ts +1 -0
  127. package/templates/auth-chat/apps/fe/tsconfig.app.json +32 -0
  128. package/templates/auth-chat/apps/fe/tsconfig.json +9 -0
  129. package/templates/auth-chat/apps/fe/tsconfig.node.json +31 -0
  130. package/templates/auth-chat/apps/fe/vite.config.ts +94 -0
  131. package/templates/auth-chat/openapi.json +1016 -0
  132. package/templates/auth-chat/oxfmt.config.js +23 -0
  133. package/templates/auth-chat/package.json +6 -13
  134. package/templates/auth-chat/rulesync.jsonc +18 -0
  135. package/templates/auth-chat/tsconfig.json +2 -4
  136. package/templates/backend/README.md +2 -2
  137. package/templates/backend/api/_lib.ts +359 -0
  138. package/templates/backend/api/aliens/labels.ts +19 -0
  139. package/templates/backend/api/files/presigned-url.ts +48 -0
  140. package/templates/backend/api/files/upload.ts +29 -0
  141. package/templates/backend/api/planets/[id]/like.ts +36 -0
  142. package/templates/backend/api/planets/[id].ts +56 -13
  143. package/templates/backend/api/planets/paginate.ts +36 -0
  144. package/templates/backend/api/planets.ts +52 -44
  145. package/templates/backend/database.ts +33 -13
  146. package/templates/backend/package.json +1 -1
  147. package/templates/catalog.json +2 -2
  148. package/templates/empty/package.json +1 -1
  149. package/templates/space/package.json +1 -1
  150. package/templates/auth-chat/index.html +0 -201
  151. package/templates/auth-chat/src/main.tsx +0 -282
  152. package/templates/auth-chat/vite.config.ts +0 -7
  153. package/templates/backend/api/health.ts +0 -7
  154. package/templates/backend/api/launch.ts +0 -29
  155. package/templates/backend/api/me.ts +0 -14
  156. package/templates/backend/api/motto.ts +0 -9
  157. package/templates/backend/api/rockets.ts +0 -43
@@ -0,0 +1,101 @@
1
+ import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/config/jwt.config";
2
+
3
+ type TokenListener = () => void;
4
+
5
+ const listeners = new Set<TokenListener>();
6
+ let storageListenerCount = 0;
7
+
8
+ const isBrowser = () => typeof window !== "undefined";
9
+
10
+ const readToken = (key: string) => {
11
+ if (!isBrowser()) {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ return window.localStorage.getItem(key);
17
+ } catch {
18
+ return null;
19
+ }
20
+ };
21
+
22
+ const writeToken = (key: string, token: string | null) => {
23
+ if (!isBrowser()) {
24
+ return;
25
+ }
26
+
27
+ try {
28
+ if (token) {
29
+ window.localStorage.setItem(key, token);
30
+ } else {
31
+ window.localStorage.removeItem(key);
32
+ }
33
+ } catch {
34
+ // Keep in-memory auth subscribers in sync even if storage is unavailable.
35
+ }
36
+ };
37
+
38
+ const emitChange = () => {
39
+ for (const listener of listeners) {
40
+ listener();
41
+ }
42
+ };
43
+
44
+ const onStorageChange = (event: StorageEvent) => {
45
+ if (event.storageArea !== window.localStorage) {
46
+ return;
47
+ }
48
+
49
+ if (event.key === ACCESS_TOKEN_KEY || event.key === REFRESH_TOKEN_KEY || event.key === null) {
50
+ emitChange();
51
+ }
52
+ };
53
+
54
+ export const authTokenStore = {
55
+ getAccessToken: () => readToken(ACCESS_TOKEN_KEY),
56
+ getRefreshToken: () => readToken(REFRESH_TOKEN_KEY),
57
+ getServerSnapshot: () => null,
58
+ subscribe: (listener: TokenListener) => {
59
+ listeners.add(listener);
60
+
61
+ if (isBrowser()) {
62
+ if (storageListenerCount === 0) {
63
+ window.addEventListener("storage", onStorageChange);
64
+ }
65
+ storageListenerCount += 1;
66
+ }
67
+
68
+ return () => {
69
+ listeners.delete(listener);
70
+
71
+ if (isBrowser()) {
72
+ storageListenerCount -= 1;
73
+ if (storageListenerCount === 0) {
74
+ window.removeEventListener("storage", onStorageChange);
75
+ }
76
+ }
77
+ };
78
+ },
79
+ setAccessToken: (token: string | null) => {
80
+ writeToken(ACCESS_TOKEN_KEY, token);
81
+ emitChange();
82
+ },
83
+ setRefreshToken: (token: string | null) => {
84
+ writeToken(REFRESH_TOKEN_KEY, token);
85
+ emitChange();
86
+ },
87
+ setTokens: (accessToken: string | null, refreshToken?: string | null) => {
88
+ writeToken(ACCESS_TOKEN_KEY, accessToken);
89
+
90
+ if (refreshToken !== undefined) {
91
+ writeToken(REFRESH_TOKEN_KEY, refreshToken);
92
+ }
93
+
94
+ emitChange();
95
+ },
96
+ clear: () => {
97
+ writeToken(ACCESS_TOKEN_KEY, null);
98
+ writeToken(REFRESH_TOKEN_KEY, null);
99
+ emitChange();
100
+ },
101
+ };
@@ -0,0 +1,31 @@
1
+ import { type ErrorEntry, ErrorHandler } from "@povio/openapi-codegen-cli";
2
+
3
+ export enum AuthErrorCodes {
4
+ InvalidCredentials = "invalid-credentials",
5
+ LocalAuthnIdentityExists = "identity-already-exists",
6
+ IdentityNotFound = "identity-not-found",
7
+ NotConfirmed = "identity-not-confirmed",
8
+ }
9
+
10
+ const authErrors: ErrorEntry<AuthErrorCodes>[] = [
11
+ {
12
+ code: AuthErrorCodes.InvalidCredentials,
13
+ getMessage: (t) => t("auth.shared.errors.invalidCredentials"),
14
+ },
15
+ {
16
+ code: AuthErrorCodes.LocalAuthnIdentityExists,
17
+ getMessage: (t) => t("auth.shared.errors.userAlreadyExists"),
18
+ },
19
+ {
20
+ code: AuthErrorCodes.IdentityNotFound,
21
+ getMessage: (t) => t("auth.shared.errors.identityNotFound"),
22
+ },
23
+ {
24
+ code: AuthErrorCodes.NotConfirmed,
25
+ getMessage: (t) => t("auth.shared.errors.notConfirmed"),
26
+ },
27
+ ];
28
+
29
+ const entries = [...authErrors];
30
+
31
+ export const AppErrorHandler = new ErrorHandler({ entries });
@@ -0,0 +1,15 @@
1
+ import { RestInterceptor } from "@povio/openapi-codegen-cli";
2
+
3
+ import { authTokenStore } from "@/clients/auth-token-store";
4
+
5
+ export const AuthorizationHeaderInterceptor = new RestInterceptor((client) => {
6
+ return client.interceptors.request.use(async (config) => {
7
+ const accessToken = authTokenStore.getAccessToken();
8
+ if (accessToken != null) {
9
+ // oxlint-disable-next-line no-param-reassign
10
+ config.headers.Authorization = `Bearer ${accessToken}`;
11
+ }
12
+
13
+ return config;
14
+ });
15
+ });
@@ -0,0 +1,37 @@
1
+ import { RestInterceptor } from "@povio/openapi-codegen-cli";
2
+
3
+ import { AuthErrorCodes } from "@/clients/rest/app-error-handler";
4
+ import { UserAuthApi } from "@/openapi/userAuth/userAuth.api";
5
+ import { authTokenStore } from "@/clients/auth-token-store";
6
+
7
+ export const RefreshTokenInterceptor = new RestInterceptor((client) => {
8
+ return client.interceptors.response.use(
9
+ (response) => response,
10
+ // oxlint-disable-next-line promise/prefer-await-to-callbacks
11
+ async (error) => {
12
+ const originalRequest = error.config;
13
+
14
+ if (error.response?.data?.code === AuthErrorCodes.InvalidCredentials) {
15
+ return Promise.reject(error);
16
+ }
17
+
18
+ if (error.response?.status === 401 && !originalRequest._retry) {
19
+ originalRequest._retry = true;
20
+ try {
21
+ const refreshToken = authTokenStore.getRefreshToken();
22
+ const { accessToken, refreshToken: newRefreshToken } = await UserAuthApi.accessToken({
23
+ refreshToken: refreshToken || "",
24
+ });
25
+ authTokenStore.setTokens(accessToken, newRefreshToken || null);
26
+
27
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
28
+
29
+ return client(originalRequest);
30
+ } catch {
31
+ authTokenStore.clear();
32
+ }
33
+ }
34
+ return Promise.reject(error);
35
+ },
36
+ );
37
+ });
@@ -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,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
+ }