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,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
+ }
@@ -0,0 +1,93 @@
1
+ import { Button, IconButton, Link, Menu } from "@povio/ui";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import clsx from "clsx";
4
+ import { ChevronDown, MenuIcon, X } from "lucide-react";
5
+ import { type Key, useState } from "react";
6
+ import { useTranslation } from "react-i18next";
7
+
8
+ import { BrandLogo } from "@/components/shared/branding/BrandLogo";
9
+ import { useAuth } from "@/hooks/useAuth";
10
+
11
+ import { MobileNavigation } from "./MobileNavigation";
12
+ import { NavLink } from "./NavLink";
13
+
14
+ export function AppHeader() {
15
+ const { t } = useTranslation();
16
+ const navigate = useNavigate();
17
+ const { isAuthenticated, user, logout } = useAuth();
18
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
19
+
20
+ const menuItems = [
21
+ { label: t(($) => $.layout.header.profile), id: "profile" },
22
+ { label: t(($) => $.layout.header.logout), id: "logout" },
23
+ ];
24
+
25
+ const accountLabel = user?.name || user?.email || t(($) => $.layout.header.profile);
26
+
27
+ const handleMenuAction = (key: Key) => {
28
+ if (key === "profile") {
29
+ navigate({ to: "/profile" });
30
+ } else if (key === "logout") {
31
+ logout();
32
+ }
33
+ };
34
+
35
+ return (
36
+ <header
37
+ className={clsx(
38
+ "relative z-11 flex shrink-0 items-center gap-4 border-elevation-outline-default-1 border-b bg-elevation-fill-default-1 px-4",
39
+ isMobileMenuOpen && "flex-wrap",
40
+ )}
41
+ >
42
+ <Link
43
+ to="/"
44
+ aria-label={t(($) => $.layout.header.logo)}
45
+ className="flex shrink-0 items-center no-underline!"
46
+ >
47
+ <BrandLogo className="text-interactive-contained-primary-idle" />
48
+ </Link>
49
+
50
+ <div className="hidden min-w-0 lg:block">
51
+ <nav className="flex flex-row items-center gap-1 overflow-visible border-elevation-outline-default-1 px-0">
52
+ <NavLink to="/">{t(($) => $.layout.header.nav.home)}</NavLink>
53
+ <NavLink to="/planets">{t(($) => $.layout.header.nav.planets)}</NavLink>
54
+ <NavLink to="/planets-feed">{t(($) => $.layout.header.nav.planetsFeed)}</NavLink>
55
+ </nav>
56
+ </div>
57
+
58
+ {isAuthenticated && (
59
+ <div className="ml-auto hidden shrink-0 items-center gap-2 lg:flex">
60
+ <Menu
61
+ trigger={
62
+ <div className="flex cursor-pointer items-center gap-1">
63
+ <Button
64
+ type="button"
65
+ color="secondary"
66
+ variant="text"
67
+ size="xs"
68
+ className="items-center gap-1 overflow-hidden whitespace-nowrap text-text-default-1 [&>span]:font-labels-default!"
69
+ >
70
+ {accountLabel}
71
+ </Button>
72
+ <ChevronDown className="size-5 shrink-0" />
73
+ </div>
74
+ }
75
+ items={menuItems}
76
+ onAction={handleMenuAction}
77
+ />
78
+ </div>
79
+ )}
80
+
81
+ <IconButton
82
+ className="ml-auto lg:hidden"
83
+ color="primary"
84
+ icon={isMobileMenuOpen ? X : MenuIcon}
85
+ label={t(($) => (isMobileMenuOpen ? $.layout.header.closeMenu : $.layout.header.openMenu))}
86
+ size="xs"
87
+ variant="ghost"
88
+ onPress={() => setIsMobileMenuOpen((isOpen) => !isOpen)}
89
+ />
90
+ {isMobileMenuOpen ? <MobileNavigation /> : null}
91
+ </header>
92
+ );
93
+ }
@@ -0,0 +1,53 @@
1
+ import { Menu, TextButton } from "@povio/ui";
2
+ import { useNavigate } from "@tanstack/react-router";
3
+ import { User } from "lucide-react";
4
+ import type { Key } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ import { useAuth } from "@/hooks/useAuth";
8
+
9
+ import { NavLink } from "./NavLink";
10
+
11
+ export function MobileNavigation() {
12
+ const { t } = useTranslation();
13
+ const navigate = useNavigate();
14
+ const { isAuthenticated, user, logout } = useAuth();
15
+ const accountLabel = user?.name || user?.email || t(($) => $.layout.header.profile);
16
+
17
+ const handleMenuAction = (key: Key) => {
18
+ if (key === "profile") {
19
+ navigate({ to: "/profile" });
20
+ } else if (key === "logout") {
21
+ logout();
22
+ }
23
+ };
24
+
25
+ return (
26
+ <div className="-mx-4 flex w-[calc(100%+2rem)] basis-full flex-col gap-2 border-elevation-outline-default-1 border-t px-4 py-3 lg:hidden">
27
+ <nav className="flex flex-col gap-1">
28
+ <NavLink to="/">{t(($) => $.layout.header.nav.home)}</NavLink>
29
+ <NavLink to="/planets">{t(($) => $.layout.header.nav.planets)}</NavLink>
30
+ <NavLink to="/planets-feed">{t(($) => $.layout.header.nav.planetsFeed)}</NavLink>
31
+ </nav>
32
+
33
+ {isAuthenticated ? (
34
+ <Menu
35
+ items={[
36
+ { id: "profile", label: t(($) => $.layout.header.profile) },
37
+ { id: "logout", label: t(($) => $.layout.header.logout) },
38
+ ]}
39
+ trigger={
40
+ <TextButton
41
+ className="mr-auto"
42
+ color="secondary"
43
+ icon={User}
44
+ >
45
+ {accountLabel}
46
+ </TextButton>
47
+ }
48
+ onAction={handleMenuAction}
49
+ />
50
+ ) : null}
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,29 @@
1
+ import { Link, type LinkProps, Typography } from "@povio/ui";
2
+ import { useMatchRoute } from "@tanstack/react-router";
3
+ import clsx from "clsx";
4
+
5
+ export function NavLink({ to, children, ...props }: LinkProps) {
6
+ const matchRoute = useMatchRoute();
7
+ const isActive = Boolean(matchRoute({ to, fuzzy: true }));
8
+
9
+ return (
10
+ <Link
11
+ to={to}
12
+ className={clsx(
13
+ "no-underline! flex min-h-auto items-center rounded-xs px-button-side-xs py-button-height-xs font-medium transition-colors",
14
+ isActive
15
+ ? "bg-elevation-fill-default-4 font-semibold text-interactive-text-primary-idle"
16
+ : "text-text-default-2 hover:bg-elevation-fill-default-2 hover:text-text-default-1",
17
+ )}
18
+ {...props}
19
+ >
20
+ <Typography
21
+ className="relative whitespace-nowrap no-underline! after:invisible after:block after:h-0 after:overflow-hidden after:font-semibold after:content-[attr(data-text)]"
22
+ size="label-2"
23
+ data-text={children}
24
+ >
25
+ {children}
26
+ </Typography>
27
+ </Link>
28
+ );
29
+ }
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx";
2
+ import type { HTMLAttributes } from "react";
3
+
4
+ export function BrandLogo({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
5
+ return (
6
+ <span
7
+ className={clsx("inline-flex h-[34px] items-center font-semibold text-xl tracking-tight", className)}
8
+ {...props}
9
+ >
10
+ Tiny
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,43 @@
1
+ import { ErrorHandler } from "@povio/openapi-codegen-cli";
2
+ import { Button, Typography } from "@povio/ui";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { ThinPageWrapper } from "@/components/shared/layout/ThinPageWrapper";
6
+
7
+ export interface ErrorFallbackProps {
8
+ error: unknown;
9
+ resetError: () => void;
10
+ }
11
+
12
+ export const ErrorFallback = ({ error, resetError }: ErrorFallbackProps) => {
13
+ const { t } = useTranslation();
14
+
15
+ return (
16
+ <ThinPageWrapper>
17
+ <main className="flex flex-col items-center justify-center gap-2 p-8">
18
+ <Typography
19
+ size="headline-1"
20
+ variant="prominent-1"
21
+ as="h2"
22
+ >
23
+ {t(($) => $.shared.errorFallback.heading)}
24
+ </Typography>
25
+ <Typography
26
+ size="title-2"
27
+ className="text-center text-text-error-1"
28
+ >
29
+ {t(($) => $.shared.errorFallback.subheading, {
30
+ error: ErrorHandler.getErrorMessage(error),
31
+ })}
32
+ </Typography>
33
+ <Button
34
+ size="s"
35
+ type="button"
36
+ onPress={resetError}
37
+ >
38
+ {t(($) => $.shared.errorFallback.retryBtn)}
39
+ </Button>
40
+ </main>
41
+ </ThinPageWrapper>
42
+ );
43
+ };
@@ -0,0 +1,20 @@
1
+ import { Typography } from "@povio/ui";
2
+ import { clsx } from "clsx";
3
+
4
+ interface Props {
5
+ error?: string;
6
+ isDisabled?: boolean;
7
+ isHidden?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ export const ErrorText = ({ error, isDisabled, isHidden, className }: Props) => {
12
+ return (
13
+ <Typography
14
+ className={clsx("text-text-error-1", !error || isDisabled || isHidden ? "sr-only" : "mt-1-5", className)}
15
+ size="label-3"
16
+ >
17
+ {error}
18
+ </Typography>
19
+ );
20
+ };
@@ -0,0 +1,36 @@
1
+ import { Button, Typography } from "@povio/ui";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ import { ThinPageWrapper } from "@/components/shared/layout/ThinPageWrapper";
5
+
6
+ export const NotFound = () => {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <ThinPageWrapper>
11
+ <div className="flex flex-col items-center justify-center gap-2 p-8 text-center">
12
+ <Typography
13
+ size="title-1"
14
+ variant="prominent-1"
15
+ as="h2"
16
+ >
17
+ {t(($) => $.shared.notFound.heading)}
18
+ </Typography>
19
+
20
+ <Typography
21
+ size="body-2"
22
+ className="text-center text-text-error-1"
23
+ >
24
+ {t(($) => $.shared.notFound.subheading)}
25
+ </Typography>
26
+
27
+ <Button
28
+ size="s"
29
+ link={{ to: "/" }}
30
+ >
31
+ {t(($) => $.shared.notFound.homeBtn)}
32
+ </Button>
33
+ </div>
34
+ </ThinPageWrapper>
35
+ );
36
+ };
@@ -0,0 +1,21 @@
1
+ import { Typography, type TypographyProps } from "@povio/ui";
2
+
3
+ interface RequiredLabelProps extends Omit<TypographyProps, "size" | "variant"> {
4
+ size?: TypographyProps["size"];
5
+ variant?: TypographyProps["variant"];
6
+ }
7
+
8
+ export function RequiredLabel({ variant = "prominent-1", size = "label-1", children, ...props }: RequiredLabelProps) {
9
+ return (
10
+ <div className="flex items-center">
11
+ <Typography
12
+ variant={variant}
13
+ size={size}
14
+ {...props}
15
+ >
16
+ {children}
17
+ <span className="ml-1 text-text-error-3">*</span>
18
+ </Typography>
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,50 @@
1
+ import { Typography, type TypographyProps } from "@povio/ui";
2
+ import clsx from "clsx";
3
+ import type { ReactNode } from "react";
4
+
5
+ import { RequiredLabel } from "@/components/shared/forms/RequiredLabel";
6
+
7
+ interface RowInputWrapperProps {
8
+ label: string;
9
+ children: ReactNode;
10
+ isRequired?: boolean;
11
+ labelClassName?: string;
12
+ labelSize?: TypographyProps["size"];
13
+ variant?: TypographyProps["variant"];
14
+ className?: string;
15
+ fieldClassName?: string;
16
+ }
17
+
18
+ export function RowInputWrapper({
19
+ label,
20
+ children,
21
+ isRequired = false,
22
+ labelClassName = "w-[140px] sm:w-[180px]",
23
+ labelSize = "label-2",
24
+ variant = "default",
25
+ className = "flex min-w-0 items-start",
26
+ fieldClassName,
27
+ }: RowInputWrapperProps) {
28
+ return (
29
+ <div className={className}>
30
+ <div className={clsx("flex h-[30px] shrink-0 items-center", labelClassName)}>
31
+ {isRequired ? (
32
+ <RequiredLabel
33
+ size={labelSize}
34
+ variant={variant}
35
+ >
36
+ {label}
37
+ </RequiredLabel>
38
+ ) : (
39
+ <Typography
40
+ size={labelSize}
41
+ variant={variant}
42
+ >
43
+ {label}
44
+ </Typography>
45
+ )}
46
+ </div>
47
+ <div className={clsx("min-w-0 flex-1", fieldClassName)}>{children}</div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,32 @@
1
+ import { useTranslation } from "react-i18next";
2
+
3
+ interface IProps {
4
+ title?: string;
5
+ description?: string;
6
+ omitSuffix?: boolean;
7
+ }
8
+
9
+ export const AppHead = ({ title: titleProp, description: descriptionProp, omitSuffix }: IProps) => {
10
+ const { t } = useTranslation();
11
+
12
+ const titleParts: string[] = [];
13
+ if (titleProp) {
14
+ titleParts.push(titleProp);
15
+ }
16
+ if (!omitSuffix) {
17
+ titleParts.push(t(($) => $.appName));
18
+ }
19
+
20
+ const title = titleParts.join(" | ");
21
+ const description = descriptionProp ?? t(($) => $.appDescription);
22
+
23
+ return (
24
+ <>
25
+ <title>{title}</title>
26
+ <meta
27
+ name="description"
28
+ content={description}
29
+ />
30
+ </>
31
+ );
32
+ };
@@ -0,0 +1,34 @@
1
+ import { useTranslation } from "react-i18next";
2
+
3
+ export const DefaultAppHead = () => {
4
+ const { t } = useTranslation();
5
+
6
+ const title = t(($) => $.appName);
7
+ const description = t(($) => $.appDescription);
8
+
9
+ return (
10
+ <>
11
+ <title>{title}</title>
12
+ <meta
13
+ name="description"
14
+ content={description}
15
+ />
16
+ <meta
17
+ property="og:title"
18
+ content={title}
19
+ />
20
+ <meta
21
+ property="og:description"
22
+ content={description}
23
+ />
24
+ <meta
25
+ name="twitter:title"
26
+ content={title}
27
+ />
28
+ <meta
29
+ name="twitter:description"
30
+ content={description}
31
+ />
32
+ </>
33
+ );
34
+ };
@@ -0,0 +1,9 @@
1
+ import { Loader } from "@povio/ui";
2
+
3
+ export function LoadingState() {
4
+ return (
5
+ <div className="flex items-center justify-center p-10 text-text-default-1">
6
+ <Loader size="l" />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { PropsWithChildren } from "react";
2
+
3
+ export const ThinPageWrapper = ({ children }: PropsWithChildren) => {
4
+ return <div className="mx-auto my-10 w-full max-w-xl">{children}</div>;
5
+ };
@@ -0,0 +1,69 @@
1
+ import { InlineIconButton, Typography } from "@povio/ui";
2
+ import { type LinkProps, useRouter } from "@tanstack/react-router";
3
+ import { ChevronLeft } from "lucide-react";
4
+ import type { ReactNode } from "react";
5
+
6
+ type PageHeaderBackProps =
7
+ | {
8
+ backLink: LinkProps;
9
+ onBack?: never;
10
+ }
11
+ | {
12
+ backLink?: never;
13
+ onBack: () => void;
14
+ };
15
+
16
+ interface PageHeaderProps {
17
+ title: ReactNode;
18
+ actions?: ReactNode;
19
+ enableBack?: boolean;
20
+ backProps?: PageHeaderBackProps;
21
+ }
22
+
23
+ export function PageHeader({ title, actions, enableBack = false, backProps }: PageHeaderProps) {
24
+ const router = useRouter();
25
+
26
+ const handleBack = () => {
27
+ if (backProps?.onBack) {
28
+ backProps.onBack();
29
+ return;
30
+ }
31
+
32
+ if (window.history.length > 1) {
33
+ router.history.back();
34
+ return;
35
+ }
36
+
37
+ if (backProps?.backLink) {
38
+ router.navigate(backProps.backLink);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className="flex flex-col gap-4">
44
+ <div className="flex items-center justify-between gap-4">
45
+ <div className="flex min-w-0 items-center gap-2">
46
+ {enableBack && (
47
+ <InlineIconButton
48
+ label="Back"
49
+ icon={ChevronLeft}
50
+ color="secondary"
51
+ onPress={handleBack}
52
+ />
53
+ )}
54
+
55
+ <Typography
56
+ as="h1"
57
+ size="title-4"
58
+ variant="prominent-1"
59
+ className="min-w-0 text-text-default-1"
60
+ >
61
+ {title}
62
+ </Typography>
63
+ </div>
64
+
65
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
66
+ </div>
67
+ </div>
68
+ );
69
+ }