robodev 0.25.0 → 0.27.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 +5 -0
- package/src/collect-files.ts +1 -0
- package/src/emit-starter.test.ts +1 -0
- package/templates/auth-chat/.rulesync/rules/project-overview.md +2 -2
- package/templates/auth-chat/.rulesync/rules/robodev-auth.md +1 -0
- package/templates/auth-chat/api/invite.ts +14 -4
- package/templates/auth-chat/database.ts +12 -2
- package/templates/auth-chat/hooks/auth.ts +17 -0
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/package.json +2 -2
- package/templates/empty/package.json +2 -2
- package/templates/marketplace/README.md +3 -3
- package/templates/marketplace/apps/fe/src/assets/locales/en/translation.json +10 -5
- package/templates/marketplace/apps/fe/src/assets/locales/sl/translation.json +10 -5
- package/templates/marketplace/apps/fe/src/components/features/marketplace/BrowsePage.tsx +31 -4
- package/templates/marketplace/apps/fe/src/components/features/marketplace/CartPage.tsx +80 -28
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingCard.tsx +16 -9
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingDetailsPage.tsx +28 -11
- package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx +70 -32
- package/templates/marketplace/apps/fe/src/components/features/marketplace/WatchlistPage.tsx +18 -4
- package/templates/marketplace/apps/fe/src/components/layout/app-header/AppHeader.tsx +6 -1
- package/templates/marketplace/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +6 -1
- package/templates/marketplace/apps/fe/src/components/shared/branding/BrandLogo.tsx +4 -1
- package/templates/marketplace/openapi.json +1 -1
- package/templates/marketplace/package.json +2 -2
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -19,6 +19,9 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
19
19
|
await writeFile(join(root, "api/invoices/[id].ts"), "export {}");
|
|
20
20
|
await mkdir(join(root, "jobs"), { recursive: true });
|
|
21
21
|
await writeFile(join(root, "jobs/x.ts"), "export {}");
|
|
22
|
+
await mkdir(join(root, "hooks"), { recursive: true });
|
|
23
|
+
await writeFile(join(root, "hooks/auth.ts"), "export {}");
|
|
24
|
+
await writeFile(join(root, "hooks/lib.ts"), "export {}");
|
|
22
25
|
await writeFile(join(root, "package.json"), "{}");
|
|
23
26
|
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
24
27
|
await writeFile(join(root, "node_modules/x.js"), "nope");
|
|
@@ -36,6 +39,8 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
36
39
|
assert.ok(paths.has("api/x.ts"));
|
|
37
40
|
assert.ok(paths.has("api/invoices/[id].ts"));
|
|
38
41
|
assert.ok(paths.has("jobs/x.ts"));
|
|
42
|
+
assert.ok(paths.has("hooks/auth.ts"));
|
|
43
|
+
assert.ok(paths.has("hooks/lib.ts"));
|
|
39
44
|
assert.ok(paths.has("package.json"));
|
|
40
45
|
assert.ok(!paths.has("node_modules/x.js"));
|
|
41
46
|
assert.ok(!paths.has("dist/x.js"));
|
package/src/collect-files.ts
CHANGED
|
@@ -100,6 +100,7 @@ function isAllowed(path: string): boolean {
|
|
|
100
100
|
if (path === "database.ts") return true;
|
|
101
101
|
if (path.startsWith("api/")) return path.endsWith(".ts");
|
|
102
102
|
if (path.startsWith("jobs/")) return path.endsWith(".ts");
|
|
103
|
+
if (path.startsWith("hooks/")) return path.endsWith(".ts");
|
|
103
104
|
if (path === "index.html") return true;
|
|
104
105
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
105
106
|
if (path.startsWith("public/")) return PUBLIC_EXTENSIONS.has(extname(path));
|
package/src/emit-starter.test.ts
CHANGED
|
@@ -71,6 +71,7 @@ test("collectFiles for auth-chat starter is Tiny APIs without apps/fe", async ()
|
|
|
71
71
|
assert.ok(paths.has("api/invite.ts"));
|
|
72
72
|
assert.ok(paths.has("api/health.ts"));
|
|
73
73
|
assert.ok(paths.has("api/me.ts"));
|
|
74
|
+
assert.ok(paths.has("hooks/auth.ts"));
|
|
74
75
|
assert.ok(!paths.has("index.html"));
|
|
75
76
|
assert.ok(!paths.has("src/main.tsx"));
|
|
76
77
|
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
@@ -11,13 +11,13 @@ cursor:
|
|
|
11
11
|
|
|
12
12
|
# Space starter AI rules
|
|
13
13
|
|
|
14
|
-
This project is a Vite React frontend in `apps/fe` plus a Robodev backend at the **repository root** (`database.ts` + `api/**/*.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
|
|
14
|
+
This project is a Vite React frontend in `apps/fe` plus a Robodev backend at the **repository root** (`database.ts` + `api/**/*.ts` + `hooks/auth.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
|
|
15
15
|
|
|
16
16
|
`robodev deploy` from this root uploads schema and APIs. It does not host the Povio SPA. Run the UI with bun in `apps/fe`.
|
|
17
17
|
|
|
18
18
|
Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
|
|
19
19
|
|
|
20
|
-
The demo is a shared chat room plus an authenticated invite-email form. Signed-in users read and post on `GET/POST /api/messages`. Invites go through `POST /api/invite` (`auth: "required"`)
|
|
20
|
+
The demo is a shared chat room plus an authenticated invite-email form. Signed-in users read and post on `GET/POST /api/messages`. Invites go through `POST /api/invite` (`auth: "required"`), which persists an `invites` row then sends email. `hooks/auth.ts` claims matching pending invites on `user.created`. Posting a chat message does not send email. Do not add `api/user/**`. Do not add a User table — store `userId` as text matching `ctx.user.id`.
|
|
21
21
|
|
|
22
22
|
When an app has multiple user roles with distinct UI or business workflows, use the conditional role-based app structure rule. Do not apply role splitting to simple single-role apps or small permission differences.
|
|
23
23
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** POST /api/invite — signed-in users
|
|
1
|
+
/** POST /api/invite — signed-in users persist an invite, then send email. */
|
|
2
|
+
import { invites } from "../database";
|
|
2
3
|
import { defineApi, z } from "@robodev-ai/sdk";
|
|
3
4
|
|
|
4
5
|
function headerValue(
|
|
@@ -28,16 +29,25 @@ export const post = defineApi({
|
|
|
28
29
|
auth: "required",
|
|
29
30
|
body: z.object({ to: z.string().email() }),
|
|
30
31
|
response: z.object({ id: z.string(), status: z.literal("sent") }),
|
|
31
|
-
handler: async ({ email, body, user, env, headers }) => {
|
|
32
|
+
handler: async ({ db, email, body, user, env, headers }) => {
|
|
33
|
+
const inviteEmail = body.to.toLowerCase();
|
|
34
|
+
const [row] = await db
|
|
35
|
+
.insert(invites)
|
|
36
|
+
.values({
|
|
37
|
+
email: inviteEmail,
|
|
38
|
+
invitedByUserId: user!.id,
|
|
39
|
+
})
|
|
40
|
+
.returning();
|
|
32
41
|
const url = resolveProjectUrl(env, headers);
|
|
33
42
|
const who = user?.name?.trim() || user?.email || "Someone";
|
|
34
43
|
const project = url ? ` Project URL: ${url}` : "";
|
|
35
|
-
|
|
36
|
-
to:
|
|
44
|
+
const sent = await email.send({
|
|
45
|
+
to: inviteEmail,
|
|
37
46
|
subject: "You're invited to the chat",
|
|
38
47
|
text:
|
|
39
48
|
`${who} invited you to a shared chat on Robodev. ` +
|
|
40
49
|
`Open the App URL and sign in to join.${project}`,
|
|
41
50
|
});
|
|
51
|
+
return { id: row?.id ?? sent.id, status: "sent" as const };
|
|
42
52
|
},
|
|
43
53
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared chat room. `robodev_auth.users` is platform-owned — store only a
|
|
3
|
-
* snapshot of the author on each message.
|
|
3
|
+
* snapshot of the author on each message. Invite `userId` columns are text
|
|
4
|
+
* matching Auth `ctx.user.id`.
|
|
4
5
|
*/
|
|
5
6
|
import { defineDatabase, pgTable, text, timestamp, uuid } from "@robodev-ai/sdk";
|
|
6
7
|
|
|
@@ -13,7 +14,16 @@ export const messages = pgTable("messages", {
|
|
|
13
14
|
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
14
15
|
});
|
|
15
16
|
|
|
17
|
+
export const invites = pgTable("invites", {
|
|
18
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
19
|
+
email: text("email").notNull(),
|
|
20
|
+
invitedByUserId: text("invitedByUserId").notNull(),
|
|
21
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
22
|
+
claimedAt: timestamp("claimedAt", { withTimezone: true }),
|
|
23
|
+
claimedByUserId: text("claimedByUserId"),
|
|
24
|
+
});
|
|
25
|
+
|
|
16
26
|
export default defineDatabase({
|
|
17
27
|
name: "chat",
|
|
18
|
-
tables: { messages },
|
|
28
|
+
tables: { messages, invites },
|
|
19
29
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { and, defineAuthHook, eq, isNull } from "@robodev-ai/sdk";
|
|
2
|
+
import { invites } from "../database";
|
|
3
|
+
|
|
4
|
+
export default defineAuthHook({
|
|
5
|
+
onUserCreated: async ({ db, event }) => {
|
|
6
|
+
const email = event.user?.email.toLowerCase();
|
|
7
|
+
const userId = event.user?.id;
|
|
8
|
+
if (!email || !userId) return;
|
|
9
|
+
await db
|
|
10
|
+
.update(invites)
|
|
11
|
+
.set({
|
|
12
|
+
claimedAt: new Date(),
|
|
13
|
+
claimedByUserId: userId,
|
|
14
|
+
})
|
|
15
|
+
.where(and(eq(invites.email, email), isNull(invites.claimedAt)));
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# {{NAME}}
|
|
2
2
|
|
|
3
|
-
Starter from `robodev create --starter marketplace`:
|
|
3
|
+
Starter from `robodev create --starter marketplace`: Marketplace C2C marketplace schema (`categories`, `listings`, `cart_items`, `orders`, `watches`, `reviews`, `media`), file-based APIs in `api/`, and a Povio UI in `apps/fe`.
|
|
4
4
|
|
|
5
|
-
After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built
|
|
5
|
+
After a CLI create or deploy with `.robodev` `frontend` set (the default for this starter), the project host serves the built Marketplace SPA at `GET /`. APIs and Swagger still win over static files.
|
|
6
6
|
|
|
7
7
|
Local Starbase occupies FE **:3000** and BE **:4000**. Point the CLI at it, then run starter Vite on **3001** so it does not steal the dashboard port:
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ npx robodev deploy
|
|
|
27
27
|
|
|
28
28
|
`robodev create` / `robodev link` write `VITE_API_URL`, `VITE_PUBLIC_API_URL`, and `APP_PUBLIC_API_URL` in `.env`, rewrite `.config/local.spa.yml` so the FE talks to the project host, and (when `apps/fe/package.json` exists) add a `frontend` block to `.robodev`. `robodev deploy` then runs the local Vite build and uploads the static output.
|
|
29
29
|
|
|
30
|
-
Dashboard create deploys schema + APIs only. The
|
|
30
|
+
Dashboard create deploys schema + APIs only. The Marketplace FE tree is copied by `robodev create`; a later CLI deploy hosts the SPA.
|
|
31
31
|
|
|
32
32
|
Auth stays reserved at `/api/user/*`. Do not add `api/user/**`.
|
|
33
33
|
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"auth": {
|
|
55
55
|
"wrapper": {
|
|
56
56
|
"title": "Administration portal",
|
|
57
|
-
"description": "
|
|
58
|
-
"watermark": "© {{year}}
|
|
57
|
+
"description": "Marketplace",
|
|
58
|
+
"watermark": "© {{year}} Marketplace • Developed and maintained by Povio"
|
|
59
59
|
},
|
|
60
60
|
"shared": {
|
|
61
61
|
"errors": {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
"layout": {
|
|
145
145
|
"header": {
|
|
146
|
-
"logo": "
|
|
146
|
+
"logo": "Marketplace",
|
|
147
147
|
"openMenu": "Open menu",
|
|
148
148
|
"closeMenu": "Close menu",
|
|
149
149
|
"nav": {
|
|
@@ -151,7 +151,8 @@
|
|
|
151
151
|
"sell": "Sell",
|
|
152
152
|
"watchlist": "Watchlist",
|
|
153
153
|
"orders": "Orders",
|
|
154
|
-
"cart": "Cart"
|
|
154
|
+
"cart": "Cart",
|
|
155
|
+
"cartCount": "Cart ({{count}})"
|
|
155
156
|
},
|
|
156
157
|
"profile": "Profile",
|
|
157
158
|
"logout": "Logout"
|
|
@@ -166,7 +167,8 @@
|
|
|
166
167
|
"search": "Search",
|
|
167
168
|
"searchPlaceholder": "Search listings",
|
|
168
169
|
"allCategories": "All categories",
|
|
169
|
-
"empty": "No listings match your search."
|
|
170
|
+
"empty": "No listings match your search.",
|
|
171
|
+
"clearFilters": "Clear filters"
|
|
170
172
|
},
|
|
171
173
|
"listing": {
|
|
172
174
|
"notFound": "Listing not found.",
|
|
@@ -190,6 +192,8 @@
|
|
|
190
192
|
"images": "Images",
|
|
191
193
|
"description": "Description",
|
|
192
194
|
"price": "Price",
|
|
195
|
+
"aboutThisItem": "About this item",
|
|
196
|
+
"noDescription": "No description.",
|
|
193
197
|
"reviews": "Reviews",
|
|
194
198
|
"noReviews": "No reviews yet."
|
|
195
199
|
},
|
|
@@ -202,6 +206,7 @@
|
|
|
202
206
|
"create": "New listing",
|
|
203
207
|
"empty": "You have no listings yet.",
|
|
204
208
|
"soldOut": "Sold out",
|
|
209
|
+
"active": "Active",
|
|
205
210
|
"inactive": "Inactive",
|
|
206
211
|
"edit": "Edit",
|
|
207
212
|
"deactivate": "Deactivate",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"auth": {
|
|
55
55
|
"wrapper": {
|
|
56
56
|
"title": "Administrativni portal",
|
|
57
|
-
"description": "
|
|
58
|
-
"watermark": "© {{year}}
|
|
57
|
+
"description": "Marketplace",
|
|
58
|
+
"watermark": "© {{year}} Marketplace • Razvija in vzdržuje Povio"
|
|
59
59
|
},
|
|
60
60
|
"shared": {
|
|
61
61
|
"errors": {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
"layout": {
|
|
145
145
|
"header": {
|
|
146
|
-
"logo": "
|
|
146
|
+
"logo": "Marketplace",
|
|
147
147
|
"openMenu": "Odpri meni",
|
|
148
148
|
"closeMenu": "Zapri meni",
|
|
149
149
|
"nav": {
|
|
@@ -151,7 +151,8 @@
|
|
|
151
151
|
"sell": "Prodaj",
|
|
152
152
|
"watchlist": "Seznam spremljanja",
|
|
153
153
|
"orders": "Naročila",
|
|
154
|
-
"cart": "Košarica"
|
|
154
|
+
"cart": "Košarica",
|
|
155
|
+
"cartCount": "Košarica ({{count}})"
|
|
155
156
|
},
|
|
156
157
|
"profile": "Profil",
|
|
157
158
|
"logout": "Odjava"
|
|
@@ -166,7 +167,8 @@
|
|
|
166
167
|
"search": "Iskanje",
|
|
167
168
|
"searchPlaceholder": "Išči oglase",
|
|
168
169
|
"allCategories": "Vse kategorije",
|
|
169
|
-
"empty": "Noben oglas ne ustreza iskanju."
|
|
170
|
+
"empty": "Noben oglas ne ustreza iskanju.",
|
|
171
|
+
"clearFilters": "Počisti filtre"
|
|
170
172
|
},
|
|
171
173
|
"listing": {
|
|
172
174
|
"notFound": "Oglas ni bil najden.",
|
|
@@ -190,6 +192,8 @@
|
|
|
190
192
|
"images": "Slike",
|
|
191
193
|
"description": "Opis",
|
|
192
194
|
"price": "Cena",
|
|
195
|
+
"aboutThisItem": "O izdelku",
|
|
196
|
+
"noDescription": "Ni opisa.",
|
|
193
197
|
"reviews": "Ocene",
|
|
194
198
|
"noReviews": "Ocene še ni."
|
|
195
199
|
},
|
|
@@ -202,6 +206,7 @@
|
|
|
202
206
|
"create": "Nov oglas",
|
|
203
207
|
"empty": "Nimate še oglasov.",
|
|
204
208
|
"soldOut": "Razprodano",
|
|
209
|
+
"active": "Aktivno",
|
|
205
210
|
"inactive": "Neaktivno",
|
|
206
211
|
"edit": "Uredi",
|
|
207
212
|
"deactivate": "Deaktiviraj",
|
|
@@ -21,6 +21,7 @@ export function BrowsePage() {
|
|
|
21
21
|
categoryId,
|
|
22
22
|
});
|
|
23
23
|
const listings = data?.items ?? [];
|
|
24
|
+
const hasFilters = Boolean(search.trim() || categoryId);
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
27
|
<div className="flex flex-col gap-6">
|
|
@@ -61,13 +62,39 @@ export function BrowsePage() {
|
|
|
61
62
|
{isLoading ? (
|
|
62
63
|
<LoadingState />
|
|
63
64
|
) : listings.length === 0 ? (
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
<div className="flex flex-col items-start gap-3">
|
|
66
|
+
<Typography
|
|
67
|
+
size="body-3"
|
|
68
|
+
className="text-text-default-2"
|
|
69
|
+
>
|
|
70
|
+
{t(($) => $.browse.empty)}
|
|
71
|
+
</Typography>
|
|
72
|
+
{hasFilters ? (
|
|
73
|
+
<Button
|
|
74
|
+
size="xs"
|
|
75
|
+
onPress={() => {
|
|
76
|
+
setSearch("");
|
|
77
|
+
setCategoryId(undefined);
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{t(($) => $.browse.clearFilters)}
|
|
81
|
+
</Button>
|
|
82
|
+
) : (
|
|
83
|
+
<Button
|
|
84
|
+
size="xs"
|
|
85
|
+
link={{ to: "/my-listings" }}
|
|
86
|
+
>
|
|
87
|
+
{t(($) => $.sell.title)}
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
67
91
|
) : (
|
|
68
92
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
69
93
|
{listings.map((listing) => (
|
|
70
|
-
<ListingCard
|
|
94
|
+
<ListingCard
|
|
95
|
+
key={listing.id}
|
|
96
|
+
listing={listing}
|
|
97
|
+
/>
|
|
71
98
|
))}
|
|
72
99
|
</div>
|
|
73
100
|
)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Link } from "@povio/ui";
|
|
1
2
|
import { Button, TextInput, Typography, useToast } from "@povio/ui/tanstack";
|
|
2
3
|
import { useQueryClient } from "@tanstack/react-query";
|
|
3
4
|
import { useTranslation } from "react-i18next";
|
|
@@ -5,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|
|
5
6
|
import { LoadingState } from "@/components/shared/layout/LoadingState";
|
|
6
7
|
import { PageHeader } from "@/components/shared/page/PageHeader";
|
|
7
8
|
import { CartQueries } from "@/openapi/cart/cart.queries";
|
|
9
|
+
import { getFallbackImageUrl } from "@/utils/image-fallback";
|
|
8
10
|
import { formatCents } from "@/utils/listing-form";
|
|
9
11
|
|
|
10
12
|
export function CartPage() {
|
|
@@ -15,6 +17,7 @@ export function CartPage() {
|
|
|
15
17
|
const updateItem = CartQueries.useUpdate();
|
|
16
18
|
const removeItem = CartQueries.useDeleteApiCartById();
|
|
17
19
|
const items = data ?? [];
|
|
20
|
+
const subtotalCents = items.reduce((sum, item) => sum + item.listing.priceCents * item.quantity, 0);
|
|
18
21
|
|
|
19
22
|
const onQty = async (id: string, quantity: number) => {
|
|
20
23
|
try {
|
|
@@ -40,7 +43,10 @@ export function CartPage() {
|
|
|
40
43
|
title={t(($) => $.cart.title)}
|
|
41
44
|
actions={
|
|
42
45
|
items.length > 0 ? (
|
|
43
|
-
<Button
|
|
46
|
+
<Button
|
|
47
|
+
size="xs"
|
|
48
|
+
link={{ to: "/checkout" }}
|
|
49
|
+
>
|
|
44
50
|
{t(($) => $.cart.checkout)}
|
|
45
51
|
</Button>
|
|
46
52
|
) : null
|
|
@@ -50,37 +56,83 @@ export function CartPage() {
|
|
|
50
56
|
{isLoading ? (
|
|
51
57
|
<LoadingState />
|
|
52
58
|
) : items.length === 0 ? (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
<div className="flex flex-col items-start gap-3">
|
|
60
|
+
<Typography
|
|
61
|
+
size="body-3"
|
|
62
|
+
className="text-text-default-2"
|
|
63
|
+
>
|
|
64
|
+
{t(($) => $.cart.empty)}
|
|
65
|
+
</Typography>
|
|
66
|
+
<Button
|
|
67
|
+
size="xs"
|
|
68
|
+
link={{ to: "/" }}
|
|
69
|
+
>
|
|
70
|
+
{t(($) => $.layout.header.nav.browse)}
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
56
73
|
) : (
|
|
57
74
|
<div className="flex flex-col gap-4">
|
|
58
|
-
{items.map((item) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
{items.map((item) => {
|
|
76
|
+
const imageSrc = item.listing.images[0]?.url ?? getFallbackImageUrl({ width: 80, height: 80 });
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
key={item.id}
|
|
81
|
+
className="flex items-start gap-4 border-elevation-outline-default-1 border-b pb-4"
|
|
82
|
+
>
|
|
83
|
+
<Link
|
|
84
|
+
to="/listings/$id"
|
|
85
|
+
params={{ id: item.listing.id }}
|
|
86
|
+
className="shrink-0 overflow-hidden rounded-m border border-elevation-outline-default-1 no-underline!"
|
|
87
|
+
>
|
|
88
|
+
<img
|
|
89
|
+
src={imageSrc}
|
|
90
|
+
alt=""
|
|
91
|
+
className="h-16 w-16 object-cover"
|
|
92
|
+
/>
|
|
93
|
+
</Link>
|
|
94
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
95
|
+
<Link
|
|
96
|
+
to="/listings/$id"
|
|
97
|
+
params={{ id: item.listing.id }}
|
|
98
|
+
className="min-w-0 no-underline!"
|
|
99
|
+
>
|
|
100
|
+
<Typography size="title-5">{item.listing.title}</Typography>
|
|
101
|
+
</Link>
|
|
102
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
103
|
+
<TextInput
|
|
104
|
+
variant="filled"
|
|
105
|
+
size="extra-small"
|
|
106
|
+
value={String(item.quantity)}
|
|
107
|
+
onChange={(value) => void onQty(item.id, Number(value) || 1)}
|
|
108
|
+
label={t(($) => $.cart.quantity)}
|
|
109
|
+
/>
|
|
110
|
+
<Button
|
|
111
|
+
size="xs"
|
|
112
|
+
variant="outlined"
|
|
113
|
+
color="secondary"
|
|
114
|
+
onPress={() => void onRemove(item.id)}
|
|
115
|
+
>
|
|
116
|
+
{t(($) => $.cart.remove)}
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<Typography
|
|
121
|
+
size="title-5"
|
|
122
|
+
variant="prominent-1"
|
|
123
|
+
className="shrink-0"
|
|
78
124
|
>
|
|
79
|
-
{
|
|
80
|
-
</
|
|
125
|
+
{formatCents(item.listing.priceCents)}
|
|
126
|
+
</Typography>
|
|
81
127
|
</div>
|
|
82
|
-
|
|
83
|
-
)
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
<Typography
|
|
131
|
+
size="title-5"
|
|
132
|
+
variant="prominent-1"
|
|
133
|
+
>
|
|
134
|
+
{t(($) => $.cart.subtotal)}: {formatCents(subtotalCents)}
|
|
135
|
+
</Typography>
|
|
84
136
|
</div>
|
|
85
137
|
)}
|
|
86
138
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Link, Typography } from "@povio/ui";
|
|
1
|
+
import { Link, Tag, Typography } from "@povio/ui";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
3
|
|
|
4
4
|
import type { ListingModels } from "@/openapi/listing/listing.models";
|
|
@@ -20,7 +20,11 @@ export function ListingCard({ listing }: ListingCardProps) {
|
|
|
20
20
|
className="relative overflow-hidden rounded-m border border-elevation-outline-default-1 bg-elevation-fill-default-1 no-underline! shadow-sm transition-all hover:border-elevation-outline-default-2 hover:bg-elevation-fill-default-2"
|
|
21
21
|
>
|
|
22
22
|
<div className="aspect-video w-full bg-elevation-fill-default-2">
|
|
23
|
-
<img
|
|
23
|
+
<img
|
|
24
|
+
src={imageSrc}
|
|
25
|
+
alt=""
|
|
26
|
+
className="h-full w-full object-cover"
|
|
27
|
+
/>
|
|
24
28
|
</div>
|
|
25
29
|
<div className="flex flex-col gap-2 p-4">
|
|
26
30
|
<Typography
|
|
@@ -31,19 +35,22 @@ export function ListingCard({ listing }: ListingCardProps) {
|
|
|
31
35
|
>
|
|
32
36
|
{listing.title}
|
|
33
37
|
</Typography>
|
|
34
|
-
<Typography
|
|
38
|
+
<Typography
|
|
39
|
+
size="title-5"
|
|
40
|
+
variant="prominent-1"
|
|
41
|
+
className="text-text-default-1"
|
|
42
|
+
>
|
|
35
43
|
{formatCents(listing.priceCents)}
|
|
36
44
|
</Typography>
|
|
37
45
|
{listing.quantity <= 0 ? (
|
|
38
|
-
<Typography
|
|
46
|
+
<Typography
|
|
47
|
+
size="label-3"
|
|
48
|
+
className="text-text-default-2"
|
|
49
|
+
>
|
|
39
50
|
{t(($) => $.sell.soldOut)}
|
|
40
51
|
</Typography>
|
|
41
52
|
) : null}
|
|
42
|
-
{listing.category ?
|
|
43
|
-
<Typography size="label-3" className="text-text-default-2">
|
|
44
|
-
{listing.category.name}
|
|
45
|
-
</Typography>
|
|
46
|
-
) : null}
|
|
53
|
+
{listing.category ? <Tag color="secondary">{listing.category.name}</Tag> : null}
|
|
47
54
|
</div>
|
|
48
55
|
</Link>
|
|
49
56
|
);
|
package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingDetailsPage.tsx
CHANGED
|
@@ -65,7 +65,7 @@ export function ListingDetailsPage({ listing }: ListingDetailsPageProps) {
|
|
|
65
65
|
title={listing.title}
|
|
66
66
|
/>
|
|
67
67
|
|
|
68
|
-
<div className="grid grid-cols-1 items-start gap-6
|
|
68
|
+
<div className="grid grid-cols-1 items-start gap-6 md:grid-cols-[minmax(0,1.4fr)_minmax(16rem,22rem)]">
|
|
69
69
|
<ListingGallery images={listing.images} />
|
|
70
70
|
|
|
71
71
|
<Card className="flex flex-col gap-4">
|
|
@@ -90,7 +90,9 @@ export function ListingDetailsPage({ listing }: ListingDetailsPageProps) {
|
|
|
90
90
|
color="secondary"
|
|
91
91
|
label={listing.watchedByMe ? t(($) => $.listing.unwatch) : t(($) => $.listing.watch)}
|
|
92
92
|
disableTooltip
|
|
93
|
-
icon={
|
|
93
|
+
icon={
|
|
94
|
+
listing.watchedByMe ? <Heart className="fill-current text-interactive-contained-error-idle" /> : Heart
|
|
95
|
+
}
|
|
94
96
|
onPress={() => void onToggleWatch()}
|
|
95
97
|
/>
|
|
96
98
|
)}
|
|
@@ -134,15 +136,6 @@ export function ListingDetailsPage({ listing }: ListingDetailsPageProps) {
|
|
|
134
136
|
: t(($) => $.listing.noRating)}
|
|
135
137
|
</Typography>
|
|
136
138
|
|
|
137
|
-
{listing.description ? (
|
|
138
|
-
<Typography
|
|
139
|
-
size="body-3"
|
|
140
|
-
className="whitespace-pre-wrap text-text-default-2"
|
|
141
|
-
>
|
|
142
|
-
{listing.description}
|
|
143
|
-
</Typography>
|
|
144
|
-
) : null}
|
|
145
|
-
|
|
146
139
|
{isOwn ? <Typography size="body-3">{t(($) => $.listing.ownListing)}</Typography> : null}
|
|
147
140
|
|
|
148
141
|
{canBuy ? (
|
|
@@ -166,6 +159,30 @@ export function ListingDetailsPage({ listing }: ListingDetailsPageProps) {
|
|
|
166
159
|
</Card>
|
|
167
160
|
</div>
|
|
168
161
|
|
|
162
|
+
<div className="flex flex-col gap-3">
|
|
163
|
+
<Typography
|
|
164
|
+
size="title-5"
|
|
165
|
+
variant="prominent-1"
|
|
166
|
+
>
|
|
167
|
+
{t(($) => $.listing.aboutThisItem)}
|
|
168
|
+
</Typography>
|
|
169
|
+
{listing.description ? (
|
|
170
|
+
<Typography
|
|
171
|
+
size="body-3"
|
|
172
|
+
className="whitespace-pre-wrap text-text-default-2"
|
|
173
|
+
>
|
|
174
|
+
{listing.description}
|
|
175
|
+
</Typography>
|
|
176
|
+
) : (
|
|
177
|
+
<Typography
|
|
178
|
+
size="body-3"
|
|
179
|
+
className="text-text-default-2"
|
|
180
|
+
>
|
|
181
|
+
{t(($) => $.listing.noDescription)}
|
|
182
|
+
</Typography>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
169
186
|
<div className="flex flex-col gap-3">
|
|
170
187
|
<Typography
|
|
171
188
|
size="title-5"
|
package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Drawer, Typography } from "@povio/ui";
|
|
1
|
+
import { Drawer, Link, Tag, Typography } from "@povio/ui";
|
|
2
2
|
import { Button, useToast } from "@povio/ui/tanstack";
|
|
3
3
|
import { useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useNavigate } from "@tanstack/react-router";
|
|
@@ -9,9 +9,9 @@ import { useTranslation } from "react-i18next";
|
|
|
9
9
|
import { LoadingState } from "@/components/shared/layout/LoadingState";
|
|
10
10
|
import { PageHeader } from "@/components/shared/page/PageHeader";
|
|
11
11
|
import { ListingQueries } from "@/openapi/listing/listing.queries";
|
|
12
|
+
import { getFallbackImageUrl } from "@/utils/image-fallback";
|
|
12
13
|
import { formatCents } from "@/utils/listing-form";
|
|
13
14
|
|
|
14
|
-
import { ListingCard } from "./ListingCard";
|
|
15
15
|
import { ListingForm } from "./ListingForm";
|
|
16
16
|
|
|
17
17
|
interface MyListingsPageProps {
|
|
@@ -86,50 +86,88 @@ export function MyListingsPage({ openCreate = false }: MyListingsPageProps) {
|
|
|
86
86
|
{isLoading ? (
|
|
87
87
|
<LoadingState />
|
|
88
88
|
) : listings.length === 0 ? (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
<div className="flex flex-col items-start gap-3">
|
|
90
|
+
<Typography
|
|
91
|
+
size="body-3"
|
|
92
|
+
className="text-text-default-2"
|
|
93
|
+
>
|
|
94
|
+
{t(($) => $.sell.empty)}
|
|
95
|
+
</Typography>
|
|
96
|
+
<Button
|
|
97
|
+
size="xs"
|
|
98
|
+
icon={Plus}
|
|
99
|
+
onPress={() => setIsCreateOpen(true)}
|
|
100
|
+
>
|
|
101
|
+
{t(($) => $.sell.create)}
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
95
104
|
) : (
|
|
96
105
|
<div className="flex flex-col gap-4">
|
|
97
|
-
{listings.map((listing) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
106
|
+
{listings.map((listing) => {
|
|
107
|
+
const imageSrc = listing.images[0]?.url ?? getFallbackImageUrl({ width: 80, height: 80 });
|
|
108
|
+
const isActive = listing.status === "active";
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
key={listing.id}
|
|
113
|
+
className="flex flex-col gap-3 border-elevation-outline-default-1 border-b pb-4 sm:flex-row sm:items-center"
|
|
114
|
+
>
|
|
115
|
+
<Link
|
|
116
|
+
to="/listings/$id"
|
|
117
|
+
params={{ id: listing.id }}
|
|
118
|
+
className="flex min-w-0 flex-1 items-center gap-3 no-underline!"
|
|
119
|
+
>
|
|
120
|
+
<img
|
|
121
|
+
src={imageSrc}
|
|
122
|
+
alt=""
|
|
123
|
+
className="h-16 w-16 shrink-0 rounded-m border border-elevation-outline-default-1 object-cover"
|
|
124
|
+
/>
|
|
125
|
+
<Typography
|
|
126
|
+
size="title-5"
|
|
127
|
+
variant="prominent-1"
|
|
128
|
+
className="min-w-0 text-text-default-1"
|
|
129
|
+
>
|
|
130
|
+
{listing.title}
|
|
131
|
+
</Typography>
|
|
132
|
+
</Link>
|
|
104
133
|
<Typography
|
|
105
|
-
size="
|
|
106
|
-
className="text-text-default-
|
|
134
|
+
size="body-3"
|
|
135
|
+
className="text-text-default-1"
|
|
107
136
|
>
|
|
108
137
|
{formatCents(listing.priceCents)}
|
|
109
|
-
{listing.quantity <= 0 ? ` · ${t(($) => $.sell.soldOut)}` : ""}
|
|
110
|
-
{listing.status === "inactive" ? ` · ${t(($) => $.sell.inactive)}` : ""}
|
|
111
138
|
</Typography>
|
|
112
|
-
<
|
|
113
|
-
size="
|
|
114
|
-
|
|
115
|
-
color="secondary"
|
|
116
|
-
link={{ to: "/sell/$id", params: { id: listing.id } }}
|
|
139
|
+
<Typography
|
|
140
|
+
size="body-3"
|
|
141
|
+
className="text-text-default-2"
|
|
117
142
|
>
|
|
118
|
-
{
|
|
119
|
-
</
|
|
120
|
-
{
|
|
143
|
+
{listing.quantity}
|
|
144
|
+
</Typography>
|
|
145
|
+
<Tag color={isActive ? "success" : "secondary"}>
|
|
146
|
+
{isActive ? t(($) => $.sell.active) : t(($) => $.sell.inactive)}
|
|
147
|
+
</Tag>
|
|
148
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
121
149
|
<Button
|
|
122
150
|
size="xs"
|
|
123
151
|
variant="outlined"
|
|
124
152
|
color="secondary"
|
|
125
|
-
|
|
153
|
+
link={{ to: "/sell/$id", params: { id: listing.id } }}
|
|
126
154
|
>
|
|
127
|
-
{t(($) => $.sell.
|
|
155
|
+
{t(($) => $.sell.edit)}
|
|
128
156
|
</Button>
|
|
129
|
-
|
|
157
|
+
{isActive ? (
|
|
158
|
+
<Button
|
|
159
|
+
size="xs"
|
|
160
|
+
variant="outlined"
|
|
161
|
+
color="secondary"
|
|
162
|
+
onPress={() => void onDeactivate(listing.id)}
|
|
163
|
+
>
|
|
164
|
+
{t(($) => $.sell.deactivate)}
|
|
165
|
+
</Button>
|
|
166
|
+
) : null}
|
|
167
|
+
</div>
|
|
130
168
|
</div>
|
|
131
|
-
|
|
132
|
-
)
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
133
171
|
</div>
|
|
134
172
|
)}
|
|
135
173
|
</div>
|
|
@@ -31,13 +31,27 @@ export function WatchlistPage() {
|
|
|
31
31
|
{isLoading ? (
|
|
32
32
|
<LoadingState />
|
|
33
33
|
) : items.length === 0 ? (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
<div className="flex flex-col items-start gap-3">
|
|
35
|
+
<Typography
|
|
36
|
+
size="body-3"
|
|
37
|
+
className="text-text-default-2"
|
|
38
|
+
>
|
|
39
|
+
{t(($) => $.watchlist.empty)}
|
|
40
|
+
</Typography>
|
|
41
|
+
<Button
|
|
42
|
+
size="xs"
|
|
43
|
+
link={{ to: "/" }}
|
|
44
|
+
>
|
|
45
|
+
{t(($) => $.layout.header.nav.browse)}
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
37
48
|
) : (
|
|
38
49
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
|
39
50
|
{items.map((item) => (
|
|
40
|
-
<div
|
|
51
|
+
<div
|
|
52
|
+
key={item.id}
|
|
53
|
+
className="flex flex-col gap-2"
|
|
54
|
+
>
|
|
41
55
|
<ListingCard listing={item.listing} />
|
|
42
56
|
<Button
|
|
43
57
|
size="xs"
|
|
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|
|
7
7
|
|
|
8
8
|
import { BrandLogo } from "@/components/shared/branding/BrandLogo";
|
|
9
9
|
import { useAuth } from "@/hooks/useAuth";
|
|
10
|
+
import { CartQueries } from "@/openapi/cart/cart.queries";
|
|
10
11
|
|
|
11
12
|
import { MobileNavigation } from "./MobileNavigation";
|
|
12
13
|
import { NavLink } from "./NavLink";
|
|
@@ -16,6 +17,10 @@ export function AppHeader() {
|
|
|
16
17
|
const navigate = useNavigate();
|
|
17
18
|
const { isAuthenticated, user, logout } = useAuth();
|
|
18
19
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
20
|
+
const { data: cartItems } = CartQueries.useGetAll();
|
|
21
|
+
const cartCount = cartItems?.length ?? 0;
|
|
22
|
+
const cartLabel =
|
|
23
|
+
cartCount > 0 ? t(($) => $.layout.header.nav.cartCount, { count: cartCount }) : t(($) => $.layout.header.nav.cart);
|
|
19
24
|
|
|
20
25
|
const menuItems = [
|
|
21
26
|
{ label: t(($) => $.layout.header.profile), id: "profile" },
|
|
@@ -53,7 +58,7 @@ export function AppHeader() {
|
|
|
53
58
|
<NavLink to="/my-listings">{t(($) => $.layout.header.nav.sell)}</NavLink>
|
|
54
59
|
<NavLink to="/watchlist">{t(($) => $.layout.header.nav.watchlist)}</NavLink>
|
|
55
60
|
<NavLink to="/orders">{t(($) => $.layout.header.nav.orders)}</NavLink>
|
|
56
|
-
<NavLink to="/cart">{
|
|
61
|
+
<NavLink to="/cart">{cartLabel}</NavLink>
|
|
57
62
|
</nav>
|
|
58
63
|
</div>
|
|
59
64
|
|
|
@@ -5,6 +5,7 @@ import type { Key } from "react";
|
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
6
|
|
|
7
7
|
import { useAuth } from "@/hooks/useAuth";
|
|
8
|
+
import { CartQueries } from "@/openapi/cart/cart.queries";
|
|
8
9
|
|
|
9
10
|
import { NavLink } from "./NavLink";
|
|
10
11
|
|
|
@@ -12,6 +13,10 @@ export function MobileNavigation() {
|
|
|
12
13
|
const { t } = useTranslation();
|
|
13
14
|
const navigate = useNavigate();
|
|
14
15
|
const { isAuthenticated, user, logout } = useAuth();
|
|
16
|
+
const { data: cartItems } = CartQueries.useGetAll();
|
|
17
|
+
const cartCount = cartItems?.length ?? 0;
|
|
18
|
+
const cartLabel =
|
|
19
|
+
cartCount > 0 ? t(($) => $.layout.header.nav.cartCount, { count: cartCount }) : t(($) => $.layout.header.nav.cart);
|
|
15
20
|
const accountLabel = user?.name || user?.email || t(($) => $.layout.header.profile);
|
|
16
21
|
|
|
17
22
|
const handleMenuAction = (key: Key) => {
|
|
@@ -29,7 +34,7 @@ export function MobileNavigation() {
|
|
|
29
34
|
<NavLink to="/my-listings">{t(($) => $.layout.header.nav.sell)}</NavLink>
|
|
30
35
|
<NavLink to="/watchlist">{t(($) => $.layout.header.nav.watchlist)}</NavLink>
|
|
31
36
|
<NavLink to="/orders">{t(($) => $.layout.header.nav.orders)}</NavLink>
|
|
32
|
-
<NavLink to="/cart">{
|
|
37
|
+
<NavLink to="/cart">{cartLabel}</NavLink>
|
|
33
38
|
</nav>
|
|
34
39
|
|
|
35
40
|
{isAuthenticated ? (
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
import type { HTMLAttributes } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
3
4
|
|
|
4
5
|
export function BrandLogo({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
|
|
5
8
|
return (
|
|
6
9
|
<span
|
|
7
10
|
className={clsx("inline-flex h-[34px] items-center font-semibold text-xl tracking-tight", className)}
|
|
8
11
|
{...props}
|
|
9
12
|
>
|
|
10
|
-
|
|
13
|
+
{t(($) => $.appName)}
|
|
11
14
|
</span>
|
|
12
15
|
);
|
|
13
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"info": {
|
|
3
3
|
"title": "Marketplace API",
|
|
4
|
-
"description": "OpenAPI spec for the
|
|
4
|
+
"description": "OpenAPI spec for the Marketplace backend and reserved /api/user auth.",
|
|
5
5
|
"version": "1.0.0"
|
|
6
6
|
},
|
|
7
7
|
"servers": [
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|