robodev 0.10.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 (197) 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/README.md +1 -1
  10. package/templates/auth-chat/api/health.ts +1 -1
  11. package/templates/auth-chat/api/me.ts +1 -1
  12. package/templates/auth-chat/api/messages.ts +1 -1
  13. package/templates/auth-chat/package.json +1 -1
  14. package/templates/auth-chat/src/main.tsx +5 -5
  15. package/templates/backend/README.md +1 -1
  16. package/templates/backend/api/health.ts +1 -1
  17. package/templates/backend/api/launch.ts +1 -1
  18. package/templates/backend/api/me.ts +1 -1
  19. package/templates/backend/api/motto.ts +1 -1
  20. package/templates/backend/api/planets/[id].ts +1 -1
  21. package/templates/backend/api/planets.ts +1 -1
  22. package/templates/backend/api/rockets.ts +1 -1
  23. package/templates/backend/package.json +1 -1
  24. package/templates/catalog.json +2 -2
  25. package/templates/empty/README.md +1 -1
  26. package/templates/empty/api/health.ts +1 -1
  27. package/templates/empty/package.json +1 -1
  28. package/templates/space/.config/local.spa.template.yml +27 -0
  29. package/templates/space/.oxlint-base.json +29 -0
  30. package/templates/space/.oxlintrc.json +78 -0
  31. package/templates/space/.rulesync/rules/frontend-api-boundary.md +36 -0
  32. package/templates/space/.rulesync/rules/frontend-component-structure.md +38 -0
  33. package/templates/space/.rulesync/rules/frontend-forms.md +32 -0
  34. package/templates/space/.rulesync/rules/frontend-notifications.md +24 -0
  35. package/templates/space/.rulesync/rules/frontend-povio-components.md +42 -0
  36. package/templates/space/.rulesync/rules/frontend-povio-ui.md +45 -0
  37. package/templates/space/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  38. package/templates/space/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  39. package/templates/space/.rulesync/rules/frontend-translations.md +26 -0
  40. package/templates/space/.rulesync/rules/project-overview.md +32 -0
  41. package/templates/space/.rulesync/rules/robodev-api.md +30 -0
  42. package/templates/space/.rulesync/rules/robodev-auth.md +40 -0
  43. package/templates/space/.rulesync/rules/robodev-database.md +22 -0
  44. package/templates/space/.rulesync/rules/role-based-app-structure.md +36 -0
  45. package/templates/space/.rulesync/skills/media-feature/SKILL.md +21 -0
  46. package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  47. package/templates/space/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  48. package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  49. package/templates/space/.rulesync/skills/robodev-table/SKILL.md +12 -0
  50. package/templates/space/README.md +26 -7
  51. package/templates/space/api/_lib.ts +359 -0
  52. package/templates/space/api/aliens/labels.ts +19 -0
  53. package/templates/space/api/files/presigned-url.ts +48 -0
  54. package/templates/space/api/files/upload.ts +29 -0
  55. package/templates/space/api/planets/[id]/like.ts +36 -0
  56. package/templates/space/api/planets/[id].ts +56 -13
  57. package/templates/space/api/planets/paginate.ts +36 -0
  58. package/templates/space/api/planets.ts +52 -44
  59. package/templates/space/apps/fe/index.html +24 -0
  60. package/templates/space/apps/fe/openapi-codegen.config.ts +37 -0
  61. package/templates/space/apps/fe/package.json +82 -0
  62. package/templates/space/apps/fe/public/apple-touch-icon.png +0 -0
  63. package/templates/space/apps/fe/public/favicon-96x96.png +0 -0
  64. package/templates/space/apps/fe/public/favicon.ico +0 -0
  65. package/templates/space/apps/fe/public/favicon.svg +3 -0
  66. package/templates/space/apps/fe/public/site.webmanifest +21 -0
  67. package/templates/space/apps/fe/public/web-app-manifest-192x192.png +0 -0
  68. package/templates/space/apps/fe/public/web-app-manifest-512x512.png +0 -0
  69. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  70. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  71. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  72. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  73. package/templates/space/apps/fe/src/assets/locales/en/translation.json +301 -0
  74. package/templates/space/apps/fe/src/assets/locales/sl/translation.json +301 -0
  75. package/templates/space/apps/fe/src/clients/app-rest-client.ts +15 -0
  76. package/templates/space/apps/fe/src/clients/auth-token-store.ts +101 -0
  77. package/templates/space/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  78. package/templates/space/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  79. package/templates/space/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  80. package/templates/space/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  81. package/templates/space/apps/fe/src/components/404.tsx +18 -0
  82. package/templates/space/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  83. package/templates/space/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  84. package/templates/space/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  85. package/templates/space/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  86. package/templates/space/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  87. package/templates/space/apps/fe/src/components/features/planets/details/PlanetDetailsPage.tsx +139 -0
  88. package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +266 -0
  89. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetFeedCard.tsx +131 -0
  90. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetsFeedPage.tsx +119 -0
  91. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCard.tsx +89 -0
  92. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +179 -0
  93. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.css +3 -0
  94. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.tsx +73 -0
  95. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsGridPage.tsx +51 -0
  96. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsInfiniteTablePage.tsx +79 -0
  97. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsPage.tsx +65 -0
  98. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTablePage.tsx +72 -0
  99. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTableSummary.tsx +29 -0
  100. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsViewTabs.tsx +79 -0
  101. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTable.tsx +30 -0
  102. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableActions.tsx +64 -0
  103. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableInfinite.tsx +36 -0
  104. package/templates/space/apps/fe/src/components/features/planets/list/table/planetsTableColumns.tsx +72 -0
  105. package/templates/space/apps/fe/src/components/features/planets/list/table/usePlanetTableFilters.tsx +102 -0
  106. package/templates/space/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  107. package/templates/space/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  108. package/templates/space/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  109. package/templates/space/apps/fe/src/components/layout/app-header/AppHeader.tsx +93 -0
  110. package/templates/space/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +53 -0
  111. package/templates/space/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  112. package/templates/space/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  113. package/templates/space/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  114. package/templates/space/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  115. package/templates/space/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  116. package/templates/space/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  117. package/templates/space/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  118. package/templates/space/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  119. package/templates/space/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  120. package/templates/space/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  121. package/templates/space/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  122. package/templates/space/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  123. package/templates/space/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  124. package/templates/space/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  125. package/templates/space/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  126. package/templates/space/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  127. package/templates/space/apps/fe/src/config/app.config.ts +35 -0
  128. package/templates/space/apps/fe/src/config/i18n.ts +43 -0
  129. package/templates/space/apps/fe/src/config/inits/a11y.ts +14 -0
  130. package/templates/space/apps/fe/src/config/inits/logger.ts +7 -0
  131. package/templates/space/apps/fe/src/config/inits/sentry.ts +21 -0
  132. package/templates/space/apps/fe/src/config/jwt.config.ts +2 -0
  133. package/templates/space/apps/fe/src/config/query.config.ts +20 -0
  134. package/templates/space/apps/fe/src/hooks/useAuth.ts +5 -0
  135. package/templates/space/apps/fe/src/main.tsx +52 -0
  136. package/templates/space/apps/fe/src/pages/(guest)/login.tsx +23 -0
  137. package/templates/space/apps/fe/src/pages/(guest)/register.tsx +23 -0
  138. package/templates/space/apps/fe/src/pages/(guest)/route.tsx +18 -0
  139. package/templates/space/apps/fe/src/pages/(private)/index.tsx +23 -0
  140. package/templates/space/apps/fe/src/pages/(private)/planets/$id/edit.tsx +53 -0
  141. package/templates/space/apps/fe/src/pages/(private)/planets/$id/index.tsx +41 -0
  142. package/templates/space/apps/fe/src/pages/(private)/planets/index.tsx +24 -0
  143. package/templates/space/apps/fe/src/pages/(private)/planets-feed.tsx +23 -0
  144. package/templates/space/apps/fe/src/pages/(private)/profile.tsx +23 -0
  145. package/templates/space/apps/fe/src/pages/(private)/route.tsx +18 -0
  146. package/templates/space/apps/fe/src/pages/(public)/auth.tsx +37 -0
  147. package/templates/space/apps/fe/src/pages/(public)/route.tsx +9 -0
  148. package/templates/space/apps/fe/src/pages/__root.tsx +164 -0
  149. package/templates/space/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  150. package/templates/space/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  151. package/templates/space/apps/fe/src/providers/index.tsx +44 -0
  152. package/templates/space/apps/fe/src/providers/jwt.provider.tsx +95 -0
  153. package/templates/space/apps/fe/src/routeTree.gen.ts +309 -0
  154. package/templates/space/apps/fe/src/styles/base.css +103 -0
  155. package/templates/space/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  156. package/templates/space/apps/fe/src/styles/fonts/general-sans.css +31 -0
  157. package/templates/space/apps/fe/src/styles/globals.css +28 -0
  158. package/templates/space/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  159. package/templates/space/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  160. package/templates/space/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  161. package/templates/space/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  162. package/templates/space/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  163. package/templates/space/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  164. package/templates/space/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  165. package/templates/space/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  166. package/templates/space/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  167. package/templates/space/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  168. package/templates/space/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  169. package/templates/space/apps/fe/src/styles/theme.css +2177 -0
  170. package/templates/space/apps/fe/src/types/i18next.d.ts +12 -0
  171. package/templates/space/apps/fe/src/types/table.d.ts +17 -0
  172. package/templates/space/apps/fe/src/types/ui.d.ts +26 -0
  173. package/templates/space/apps/fe/src/types/vite-env.d.ts +20 -0
  174. package/templates/space/apps/fe/src/utils/date.utils.ts +17 -0
  175. package/templates/space/apps/fe/src/utils/image-fallback.ts +9 -0
  176. package/templates/space/apps/fe/src/utils/media-upload.ts +63 -0
  177. package/templates/space/apps/fe/src/utils/number.utils.ts +12 -0
  178. package/templates/space/apps/fe/src/utils/string.utils.ts +5 -0
  179. package/templates/space/apps/fe/src/vite-env.d.ts +1 -0
  180. package/templates/space/apps/fe/tsconfig.app.json +32 -0
  181. package/templates/space/apps/fe/tsconfig.json +9 -0
  182. package/templates/space/apps/fe/tsconfig.node.json +31 -0
  183. package/templates/space/apps/fe/vite.config.ts +94 -0
  184. package/templates/space/database.ts +33 -13
  185. package/templates/space/openapi.json +1819 -0
  186. package/templates/space/oxfmt.config.js +23 -0
  187. package/templates/space/package.json +6 -13
  188. package/templates/space/rulesync.jsonc +18 -0
  189. package/templates/space/tsconfig.json +2 -4
  190. package/templates/space/api/health.ts +0 -7
  191. package/templates/space/api/launch.ts +0 -29
  192. package/templates/space/api/me.ts +0 -14
  193. package/templates/space/api/motto.ts +0 -9
  194. package/templates/space/api/rockets.ts +0 -43
  195. package/templates/space/index.html +0 -138
  196. package/templates/space/src/main.tsx +0 -229
  197. package/templates/space/vite.config.ts +0 -7
@@ -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
+ }
@@ -0,0 +1,60 @@
1
+ import { Typography, type TypographyProps } from "@povio/ui";
2
+ import { type VariantProps, cva } from "class-variance-authority";
3
+
4
+ const cardVariants = cva(
5
+ "flex flex-1 flex-col gap-5 rounded-sm border-elevation-outline-default-1 border-b bg-elevation-fill-default-1 shadow-1",
6
+ {
7
+ variants: {
8
+ padding: {
9
+ sm: "p-3",
10
+ md: "p-4",
11
+ lg: "p-5",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ padding: "md",
16
+ },
17
+ },
18
+ );
19
+
20
+ interface CardVariantProps extends VariantProps<typeof cardVariants>, React.HTMLAttributes<HTMLDivElement> {}
21
+
22
+ export interface CardProps extends CardVariantProps {
23
+ children: React.ReactNode;
24
+ title?: string;
25
+ titleSize?: TypographyProps["size"];
26
+ titleVariant?: TypographyProps["variant"];
27
+ isDisabled?: boolean;
28
+ }
29
+
30
+ export function Card({
31
+ children,
32
+ className,
33
+ padding,
34
+ title,
35
+ titleSize = "title-5",
36
+ titleVariant = "prominent-1",
37
+ isDisabled = false,
38
+ ...props
39
+ }: CardProps) {
40
+ return (
41
+ <div
42
+ className={cardVariants({
43
+ padding,
44
+ className: [className, isDisabled && "pointer-events-none opacity-50"],
45
+ })}
46
+ >
47
+ {title && (
48
+ <Typography
49
+ size={titleSize}
50
+ variant={titleVariant}
51
+ {...props}
52
+ >
53
+ {title}
54
+ </Typography>
55
+ )}
56
+
57
+ {children}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,19 @@
1
+ import { Button } from "@povio/ui";
2
+
3
+ import { AppConfig } from "@/config/app.config";
4
+
5
+ export function GoogleLoginButton() {
6
+ const handleGoogleLogin = () => {
7
+ window.location.href = AppConfig.auth.googleRedirectUrl;
8
+ };
9
+
10
+ return (
11
+ <Button
12
+ size="s"
13
+ width="hug"
14
+ onPress={handleGoogleLogin}
15
+ >
16
+ Continue with Google
17
+ </Button>
18
+ );
19
+ }
@@ -0,0 +1,22 @@
1
+ import { Typography, type TypographyProps } from "@povio/ui";
2
+ import type { ComponentProps } from "react";
3
+
4
+ interface RequiredLabelProps extends Omit<TypographyProps, "variant" | "size"> {
5
+ variant?: ComponentProps<typeof Typography>["variant"];
6
+ size?: ComponentProps<typeof Typography>["size"];
7
+ }
8
+
9
+ export function RequiredLabel({ variant = "prominent-1", size = "label-1", children, ...props }: RequiredLabelProps) {
10
+ return (
11
+ <div className="flex items-center">
12
+ <Typography
13
+ variant={variant}
14
+ size={size}
15
+ {...props}
16
+ >
17
+ {children}
18
+ <span className="text-text-error-3">*</span>
19
+ </Typography>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ import clsx from "clsx";
2
+ import type { HTMLAttributes } from "react";
3
+
4
+ export function TableActions({ children, className, onClick, onKeyDown, ...props }: HTMLAttributes<HTMLDivElement>) {
5
+ return (
6
+ <div
7
+ {...props}
8
+ role="presentation"
9
+ className={clsx("flex items-center justify-end gap-1", className)}
10
+ onClick={(event) => {
11
+ event.stopPropagation();
12
+ onClick?.(event);
13
+ }}
14
+ onKeyDown={(event) => {
15
+ event.stopPropagation();
16
+ onKeyDown?.(event);
17
+ }}
18
+ >
19
+ {children}
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,35 @@
1
+ const authCustomJWT = {
2
+ enableMagicLink: import.meta.env.VITE_PUBLIC_AUTH_CUSTOM_JWT_ENABLE_MAGIC_LINK === "true",
3
+ };
4
+
5
+ const apiUrl = import.meta.env.VITE_PUBLIC_API_URL ?? "http://localhost:4000";
6
+ const apiOrigin = apiUrl.replace(/\/+$/, "") || "/";
7
+ const apiMode = import.meta.env.VITE_PUBLIC_API_MODE ?? "real";
8
+ export const AppConfig = {
9
+ isProduction: import.meta.env.NODE_ENV === "production",
10
+ enableWebVitals: import.meta.env.VITE_PUBLIC_ENABLE_WEB_VITALS === "true",
11
+ release: import.meta.env.VITE_PUBLIC_RELEASE,
12
+ stage: import.meta.env.VITE_PUBLIC_STAGE,
13
+ api: {
14
+ url: apiOrigin,
15
+ mode: apiMode,
16
+ },
17
+ log: {
18
+ level: import.meta.env.VITE_PUBLIC_LOG_LEVEL ?? "error",
19
+ },
20
+ sentry: {
21
+ enabled: import.meta.env.VITE_PUBLIC_SENTRY_DSN ? true : false,
22
+ dsn: import.meta.env.VITE_PUBLIC_SENTRY_DSN,
23
+ environment: import.meta.env.VITE_PUBLIC_SENTRY_ENVIRONMENT,
24
+ tracesSampleRate: parseFloat(import.meta.env.VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ?? "0"),
25
+ replaysSessionSampleRate: parseFloat(import.meta.env.VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0"),
26
+ replaysOnErrorSampleRate: parseFloat(import.meta.env.VITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0"),
27
+ },
28
+ auth: {
29
+ customJWT: authCustomJWT,
30
+ googleRedirectUrl: `${apiOrigin}/api/user/auth/google`,
31
+ },
32
+ googleAnalytics: {
33
+ measurementId: import.meta.env.VITE_PUBLIC_GOOGLE_ANALYTICS_MEASUREMENT_ID,
34
+ },
35
+ };
@@ -0,0 +1,43 @@
1
+ import { resources as uiResources } from "@povio/ui";
2
+ import {
3
+ configureTranslations as configureOpenApiTranslations,
4
+ resources as openApiResources,
5
+ } from "@povio/openapi-codegen-cli";
6
+ import i18n from "i18next";
7
+ import { initReactI18next } from "react-i18next";
8
+
9
+ import translationEN from "@/assets/locales/en/translation.json";
10
+ import translationSL from "@/assets/locales/sl/translation.json";
11
+
12
+ export const defaultLanguage = "en";
13
+ export const defaultNS = "main";
14
+ export const resources = {
15
+ en: {
16
+ ...openApiResources.en,
17
+ ...uiResources.en,
18
+ [defaultNS]: translationEN,
19
+ },
20
+ sl: {
21
+ ...openApiResources.sl,
22
+ ...uiResources.sl,
23
+ [defaultNS]: translationSL,
24
+ },
25
+ } as const;
26
+
27
+ i18n.use(initReactI18next).init({
28
+ compatibilityJSON: "v4",
29
+ fallbackLng: defaultLanguage,
30
+ resources,
31
+ ns: Object.keys(resources.en),
32
+ defaultNS,
33
+ lng: defaultLanguage,
34
+ returnNull: false,
35
+ interpolation: {
36
+ escapeValue: false,
37
+ },
38
+ react: {
39
+ useSuspense: false,
40
+ },
41
+ });
42
+
43
+ configureOpenApiTranslations(() => i18n.t);
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+
3
+ import { AppConfig } from "@/config/app.config";
4
+
5
+ export async function initA11y() {
6
+ if (typeof window !== "undefined" && !AppConfig.isProduction) {
7
+ const ReactDOM = await import("react-dom");
8
+
9
+ // oxlint-disable-next-line import/no-extraneous-dependencies
10
+ const axe = await import("@axe-core/react");
11
+
12
+ axe.default(React, ReactDOM, 1000);
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import log from "loglevel";
2
+
3
+ import { AppConfig } from "@/config/app.config";
4
+
5
+ export function initLogger() {
6
+ log.setLevel(AppConfig.log.level as log.LogLevelDesc);
7
+ }
@@ -0,0 +1,21 @@
1
+ // oxlint-disable-next-line import/no-namespace
2
+ import * as Sentry from "@sentry/react";
3
+
4
+ import { AppConfig } from "@/config/app.config";
5
+
6
+ export function initSentry() {
7
+ Sentry.init({
8
+ enabled: AppConfig.sentry.enabled,
9
+ dsn: AppConfig.sentry.dsn,
10
+ release: AppConfig.release,
11
+ integrations: [
12
+ ...(AppConfig.sentry.tracesSampleRate > 0 ? [] : []),
13
+ ...(AppConfig.sentry.replaysOnErrorSampleRate > 0 ? [Sentry.replayIntegration()] : []),
14
+ ],
15
+
16
+ tracesSampleRate: AppConfig.sentry.tracesSampleRate,
17
+
18
+ replaysSessionSampleRate: AppConfig.sentry.replaysSessionSampleRate,
19
+ replaysOnErrorSampleRate: AppConfig.sentry.replaysOnErrorSampleRate,
20
+ });
21
+ }
@@ -0,0 +1,2 @@
1
+ export const ACCESS_TOKEN_KEY = "auth-custom-jwt:access-token";
2
+ export const REFRESH_TOKEN_KEY = "auth-custom-jwt:refresh-token";
@@ -0,0 +1,20 @@
1
+ import type { ApplicationException, GeneralErrorCodes } from "@povio/openapi-codegen-cli";
2
+ import { QueryClient, type QueryClientConfig } from "@tanstack/react-query";
3
+
4
+ export const QueryConfig: QueryClientConfig = {
5
+ defaultOptions: {
6
+ queries: {
7
+ retry: false,
8
+ refetchOnWindowFocus: false,
9
+ staleTime: 2 * 60 * 1000,
10
+ },
11
+ },
12
+ };
13
+
14
+ export const queryClient = new QueryClient(QueryConfig);
15
+
16
+ declare module "@tanstack/react-query" {
17
+ interface Register {
18
+ defaultError: ApplicationException<GeneralErrorCodes> | null;
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ import { AuthContext } from "@povio/openapi-codegen-cli";
2
+
3
+ import type { UserModels } from "@/openapi/user/user.models";
4
+
5
+ export const useAuth = () => AuthContext.useAuth<UserModels.UserMeResponse>();