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.
- package/package.json +1 -1
- package/src/collect-files.test.ts +19 -0
- package/src/collect-files.ts +1 -0
- package/src/config.test.ts +26 -0
- package/src/config.ts +34 -5
- package/src/emit-starter.test.ts +54 -0
- package/src/emit-starter.ts +47 -13
- package/src/index.ts +37 -4
- package/templates/auth-chat/README.md +1 -1
- package/templates/auth-chat/api/health.ts +1 -1
- package/templates/auth-chat/api/me.ts +1 -1
- package/templates/auth-chat/api/messages.ts +1 -1
- package/templates/auth-chat/package.json +1 -1
- package/templates/auth-chat/src/main.tsx +5 -5
- package/templates/backend/README.md +1 -1
- package/templates/backend/api/health.ts +1 -1
- package/templates/backend/api/launch.ts +1 -1
- package/templates/backend/api/me.ts +1 -1
- package/templates/backend/api/motto.ts +1 -1
- package/templates/backend/api/planets/[id].ts +1 -1
- package/templates/backend/api/planets.ts +1 -1
- package/templates/backend/api/rockets.ts +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/catalog.json +2 -2
- package/templates/empty/README.md +1 -1
- package/templates/empty/api/health.ts +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/space/.config/local.spa.template.yml +27 -0
- package/templates/space/.oxlint-base.json +29 -0
- package/templates/space/.oxlintrc.json +78 -0
- package/templates/space/.rulesync/rules/frontend-api-boundary.md +36 -0
- package/templates/space/.rulesync/rules/frontend-component-structure.md +38 -0
- package/templates/space/.rulesync/rules/frontend-forms.md +32 -0
- package/templates/space/.rulesync/rules/frontend-notifications.md +24 -0
- package/templates/space/.rulesync/rules/frontend-povio-components.md +42 -0
- package/templates/space/.rulesync/rules/frontend-povio-ui.md +45 -0
- package/templates/space/.rulesync/rules/frontend-query-autocomplete.md +29 -0
- package/templates/space/.rulesync/rules/frontend-tables-and-lists.md +34 -0
- package/templates/space/.rulesync/rules/frontend-translations.md +26 -0
- package/templates/space/.rulesync/rules/project-overview.md +32 -0
- package/templates/space/.rulesync/rules/robodev-api.md +30 -0
- package/templates/space/.rulesync/rules/robodev-auth.md +40 -0
- package/templates/space/.rulesync/rules/robodev-database.md +22 -0
- package/templates/space/.rulesync/rules/role-based-app-structure.md +36 -0
- package/templates/space/.rulesync/skills/media-feature/SKILL.md +21 -0
- package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
- package/templates/space/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
- package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
- package/templates/space/.rulesync/skills/robodev-table/SKILL.md +12 -0
- package/templates/space/README.md +26 -7
- package/templates/space/api/_lib.ts +359 -0
- package/templates/space/api/aliens/labels.ts +19 -0
- package/templates/space/api/files/presigned-url.ts +48 -0
- package/templates/space/api/files/upload.ts +29 -0
- package/templates/space/api/planets/[id]/like.ts +36 -0
- package/templates/space/api/planets/[id].ts +56 -13
- package/templates/space/api/planets/paginate.ts +36 -0
- package/templates/space/api/planets.ts +52 -44
- package/templates/space/apps/fe/index.html +24 -0
- package/templates/space/apps/fe/openapi-codegen.config.ts +37 -0
- package/templates/space/apps/fe/package.json +82 -0
- package/templates/space/apps/fe/public/apple-touch-icon.png +0 -0
- package/templates/space/apps/fe/public/favicon-96x96.png +0 -0
- package/templates/space/apps/fe/public/favicon.ico +0 -0
- package/templates/space/apps/fe/public/favicon.svg +3 -0
- package/templates/space/apps/fe/public/site.webmanifest +21 -0
- package/templates/space/apps/fe/public/web-app-manifest-192x192.png +0 -0
- package/templates/space/apps/fe/public/web-app-manifest-512x512.png +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
- package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
- package/templates/space/apps/fe/src/assets/locales/en/translation.json +301 -0
- package/templates/space/apps/fe/src/assets/locales/sl/translation.json +301 -0
- package/templates/space/apps/fe/src/clients/app-rest-client.ts +15 -0
- package/templates/space/apps/fe/src/clients/auth-token-store.ts +101 -0
- package/templates/space/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
- package/templates/space/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
- package/templates/space/apps/fe/src/components/404.tsx +18 -0
- package/templates/space/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
- package/templates/space/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
- package/templates/space/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
- package/templates/space/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
- package/templates/space/apps/fe/src/components/features/home/HomePage.tsx +21 -0
- package/templates/space/apps/fe/src/components/features/planets/details/PlanetDetailsPage.tsx +139 -0
- package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +266 -0
- package/templates/space/apps/fe/src/components/features/planets/feed/PlanetFeedCard.tsx +131 -0
- package/templates/space/apps/fe/src/components/features/planets/feed/PlanetsFeedPage.tsx +119 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetCard.tsx +89 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +179 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.css +3 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.tsx +73 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsGridPage.tsx +51 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsInfiniteTablePage.tsx +79 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsPage.tsx +65 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTablePage.tsx +72 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTableSummary.tsx +29 -0
- package/templates/space/apps/fe/src/components/features/planets/list/PlanetsViewTabs.tsx +79 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTable.tsx +30 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableActions.tsx +64 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableInfinite.tsx +36 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/planetsTableColumns.tsx +72 -0
- package/templates/space/apps/fe/src/components/features/planets/list/table/usePlanetTableFilters.tsx +102 -0
- package/templates/space/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
- package/templates/space/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
- package/templates/space/apps/fe/src/components/layout/AppLayout.tsx +19 -0
- package/templates/space/apps/fe/src/components/layout/app-header/AppHeader.tsx +93 -0
- package/templates/space/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +53 -0
- package/templates/space/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
- package/templates/space/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
- package/templates/space/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
- package/templates/space/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
- package/templates/space/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
- package/templates/space/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
- package/templates/space/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
- package/templates/space/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
- package/templates/space/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
- package/templates/space/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
- package/templates/space/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
- package/templates/space/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
- package/templates/space/apps/fe/src/components/shared/ui/Card.tsx +60 -0
- package/templates/space/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
- package/templates/space/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
- package/templates/space/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
- package/templates/space/apps/fe/src/config/app.config.ts +35 -0
- package/templates/space/apps/fe/src/config/i18n.ts +43 -0
- package/templates/space/apps/fe/src/config/inits/a11y.ts +14 -0
- package/templates/space/apps/fe/src/config/inits/logger.ts +7 -0
- package/templates/space/apps/fe/src/config/inits/sentry.ts +21 -0
- package/templates/space/apps/fe/src/config/jwt.config.ts +2 -0
- package/templates/space/apps/fe/src/config/query.config.ts +20 -0
- package/templates/space/apps/fe/src/hooks/useAuth.ts +5 -0
- package/templates/space/apps/fe/src/main.tsx +52 -0
- package/templates/space/apps/fe/src/pages/(guest)/login.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(guest)/register.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(guest)/route.tsx +18 -0
- package/templates/space/apps/fe/src/pages/(private)/index.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/$id/edit.tsx +53 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/$id/index.tsx +41 -0
- package/templates/space/apps/fe/src/pages/(private)/planets/index.tsx +24 -0
- package/templates/space/apps/fe/src/pages/(private)/planets-feed.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/profile.tsx +23 -0
- package/templates/space/apps/fe/src/pages/(private)/route.tsx +18 -0
- package/templates/space/apps/fe/src/pages/(public)/auth.tsx +37 -0
- package/templates/space/apps/fe/src/pages/(public)/route.tsx +9 -0
- package/templates/space/apps/fe/src/pages/__root.tsx +164 -0
- package/templates/space/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
- package/templates/space/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
- package/templates/space/apps/fe/src/providers/index.tsx +44 -0
- package/templates/space/apps/fe/src/providers/jwt.provider.tsx +95 -0
- package/templates/space/apps/fe/src/routeTree.gen.ts +309 -0
- package/templates/space/apps/fe/src/styles/base.css +103 -0
- package/templates/space/apps/fe/src/styles/fonts/fonts.tsx +10 -0
- package/templates/space/apps/fe/src/styles/fonts/general-sans.css +31 -0
- package/templates/space/apps/fe/src/styles/globals.css +28 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
- package/templates/space/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
- package/templates/space/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
- package/templates/space/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
- package/templates/space/apps/fe/src/styles/theme.css +2177 -0
- package/templates/space/apps/fe/src/types/i18next.d.ts +12 -0
- package/templates/space/apps/fe/src/types/table.d.ts +17 -0
- package/templates/space/apps/fe/src/types/ui.d.ts +26 -0
- package/templates/space/apps/fe/src/types/vite-env.d.ts +20 -0
- package/templates/space/apps/fe/src/utils/date.utils.ts +17 -0
- package/templates/space/apps/fe/src/utils/image-fallback.ts +9 -0
- package/templates/space/apps/fe/src/utils/media-upload.ts +63 -0
- package/templates/space/apps/fe/src/utils/number.utils.ts +12 -0
- package/templates/space/apps/fe/src/utils/string.utils.ts +5 -0
- package/templates/space/apps/fe/src/vite-env.d.ts +1 -0
- package/templates/space/apps/fe/tsconfig.app.json +32 -0
- package/templates/space/apps/fe/tsconfig.json +9 -0
- package/templates/space/apps/fe/tsconfig.node.json +31 -0
- package/templates/space/apps/fe/vite.config.ts +94 -0
- package/templates/space/database.ts +33 -13
- package/templates/space/openapi.json +1819 -0
- package/templates/space/oxfmt.config.js +23 -0
- package/templates/space/package.json +6 -13
- package/templates/space/rulesync.jsonc +18 -0
- package/templates/space/tsconfig.json +2 -4
- package/templates/space/api/health.ts +0 -7
- package/templates/space/api/launch.ts +0 -29
- package/templates/space/api/me.ts +0 -14
- package/templates/space/api/motto.ts +0 -9
- package/templates/space/api/rockets.ts +0 -43
- package/templates/space/index.html +0 -138
- package/templates/space/src/main.tsx +0 -229
- package/templates/space/vite.config.ts +0 -7
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: povio-ui-styling
|
|
3
|
+
description: Mandatory first skill for any Tiny Template web frontend UI work in apps/fe. Load and apply it before other frontend, design, UX, accessibility, responsive, animation, or performance skills when creating, changing, reviewing, debugging, polishing, or refactoring pages and React components. It requires checking and using @povio/ui primitives before raw HTML or custom controls, and governs UIConfig defaults, UIOverrides, Tailwind utilities, semantic tokens, shared UI wrappers, and dedicated CSS files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Povio UI Styling
|
|
7
|
+
|
|
8
|
+
Apply the narrowest styling layer that expresses the intended scope. Preserve one source of truth for each visual rule.
|
|
9
|
+
|
|
10
|
+
## Priority and Coordination
|
|
11
|
+
|
|
12
|
+
This is the foundation skill for every UI task under `apps/fe`. Apply it first, before broader design, UX, accessibility, animation, responsive, or React performance skills. Those skills may improve the result, but they must preserve the component and styling decisions defined here.
|
|
13
|
+
|
|
14
|
+
Use this skill for all work that creates, changes, reviews, or diagnoses visible or interactive frontend UI, including:
|
|
15
|
+
|
|
16
|
+
- Pages, layouts, navigation, dashboards, cards, lists, and empty states
|
|
17
|
+
- Forms, filters, search, inputs, validation, uploads, and settings
|
|
18
|
+
- Buttons, links, menus, dialogs, drawers, popovers, tooltips, and toasts
|
|
19
|
+
- Tables, pagination, tags, badges, typography, icons, and loading states
|
|
20
|
+
- Responsive behavior, theming, accessibility, motion, and visual polish
|
|
21
|
+
- Component extraction, shared wrappers, design-system alignment, and Figma implementation
|
|
22
|
+
|
|
23
|
+
This skill does not replace feature-specific skills. Apply the relevant feature skill as well, after establishing the Povio UI component and styling layer.
|
|
24
|
+
|
|
25
|
+
## Inspect Before Styling
|
|
26
|
+
|
|
27
|
+
1. Read the target component and nearby feature components.
|
|
28
|
+
2. Inspect existing `@povio/ui` usage in `apps/fe` for the relevant primitive.
|
|
29
|
+
3. Inspect `apps/fe/src/styles/overrides/uiOverrides.override.ts` and the matching file under `styles/overrides/defaults` before changing a Povio primitive.
|
|
30
|
+
4. Inspect `apps/fe/src/styles/theme.css` for available generated tokens, but never edit that file manually.
|
|
31
|
+
5. Inspect `apps/fe/src/styles/base.css` and `globals.css` before adding global rules or tokens.
|
|
32
|
+
|
|
33
|
+
## Use This Decision Order
|
|
34
|
+
|
|
35
|
+
### 1. Use an Existing Povio UI Component
|
|
36
|
+
|
|
37
|
+
Use `@povio/ui` whenever it provides the semantic control or presentation primitive. This includes buttons, links, typography, icons, inputs, textareas, selects, checkboxes, radios, switches, forms, uploads, tags, badges, tables, pagination, menus, tooltips, modals, confirmations, drawers, popovers, toasts, loading indicators, and empty or feedback states.
|
|
38
|
+
|
|
39
|
+
Prefer the component's supported props and variants over restyling its internals. Do not recreate a primitive with a raw HTML element merely to get a different appearance.
|
|
40
|
+
|
|
41
|
+
Before introducing a native interactive element, a custom control, or a third-party UI component, search `@povio/ui` exports and existing feature usage for an existing equivalent. Native structural elements such as `main`, `section`, `article`, `nav`, and layout containers remain appropriate when no Povio semantic component exists.
|
|
42
|
+
|
|
43
|
+
### 2. Use `UIConfig` for Application-Wide Defaults
|
|
44
|
+
|
|
45
|
+
Use the root `UIConfig.Provider` when an existing Povio variant or option is correct and only its default should change across the application.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
|
|
49
|
+
- Make all inputs default to `small`.
|
|
50
|
+
- Set the default table-cell typography size.
|
|
51
|
+
- Set the default tag text size.
|
|
52
|
+
|
|
53
|
+
Do not use `UIConfig` for a one-off screen or to invent a new variant.
|
|
54
|
+
|
|
55
|
+
### 3. Use Tailwind for Composition and Local Presentation
|
|
56
|
+
|
|
57
|
+
Use Tailwind classes in JSX for:
|
|
58
|
+
|
|
59
|
+
- Page and component layout
|
|
60
|
+
- Grid and flex behavior
|
|
61
|
+
- Width, height, spacing, alignment, and overflow
|
|
62
|
+
- Responsive composition
|
|
63
|
+
- A one-instance surface, border, or text treatment
|
|
64
|
+
- State selectors that remain short and readable
|
|
65
|
+
|
|
66
|
+
Use semantic project utilities such as `bg-elevation-fill-default-1`, `text-text-default-1`, and `border-elevation-outline-default-1`. Do not use removed default palette utilities such as `bg-blue-500`, and do not repeat raw hex values in components.
|
|
67
|
+
|
|
68
|
+
Keep Povio component `className` additions focused on outer layout or a genuinely local treatment. Do not pile conflicting utilities onto a component to fight its configured CVA variants. If the same correction is required on most instances, move it to `UIConfig` or `UIOverrides`.
|
|
69
|
+
|
|
70
|
+
### 4. Use `UIOverrides` for Design-System-Level Povio Changes
|
|
71
|
+
|
|
72
|
+
Use `UIOverrides.defineOverride(...)` when a Povio primitive must change consistently throughout the application:
|
|
73
|
+
|
|
74
|
+
- Change the base classes of every instance.
|
|
75
|
+
- Add or change supported variants.
|
|
76
|
+
- Change compound-variant behavior.
|
|
77
|
+
- Change internal slots not exposed by ordinary component props.
|
|
78
|
+
- Align a primitive with the application's Figma design system.
|
|
79
|
+
|
|
80
|
+
Place overrides under `apps/fe/src/styles/overrides/defaults/<component>.override.ts` and register them in `uiOverrides.override.ts`. Use the existing `mode`, `base`, `config`, variant, and compound-variant patterns.
|
|
81
|
+
|
|
82
|
+
Update `apps/fe/src/types/ui.d.ts` when a new override variant must augment public Povio component types.
|
|
83
|
+
|
|
84
|
+
Do not use `UIOverrides` for:
|
|
85
|
+
|
|
86
|
+
- Page layout
|
|
87
|
+
- One feature's special card
|
|
88
|
+
- One exceptional component instance
|
|
89
|
+
- A workaround that can be expressed through an existing prop
|
|
90
|
+
|
|
91
|
+
Changing an override affects every consumer. Search all uses of the primitive and verify representative screens before finishing.
|
|
92
|
+
|
|
93
|
+
### 5. Use a Shared Wrapper for Repeated Product Semantics
|
|
94
|
+
|
|
95
|
+
Create a component under `apps/fe/src/components/shared` when several features need the same product-specific composition but the base Povio primitive should remain unchanged.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
|
|
99
|
+
- `StatusTag` mapping domain statuses to Povio `Tag` variants
|
|
100
|
+
- `WalkSchedule` composing typography, icons, and layout
|
|
101
|
+
- `PageCard` applying a repeated application surface
|
|
102
|
+
|
|
103
|
+
Expose semantic props rather than forwarding a growing collection of styling flags.
|
|
104
|
+
|
|
105
|
+
### 6. Use Dedicated CSS for Selector-Heavy or Browser-Level Behavior
|
|
106
|
+
|
|
107
|
+
Create or extend a `.css` file when CSS communicates the rule more clearly than Tailwind:
|
|
108
|
+
|
|
109
|
+
- Pseudo-elements used for connected timelines or route lines
|
|
110
|
+
- Multi-step keyframes
|
|
111
|
+
- Complex descendant, sibling, or state selectors
|
|
112
|
+
- Calendar grids and overlapping appointments
|
|
113
|
+
- Map-library markers, popups, or third-party DOM overrides
|
|
114
|
+
- Print styles
|
|
115
|
+
- Browser-global behavior such as scrollbars
|
|
116
|
+
- Rules driven by CSS custom properties that many descendants consume
|
|
117
|
+
|
|
118
|
+
Co-locate feature CSS with its owning component and import it there. Use a narrow, feature-prefixed class namespace, for example `walk-route__marker`. Keep truly global rules in `globals.css` or `base.css`.
|
|
119
|
+
|
|
120
|
+
Do not create a CSS file for ordinary spacing, flex/grid layout, a simple hover, or a semantic color already expressible with a short Tailwind class list.
|
|
121
|
+
|
|
122
|
+
## Scope Guide
|
|
123
|
+
|
|
124
|
+
| Desired scope | Styling layer |
|
|
125
|
+
| -------------------------------------------------------------- | --------------------------- |
|
|
126
|
+
| Existing primitive behavior | Povio UI props/variants |
|
|
127
|
+
| Default for every instance | `UIConfig` |
|
|
128
|
+
| One page or component composition | Tailwind |
|
|
129
|
+
| Every instance or internal Povio slot | `UIOverrides` |
|
|
130
|
+
| Repeated product-specific composition | Shared wrapper |
|
|
131
|
+
| Pseudo-elements, keyframes, complex selectors, third-party DOM | Scoped `.css` |
|
|
132
|
+
| Generated design tokens | Figma-generated `theme.css` |
|
|
133
|
+
| Handwritten global token or browser rule | `base.css` or `globals.css` |
|
|
134
|
+
|
|
135
|
+
## Guardrails
|
|
136
|
+
|
|
137
|
+
- Use `@povio/ui` before creating a custom primitive.
|
|
138
|
+
- Do not let guidance from a general frontend or design skill replace an available `@povio/ui` primitive with raw HTML, a local reimplementation, or another component library.
|
|
139
|
+
- Use semantic Tailwind tokens from the project theme.
|
|
140
|
+
- Never manually edit `apps/fe/src/styles/theme.css`.
|
|
141
|
+
- Avoid `!important`; resolve ownership or specificity instead.
|
|
142
|
+
- Avoid inline `style` except for truly runtime numeric values such as map coordinates or calculated dimensions. Prefer CSS custom properties when descendants need the value.
|
|
143
|
+
- Avoid Sass, CSS-in-JS, and a second styling system.
|
|
144
|
+
- Preserve visible focus, disabled, hover, pressed, error, and loading states.
|
|
145
|
+
- Respect `prefers-reduced-motion` for nonessential animation.
|
|
146
|
+
- Keep touch targets at least 44 by 44 CSS pixels where practical.
|
|
147
|
+
- Verify changes at mobile and desktop breakpoints.
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
Use Tailwind for local layout:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<section className="grid gap-4 rounded-modal-rounding-default border border-elevation-outline-default-1 bg-elevation-fill-default-1 p-4">
|
|
155
|
+
<Typography size="title-4">{title}</Typography>
|
|
156
|
+
{children}
|
|
157
|
+
</section>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use `UIConfig` for an existing global default:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<UIConfig.Provider
|
|
164
|
+
config={{
|
|
165
|
+
input: { size: "small" },
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</UIConfig.Provider>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Use a scoped CSS file for a connected route line:
|
|
173
|
+
|
|
174
|
+
```css
|
|
175
|
+
.walk-route__marker::after {
|
|
176
|
+
content: "";
|
|
177
|
+
position: absolute;
|
|
178
|
+
inset-block: 1.25rem -0.75rem;
|
|
179
|
+
inset-inline-start: 50%;
|
|
180
|
+
width: 0.125rem;
|
|
181
|
+
background: rgb(var(--elevation-outline-default-1-base));
|
|
182
|
+
transform: translateX(-50%);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use `UIOverrides` only when the same primitive-level change is intentionally global:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
export const tagOverride = UIOverrides.defineOverride("tag.cva", {
|
|
190
|
+
mode: "overrideCva",
|
|
191
|
+
base: ["inline-flex items-center border"],
|
|
192
|
+
config: {
|
|
193
|
+
variants: {
|
|
194
|
+
color: {
|
|
195
|
+
success: "border-interactive-subtle-success-idle bg-interactive-subtle-success-idle",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Verification
|
|
203
|
+
|
|
204
|
+
1. Run formatting, linting, and TypeScript checks relevant to `apps/fe`.
|
|
205
|
+
2. Inspect at least one representative use for every globally changed primitive.
|
|
206
|
+
3. Check keyboard focus and disabled/error states.
|
|
207
|
+
4. Check mobile and desktop layouts.
|
|
208
|
+
5. Confirm no raw default-palette classes or duplicated design values were introduced.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: robodev-api-route
|
|
3
|
+
description: Add a defineApi route under api/.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a Robodev API route
|
|
7
|
+
|
|
8
|
+
1. Create `api/<path>.ts` or `api/<path>/[id].ts`.
|
|
9
|
+
2. Export HTTP methods with `defineApi` from `@robodev-ai/sdk`.
|
|
10
|
+
3. Use `auth: "required"` unless the route is public.
|
|
11
|
+
4. Keep JSON shapes aligned with `openapi.json`.
|
|
12
|
+
5. Do not create `api/user/**` files.
|
|
13
|
+
6. Deploy from the project root (`robodev deploy`), then `bun openapi:gen` in `apps/fe` if the contract changed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: robodev-table
|
|
3
|
+
description: Add a Drizzle table to database.ts and wire it into defineApi handlers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a Robodev table
|
|
7
|
+
|
|
8
|
+
1. Add a `pgTable` to `database.ts` and include it in `defineDatabase({ tables })`.
|
|
9
|
+
2. Do not add a User table. Auth users are platform-managed; store `userId` as text.
|
|
10
|
+
3. Import the table in `api/_lib.ts` or the route file that needs it.
|
|
11
|
+
4. Deploy from the project root with `robodev deploy`. Destructive drops need confirmation or `--force`.
|
|
12
|
+
5. If the frontend needs the new fields, update `openapi.json` and run `bun openapi:gen` from `apps/fe`.
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
# {{NAME}}
|
|
2
2
|
|
|
3
|
-
Starter
|
|
3
|
+
Starter from `robodev create`: Tiny Template schema (`planets`, `aliens`, `media`, `planet_likes`), file-based APIs in `api/`, and a **local** Povio UI in `apps/fe`.
|
|
4
4
|
|
|
5
|
-
The project host is
|
|
5
|
+
The project host is **API-only**. After create or deploy, the printed App URL serves APIs and Swagger — not the SPA. Run the UI with bun:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
cd apps/fe
|
|
9
|
+
bun install
|
|
10
|
+
bun openapi:gen
|
|
11
|
+
bun dev
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Vite defaults to port **3000** (`VITE_DEV_PORT`). `bun` is required for `apps/fe`.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
Auth, link, and deploy from this **root**:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx robodev auth
|
|
20
|
+
npx robodev link
|
|
21
|
+
npx robodev deploy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`robodev create` / `robodev link` write `VITE_API_URL`, `VITE_PUBLIC_API_URL`, and `APP_PUBLIC_API_URL` in `.env`, and rewrite `.config/local.spa.yml` so the FE talks to the project host.
|
|
25
|
+
|
|
26
|
+
Dashboard create deploys schema + APIs only. The Tiny FE tree is copied by `robodev create`.
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
Auth stays reserved at `/api/user/*`. Do not add `api/user/**`.
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
Regenerate AI rules after editing `.rulesync/`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun rules:gen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Docs: https://robodev.povio.dev/docs
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { asc, eq, type InferSelectModel } from "@robodev-ai/sdk";
|
|
2
|
+
import type { ApiHandlerContext } from "@robodev-ai/sdk";
|
|
3
|
+
import { aliens, media, planetLikes, planets } from "../database";
|
|
4
|
+
|
|
5
|
+
export const PLANET_IMAGE_RESOURCE = "planet-image";
|
|
6
|
+
const DEFAULT_ORDER = "+name";
|
|
7
|
+
const SORTABLE = new Set(["id", "name", "discoveryDate", "createdAt", "updatedAt"]);
|
|
8
|
+
|
|
9
|
+
export const ALIEN_NAMES = [
|
|
10
|
+
"Andromedan",
|
|
11
|
+
"Arcturian",
|
|
12
|
+
"Betelgeusian",
|
|
13
|
+
"Centaurian",
|
|
14
|
+
"Denebian",
|
|
15
|
+
"Eridanian",
|
|
16
|
+
"Fomalhautian",
|
|
17
|
+
"Ganymedean",
|
|
18
|
+
"Hydran",
|
|
19
|
+
"Lyran",
|
|
20
|
+
"Orionid",
|
|
21
|
+
"Vegan",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export const PLANET_SEEDS = [
|
|
25
|
+
["Mercury", "Small rocky world closest to the system star"],
|
|
26
|
+
["Venus", "Dense atmosphere with extreme surface temperatures"],
|
|
27
|
+
["Earth", "Temperate ocean world with abundant life"],
|
|
28
|
+
["Mars", "Cold desert world with polar ice caps"],
|
|
29
|
+
["Jupiter", "Massive gas giant with a long-lived storm system"],
|
|
30
|
+
["Saturn", "Ringed gas giant with many icy moons"],
|
|
31
|
+
["Uranus", "Ice giant with a tilted rotation axis"],
|
|
32
|
+
["Neptune", "Distant ice giant with high-speed winds"],
|
|
33
|
+
["Kepler-22b", "Exoplanet candidate in a habitable-zone orbit"],
|
|
34
|
+
["Proxima Centauri b", "Nearby terrestrial exoplanet orbiting a red dwarf"],
|
|
35
|
+
["TRAPPIST-1e", "Compact-system rocky planet in a temperate orbit"],
|
|
36
|
+
["HD 209458 b", "Hot gas giant with an observed atmosphere"],
|
|
37
|
+
["Gliese 581g", "Candidate super-Earth in a nearby stellar system"],
|
|
38
|
+
["LHS 1140 b", "Dense rocky super-Earth in a habitable-zone orbit"],
|
|
39
|
+
["TOI-700 d", "Earth-size planet orbiting a small cool star"],
|
|
40
|
+
["K2-18 b", "Sub-Neptune with detected atmospheric molecules"],
|
|
41
|
+
["WASP-12b", "Ultra-hot gas giant closely orbiting its star"],
|
|
42
|
+
["55 Cancri e", "Super-Earth with a very short orbital period"],
|
|
43
|
+
["Kepler-452b", "Large terrestrial candidate orbiting a Sun-like star"],
|
|
44
|
+
["GJ 1214 b", "Sub-Neptune with a hazy atmosphere"],
|
|
45
|
+
["Ross 128 b", "Nearby temperate exoplanet around a quiet red dwarf"],
|
|
46
|
+
["Tau Ceti e", "Candidate planet in a nearby Sun-like system"],
|
|
47
|
+
["Wolf 1061 c", "Rocky candidate near the inner habitable zone"],
|
|
48
|
+
["Teegarden's Star b", "Temperate Earth-mass candidate around a red dwarf"],
|
|
49
|
+
["Kapteyn b", "Candidate super-Earth around an old nearby star"],
|
|
50
|
+
["Kepler-186f", "Earth-size planet in a cool star's habitable zone"],
|
|
51
|
+
["OGLE-2005-BLG-390Lb", "Cold distant world found by microlensing"],
|
|
52
|
+
["HAT-P-7b", "Hot Jupiter with unusual atmospheric circulation"],
|
|
53
|
+
["WASP-19b", "Hot Jupiter with a compact orbit"],
|
|
54
|
+
["CoRoT-7b", "Rocky exoplanet with extreme dayside heating"],
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
export type PlanetRow = InferSelectModel<typeof planets>;
|
|
58
|
+
export type AlienRow = InferSelectModel<typeof aliens>;
|
|
59
|
+
export type MediaRow = InferSelectModel<typeof media>;
|
|
60
|
+
|
|
61
|
+
export type PlanetFilter = {
|
|
62
|
+
search?: string | null;
|
|
63
|
+
alienId?: string | null;
|
|
64
|
+
discoveryDate?: { start?: string | null; end?: string | null } | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type PlanetDto = {
|
|
68
|
+
id: string;
|
|
69
|
+
userId: string;
|
|
70
|
+
alienId?: string | null;
|
|
71
|
+
discoveryDate?: string | null;
|
|
72
|
+
name: string;
|
|
73
|
+
description?: string | null;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
image?: { id: string; url: string } | null;
|
|
77
|
+
creatorName?: string;
|
|
78
|
+
alienName?: string;
|
|
79
|
+
likesCount: number;
|
|
80
|
+
likedByMe: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function fail(status: number, message: string) {
|
|
84
|
+
return { status, body: { message } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function iso(value: Date | string | null | undefined): string | null {
|
|
88
|
+
if (!value) return null;
|
|
89
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function requiredIso(value: Date | string | null | undefined): string {
|
|
93
|
+
return iso(value) ?? new Date().toISOString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseFilter(query: Record<string, unknown>): PlanetFilter {
|
|
97
|
+
const raw = query.filter;
|
|
98
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
99
|
+
try {
|
|
100
|
+
return parseFilter(JSON.parse(raw) as Record<string, unknown>);
|
|
101
|
+
} catch {
|
|
102
|
+
return { search: raw };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (raw && typeof raw === "object") {
|
|
106
|
+
const filter = raw as Record<string, unknown>;
|
|
107
|
+
const discovery = filter.discoveryDate;
|
|
108
|
+
return {
|
|
109
|
+
search: typeof filter.search === "string" ? filter.search : undefined,
|
|
110
|
+
alienId: typeof filter.alienId === "string" ? filter.alienId : null,
|
|
111
|
+
discoveryDate:
|
|
112
|
+
discovery && typeof discovery === "object"
|
|
113
|
+
? (discovery as PlanetFilter["discoveryDate"])
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const start = query["filter[discoveryDate][start]"] ?? query.discoveryDateStart;
|
|
118
|
+
const end = query["filter[discoveryDate][end]"] ?? query.discoveryDateEnd;
|
|
119
|
+
return {
|
|
120
|
+
search:
|
|
121
|
+
typeof query["filter[search]"] === "string"
|
|
122
|
+
? (query["filter[search]"] as string)
|
|
123
|
+
: typeof query.search === "string"
|
|
124
|
+
? query.search
|
|
125
|
+
: undefined,
|
|
126
|
+
alienId:
|
|
127
|
+
typeof query["filter[alienId]"] === "string"
|
|
128
|
+
? (query["filter[alienId]"] as string)
|
|
129
|
+
: typeof query.alienId === "string"
|
|
130
|
+
? query.alienId
|
|
131
|
+
: undefined,
|
|
132
|
+
discoveryDate:
|
|
133
|
+
typeof start === "string" || typeof end === "string"
|
|
134
|
+
? {
|
|
135
|
+
start: typeof start === "string" ? start : null,
|
|
136
|
+
end: typeof end === "string" ? end : null,
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseOrder(order: string | null | undefined) {
|
|
143
|
+
return (order || DEFAULT_ORDER)
|
|
144
|
+
.split(",")
|
|
145
|
+
.map((item) => item.trim())
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.map((item) => {
|
|
148
|
+
const direction = item.startsWith("-") ? "desc" : "asc";
|
|
149
|
+
const key = item.replace(/^[+-]/, "");
|
|
150
|
+
return SORTABLE.has(key) ? { key, direction } : undefined;
|
|
151
|
+
})
|
|
152
|
+
.filter((item): item is { key: string; direction: "asc" | "desc" } => Boolean(item));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compare(a: PlanetRow, b: PlanetRow, key: string, direction: "asc" | "desc") {
|
|
156
|
+
const aValue = a[key as keyof PlanetRow];
|
|
157
|
+
const bValue = b[key as keyof PlanetRow];
|
|
158
|
+
if (!aValue && !bValue) return 0;
|
|
159
|
+
if (!aValue) return 1;
|
|
160
|
+
if (!bValue) return -1;
|
|
161
|
+
const left = aValue instanceof Date ? aValue.toISOString() : String(aValue);
|
|
162
|
+
const right = bValue instanceof Date ? bValue.toISOString() : String(bValue);
|
|
163
|
+
const result = left.localeCompare(right);
|
|
164
|
+
return direction === "asc" ? result : -result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function sortPlanets(rows: PlanetRow[], order: string | null | undefined) {
|
|
168
|
+
const sorting = parseOrder(order);
|
|
169
|
+
return [...rows].sort((a, b) => {
|
|
170
|
+
for (const sort of sorting) {
|
|
171
|
+
const result = compare(a, b, sort.key, sort.direction);
|
|
172
|
+
if (result !== 0) return result;
|
|
173
|
+
}
|
|
174
|
+
return a.name.localeCompare(b.name);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function filterPlanetRows(
|
|
179
|
+
rows: PlanetRow[],
|
|
180
|
+
alienById: Map<string, AlienRow>,
|
|
181
|
+
filters: PlanetFilter | undefined,
|
|
182
|
+
) {
|
|
183
|
+
if (!filters) return rows;
|
|
184
|
+
const search = filters.search?.trim().toLocaleLowerCase();
|
|
185
|
+
return rows.filter((planet) => {
|
|
186
|
+
if (search) {
|
|
187
|
+
const haystack = [
|
|
188
|
+
planet.name,
|
|
189
|
+
planet.description,
|
|
190
|
+
planet.creatorName,
|
|
191
|
+
planet.alienId ? alienById.get(planet.alienId)?.name : undefined,
|
|
192
|
+
];
|
|
193
|
+
if (!haystack.some((value) => value?.toLocaleLowerCase().includes(search))) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (filters.alienId && planet.alienId !== filters.alienId) return false;
|
|
198
|
+
const range = filters.discoveryDate;
|
|
199
|
+
if (range?.start || range?.end) {
|
|
200
|
+
if (!planet.discoveryDate) return false;
|
|
201
|
+
const timestamp = Date.parse(requiredIso(planet.discoveryDate));
|
|
202
|
+
if (range.start && timestamp < Date.parse(range.start)) return false;
|
|
203
|
+
if (range.end && timestamp > Date.parse(range.end)) return false;
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function paginate<T>(items: T[], page?: number, limit?: number) {
|
|
210
|
+
const currentPage = page && page > 0 ? page : 1;
|
|
211
|
+
const currentLimit = limit && limit > 0 ? Math.min(limit, 20) : 20;
|
|
212
|
+
const start = (currentPage - 1) * currentLimit;
|
|
213
|
+
const nextPage = currentPage * currentLimit < items.length ? currentPage + 1 : null;
|
|
214
|
+
return {
|
|
215
|
+
items: items.slice(start, start + currentLimit),
|
|
216
|
+
page: currentPage,
|
|
217
|
+
cursor: String(currentPage),
|
|
218
|
+
nextCursor: nextPage ? String(nextPage) : null,
|
|
219
|
+
limit: currentLimit,
|
|
220
|
+
totalItems: items.length,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function ensureSeed(db: ApiHandlerContext<unknown, unknown>["db"], userId: string) {
|
|
225
|
+
const existingAliens = await db.select({ id: aliens.id }).from(aliens).limit(1);
|
|
226
|
+
if (existingAliens.length === 0) {
|
|
227
|
+
const now = new Date();
|
|
228
|
+
try {
|
|
229
|
+
await db.insert(aliens).values(
|
|
230
|
+
ALIEN_NAMES.map((name) => ({
|
|
231
|
+
name,
|
|
232
|
+
createdAt: now,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
})),
|
|
235
|
+
);
|
|
236
|
+
} catch {
|
|
237
|
+
// First request won the seed race.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const existingPlanets = await db.select({ id: planets.id }).from(planets).limit(1);
|
|
242
|
+
if (existingPlanets.length > 0) return;
|
|
243
|
+
|
|
244
|
+
const alienRows = await db.select().from(aliens).orderBy(asc(aliens.name));
|
|
245
|
+
if (alienRows.length === 0) return;
|
|
246
|
+
const now = new Date();
|
|
247
|
+
try {
|
|
248
|
+
await db.insert(planets).values(
|
|
249
|
+
PLANET_SEEDS.map(([name, description], index) => ({
|
|
250
|
+
userId,
|
|
251
|
+
creatorName: "Demo User",
|
|
252
|
+
alienId: alienRows[index % alienRows.length]?.id ?? null,
|
|
253
|
+
discoveryDate:
|
|
254
|
+
index % 2 === 0
|
|
255
|
+
? new Date(`20${String(index + 1).padStart(2, "0")}-01-01T00:00:00.000Z`)
|
|
256
|
+
: null,
|
|
257
|
+
name,
|
|
258
|
+
description,
|
|
259
|
+
createdAt: now,
|
|
260
|
+
updatedAt: now,
|
|
261
|
+
})),
|
|
262
|
+
);
|
|
263
|
+
} catch {
|
|
264
|
+
// First request won the seed race.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function resolveImage(
|
|
269
|
+
ctx: ApiHandlerContext<unknown, unknown>,
|
|
270
|
+
imageId: string | null | undefined,
|
|
271
|
+
): Promise<{ id: string; url: string } | null> {
|
|
272
|
+
if (!imageId) return null;
|
|
273
|
+
const [row] = await ctx.db.select().from(media).where(eq(media.id, imageId)).limit(1);
|
|
274
|
+
if (!row?.uploaded) return null;
|
|
275
|
+
try {
|
|
276
|
+
const url = await ctx.storage.getUrl(row.key);
|
|
277
|
+
return { id: row.id, url };
|
|
278
|
+
} catch {
|
|
279
|
+
return { id: row.id, url: `/storage/objects/${encodeURIComponent(row.key)}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function enrichPlanet(
|
|
284
|
+
ctx: ApiHandlerContext<unknown, unknown>,
|
|
285
|
+
planet: PlanetRow,
|
|
286
|
+
currentUserId: string,
|
|
287
|
+
alienById: Map<string, AlienRow>,
|
|
288
|
+
likes: { planetId: string; userId: string }[],
|
|
289
|
+
): Promise<PlanetDto> {
|
|
290
|
+
const planetLikes = likes.filter((like) => like.planetId === planet.id);
|
|
291
|
+
return {
|
|
292
|
+
id: planet.id,
|
|
293
|
+
userId: planet.userId,
|
|
294
|
+
alienId: planet.alienId,
|
|
295
|
+
discoveryDate: iso(planet.discoveryDate),
|
|
296
|
+
name: planet.name,
|
|
297
|
+
description: planet.description,
|
|
298
|
+
createdAt: requiredIso(planet.createdAt),
|
|
299
|
+
updatedAt: requiredIso(planet.updatedAt),
|
|
300
|
+
image: await resolveImage(ctx, planet.imageId),
|
|
301
|
+
creatorName: planet.creatorName ?? undefined,
|
|
302
|
+
alienName: planet.alienId ? alienById.get(planet.alienId)?.name : undefined,
|
|
303
|
+
likesCount: planetLikes.length,
|
|
304
|
+
likedByMe: planetLikes.some((like) => like.userId === currentUserId),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function loadLookups(db: ApiHandlerContext<unknown, unknown>["db"]) {
|
|
309
|
+
const [alienRows, likeRows] = await Promise.all([
|
|
310
|
+
db.select().from(aliens),
|
|
311
|
+
db.select({ planetId: planetLikes.planetId, userId: planetLikes.userId }).from(planetLikes),
|
|
312
|
+
]);
|
|
313
|
+
return {
|
|
314
|
+
alienById: new Map(alienRows.map((row) => [row.id, row])),
|
|
315
|
+
likes: likeRows,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function enrichOne(
|
|
320
|
+
ctx: ApiHandlerContext<unknown, unknown>,
|
|
321
|
+
planet: PlanetRow,
|
|
322
|
+
currentUserId: string,
|
|
323
|
+
) {
|
|
324
|
+
const lookups = await loadLookups(ctx.db);
|
|
325
|
+
return enrichPlanet(ctx, planet, currentUserId, lookups.alienById, lookups.likes);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function findPlanet(db: ApiHandlerContext<unknown, unknown>["db"], id: string) {
|
|
329
|
+
const [row] = await db.select().from(planets).where(eq(planets.id, id)).limit(1);
|
|
330
|
+
return row ?? null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function validateAlien(
|
|
334
|
+
db: ApiHandlerContext<unknown, unknown>["db"],
|
|
335
|
+
alienId: string | null | undefined,
|
|
336
|
+
) {
|
|
337
|
+
if (!alienId) return null;
|
|
338
|
+
const [row] = await db.select().from(aliens).where(eq(aliens.id, alienId)).limit(1);
|
|
339
|
+
return row ?? null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function validateImage(
|
|
343
|
+
db: ApiHandlerContext<unknown, unknown>["db"],
|
|
344
|
+
image: { id: string } | null | undefined,
|
|
345
|
+
userId: string,
|
|
346
|
+
) {
|
|
347
|
+
if (!image) return null;
|
|
348
|
+
const [row] = await db.select().from(media).where(eq(media.id, image.id)).limit(1);
|
|
349
|
+
if (!row) return { error: fail(404, "Media not found") };
|
|
350
|
+
if (row.userId !== userId) return { error: fail(403, "You can only use your own media") };
|
|
351
|
+
if (row.resourceName !== PLANET_IMAGE_RESOURCE) {
|
|
352
|
+
return { error: fail(400, "Media cannot be used as a planet image") };
|
|
353
|
+
}
|
|
354
|
+
if (!row.uploaded)
|
|
355
|
+
return { error: fail(400, "Media must be uploaded before it can be attached") };
|
|
356
|
+
return { id: row.id };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export { aliens, media, planetLikes, planets };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineApi, z } from "@robodev-ai/sdk";
|
|
2
|
+
import { aliens, ensureSeed } from "../_lib";
|
|
3
|
+
|
|
4
|
+
export const get = defineApi({
|
|
5
|
+
auth: "required",
|
|
6
|
+
query: z.object({
|
|
7
|
+
search: z.string().optional(),
|
|
8
|
+
}),
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
await ensureSeed(ctx.db, ctx.user!.id);
|
|
11
|
+
const search = ctx.query.search?.trim().toLocaleLowerCase();
|
|
12
|
+
const rows = await ctx.db.select().from(aliens);
|
|
13
|
+
return rows
|
|
14
|
+
.filter((row) => !search || row.name.toLocaleLowerCase().includes(search))
|
|
15
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
16
|
+
.slice(0, 50)
|
|
17
|
+
.map((row) => ({ id: row.id, label: row.name }));
|
|
18
|
+
},
|
|
19
|
+
});
|