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,48 @@
1
+ import { defineApi, z } from "@robodev-ai/sdk";
2
+ import { media, PLANET_IMAGE_RESOURCE } from "../_lib";
3
+
4
+ const ALLOWED = new Set(["image/jpeg", "image/png", "image/webp"]);
5
+ const MAX_BYTES = 2 * 1024 * 1024;
6
+
7
+ export const post = defineApi({
8
+ auth: "required",
9
+ body: z.object({
10
+ resourceName: z.string().optional(),
11
+ fileName: z.string().min(1),
12
+ fileSize: z.number().int().positive(),
13
+ mimeType: z.string().optional(),
14
+ method: z.string().optional(),
15
+ }),
16
+ handler: async (ctx) => {
17
+ const mimeType = ctx.body.mimeType || "application/octet-stream";
18
+ if (ctx.body.fileSize > MAX_BYTES) {
19
+ return { status: 400, body: { message: "File is too large" } };
20
+ }
21
+ if (ctx.body.resourceName === PLANET_IMAGE_RESOURCE && !ALLOWED.has(mimeType)) {
22
+ return { status: 400, body: { message: "Unsupported image type" } };
23
+ }
24
+
25
+ const key = `planet-images/${ctx.user!.id}/${crypto.randomUUID()}-${ctx.body.fileName.replaceAll("/", "-")}`;
26
+ const [row] = await ctx.db
27
+ .insert(media)
28
+ .values({
29
+ key,
30
+ userId: ctx.user!.id,
31
+ resourceName: ctx.body.resourceName || PLANET_IMAGE_RESOURCE,
32
+ fileName: ctx.body.fileName,
33
+ fileSize: ctx.body.fileSize,
34
+ mimeType,
35
+ })
36
+ .returning();
37
+
38
+ return {
39
+ id: row!.id,
40
+ method: "post",
41
+ url: "/api/files/upload",
42
+ fields: [
43
+ ["id", row!.id],
44
+ ["key", key],
45
+ ],
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,29 @@
1
+ import { defineApi, eq } from "@robodev-ai/sdk";
2
+ import { fail, media } from "../_lib";
3
+
4
+ export const post = defineApi({
5
+ auth: "required",
6
+ multipart: true,
7
+ handler: async (ctx) => {
8
+ const id =
9
+ typeof ctx.body === "object" && ctx.body && "id" in ctx.body ? String(ctx.body.id) : "";
10
+ const file = ctx.files?.[0];
11
+ if (!id || !file) return fail(400, "Missing file upload");
12
+
13
+ const [row] = await ctx.db.select().from(media).where(eq(media.id, id)).limit(1);
14
+ if (!row) return fail(404, "Media not found");
15
+ if (row.userId !== ctx.user!.id) return fail(403, "You can only upload your own media");
16
+
17
+ try {
18
+ await ctx.storage.upload(row.key, file.data, {
19
+ public: true,
20
+ contentType: file.mimetype || row.mimeType,
21
+ });
22
+ } catch (error) {
23
+ return fail(503, error instanceof Error ? error.message : "Storage is not configured");
24
+ }
25
+
26
+ await ctx.db.update(media).set({ uploaded: new Date() }).where(eq(media.id, row.id));
27
+ return { ok: true, id: row.id };
28
+ },
29
+ });
@@ -0,0 +1,36 @@
1
+ import { and, defineApi, eq } from "@robodev-ai/sdk";
2
+ import { enrichOne, fail, findPlanet, planetLikes } from "../../_lib";
3
+
4
+ export const post = defineApi({
5
+ auth: "required",
6
+ handler: async (ctx) => {
7
+ const planet = await findPlanet(ctx.db, ctx.params.id);
8
+ if (!planet) return fail(404, "Planet not found");
9
+ const [existing] = await ctx.db
10
+ .select({ id: planetLikes.id })
11
+ .from(planetLikes)
12
+ .where(and(eq(planetLikes.planetId, planet.id), eq(planetLikes.userId, ctx.user!.id)))
13
+ .limit(1);
14
+ if (!existing) {
15
+ await ctx.db.insert(planetLikes).values({
16
+ planetId: planet.id,
17
+ userId: ctx.user!.id,
18
+ });
19
+ }
20
+ return enrichOne(ctx, planet, ctx.user!.id);
21
+ },
22
+ });
23
+
24
+ const unlikePlanet = defineApi({
25
+ auth: "required",
26
+ handler: async (ctx) => {
27
+ const planet = await findPlanet(ctx.db, ctx.params.id);
28
+ if (!planet) return fail(404, "Planet not found");
29
+ await ctx.db
30
+ .delete(planetLikes)
31
+ .where(and(eq(planetLikes.planetId, planet.id), eq(planetLikes.userId, ctx.user!.id)));
32
+ return enrichOne(ctx, planet, ctx.user!.id);
33
+ },
34
+ });
35
+
36
+ export { unlikePlanet as delete };
@@ -1,21 +1,64 @@
1
- /** GET /planets/:id — one planet by path param. */
2
1
  import { defineApi, eq, z } from "@robodev-ai/sdk";
3
- import { planets } from "../../database";
2
+ import { enrichOne, fail, findPlanet, planets, validateAlien, validateImage } from "../_lib";
4
3
 
5
- const Planet = z.object({
6
- id: z.string(),
7
- name: z.string(),
8
- climate: z.string(),
9
- moons: z.coerce.number(),
4
+ const planetBody = z.object({
5
+ name: z.string().min(1),
6
+ alienId: z.string().nullish(),
7
+ discoveryDate: z.string().nullish(),
8
+ description: z.string().nullish(),
9
+ image: z.object({ id: z.string() }).nullish(),
10
10
  });
11
11
 
12
12
  export const get = defineApi({
13
- response: Planet,
14
- handler: async ({ db, params }) => {
15
- const [row] = await db.select().from(planets).where(eq(planets.id, params.id)).limit(1);
16
- if (!row) {
17
- return { status: 404, body: { error: "not_found" } };
13
+ auth: "required",
14
+ handler: async (ctx) => {
15
+ const planet = await findPlanet(ctx.db, ctx.params.id);
16
+ if (!planet) return fail(404, "Planet not found");
17
+ return enrichOne(ctx, planet, ctx.user!.id);
18
+ },
19
+ });
20
+
21
+ export const put = defineApi({
22
+ auth: "required",
23
+ body: planetBody,
24
+ handler: async (ctx) => {
25
+ const planet = await findPlanet(ctx.db, ctx.params.id);
26
+ if (!planet) return fail(404, "Planet not found");
27
+ if (planet.userId !== ctx.user!.id) return fail(403, "You can only update your own planets");
28
+
29
+ const alienId = ctx.body.alienId || null;
30
+ if (alienId && !(await validateAlien(ctx.db, alienId))) {
31
+ return fail(404, "Alien not found");
18
32
  }
19
- return Planet.parse(row);
33
+ const image = await validateImage(ctx.db, ctx.body.image, ctx.user!.id);
34
+ if (image && "error" in image) return image.error;
35
+
36
+ const [updated] = await ctx.db
37
+ .update(planets)
38
+ .set({
39
+ name: ctx.body.name,
40
+ alienId,
41
+ discoveryDate: ctx.body.discoveryDate ? new Date(ctx.body.discoveryDate) : null,
42
+ description: ctx.body.description ?? null,
43
+ imageId: image?.id ?? null,
44
+ updatedAt: new Date(),
45
+ })
46
+ .where(eq(planets.id, planet.id))
47
+ .returning();
48
+ if (!updated) return fail(404, "Planet not found");
49
+ return enrichOne(ctx, updated, ctx.user!.id);
20
50
  },
21
51
  });
52
+
53
+ const deletePlanet = defineApi({
54
+ auth: "required",
55
+ handler: async (ctx) => {
56
+ const planet = await findPlanet(ctx.db, ctx.params.id);
57
+ if (!planet) return fail(404, "Planet not found");
58
+ if (planet.userId !== ctx.user!.id) return fail(403, "You can only delete your own planets");
59
+ await ctx.db.delete(planets).where(eq(planets.id, planet.id));
60
+ return { status: 204, body: null };
61
+ },
62
+ });
63
+
64
+ export { deletePlanet as delete };
@@ -0,0 +1,36 @@
1
+ import { defineApi } from "@robodev-ai/sdk";
2
+ import {
3
+ ensureSeed,
4
+ enrichPlanet,
5
+ filterPlanetRows,
6
+ loadLookups,
7
+ paginate,
8
+ parseFilter,
9
+ planets,
10
+ sortPlanets,
11
+ } from "../_lib";
12
+
13
+ export const get = defineApi({
14
+ auth: "required",
15
+ handler: async (ctx) => {
16
+ await ensureSeed(ctx.db, ctx.user!.id);
17
+ const query = ctx.query as Record<string, unknown>;
18
+ const rows = await ctx.db.select().from(planets);
19
+ const lookups = await loadLookups(ctx.db);
20
+ const filter = parseFilter(query);
21
+ const filtered = filterPlanetRows(rows, lookups.alienById, filter);
22
+ const page = paginate(
23
+ sortPlanets(filtered, typeof query.order === "string" ? query.order : undefined),
24
+ Number(query.page) || undefined,
25
+ Number(query.limit) || undefined,
26
+ );
27
+ return {
28
+ ...page,
29
+ items: await Promise.all(
30
+ page.items.map((row) =>
31
+ enrichPlanet(ctx, row, ctx.user!.id, lookups.alienById, lookups.likes),
32
+ ),
33
+ ),
34
+ };
35
+ },
36
+ });
@@ -1,60 +1,68 @@
1
- /** GET/POST /planets — list (seeds sample rows on first GET) and create planets. */
2
- import { planets, rockets } from "../database";
3
1
  import { defineApi, z } from "@robodev-ai/sdk";
2
+ import {
3
+ ensureSeed,
4
+ enrichOne,
5
+ enrichPlanet,
6
+ fail,
7
+ filterPlanetRows,
8
+ loadLookups,
9
+ parseFilter,
10
+ planets,
11
+ sortPlanets,
12
+ validateAlien,
13
+ validateImage,
14
+ } from "./_lib";
4
15
 
5
- const EARTH = "11111111-1111-1111-1111-111111111111";
6
- const MARS = "22222222-2222-2222-2222-222222222222";
7
- const KEPLER = "33333333-3333-3333-3333-333333333333";
8
- const VOYAGER = "44444444-4444-4444-4444-444444444444";
9
- const ODYSSEY = "55555555-5555-5555-5555-555555555555";
16
+ const imageRequest = z.object({ id: z.string() }).nullish();
10
17
 
11
- const Planet = z.object({
12
- id: z.string(),
13
- name: z.string(),
14
- climate: z.string(),
15
- moons: z.coerce.number(),
18
+ const planetBody = z.object({
19
+ name: z.string().min(1),
20
+ alienId: z.string().nullish(),
21
+ discoveryDate: z.string().nullish(),
22
+ description: z.string().nullish(),
23
+ image: imageRequest,
16
24
  });
17
25
 
18
26
  export const get = defineApi({
19
- response: z.array(Planet),
20
- handler: async ({ db }) => {
21
- const existing = await db.select().from(planets).limit(1);
22
- if (existing.length === 0) {
23
- try {
24
- await db.insert(planets).values([
25
- { id: EARTH, name: "Earth", climate: "temperate", moons: 1 },
26
- { id: MARS, name: "Mars", climate: "arid", moons: 2 },
27
- { id: KEPLER, name: "Kepler-452b", climate: "unknown", moons: 0 },
28
- ]);
29
- await db.insert(rockets).values([
30
- { id: VOYAGER, name: "Voyager", destinationId: MARS, launched: false },
31
- { id: ODYSSEY, name: "Odyssey", destinationId: KEPLER, launched: true },
32
- ]);
33
- } catch {
34
- // First request won the seed race.
35
- }
36
- }
37
- const rows = await db.select().from(planets);
38
- return Planet.array().parse(rows);
27
+ auth: "required",
28
+ handler: async (ctx) => {
29
+ await ensureSeed(ctx.db, ctx.user!.id);
30
+ const rows = await ctx.db.select().from(planets);
31
+ const lookups = await loadLookups(ctx.db);
32
+ const query = ctx.query as Record<string, unknown>;
33
+ const filtered = filterPlanetRows(rows, lookups.alienById, parseFilter(query));
34
+ const order = typeof query.order === "string" ? query.order : undefined;
35
+ return Promise.all(
36
+ sortPlanets(filtered, order).map((row) =>
37
+ enrichPlanet(ctx, row, ctx.user!.id, lookups.alienById, lookups.likes),
38
+ ),
39
+ );
39
40
  },
40
41
  });
41
42
 
42
43
  export const post = defineApi({
43
- body: z.object({
44
- name: z.string().min(1),
45
- climate: z.string().min(1),
46
- moons: z.coerce.number().int().min(0).default(0),
47
- }),
48
- response: Planet,
49
- handler: async ({ db, body }) => {
50
- const [row] = await db
44
+ auth: "required",
45
+ body: planetBody,
46
+ handler: async (ctx) => {
47
+ const alienId = ctx.body.alienId || null;
48
+ if (alienId && !(await validateAlien(ctx.db, alienId))) {
49
+ return fail(404, "Alien not found");
50
+ }
51
+ const image = await validateImage(ctx.db, ctx.body.image, ctx.user!.id);
52
+ if (image && "error" in image) return image.error;
53
+
54
+ const [row] = await ctx.db
51
55
  .insert(planets)
52
56
  .values({
53
- name: body.name,
54
- climate: body.climate,
55
- moons: body.moons,
57
+ userId: ctx.user!.id,
58
+ creatorName: ctx.user!.name ?? ctx.user!.email,
59
+ alienId,
60
+ discoveryDate: ctx.body.discoveryDate ? new Date(ctx.body.discoveryDate) : null,
61
+ name: ctx.body.name,
62
+ description: ctx.body.description ?? null,
63
+ imageId: image?.id ?? null,
56
64
  })
57
65
  .returning();
58
- return Planet.parse(row);
66
+ return enrichOne(ctx, row!, ctx.user!.id);
59
67
  },
60
68
  });
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link
6
+ rel="icon"
7
+ type="image/svg+xml"
8
+ href="/favicon.svg"
9
+ />
10
+ <meta
11
+ name="viewport"
12
+ content="width=device-width, initial-scale=1.0"
13
+ />
14
+ <title>Space</title>
15
+ </head>
16
+
17
+ <body class="bg-elevation-fill-default-2 text-text-default-1">
18
+ <div id="app"></div>
19
+ <script
20
+ type="module"
21
+ src="/src/main.tsx"
22
+ ></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,37 @@
1
+ /* oxlint-disable import/no-nodejs-modules */
2
+ import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";
3
+ import { resolveConfigSync } from "@povio/resolve-config";
4
+ import { resolve } from "node:path";
5
+
6
+ const COMPACT_OPENAPI_INPUT = "../../openapi.json";
7
+ const repoRoot = resolve(process.cwd(), "../..");
8
+ const env = resolveConfigSync({ cwd: repoRoot, module: "spa", target: "resolved" }) as Record<
9
+ string,
10
+ string | undefined
11
+ >;
12
+
13
+ function resolveLiveOpenApiInput(apiUrl = env.VITE_PUBLIC_API_URL) {
14
+ return `${apiUrl?.replace(/\/+$/, "")}/openapi.json`;
15
+ }
16
+
17
+ export function resolveOpenApiInput() {
18
+ return process.env.OPENAPI_INPUT || (process.env.OPENAPI_LIVE === "true" ? resolveLiveOpenApiInput() : COMPACT_OPENAPI_INPUT);
19
+ }
20
+
21
+ const config: OpenAPICodegenConfig = {
22
+ input: resolveOpenApiInput(),
23
+ output: "src/openapi",
24
+ tsPath: "@/openapi",
25
+ restClientImportPath: "@/clients/app-rest-client",
26
+ clearOutput: true,
27
+ excludeRedundantZodSchemas: false,
28
+ checkAcl: false,
29
+ acl: false,
30
+ replaceOptionalWithNullish: true,
31
+ infiniteQueries: true,
32
+ mutationDefaultOnError: true,
33
+ modelsInCommon: true,
34
+ axiosRequestConfig: true,
35
+ };
36
+
37
+ export default config;
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "fe",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Space starter frontend",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "bunx --bun vite",
9
+ "build": "bunx --bun vite build",
10
+ "clean": "rimraf dist src/openapi",
11
+ "lint": "oxlint --type-aware . ../../api ../../database.ts --fix",
12
+ "lint:check": "oxlint --type-aware . ../../api ../../database.ts --quiet",
13
+ "format": "oxfmt -c \"../../oxfmt.config.js\" . ../../api ../../database.ts",
14
+ "format:check": "oxfmt -c \"../../oxfmt.config.js\" --check . ../../api ../../database.ts",
15
+ "ts:check": "tsc -b --noEmit",
16
+ "test": "vitest run",
17
+ "check": "bun ts:check && bun lint:check && bun format:check",
18
+ "openapi:gen": "openapi-codegen generate && oxfmt -c \"../../oxfmt.config.js\" ./src/openapi",
19
+ "openapi:check": "openapi-codegen check"
20
+ },
21
+ "dependencies": {
22
+ "@casl/ability": "^6.8.1",
23
+ "@casl/react": "^5.0.1",
24
+ "@fontsource-variable/geist": "^5.2.8",
25
+ "@fontsource-variable/inter": "^5.2.8",
26
+ "@internationalized/date": "^3.11.0",
27
+ "@povio/openapi-codegen-cli": "3.1.0",
28
+ "@povio/resolve-config": "1.3.0",
29
+ "@povio/ui": "3.4.0-rc.12",
30
+ "@sentry/react": "^10.38.0",
31
+ "@tanstack/devtools-vite": "^0.3.11",
32
+ "@tanstack/react-devtools": "^0.8.1",
33
+ "@tanstack/react-form": "^1.33.2",
34
+ "@tanstack/react-query": "^5.90.21",
35
+ "@tanstack/react-query-devtools": "~5.90.0",
36
+ "@tanstack/react-router": "^1.136.8",
37
+ "@tanstack/react-router-devtools": "^1.136.8",
38
+ "@tanstack/react-table": "^8.21.3",
39
+ "@tanstack/router-core": "^1.136.8",
40
+ "@tanstack/router-plugin": "^1.136.8",
41
+ "axios": "^1.13.2",
42
+ "class-variance-authority": "^0.7.1",
43
+ "clsx": "^2.1.1",
44
+ "i18next": "^25.7.3",
45
+ "loglevel": "^1.9.2",
46
+ "lucide-react": "^1.28.0",
47
+ "luxon": "^3.7.2",
48
+ "react": "19.2.4",
49
+ "react-aria": "^3.46.0",
50
+ "react-aria-components": "^1.15.1",
51
+ "react-dom": "19.2.4",
52
+ "react-i18next": "^16.5.1",
53
+ "web-vitals": "^5.1.0",
54
+ "zod": "^4.4.3",
55
+ "zustand": "^5.0.9"
56
+ },
57
+ "devDependencies": {
58
+ "@axe-core/react": "^4.11.0",
59
+ "@povio/react-scan": "0.0.0",
60
+ "@povio/vite-plugin-react-scan": "0.0.2",
61
+ "@tailwindcss/vite": "^4.2.1",
62
+ "@types/luxon": "^3.7.1",
63
+ "@types/node": "^24.10.9",
64
+ "@types/react": "^19.2.7",
65
+ "@types/react-dom": "^19.2.3",
66
+ "@typescript/native": "npm:typescript@^7.0.2",
67
+ "@vitejs/plugin-react": "^5.1.2",
68
+ "babel-plugin-react-compiler": "1.0.0",
69
+ "oxfmt": "^0.45.0",
70
+ "oxlint": "1.60.0",
71
+ "oxlint-tsgolint": "0.21.0",
72
+ "react-icons-sprite": "^1.1.1",
73
+ "rimraf": "^6.1.2",
74
+ "tailwindcss": "^4.2.4",
75
+ "tailwindcss-react-aria-components": "^2.1.0",
76
+ "tsx": "^4.22.1",
77
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
78
+ "vite": "^8.0.0",
79
+ "vite-plugin-devtools-json": "^1.0.0",
80
+ "vitest": "^3.2.4"
81
+ }
82
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 239.36 239.36"><defs><style>.cls-1{fill:#3c60ad;}.cls-2{fill:none;}.cls-3{clip-path:url(#clip-path);}.cls-4{mask:url(#mask);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><rect class="cls-2" width="239.36" height="239.36"></rect></clipPath><mask id="mask" x="0" y="0" width="239.36" height="239.36" maskUnits="userSpaceOnUse"><g id="mask0_21778_2258" data-name="mask0 21778 2258"><path class="cls-1" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path></g></mask></defs><g class="cls-3"><g class="cls-4"><path class="cls-5" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path><path d="M119.8,49.55c26.68,0,48.41,22.8,48.5,50.63,0,28.18-21.73,51.06-48.5,51.06A46.64,46.64,0,0,1,83.31,133.4v56.54h-12V100.27C71.29,72.35,93,49.55,119.8,49.55Zm0,12.29c-20.15,0-36.49,17.22-36.49,38.43,0,21.46,16.34,39,36.49,39s36.39-17.5,36.48-39C156.28,79.06,139.94,61.84,119.8,61.84Z"></path></g></g></svg></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
2
+ @media (prefers-color-scheme: dark) { :root { filter: none; } }
3
+ </style></svg>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Povio",
3
+ "short_name": "Povio",
4
+ "icons": [
5
+ {
6
+ "src": "/web-app-manifest-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png",
9
+ "purpose": "maskable"
10
+ },
11
+ {
12
+ "src": "/web-app-manifest-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "maskable"
16
+ }
17
+ ],
18
+ "theme_color": "#ffffff",
19
+ "background_color": "#ffffff",
20
+ "display": "standalone"
21
+ }