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,79 @@
1
+ import { Typography, useSorting } from "@povio/ui";
2
+ import type { SortingState } from "@tanstack/react-table";
3
+ import { useCallback } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { LoadingState } from "@/components/shared/layout/LoadingState";
7
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
8
+ import { PlanetQueries } from "@/openapi/planet/planet.queries";
9
+ import { StringUtils } from "@/utils/string.utils";
10
+
11
+ import { PlanetsTableInfinite } from "./table/PlanetsTableInfinite";
12
+ import { usePlanetTableFilters } from "./table/usePlanetTableFilters";
13
+ import { PlanetsTableSummary } from "./PlanetsTableSummary";
14
+
15
+ // Keep the first two pages tall enough for the intersection sentinel to leave and re-enter
16
+ // the viewport on larger collections. With 10-row pages it stayed visible after page two,
17
+ // so the observer never emitted another intersection and the loader remained indefinitely.
18
+ const PAGE_SIZE = 20;
19
+ const DEFAULT_SORTING: SortingState = [{ id: "discoveryDate", desc: true }];
20
+ const getPlanetRowId = (planet: PlanetModels.PlanetsGetResponseDto) => planet.id;
21
+
22
+ interface PlanetsInfiniteTablePageProps {
23
+ onEdit: (planet: PlanetModels.PlanetsGetResponseDto) => void;
24
+ }
25
+
26
+ export function PlanetsInfiniteTablePage({ onEdit }: PlanetsInfiniteTablePageProps) {
27
+ const { t } = useTranslation();
28
+
29
+ const { apiFilters, filterControls } = usePlanetTableFilters();
30
+
31
+ const { order, setSorting, sorting } = useSorting();
32
+ const effectiveSorting = sorting.length > 0 ? sorting : DEFAULT_SORTING;
33
+ const effectiveOrder = order || "-discoveryDate";
34
+
35
+ const { data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } =
36
+ PlanetQueries.usePaginateInfinite({
37
+ cursor: undefined,
38
+ limit: PAGE_SIZE,
39
+ filter: apiFilters,
40
+ order: StringUtils.emptyToUndefined(effectiveOrder),
41
+ });
42
+
43
+ const planets = data?.pages.flatMap((page) => page.items ?? []) ?? [];
44
+ const totalCount = data?.pages[0]?.totalItems ?? planets.length;
45
+ const handleFetchNextPage = useCallback(() => void fetchNextPage(), [fetchNextPage]);
46
+
47
+ return (
48
+ <div className="flex flex-col gap-4 pb-12">
49
+ {filterControls}
50
+
51
+ {isLoading ? (
52
+ <LoadingState />
53
+ ) : (
54
+ <PlanetsTableInfinite
55
+ className="min-w-[920px]"
56
+ items={planets}
57
+ getRowId={getPlanetRowId}
58
+ onRowClick={onEdit}
59
+ onEdit={onEdit}
60
+ setSorting={setSorting}
61
+ sorting={effectiveSorting}
62
+ hasNextPage={Boolean(hasNextPage)}
63
+ isFetchingNextPage={isFetchingNextPage}
64
+ fetchNextPage={handleFetchNextPage}
65
+ />
66
+ )}
67
+
68
+ {!isLoading && planets.length === 0 && (
69
+ <Typography size="body-3" className="text-text-default-2">
70
+ {t(($) => $.planets.page.empty)}
71
+ </Typography>
72
+ )}
73
+
74
+ {!isLoading ? (
75
+ <PlanetsTableSummary displayedCount={planets.length} totalCount={totalCount} />
76
+ ) : null}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,65 @@
1
+ import { Button } from "@povio/ui";
2
+ import { Plus } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { PlanetEditPage } from "@/components/features/planets/details/PlanetEditPage";
7
+ import { PageHeader } from "@/components/shared/page/PageHeader";
8
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
9
+
10
+ import { PlanetCreateModal } from "./PlanetCreateModal";
11
+ import { PlanetsGridPage } from "./PlanetsGridPage";
12
+ import { PlanetsInfiniteTablePage } from "./PlanetsInfiniteTablePage";
13
+ import { PlanetsTablePage } from "./PlanetsTablePage";
14
+ import { type PlanetsView, PlanetsViewTabs } from "./PlanetsViewTabs";
15
+
16
+ export function PlanetsPage() {
17
+ const { t } = useTranslation();
18
+ const [view, setView] = useState<PlanetsView>("table");
19
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
20
+ const [editingPlanet, setEditingPlanet] = useState<PlanetModels.PlanetsGetResponseDto>();
21
+
22
+ return (
23
+ <div className="flex flex-col gap-4">
24
+ <PageHeader
25
+ actions={
26
+ <Button
27
+ color="primary"
28
+ icon={Plus}
29
+ size="xs"
30
+ variant="contained"
31
+ onPress={() => setIsCreateOpen(true)}
32
+ >
33
+ {t(($) => $.planets.page.addPlanet)}
34
+ </Button>
35
+ }
36
+ title={t(($) => $.planets.page.title)}
37
+ />
38
+
39
+ <div className="flex w-full justify-between gap-1 border-elevation-outline-default-1 border-b pb-1-5">
40
+ <PlanetsViewTabs
41
+ value={view}
42
+ onChange={setView}
43
+ />
44
+ </div>
45
+
46
+ {view === "grid" && <PlanetsGridPage />}
47
+ {view === "table" && <PlanetsTablePage onEdit={setEditingPlanet} />}
48
+ {view === "infinite" && <PlanetsInfiniteTablePage onEdit={setEditingPlanet} />}
49
+
50
+ <PlanetCreateModal
51
+ isOpen={isCreateOpen}
52
+ onClose={() => setIsCreateOpen(false)}
53
+ />
54
+
55
+ {editingPlanet ? (
56
+ <PlanetEditPage
57
+ planet={editingPlanet}
58
+ onClose={() => setEditingPlanet(undefined)}
59
+ onDeleted={() => setEditingPlanet(undefined)}
60
+ onSaved={() => setEditingPlanet(undefined)}
61
+ />
62
+ ) : null}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,72 @@
1
+ import { Typography, useSorting } from "@povio/ui";
2
+ import type { SortingState } from "@tanstack/react-table";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { LoadingState } from "@/components/shared/layout/LoadingState";
6
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
7
+ import { PlanetQueries } from "@/openapi/planet/planet.queries";
8
+ import { StringUtils } from "@/utils/string.utils";
9
+
10
+ import { PlanetsTable } from "./table/PlanetsTable";
11
+ import { usePlanetTableFilters } from "./table/usePlanetTableFilters";
12
+ import { PlanetsTableSummary } from "./PlanetsTableSummary";
13
+
14
+ interface PlanetsTablePageProps {
15
+ onEdit: (planet: PlanetModels.PlanetsGetResponseDto) => void;
16
+ }
17
+
18
+ const DEFAULT_SORTING: SortingState = [{ id: "discoveryDate", desc: true }];
19
+ const EMPTY_PLANETS: PlanetModels.PlanetsGetResponseDto[] = [];
20
+ const getPlanetRowId = (planet: PlanetModels.PlanetsGetResponseDto) => planet.id;
21
+
22
+ export function PlanetsTablePage({ onEdit }: PlanetsTablePageProps) {
23
+ const { t } = useTranslation();
24
+
25
+ const { apiFilters, filterControls } = usePlanetTableFilters();
26
+
27
+ const { order, setSorting, sorting } = useSorting();
28
+ const effectiveSorting = sorting.length > 0 ? sorting : DEFAULT_SORTING;
29
+ const effectiveOrder = order || "-discoveryDate";
30
+
31
+ const { data, isLoading } = PlanetQueries.usePaginate({
32
+ page: 1,
33
+ cursor: undefined,
34
+ limit: 20,
35
+ filter: apiFilters,
36
+ order: StringUtils.emptyToUndefined(effectiveOrder),
37
+ });
38
+ const planets = data?.items ?? EMPTY_PLANETS;
39
+
40
+ return (
41
+ <div className="flex flex-col gap-4 pb-12">
42
+ {filterControls}
43
+
44
+ {isLoading ? (
45
+ <LoadingState />
46
+ ) : (
47
+ <PlanetsTable
48
+ className="min-w-[920px]"
49
+ items={planets}
50
+ getRowId={getPlanetRowId}
51
+ onRowClick={onEdit}
52
+ onEdit={onEdit}
53
+ setSorting={setSorting}
54
+ sorting={effectiveSorting}
55
+ />
56
+ )}
57
+
58
+ {!isLoading && planets.length === 0 && (
59
+ <Typography size="body-3" className="text-text-default-2">
60
+ {t(($) => $.planets.page.empty)}
61
+ </Typography>
62
+ )}
63
+
64
+ {!isLoading ? (
65
+ <PlanetsTableSummary
66
+ displayedCount={planets.length}
67
+ totalCount={data?.totalItems ?? planets.length}
68
+ />
69
+ ) : null}
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,29 @@
1
+ import { Typography } from "@povio/ui";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ interface PlanetsTableSummaryProps {
5
+ displayedCount: number;
6
+ totalCount: number;
7
+ }
8
+
9
+ export function PlanetsTableSummary({ displayedCount, totalCount }: PlanetsTableSummaryProps) {
10
+ const { t } = useTranslation();
11
+
12
+ if (totalCount === 0) {
13
+ return null;
14
+ }
15
+
16
+ return (
17
+ <div className="fixed inset-x-0 bottom-0 z-sticky flex min-h-10 justify-end gap-2 border-elevation-outline-default-1 border-t bg-elevation-fill-default-1 px-2 py-3 shadow-1">
18
+ {displayedCount >= totalCount ? (
19
+ <Typography size="label-3">{t(($) => $.planets.table.allLoaded)}</Typography>
20
+ ) : null}
21
+ <Typography
22
+ className="text-text-default-3"
23
+ size="label-3"
24
+ >
25
+ {t(($) => $.planets.table.showing, { displayed: displayedCount, total: totalCount })}
26
+ </Typography>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,79 @@
1
+ import { Button, Menu } from "@povio/ui";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { type Key, useEffect, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ export type PlanetsView = "grid" | "table" | "infinite";
7
+
8
+ interface PlanetsViewTabsProps {
9
+ value: PlanetsView;
10
+ onChange: (view: PlanetsView) => void;
11
+ }
12
+
13
+ export function PlanetsViewTabs({ value, onChange }: PlanetsViewTabsProps) {
14
+ const { t } = useTranslation();
15
+ const [isCompact, setIsCompact] = useState(false);
16
+ const items: { value: PlanetsView; label: string }[] = [
17
+ { value: "grid", label: t(($) => $.planets.views.grid) },
18
+ { value: "table", label: t(($) => $.planets.views.table) },
19
+ { value: "infinite", label: t(($) => $.planets.views.infinite) },
20
+ ];
21
+ const activeItem = items.find((item) => item.value === value) ?? items[0];
22
+
23
+ useEffect(() => {
24
+ const update = () => setIsCompact(window.innerWidth < 850);
25
+ update();
26
+ window.addEventListener("resize", update);
27
+ return () => window.removeEventListener("resize", update);
28
+ }, []);
29
+
30
+ if (isCompact) {
31
+ return (
32
+ <Menu
33
+ items={items.map((item) => ({ id: item.value, label: item.label }))}
34
+ onAction={(key: Key) => onChange(key as PlanetsView)}
35
+ trigger={
36
+ <Button
37
+ type="button"
38
+ color="primary"
39
+ variant="ghost"
40
+ size="xs"
41
+ toggle
42
+ isSelected
43
+ icon={ChevronDown}
44
+ iconPosition="right"
45
+ >
46
+ {activeItem.label}
47
+ </Button>
48
+ }
49
+ />
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div
55
+ aria-label={t(($) => $.planets.views.label)}
56
+ className="flex shrink-0 gap-1"
57
+ role="group"
58
+ >
59
+ {items.map((item) => {
60
+ const isActive = item.value === value;
61
+
62
+ return (
63
+ <Button
64
+ key={item.value}
65
+ type="button"
66
+ color="primary"
67
+ variant="ghost"
68
+ size="xs"
69
+ toggle
70
+ isSelected={isActive}
71
+ onPress={() => onChange(item.value)}
72
+ >
73
+ {item.label}
74
+ </Button>
75
+ );
76
+ })}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,30 @@
1
+ import { Table } from "@povio/ui";
2
+ import type { TableWrapperProps } from "@povio/ui";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
6
+
7
+ import { getPlanetsTableColumns } from "./planetsTableColumns";
8
+
9
+ type PlanetTableItem = PlanetModels.PlanetsGetResponseDto;
10
+ interface PlanetsTableProps extends TableWrapperProps<PlanetTableItem> {
11
+ onEdit: (planet: PlanetTableItem) => void;
12
+ }
13
+
14
+ export function PlanetsTable({ className, onEdit, onRowClick, showCellBorder = false, ...props }: PlanetsTableProps) {
15
+ const { t } = useTranslation();
16
+
17
+ const columns = getPlanetsTableColumns(t, onEdit);
18
+
19
+ return (
20
+ <div className="-mx-4 overflow-x-auto">
21
+ <Table
22
+ {...props}
23
+ className={className}
24
+ columns={columns}
25
+ onRowClick={onRowClick}
26
+ showCellBorder={showCellBorder}
27
+ />
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,64 @@
1
+ import { Confirmation, InlineIconButton, useToast } from "@povio/ui";
2
+ import { Pencil, Trash } from "lucide-react";
3
+ import { useCallback } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { TableActions } from "@/components/shared/ui/TableActions";
7
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
8
+ import { PlanetQueries } from "@/openapi/planet/planet.queries";
9
+
10
+ interface PlanetsTableActionsProps {
11
+ onEdit: (planet: PlanetModels.PlanetsGetResponseDto) => void;
12
+ planet: PlanetModels.PlanetsGetResponseDto;
13
+ }
14
+
15
+ export function PlanetsTableActions({ onEdit, planet }: PlanetsTableActionsProps) {
16
+ const { t } = useTranslation();
17
+
18
+ const { successToast, errorToast } = useToast();
19
+ const { confirm } = Confirmation.useConfirmation();
20
+
21
+ const deletePlanet = PlanetQueries.useDeleteApiPlanetsById();
22
+
23
+ const handleRemove = useCallback(async () => {
24
+ // Row actions should stop propagation in TableActions so the row click does not also open details.
25
+ const confirmed = await confirm({
26
+ heading: t(($) => $.planets.removeConfirm.heading),
27
+ description: t(($) => $.planets.removeConfirm.description, { name: planet.name }),
28
+ confirmLabel: t(($) => $.planets.removeConfirm.confirm),
29
+ cancelLabel: t(($) => $.planets.removeConfirm.cancel),
30
+ confirmColor: "error",
31
+ cancelVariant: "subtle",
32
+ textAlign: "center",
33
+ buttonSize: "s",
34
+ });
35
+
36
+ if (!confirmed) {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ await deletePlanet.mutateAsync({ id: planet.id });
42
+ successToast({ text: t(($) => $.planets.removeConfirm.success) });
43
+ } catch {
44
+ errorToast({ text: t(($) => $.planets.removeConfirm.error) });
45
+ }
46
+ }, [confirm, deletePlanet, errorToast, planet, successToast, t]);
47
+
48
+ return (
49
+ <TableActions>
50
+ <InlineIconButton
51
+ label={t(($) => $.planets.actions.edit)}
52
+ icon={Pencil}
53
+ color="secondary"
54
+ onPress={() => onEdit(planet)}
55
+ />
56
+ <InlineIconButton
57
+ label={t(($) => $.planets.actions.remove)}
58
+ icon={Trash}
59
+ color="error"
60
+ onPress={handleRemove}
61
+ />
62
+ </TableActions>
63
+ );
64
+ }
@@ -0,0 +1,36 @@
1
+ import { InfiniteTable } from "@povio/ui";
2
+ import type { InfiniteTableWrapperProps } from "@povio/ui";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import type { PlanetModels } from "@/openapi/planet/planet.models";
6
+
7
+ import { getPlanetsTableColumns } from "./planetsTableColumns";
8
+
9
+ type PlanetInfiniteTableItem = PlanetModels.PlanetsGetResponseDto;
10
+ interface PlanetsTableInfiniteProps extends InfiniteTableWrapperProps<PlanetInfiniteTableItem> {
11
+ onEdit: (planet: PlanetInfiniteTableItem) => void;
12
+ }
13
+
14
+ export function PlanetsTableInfinite({
15
+ className,
16
+ onEdit,
17
+ onRowClick,
18
+ showCellBorder = false,
19
+ ...props
20
+ }: PlanetsTableInfiniteProps) {
21
+ const { t } = useTranslation();
22
+
23
+ const columns = getPlanetsTableColumns(t, onEdit);
24
+
25
+ return (
26
+ <div className="-mx-4 overflow-x-auto">
27
+ <InfiniteTable
28
+ {...props}
29
+ className={className}
30
+ columns={columns}
31
+ onRowClick={onRowClick}
32
+ showCellBorder={showCellBorder}
33
+ />
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,72 @@
1
+ import { dynamicColumns } from "@povio/ui";
2
+ import type { TFunction } from "i18next";
3
+ import { z } from "zod";
4
+
5
+ import { PlanetModels } from "@/openapi/planet/planet.models";
6
+ import { DateUtils } from "@/utils/date.utils";
7
+
8
+ import { PlanetsTableActions } from "./PlanetsTableActions";
9
+
10
+ const PLANET_SORTABLE_COLUMN_SCHEMA = z.enum(["name", "discoveryDate"]);
11
+
12
+ export function getPlanetsTableColumns(t: TFunction, onEdit: (planet: PlanetModels.PlanetsGetResponseDto) => void) {
13
+ return dynamicColumns({
14
+ schema: PlanetModels.PlanetsGetResponseDtoSchema,
15
+ options: {
16
+ // dynamicColumns keeps table columns aligned with generated OpenAPI models and sortable enum metadata.
17
+ columns: {
18
+ name: {
19
+ header: t(($) => $.planets.table.name),
20
+ meta: { filterKey: "search" },
21
+ size: 224,
22
+ minSize: 180,
23
+ maxSize: 320,
24
+ },
25
+ description: {
26
+ header: t(($) => $.planets.table.description),
27
+ size: 360,
28
+ minSize: 280,
29
+ },
30
+ alienName: {
31
+ header: t(($) => $.planets.table.alien),
32
+ meta: { filterKey: "alienId" },
33
+ size: 192,
34
+ minSize: 160,
35
+ maxSize: 260,
36
+ },
37
+ discoveryDate: {
38
+ header: t(($) => $.planets.table.discoveryDate),
39
+ size: 176,
40
+ minSize: 140,
41
+ maxSize: 220,
42
+ cell: ({ value }) => (value ? DateUtils.formatDate(value) : null),
43
+ },
44
+ creatorName: {
45
+ header: t(($) => $.planets.table.creator),
46
+ size: 160,
47
+ minSize: 128,
48
+ maxSize: 220,
49
+ },
50
+ },
51
+ sortable: PLANET_SORTABLE_COLUMN_SCHEMA,
52
+ customColumns: {
53
+ // Keep per-row actions as a custom column so generated data columns stay declarative.
54
+ actions: {
55
+ header: t(($) => $.planets.table.actions),
56
+ meta: { filterKey: "clear", headerClass: "[&>*]:ml-auto" },
57
+ size: 112,
58
+ minSize: 96,
59
+ maxSize: 140,
60
+ cell: ({ row }) => (
61
+ <PlanetsTableActions
62
+ planet={row.original}
63
+ onEdit={onEdit}
64
+ />
65
+ ),
66
+ },
67
+ },
68
+ order: ["name", "description", "alienName", "discoveryDate", "creatorName", "actions"],
69
+ emptyValue: "-",
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,102 @@
1
+ import { Button, DateRangePicker, QueryAutocomplete, TextInput, useFilters } from "@povio/ui/tanstack";
2
+ import { Search } from "lucide-react";
3
+ import { type Key, useCallback } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { AlienQueries } from "@/openapi/alien/alien.queries";
7
+ import { PlanetModels } from "@/openapi/planet/planet.models";
8
+
9
+ import "../PlanetsFilters.css";
10
+
11
+ export type PlanetTableFilterValues = NonNullable<PlanetModels.PlanetPaginateFilterParam>;
12
+
13
+ export function usePlanetTableFilters() {
14
+ const { t } = useTranslation();
15
+ const { clearAllFilters, filterData, setFilterValue } = useFilters<PlanetTableFilterValues>(
16
+ {},
17
+ "",
18
+ PlanetModels.PlanetPaginateFilterParamSchema,
19
+ );
20
+ const handleSearchChange = useCallback(
21
+ (search: string) => setFilterValue({ search: search || undefined }),
22
+ [setFilterValue],
23
+ );
24
+ const handleAlienChange = useCallback(
25
+ (alienId: Key | null) => setFilterValue({ alienId: alienId ? String(alienId) : undefined }),
26
+ [setFilterValue],
27
+ );
28
+ const handleDiscoveryDateChange = useCallback(
29
+ (discoveryDate: PlanetTableFilterValues["discoveryDate"] | null) =>
30
+ setFilterValue({ discoveryDate: discoveryDate ?? undefined }),
31
+ [setFilterValue],
32
+ );
33
+ const handleClearAllFilters = useCallback(() => clearAllFilters(), [clearAllFilters]);
34
+
35
+ const discoveryDateValue = filterData.discoveryDate
36
+ ? {
37
+ start: filterData.discoveryDate.start ?? null,
38
+ end: filterData.discoveryDate.end ?? null,
39
+ }
40
+ : null;
41
+
42
+ const filterControls = (
43
+ <div className="flex flex-wrap items-end gap-1">
44
+ <div className="min-w-48 flex-1">
45
+ <TextInput
46
+ value={filterData.search ?? ""}
47
+ onChange={handleSearchChange}
48
+ label={t(($) => $.planets.filters.search)}
49
+ leadingIcon={Search}
50
+ as="filter"
51
+ size="extra-small"
52
+ variant="filled"
53
+ hideLabel
54
+ isClearable
55
+ />
56
+ </div>
57
+ <div className="planets-filters__control-without-label min-w-48 flex-1">
58
+ <QueryAutocomplete
59
+ key={filterData.alienId ?? "empty"}
60
+ value={filterData.alienId ?? null}
61
+ onChange={handleAlienChange}
62
+ query={AlienQueries.useGetLabels}
63
+ label={t(($) => $.planets.filters.alien)}
64
+ as="filter"
65
+ size="extra-small"
66
+ variant="filled"
67
+ isClearable
68
+ />
69
+ </div>
70
+ <div className="planets-filters__control-without-label min-w-48 flex-1">
71
+ <DateRangePicker
72
+ value={discoveryDateValue}
73
+ onChange={handleDiscoveryDateChange}
74
+ label={t(($) => $.planets.filters.discoveryDate)}
75
+ as="filter"
76
+ size="extra-small"
77
+ variant="filled"
78
+ isClearable
79
+ fullIso
80
+ />
81
+ </div>
82
+ <Button
83
+ variant="subtle"
84
+ color="secondary"
85
+ size="xs"
86
+ onPress={handleClearAllFilters}
87
+ >
88
+ {t(($) => $.planets.filters.clearAll)}
89
+ </Button>
90
+ </div>
91
+ );
92
+
93
+ const apiFilters: PlanetModels.PlanetPaginateFilterParam = {
94
+ search: filterData.search || undefined,
95
+ alienId: filterData.alienId || undefined,
96
+ discoveryDate: filterData.discoveryDate || undefined,
97
+ };
98
+ return {
99
+ apiFilters,
100
+ filterControls,
101
+ };
102
+ }