robodev 0.12.0 → 0.13.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 (155) hide show
  1. package/package.json +1 -1
  2. package/src/emit-starter.test.ts +14 -0
  3. package/src/index.ts +12 -4
  4. package/templates/auth-chat/.config/local.spa.template.yml +27 -0
  5. package/templates/auth-chat/.oxlint-base.json +29 -0
  6. package/templates/auth-chat/.oxlintrc.json +78 -0
  7. package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +36 -0
  8. package/templates/auth-chat/.rulesync/rules/frontend-component-structure.md +38 -0
  9. package/templates/auth-chat/.rulesync/rules/frontend-forms.md +32 -0
  10. package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +24 -0
  11. package/templates/auth-chat/.rulesync/rules/frontend-povio-components.md +42 -0
  12. package/templates/auth-chat/.rulesync/rules/frontend-povio-ui.md +45 -0
  13. package/templates/auth-chat/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  14. package/templates/auth-chat/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  15. package/templates/auth-chat/.rulesync/rules/frontend-translations.md +26 -0
  16. package/templates/auth-chat/.rulesync/rules/project-overview.md +32 -0
  17. package/templates/auth-chat/.rulesync/rules/robodev-api.md +30 -0
  18. package/templates/auth-chat/.rulesync/rules/robodev-auth.md +40 -0
  19. package/templates/auth-chat/.rulesync/rules/robodev-database.md +22 -0
  20. package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +36 -0
  21. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +21 -0
  22. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  23. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  24. package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  25. package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +12 -0
  26. package/templates/auth-chat/README.md +26 -7
  27. package/templates/auth-chat/api/invite.ts +43 -0
  28. package/templates/auth-chat/apps/fe/index.html +24 -0
  29. package/templates/auth-chat/apps/fe/openapi-codegen.config.ts +37 -0
  30. package/templates/auth-chat/apps/fe/package.json +82 -0
  31. package/templates/auth-chat/apps/fe/public/apple-touch-icon.png +0 -0
  32. package/templates/auth-chat/apps/fe/public/favicon-96x96.png +0 -0
  33. package/templates/auth-chat/apps/fe/public/favicon.ico +0 -0
  34. package/templates/auth-chat/apps/fe/public/favicon.svg +3 -0
  35. package/templates/auth-chat/apps/fe/public/site.webmanifest +21 -0
  36. package/templates/auth-chat/apps/fe/public/web-app-manifest-192x192.png +0 -0
  37. package/templates/auth-chat/apps/fe/public/web-app-manifest-512x512.png +0 -0
  38. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  39. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  40. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  41. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  42. package/templates/auth-chat/apps/fe/src/assets/locales/en/translation.json +202 -0
  43. package/templates/auth-chat/apps/fe/src/assets/locales/sl/translation.json +202 -0
  44. package/templates/auth-chat/apps/fe/src/clients/app-rest-client.ts +15 -0
  45. package/templates/auth-chat/apps/fe/src/clients/auth-token-store.ts +101 -0
  46. package/templates/auth-chat/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  47. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  48. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  49. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  50. package/templates/auth-chat/apps/fe/src/components/404.tsx +18 -0
  51. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  52. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  53. package/templates/auth-chat/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  54. package/templates/auth-chat/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  55. package/templates/auth-chat/apps/fe/src/components/features/chat/ChatPage.tsx +105 -0
  56. package/templates/auth-chat/apps/fe/src/components/features/chat/InviteForm.tsx +84 -0
  57. package/templates/auth-chat/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  58. package/templates/auth-chat/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  59. package/templates/auth-chat/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  60. package/templates/auth-chat/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  61. package/templates/auth-chat/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  62. package/templates/auth-chat/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  63. package/templates/auth-chat/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  64. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  65. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  66. package/templates/auth-chat/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  67. package/templates/auth-chat/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  68. package/templates/auth-chat/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  69. package/templates/auth-chat/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  70. package/templates/auth-chat/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  71. package/templates/auth-chat/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  72. package/templates/auth-chat/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  73. package/templates/auth-chat/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  74. package/templates/auth-chat/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  75. package/templates/auth-chat/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  76. package/templates/auth-chat/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  77. package/templates/auth-chat/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  78. package/templates/auth-chat/apps/fe/src/config/app.config.ts +35 -0
  79. package/templates/auth-chat/apps/fe/src/config/i18n.ts +43 -0
  80. package/templates/auth-chat/apps/fe/src/config/inits/a11y.ts +14 -0
  81. package/templates/auth-chat/apps/fe/src/config/inits/logger.ts +7 -0
  82. package/templates/auth-chat/apps/fe/src/config/inits/sentry.ts +21 -0
  83. package/templates/auth-chat/apps/fe/src/config/jwt.config.ts +2 -0
  84. package/templates/auth-chat/apps/fe/src/config/query.config.ts +20 -0
  85. package/templates/auth-chat/apps/fe/src/hooks/useAuth.ts +5 -0
  86. package/templates/auth-chat/apps/fe/src/main.tsx +52 -0
  87. package/templates/auth-chat/apps/fe/src/pages/(guest)/login.tsx +23 -0
  88. package/templates/auth-chat/apps/fe/src/pages/(guest)/register.tsx +23 -0
  89. package/templates/auth-chat/apps/fe/src/pages/(guest)/route.tsx +18 -0
  90. package/templates/auth-chat/apps/fe/src/pages/(private)/index.tsx +23 -0
  91. package/templates/auth-chat/apps/fe/src/pages/(private)/profile.tsx +23 -0
  92. package/templates/auth-chat/apps/fe/src/pages/(private)/route.tsx +18 -0
  93. package/templates/auth-chat/apps/fe/src/pages/(public)/auth.tsx +37 -0
  94. package/templates/auth-chat/apps/fe/src/pages/(public)/route.tsx +9 -0
  95. package/templates/auth-chat/apps/fe/src/pages/__root.tsx +164 -0
  96. package/templates/auth-chat/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  97. package/templates/auth-chat/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  98. package/templates/auth-chat/apps/fe/src/providers/index.tsx +44 -0
  99. package/templates/auth-chat/apps/fe/src/providers/jwt.provider.tsx +95 -0
  100. package/templates/auth-chat/apps/fe/src/routeTree.gen.ts +225 -0
  101. package/templates/auth-chat/apps/fe/src/styles/base.css +103 -0
  102. package/templates/auth-chat/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  103. package/templates/auth-chat/apps/fe/src/styles/fonts/general-sans.css +31 -0
  104. package/templates/auth-chat/apps/fe/src/styles/globals.css +28 -0
  105. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  106. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  107. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  108. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  109. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  110. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  111. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  112. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  113. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  114. package/templates/auth-chat/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  115. package/templates/auth-chat/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  116. package/templates/auth-chat/apps/fe/src/styles/theme.css +2177 -0
  117. package/templates/auth-chat/apps/fe/src/types/i18next.d.ts +12 -0
  118. package/templates/auth-chat/apps/fe/src/types/table.d.ts +17 -0
  119. package/templates/auth-chat/apps/fe/src/types/ui.d.ts +26 -0
  120. package/templates/auth-chat/apps/fe/src/types/vite-env.d.ts +20 -0
  121. package/templates/auth-chat/apps/fe/src/utils/date.utils.ts +17 -0
  122. package/templates/auth-chat/apps/fe/src/utils/number.utils.ts +12 -0
  123. package/templates/auth-chat/apps/fe/src/utils/string.utils.ts +5 -0
  124. package/templates/auth-chat/apps/fe/src/vite-env.d.ts +1 -0
  125. package/templates/auth-chat/apps/fe/tsconfig.app.json +32 -0
  126. package/templates/auth-chat/apps/fe/tsconfig.json +9 -0
  127. package/templates/auth-chat/apps/fe/tsconfig.node.json +31 -0
  128. package/templates/auth-chat/apps/fe/vite.config.ts +94 -0
  129. package/templates/auth-chat/openapi.json +1016 -0
  130. package/templates/auth-chat/oxfmt.config.js +23 -0
  131. package/templates/auth-chat/package.json +6 -13
  132. package/templates/auth-chat/rulesync.jsonc +18 -0
  133. package/templates/auth-chat/tsconfig.json +2 -4
  134. package/templates/backend/README.md +2 -2
  135. package/templates/backend/api/_lib.ts +359 -0
  136. package/templates/backend/api/aliens/labels.ts +19 -0
  137. package/templates/backend/api/files/presigned-url.ts +48 -0
  138. package/templates/backend/api/files/upload.ts +29 -0
  139. package/templates/backend/api/planets/[id]/like.ts +36 -0
  140. package/templates/backend/api/planets/[id].ts +56 -13
  141. package/templates/backend/api/planets/paginate.ts +36 -0
  142. package/templates/backend/api/planets.ts +52 -44
  143. package/templates/backend/database.ts +33 -13
  144. package/templates/backend/package.json +1 -1
  145. package/templates/catalog.json +2 -2
  146. package/templates/empty/package.json +1 -1
  147. package/templates/space/package.json +1 -1
  148. package/templates/auth-chat/index.html +0 -201
  149. package/templates/auth-chat/src/main.tsx +0 -282
  150. package/templates/auth-chat/vite.config.ts +0 -7
  151. package/templates/backend/api/health.ts +0 -7
  152. package/templates/backend/api/launch.ts +0 -29
  153. package/templates/backend/api/me.ts +0 -14
  154. package/templates/backend/api/motto.ts +0 -9
  155. package/templates/backend/api/rockets.ts +0 -43
@@ -0,0 +1,91 @@
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
+ </nav>
54
+ </div>
55
+
56
+ {isAuthenticated && (
57
+ <div className="ml-auto hidden shrink-0 items-center gap-2 lg:flex">
58
+ <Menu
59
+ trigger={
60
+ <div className="flex cursor-pointer items-center gap-1">
61
+ <Button
62
+ type="button"
63
+ color="secondary"
64
+ variant="text"
65
+ size="xs"
66
+ className="items-center gap-1 overflow-hidden whitespace-nowrap text-text-default-1 [&>span]:font-labels-default!"
67
+ >
68
+ {accountLabel}
69
+ </Button>
70
+ <ChevronDown className="size-5 shrink-0" />
71
+ </div>
72
+ }
73
+ items={menuItems}
74
+ onAction={handleMenuAction}
75
+ />
76
+ </div>
77
+ )}
78
+
79
+ <IconButton
80
+ className="ml-auto lg:hidden"
81
+ color="primary"
82
+ icon={isMobileMenuOpen ? X : MenuIcon}
83
+ label={t(($) => (isMobileMenuOpen ? $.layout.header.closeMenu : $.layout.header.openMenu))}
84
+ size="xs"
85
+ variant="ghost"
86
+ onPress={() => setIsMobileMenuOpen((isOpen) => !isOpen)}
87
+ />
88
+ {isMobileMenuOpen ? <MobileNavigation /> : null}
89
+ </header>
90
+ );
91
+ }
@@ -0,0 +1,51 @@
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
+ </nav>
30
+
31
+ {isAuthenticated ? (
32
+ <Menu
33
+ items={[
34
+ { id: "profile", label: t(($) => $.layout.header.profile) },
35
+ { id: "logout", label: t(($) => $.layout.header.logout) },
36
+ ]}
37
+ trigger={
38
+ <TextButton
39
+ className="mr-auto"
40
+ color="secondary"
41
+ icon={User}
42
+ >
43
+ {accountLabel}
44
+ </TextButton>
45
+ }
46
+ onAction={handleMenuAction}
47
+ />
48
+ ) : null}
49
+ </div>
50
+ );
51
+ }
@@ -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
+ };