robodev 0.11.0 → 0.12.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 (183) hide show
  1. package/package.json +1 -1
  2. package/src/collect-files.test.ts +19 -0
  3. package/src/collect-files.ts +1 -0
  4. package/src/config.test.ts +26 -0
  5. package/src/config.ts +34 -5
  6. package/src/emit-starter.test.ts +54 -0
  7. package/src/emit-starter.ts +47 -13
  8. package/src/index.ts +37 -4
  9. package/templates/auth-chat/package.json +1 -1
  10. package/templates/auth-chat/src/main.tsx +1 -1
  11. package/templates/backend/package.json +1 -1
  12. package/templates/catalog.json +1 -1
  13. package/templates/empty/package.json +1 -1
  14. package/templates/space/.config/local.spa.template.yml +27 -0
  15. package/templates/space/.oxlint-base.json +29 -0
  16. package/templates/space/.oxlintrc.json +78 -0
  17. package/templates/space/.rulesync/rules/frontend-api-boundary.md +36 -0
  18. package/templates/space/.rulesync/rules/frontend-component-structure.md +38 -0
  19. package/templates/space/.rulesync/rules/frontend-forms.md +32 -0
  20. package/templates/space/.rulesync/rules/frontend-notifications.md +24 -0
  21. package/templates/space/.rulesync/rules/frontend-povio-components.md +42 -0
  22. package/templates/space/.rulesync/rules/frontend-povio-ui.md +45 -0
  23. package/templates/space/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  24. package/templates/space/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  25. package/templates/space/.rulesync/rules/frontend-translations.md +26 -0
  26. package/templates/space/.rulesync/rules/project-overview.md +32 -0
  27. package/templates/space/.rulesync/rules/robodev-api.md +30 -0
  28. package/templates/space/.rulesync/rules/robodev-auth.md +40 -0
  29. package/templates/space/.rulesync/rules/robodev-database.md +22 -0
  30. package/templates/space/.rulesync/rules/role-based-app-structure.md +36 -0
  31. package/templates/space/.rulesync/skills/media-feature/SKILL.md +21 -0
  32. package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  33. package/templates/space/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  34. package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  35. package/templates/space/.rulesync/skills/robodev-table/SKILL.md +12 -0
  36. package/templates/space/README.md +26 -7
  37. package/templates/space/api/_lib.ts +359 -0
  38. package/templates/space/api/aliens/labels.ts +19 -0
  39. package/templates/space/api/files/presigned-url.ts +48 -0
  40. package/templates/space/api/files/upload.ts +29 -0
  41. package/templates/space/api/planets/[id]/like.ts +36 -0
  42. package/templates/space/api/planets/[id].ts +56 -13
  43. package/templates/space/api/planets/paginate.ts +36 -0
  44. package/templates/space/api/planets.ts +52 -44
  45. package/templates/space/apps/fe/index.html +24 -0
  46. package/templates/space/apps/fe/openapi-codegen.config.ts +37 -0
  47. package/templates/space/apps/fe/package.json +82 -0
  48. package/templates/space/apps/fe/public/apple-touch-icon.png +0 -0
  49. package/templates/space/apps/fe/public/favicon-96x96.png +0 -0
  50. package/templates/space/apps/fe/public/favicon.ico +0 -0
  51. package/templates/space/apps/fe/public/favicon.svg +3 -0
  52. package/templates/space/apps/fe/public/site.webmanifest +21 -0
  53. package/templates/space/apps/fe/public/web-app-manifest-192x192.png +0 -0
  54. package/templates/space/apps/fe/public/web-app-manifest-512x512.png +0 -0
  55. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  56. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  57. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  58. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  59. package/templates/space/apps/fe/src/assets/locales/en/translation.json +301 -0
  60. package/templates/space/apps/fe/src/assets/locales/sl/translation.json +301 -0
  61. package/templates/space/apps/fe/src/clients/app-rest-client.ts +15 -0
  62. package/templates/space/apps/fe/src/clients/auth-token-store.ts +101 -0
  63. package/templates/space/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  64. package/templates/space/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  65. package/templates/space/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  66. package/templates/space/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  67. package/templates/space/apps/fe/src/components/404.tsx +18 -0
  68. package/templates/space/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  69. package/templates/space/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  70. package/templates/space/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  71. package/templates/space/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  72. package/templates/space/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  73. package/templates/space/apps/fe/src/components/features/planets/details/PlanetDetailsPage.tsx +139 -0
  74. package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +266 -0
  75. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetFeedCard.tsx +131 -0
  76. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetsFeedPage.tsx +119 -0
  77. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCard.tsx +89 -0
  78. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +179 -0
  79. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.css +3 -0
  80. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.tsx +73 -0
  81. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsGridPage.tsx +51 -0
  82. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsInfiniteTablePage.tsx +79 -0
  83. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsPage.tsx +65 -0
  84. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTablePage.tsx +72 -0
  85. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTableSummary.tsx +29 -0
  86. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsViewTabs.tsx +79 -0
  87. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTable.tsx +30 -0
  88. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableActions.tsx +64 -0
  89. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableInfinite.tsx +36 -0
  90. package/templates/space/apps/fe/src/components/features/planets/list/table/planetsTableColumns.tsx +72 -0
  91. package/templates/space/apps/fe/src/components/features/planets/list/table/usePlanetTableFilters.tsx +102 -0
  92. package/templates/space/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  93. package/templates/space/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  94. package/templates/space/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  95. package/templates/space/apps/fe/src/components/layout/app-header/AppHeader.tsx +93 -0
  96. package/templates/space/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +53 -0
  97. package/templates/space/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  98. package/templates/space/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  99. package/templates/space/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  100. package/templates/space/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  101. package/templates/space/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  102. package/templates/space/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  103. package/templates/space/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  104. package/templates/space/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  105. package/templates/space/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  106. package/templates/space/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  107. package/templates/space/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  108. package/templates/space/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  109. package/templates/space/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  110. package/templates/space/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  111. package/templates/space/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  112. package/templates/space/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  113. package/templates/space/apps/fe/src/config/app.config.ts +35 -0
  114. package/templates/space/apps/fe/src/config/i18n.ts +43 -0
  115. package/templates/space/apps/fe/src/config/inits/a11y.ts +14 -0
  116. package/templates/space/apps/fe/src/config/inits/logger.ts +7 -0
  117. package/templates/space/apps/fe/src/config/inits/sentry.ts +21 -0
  118. package/templates/space/apps/fe/src/config/jwt.config.ts +2 -0
  119. package/templates/space/apps/fe/src/config/query.config.ts +20 -0
  120. package/templates/space/apps/fe/src/hooks/useAuth.ts +5 -0
  121. package/templates/space/apps/fe/src/main.tsx +52 -0
  122. package/templates/space/apps/fe/src/pages/(guest)/login.tsx +23 -0
  123. package/templates/space/apps/fe/src/pages/(guest)/register.tsx +23 -0
  124. package/templates/space/apps/fe/src/pages/(guest)/route.tsx +18 -0
  125. package/templates/space/apps/fe/src/pages/(private)/index.tsx +23 -0
  126. package/templates/space/apps/fe/src/pages/(private)/planets/$id/edit.tsx +53 -0
  127. package/templates/space/apps/fe/src/pages/(private)/planets/$id/index.tsx +41 -0
  128. package/templates/space/apps/fe/src/pages/(private)/planets/index.tsx +24 -0
  129. package/templates/space/apps/fe/src/pages/(private)/planets-feed.tsx +23 -0
  130. package/templates/space/apps/fe/src/pages/(private)/profile.tsx +23 -0
  131. package/templates/space/apps/fe/src/pages/(private)/route.tsx +18 -0
  132. package/templates/space/apps/fe/src/pages/(public)/auth.tsx +37 -0
  133. package/templates/space/apps/fe/src/pages/(public)/route.tsx +9 -0
  134. package/templates/space/apps/fe/src/pages/__root.tsx +164 -0
  135. package/templates/space/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  136. package/templates/space/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  137. package/templates/space/apps/fe/src/providers/index.tsx +44 -0
  138. package/templates/space/apps/fe/src/providers/jwt.provider.tsx +95 -0
  139. package/templates/space/apps/fe/src/routeTree.gen.ts +309 -0
  140. package/templates/space/apps/fe/src/styles/base.css +103 -0
  141. package/templates/space/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  142. package/templates/space/apps/fe/src/styles/fonts/general-sans.css +31 -0
  143. package/templates/space/apps/fe/src/styles/globals.css +28 -0
  144. package/templates/space/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  145. package/templates/space/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  146. package/templates/space/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  147. package/templates/space/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  148. package/templates/space/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  149. package/templates/space/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  150. package/templates/space/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  151. package/templates/space/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  152. package/templates/space/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  153. package/templates/space/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  154. package/templates/space/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  155. package/templates/space/apps/fe/src/styles/theme.css +2177 -0
  156. package/templates/space/apps/fe/src/types/i18next.d.ts +12 -0
  157. package/templates/space/apps/fe/src/types/table.d.ts +17 -0
  158. package/templates/space/apps/fe/src/types/ui.d.ts +26 -0
  159. package/templates/space/apps/fe/src/types/vite-env.d.ts +20 -0
  160. package/templates/space/apps/fe/src/utils/date.utils.ts +17 -0
  161. package/templates/space/apps/fe/src/utils/image-fallback.ts +9 -0
  162. package/templates/space/apps/fe/src/utils/media-upload.ts +63 -0
  163. package/templates/space/apps/fe/src/utils/number.utils.ts +12 -0
  164. package/templates/space/apps/fe/src/utils/string.utils.ts +5 -0
  165. package/templates/space/apps/fe/src/vite-env.d.ts +1 -0
  166. package/templates/space/apps/fe/tsconfig.app.json +32 -0
  167. package/templates/space/apps/fe/tsconfig.json +9 -0
  168. package/templates/space/apps/fe/tsconfig.node.json +31 -0
  169. package/templates/space/apps/fe/vite.config.ts +94 -0
  170. package/templates/space/database.ts +33 -13
  171. package/templates/space/openapi.json +1819 -0
  172. package/templates/space/oxfmt.config.js +23 -0
  173. package/templates/space/package.json +6 -13
  174. package/templates/space/rulesync.jsonc +18 -0
  175. package/templates/space/tsconfig.json +2 -4
  176. package/templates/space/api/health.ts +0 -7
  177. package/templates/space/api/launch.ts +0 -29
  178. package/templates/space/api/me.ts +0 -14
  179. package/templates/space/api/motto.ts +0 -9
  180. package/templates/space/api/rockets.ts +0 -43
  181. package/templates/space/index.html +0 -138
  182. package/templates/space/src/main.tsx +0 -229
  183. package/templates/space/vite.config.ts +0 -7
@@ -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,139 @@
1
+ import { Button, Typography } from "@povio/ui";
2
+ import { Pencil } from "lucide-react";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { PageHeader } from "@/components/shared/page/PageHeader";
6
+ import { Card } from "@/components/shared/ui/Card";
7
+ import { useAuth } from "@/hooks/useAuth";
8
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
9
+ import { DateUtils } from "@/utils/date.utils";
10
+ import { getFallbackImageUrl } from "@/utils/image-fallback";
11
+
12
+ interface PlanetDetailsPageProps {
13
+ planet: PlanetModels.PlanetsGetResponseDto;
14
+ }
15
+
16
+ export function PlanetDetailsPage({ planet }: PlanetDetailsPageProps) {
17
+ const { t } = useTranslation();
18
+ const { user } = useAuth();
19
+
20
+ const imageSrc = planet.image?.url ?? getFallbackImageUrl({ width: 800, height: 450 });
21
+ const createdDate = DateUtils.formatDate(new Date(planet.createdAt));
22
+ const updatedDate = DateUtils.formatDate(new Date(planet.updatedAt));
23
+ const discoveryDate = planet.discoveryDate ? DateUtils.formatDate(new Date(planet.discoveryDate)) : null;
24
+ const creatorName = planet.creatorName ?? t(($) => $.planets.card.unknownCreator);
25
+ // Frontend ownership checks are for UI affordances only; the router enforces ownership again on update/delete.
26
+ const canEdit = user?.id === planet.userId;
27
+
28
+ return (
29
+ <div className="flex flex-col gap-6">
30
+ <PageHeader
31
+ enableBack
32
+ backProps={{ backLink: { to: "/planets" } }}
33
+ title={planet.name}
34
+ actions={
35
+ canEdit && (
36
+ <Button
37
+ size="s"
38
+ color="primary"
39
+ icon={Pencil}
40
+ link={{ to: "/planets/$id/edit", params: { id: planet.id } }}
41
+ >
42
+ {t(($) => $.planets.actions.edit)}
43
+ </Button>
44
+ )
45
+ }
46
+ />
47
+
48
+ <Card className="max-w-3xl overflow-hidden p-0">
49
+ <div className="aspect-video w-full bg-elevation-fill-default-2">
50
+ <img
51
+ src={imageSrc}
52
+ alt=""
53
+ className="h-full w-full object-cover"
54
+ />
55
+ </div>
56
+ <div className="flex flex-col gap-5 p-1">
57
+ <Typography
58
+ size="title-5"
59
+ variant="prominent-1"
60
+ as="h3"
61
+ >
62
+ {planet.name}
63
+ </Typography>
64
+
65
+ {planet.description && (
66
+ <Typography
67
+ size="body-3"
68
+ as="p"
69
+ className="whitespace-pre-wrap text-text-default-2"
70
+ >
71
+ {planet.description}
72
+ </Typography>
73
+ )}
74
+
75
+ <dl className="flex flex-col gap-4 border-elevation-outline-default-1 border-t pt-4 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:gap-y-4">
76
+ <div className="flex flex-col gap-1">
77
+ <Typography
78
+ size="label-3"
79
+ className="text-text-default-2"
80
+ >
81
+ {t(($) => $.planets.detail.createdBy)}
82
+ </Typography>
83
+ <Typography
84
+ size="body-3"
85
+ className="text-text-default-1"
86
+ >
87
+ {creatorName}
88
+ </Typography>
89
+ </div>
90
+ <div className="flex flex-col gap-1">
91
+ <Typography
92
+ size="label-3"
93
+ className="text-text-default-2"
94
+ >
95
+ {t(($) => $.planets.detail.created)}
96
+ </Typography>
97
+ <Typography
98
+ size="body-3"
99
+ className="text-text-default-1"
100
+ >
101
+ {createdDate}
102
+ </Typography>
103
+ </div>
104
+ <div className="flex flex-col gap-1 sm:col-span-2">
105
+ <Typography
106
+ size="label-3"
107
+ className="text-text-default-2"
108
+ >
109
+ {t(($) => $.planets.detail.updated)}
110
+ </Typography>
111
+ <Typography
112
+ size="body-3"
113
+ className="text-text-default-1"
114
+ >
115
+ {updatedDate}
116
+ </Typography>
117
+ </div>
118
+ {discoveryDate && (
119
+ <div className="flex flex-col gap-1 sm:col-span-2">
120
+ <Typography
121
+ size="label-3"
122
+ className="text-text-default-2"
123
+ >
124
+ {t(($) => $.planets.detail.discoveryDate)}
125
+ </Typography>
126
+ <Typography
127
+ size="body-3"
128
+ className="text-text-default-1"
129
+ >
130
+ {discoveryDate}
131
+ </Typography>
132
+ </div>
133
+ )}
134
+ </dl>
135
+ </div>
136
+ </Card>
137
+ </div>
138
+ );
139
+ }
@@ -0,0 +1,266 @@
1
+ import {
2
+ Button,
3
+ Confirmation,
4
+ DatePicker,
5
+ FileUpload,
6
+ Modal,
7
+ QueryAutocomplete,
8
+ TextArea,
9
+ TextInput,
10
+ Typography,
11
+ useForm,
12
+ useFormValue,
13
+ useToast,
14
+ } from "@povio/ui/tanstack";
15
+ import { useNavigate } from "@tanstack/react-router";
16
+ import { Trash } from "lucide-react";
17
+ import { useEffect, useState } from "react";
18
+ import { useTranslation } from "react-i18next";
19
+
20
+ import { RowInputWrapper } from "@/components/shared/forms/RowInputWrapper";
21
+ import { AlienQueries } from "@/openapi/alien/alien.queries";
22
+ import { MediaModels } from "@/openapi/media/media.models";
23
+ import { PlanetModels } from "@/openapi/planet/planet.models";
24
+ import { PlanetQueries } from "@/openapi/planet/planet.queries";
25
+ import { useMediaUploadHandler } from "@/utils/media-upload";
26
+
27
+ interface PlanetEditPageProps {
28
+ planet: PlanetModels.PlanetsGetResponseDto;
29
+ onClose?: () => void;
30
+ onDeleted?: () => void;
31
+ onSaved?: () => void;
32
+ }
33
+
34
+ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEditPageProps) {
35
+ const { t } = useTranslation();
36
+ const navigate = useNavigate();
37
+ const updatePlanet = PlanetQueries.useUpdate();
38
+ const deletePlanet = PlanetQueries.useDeleteApiPlanetsById();
39
+ const { successToast, errorToast } = useToast();
40
+ const { confirm } = Confirmation.useConfirmation();
41
+ const [previewImageUrl, setPreviewImageUrl] = useState<string | undefined>(planet.image?.url);
42
+ // Seed the form from the fetched record, then reset when route data changes between planet IDs.
43
+ const form = useForm({
44
+ zodSchema: PlanetModels.PlanetsCreateRequestDtoSchema,
45
+ defaultValues: {
46
+ name: planet.name,
47
+ alienId: planet.alienId ?? undefined,
48
+ discoveryDate: planet.discoveryDate ?? undefined,
49
+ description: planet.description ?? "",
50
+ image: planet.image ? { id: planet.image.id } : undefined,
51
+ },
52
+ });
53
+
54
+ useEffect(() => {
55
+ form.reset({
56
+ name: planet.name,
57
+ alienId: planet.alienId ?? undefined,
58
+ discoveryDate: planet.discoveryDate ?? undefined,
59
+ description: planet.description ?? "",
60
+ image: planet.image ? { id: planet.image.id } : undefined,
61
+ });
62
+ setPreviewImageUrl(planet.image?.url);
63
+ }, [form, planet]);
64
+
65
+ const handleImageUpload = useMediaUploadHandler({
66
+ resourceName: MediaModels.MediaResourceName["planet-image"],
67
+ onUploaded: (media) => {
68
+ form.setFieldValue("image", { id: media.id });
69
+ setPreviewImageUrl(media.previewUrl);
70
+ },
71
+ });
72
+
73
+ const handleImageChange = (_file: File | null) => {
74
+ // When clearing file, keep current image
75
+ if (!_file) {
76
+ form.setFieldValue("image", planet.image ? { id: planet.image.id } : undefined);
77
+ setPreviewImageUrl(planet.image?.url);
78
+ }
79
+ };
80
+
81
+ const onSubmit = async (data: PlanetModels.PlanetsCreateRequestDto) => {
82
+ try {
83
+ await updatePlanet.mutateAsync({ id: planet.id, data });
84
+ successToast({ text: t(($) => $.planets.edit.success) });
85
+ if (onSaved) {
86
+ onSaved();
87
+ } else {
88
+ navigate({ to: "/planets/$id", params: { id: planet.id } });
89
+ }
90
+ } catch {
91
+ errorToast({ text: t(($) => $.planets.edit.error) });
92
+ }
93
+ };
94
+
95
+ const handleDelete = async () => {
96
+ // Destructive actions should go through a shared confirmation flow before calling the mutation.
97
+ const confirmed = await confirm({
98
+ heading: t(($) => $.planets.removeConfirm.heading),
99
+ description: t(($) => $.planets.removeConfirm.description, { name: planet.name }),
100
+ confirmLabel: t(($) => $.planets.removeConfirm.confirm),
101
+ cancelLabel: t(($) => $.planets.removeConfirm.cancel),
102
+ confirmColor: "error",
103
+ cancelVariant: "outlined",
104
+ buttonSize: "s",
105
+ });
106
+
107
+ if (!confirmed) {
108
+ return;
109
+ }
110
+
111
+ try {
112
+ await deletePlanet.mutateAsync({ id: planet.id });
113
+ successToast({ text: t(($) => $.planets.removeConfirm.success) });
114
+ if (onDeleted) {
115
+ onDeleted();
116
+ } else {
117
+ navigate({ to: "/planets" });
118
+ }
119
+ } catch {
120
+ errorToast({ text: t(($) => $.planets.removeConfirm.error) });
121
+ }
122
+ };
123
+
124
+ const currentImage = useFormValue(form, (values) => values.image);
125
+ const currentImageUrl = planet.image && currentImage?.id === planet.image.id ? planet.image.url : previewImageUrl;
126
+ const handleClose = () => {
127
+ if (onClose) {
128
+ onClose();
129
+ } else {
130
+ navigate({ to: "/planets/$id", params: { id: planet.id } });
131
+ }
132
+ };
133
+
134
+ return (
135
+ <Modal
136
+ isOpen
137
+ onClose={handleClose}
138
+ aside="right"
139
+ showCloseIcon
140
+ modalClassName="h-full min-w-0 overflow-y-auto"
141
+ >
142
+ <div className="mb-6 flex items-center justify-between gap-4 pr-10">
143
+ <Typography
144
+ as="h2"
145
+ size="title-5"
146
+ variant="prominent-1"
147
+ className="text-text-default-1"
148
+ >
149
+ {t(($) => $.planets.edit.title)}
150
+ </Typography>
151
+
152
+ <Button
153
+ type="button"
154
+ size="xs"
155
+ color="error"
156
+ variant="outlined"
157
+ icon={Trash}
158
+ onPress={handleDelete}
159
+ isDisabled={deletePlanet.isPending}
160
+ >
161
+ {t(($) => $.planets.actions.remove)}
162
+ </Button>
163
+ </div>
164
+
165
+ <form
166
+ onSubmit={form.handleSubmit(onSubmit)}
167
+ className="flex w-full min-w-0 flex-col gap-4"
168
+ >
169
+ <RowInputWrapper
170
+ label={t(($) => $.planets.edit.name)}
171
+ isRequired
172
+ >
173
+ <TextInput
174
+ variant="filled"
175
+ size="extra-small"
176
+ field={{ form, name: "name" }}
177
+ label={t(($) => $.planets.edit.name)}
178
+ placeholder={t(($) => $.planets.edit.namePlaceholder)}
179
+ hideLabel
180
+ isRequired
181
+ />
182
+ </RowInputWrapper>
183
+
184
+ <RowInputWrapper label={t(($) => $.planets.edit.alien)}>
185
+ <QueryAutocomplete
186
+ variant="filled"
187
+ size="extra-small"
188
+ field={{ form, name: "alienId" }}
189
+ query={AlienQueries.useGetLabels}
190
+ label={t(($) => $.planets.edit.alien)}
191
+ placeholder={t(($) => $.planets.edit.alienPlaceholder)}
192
+ hideLabel
193
+ />
194
+ </RowInputWrapper>
195
+
196
+ <RowInputWrapper label={t(($) => $.planets.edit.discoveryDate)}>
197
+ <DatePicker
198
+ variant="filled"
199
+ size="extra-small"
200
+ field={{ form, name: "discoveryDate" }}
201
+ label={t(($) => $.planets.edit.discoveryDate)}
202
+ placeholder={t(($) => $.planets.edit.discoveryDatePlaceholder)}
203
+ hideLabel
204
+ />
205
+ </RowInputWrapper>
206
+
207
+ <RowInputWrapper label={t(($) => $.planets.edit.description)}>
208
+ <TextArea
209
+ variant="filled"
210
+ size="extra-small"
211
+ field={{ form, name: "description" }}
212
+ label={t(($) => $.planets.edit.description)}
213
+ placeholder={t(($) => $.planets.edit.descriptionPlaceholder)}
214
+ hideLabel
215
+ />
216
+ </RowInputWrapper>
217
+
218
+ <div className="flex flex-col gap-2">
219
+ <FileUpload
220
+ label={t(($) => $.planets.edit.image)}
221
+ emptyText={t(($) => $.planets.edit.imageEmptyText)}
222
+ uploadText={t(($) => $.planets.edit.imageUploadText)}
223
+ browseText={t(($) => $.planets.edit.imageBrowseText)}
224
+ acceptedFileTypes={["image/jpeg", "image/png", "image/webp"]}
225
+ onChange={handleImageChange}
226
+ fileUpload={handleImageUpload}
227
+ />
228
+ {currentImageUrl && (
229
+ <div className="aspect-video w-full overflow-hidden rounded-sm border border-elevation-outline-default-1 bg-elevation-fill-default-2">
230
+ <img
231
+ src={currentImageUrl}
232
+ alt=""
233
+ className="h-full w-full object-cover"
234
+ />
235
+ </div>
236
+ )}
237
+ </div>
238
+
239
+ <div className="mt-6 flex items-center justify-end gap-2 self-end">
240
+ <Button
241
+ type="button"
242
+ variant="outlined"
243
+ size="xs"
244
+ color="secondary"
245
+ width="hug"
246
+ onPress={handleClose}
247
+ >
248
+ {t(($) => $.planets.edit.cancel)}
249
+ </Button>
250
+
251
+ <Button
252
+ type="submit"
253
+ variant="contained"
254
+ size="xs"
255
+ color="primary"
256
+ width="hug"
257
+ isDisabled={updatePlanet.isPending}
258
+ isLoading={updatePlanet.isPending}
259
+ >
260
+ {t(($) => $.planets.edit.submit)}
261
+ </Button>
262
+ </div>
263
+ </form>
264
+ </Modal>
265
+ );
266
+ }