robodev 0.24.0 → 0.26.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 (33) hide show
  1. package/package.json +1 -1
  2. package/templates/auth-chat/package.json +1 -1
  3. package/templates/backend/package.json +1 -1
  4. package/templates/empty/package.json +1 -1
  5. package/templates/marketplace/README.md +3 -3
  6. package/templates/marketplace/api/_lib.seed.test.ts +10 -0
  7. package/templates/marketplace/api/_lib.ts +114 -57
  8. package/templates/marketplace/api/categories.ts +1 -1
  9. package/templates/marketplace/api/listings/[id].ts +1 -1
  10. package/templates/marketplace/api/listings/paginate.ts +1 -1
  11. package/templates/marketplace/api/listings.ts +1 -1
  12. package/templates/marketplace/apps/fe/src/assets/locales/en/translation.json +11 -5
  13. package/templates/marketplace/apps/fe/src/assets/locales/sl/translation.json +11 -5
  14. package/templates/marketplace/apps/fe/src/components/features/marketplace/BrowsePage.tsx +31 -4
  15. package/templates/marketplace/apps/fe/src/components/features/marketplace/CartPage.tsx +80 -28
  16. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingCard.tsx +16 -9
  17. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingDetailsPage.tsx +107 -48
  18. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingForm.tsx +278 -0
  19. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingFormPage.tsx +6 -222
  20. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingGallery.tsx +54 -0
  21. package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx +120 -29
  22. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderStatusTag.tsx +24 -0
  23. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderSummaryCard.tsx +39 -0
  24. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrdersPage.tsx +57 -45
  25. package/templates/marketplace/apps/fe/src/components/features/marketplace/WatchlistPage.tsx +18 -4
  26. package/templates/marketplace/apps/fe/src/components/layout/app-header/AppHeader.tsx +6 -1
  27. package/templates/marketplace/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +6 -1
  28. package/templates/marketplace/apps/fe/src/components/shared/branding/BrandLogo.tsx +4 -1
  29. package/templates/marketplace/apps/fe/src/pages/(private)/my-listings.tsx +11 -1
  30. package/templates/marketplace/apps/fe/src/pages/(private)/sell/index.tsx +6 -10
  31. package/templates/marketplace/openapi.json +1 -1
  32. package/templates/marketplace/package.json +1 -1
  33. package/templates/space/package.json +1 -1
@@ -1,12 +1,4 @@
1
- import {
2
- Button,
3
- TextArea,
4
- TextInput,
5
- Typography,
6
- useForm,
7
- useFormValue,
8
- useToast,
9
- } from "@povio/ui/tanstack";
1
+ import { Button, TextArea, TextInput, Typography, useForm, useFormValue, useToast } from "@povio/ui/tanstack";
10
2
  import { useQueryClient } from "@tanstack/react-query";
11
3
  import { useTranslation } from "react-i18next";
12
4
  import { z } from "zod";
@@ -21,6 +13,9 @@ import { ReviewQueries } from "@/openapi/review/review.queries";
21
13
  import { SaleQueries } from "@/openapi/sale/sale.queries";
22
14
  import { formatCents } from "@/utils/listing-form";
23
15
 
16
+ import { OrderStatusTag } from "./OrderStatusTag";
17
+ import { OrderSummaryCard } from "./OrderSummaryCard";
18
+
24
19
  export function OrdersPage() {
25
20
  const { t } = useTranslation();
26
21
  const { data: purchases, isLoading: loadingPurchases } = PurchaseQueries.useGetAll();
@@ -30,58 +25,59 @@ export function OrdersPage() {
30
25
  <div className="flex flex-col gap-8">
31
26
  <PageHeader title={t(($) => $.orders.title)} />
32
27
  <section className="flex flex-col gap-3">
33
- <Typography size="title-5" variant="prominent-1">
28
+ <Typography
29
+ size="title-5"
30
+ variant="prominent-1"
31
+ >
34
32
  {t(($) => $.orders.purchases)}
35
33
  </Typography>
36
34
  {loadingPurchases ? (
37
35
  <LoadingState />
38
36
  ) : !purchases || purchases.length === 0 ? (
39
- <Typography size="body-3" className="text-text-default-2">
37
+ <Typography
38
+ size="body-3"
39
+ className="text-text-default-2"
40
+ >
40
41
  {t(($) => $.orders.emptyPurchases)}
41
42
  </Typography>
42
43
  ) : (
43
- purchases.map((order) => <OrderSummary key={order.id} order={order} />)
44
+ purchases.map((order) => (
45
+ <OrderSummaryCard
46
+ key={order.id}
47
+ order={order}
48
+ />
49
+ ))
44
50
  )}
45
51
  </section>
46
52
  <section className="flex flex-col gap-3">
47
- <Typography size="title-5" variant="prominent-1">
53
+ <Typography
54
+ size="title-5"
55
+ variant="prominent-1"
56
+ >
48
57
  {t(($) => $.orders.sales)}
49
58
  </Typography>
50
59
  {loadingSales ? (
51
60
  <LoadingState />
52
61
  ) : !sales || sales.length === 0 ? (
53
- <Typography size="body-3" className="text-text-default-2">
62
+ <Typography
63
+ size="body-3"
64
+ className="text-text-default-2"
65
+ >
54
66
  {t(($) => $.orders.emptySales)}
55
67
  </Typography>
56
68
  ) : (
57
- sales.map((order) => <OrderSummary key={order.id} order={order} />)
69
+ sales.map((order) => (
70
+ <OrderSummaryCard
71
+ key={order.id}
72
+ order={order}
73
+ />
74
+ ))
58
75
  )}
59
76
  </section>
60
77
  </div>
61
78
  );
62
79
  }
63
80
 
64
- function OrderSummary({ order }: { order: OrderModels.OrderDto }) {
65
- const { t } = useTranslation();
66
- return (
67
- <Card>
68
- <Typography size="title-5">{order.items.map((item) => item.title).join(", ")}</Typography>
69
- <Typography size="label-3" className="text-text-default-2">
70
- {t(($) => $.orders.status[order.status as "paid" | "shipped" | "completed"])} ·{" "}
71
- {formatCents(order.totalCents)}
72
- </Typography>
73
- <Button
74
- size="xs"
75
- variant="outlined"
76
- color="secondary"
77
- link={{ to: "/orders/$id", params: { id: order.id } }}
78
- >
79
- {order.id.slice(0, 8)}
80
- </Button>
81
- </Card>
82
- );
83
- }
84
-
85
81
  interface OrderDetailPageProps {
86
82
  order: OrderModels.OrderDto;
87
83
  role: "buyer" | "seller";
@@ -159,34 +155,47 @@ export function OrderDetailPage({ order, role }: OrderDetailPageProps) {
159
155
  title={t(($) => $.orders.title)}
160
156
  />
161
157
  <Card>
162
- <Typography size="label-3" className="text-text-default-2">
163
- {t(($) => $.orders.status[order.status as "paid" | "shipped" | "completed"])}
164
- </Typography>
158
+ <OrderStatusTag status={order.status} />
165
159
  {order.items.map((item) => (
166
- <Typography key={item.id} size="body-3">
160
+ <Typography
161
+ key={item.id}
162
+ size="body-3"
163
+ >
167
164
  {item.qty} × {item.title} · {formatCents(item.unitPriceCents)}
168
165
  </Typography>
169
166
  ))}
170
167
  <Typography size="body-3">
171
168
  {t(($) => $.orders.total)}: {formatCents(order.totalCents)}
172
169
  </Typography>
173
- <Typography size="label-3" className="text-text-default-2">
170
+ <Typography
171
+ size="label-3"
172
+ className="text-text-default-2"
173
+ >
174
174
  {order.shippingName}, {order.shippingLine1}, {order.shippingCity}
175
175
  </Typography>
176
176
  {role === "seller" && order.status === "paid" ? (
177
- <Button size="xs" onPress={() => void onShip()}>
177
+ <Button
178
+ size="xs"
179
+ onPress={() => void onShip()}
180
+ >
178
181
  {t(($) => $.orders.ship)}
179
182
  </Button>
180
183
  ) : null}
181
184
  {role === "buyer" && order.status === "shipped" ? (
182
- <Button size="xs" onPress={() => void onComplete()}>
185
+ <Button
186
+ size="xs"
187
+ onPress={() => void onComplete()}
188
+ >
183
189
  {t(($) => $.orders.complete)}
184
190
  </Button>
185
191
  ) : null}
186
192
  </Card>
187
193
 
188
194
  {role === "buyer" && order.status === "completed" ? (
189
- <form onSubmit={reviewForm.handleSubmit(onReview)} className="flex max-w-xl flex-col gap-3">
195
+ <form
196
+ onSubmit={reviewForm.handleSubmit(onReview)}
197
+ className="flex max-w-xl flex-col gap-3"
198
+ >
190
199
  <Typography size="title-5">{t(($) => $.orders.review)}</Typography>
191
200
  <div className="flex flex-wrap gap-1">
192
201
  {order.items.map((item) => (
@@ -214,7 +223,10 @@ export function OrderDetailPage({ order, role }: OrderDetailPageProps) {
214
223
  field={{ form: reviewForm, name: "body" }}
215
224
  label={t(($) => $.orders.reviewBody)}
216
225
  />
217
- <Button type="submit" size="xs">
226
+ <Button
227
+ type="submit"
228
+ size="xs"
229
+ >
218
230
  {t(($) => $.orders.reviewSubmit)}
219
231
  </Button>
220
232
  </form>
@@ -31,13 +31,27 @@ export function WatchlistPage() {
31
31
  {isLoading ? (
32
32
  <LoadingState />
33
33
  ) : items.length === 0 ? (
34
- <Typography size="body-3" className="text-text-default-2">
35
- {t(($) => $.watchlist.empty)}
36
- </Typography>
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 key={item.id} className="flex flex-col gap-2">
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">{t(($) => $.layout.header.nav.cart)}</NavLink>
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">{t(($) => $.layout.header.nav.cart)}</NavLink>
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
- Tiny
13
+ {t(($) => $.appName)}
11
14
  </span>
12
15
  );
13
16
  }
@@ -1,22 +1,32 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { useTranslation } from "react-i18next";
3
+ import { z } from "zod";
3
4
 
4
5
  import { MyListingsPage } from "@/components/features/marketplace/MyListingsPage";
5
6
  import { AppHead } from "@/components/shared/head/AppHead";
6
7
 
8
+ const myListingsSearchSchema = z.object({
9
+ create: z
10
+ .union([z.boolean(), z.string()])
11
+ .optional()
12
+ .transform((value) => value === true || value === "true"),
13
+ });
14
+
7
15
  function PageComponent() {
8
16
  const { t } = useTranslation();
17
+ const { create } = Route.useSearch();
9
18
  return (
10
19
  <>
11
20
  <AppHead
12
21
  title={t(($) => $.sell.head.title)}
13
22
  description={t(($) => $.sell.head.description)}
14
23
  />
15
- <MyListingsPage />
24
+ <MyListingsPage openCreate={create} />
16
25
  </>
17
26
  );
18
27
  }
19
28
 
20
29
  export const Route = createFileRoute("/(private)/my-listings")({
21
30
  component: PageComponent,
31
+ validateSearch: myListingsSearchSchema,
22
32
  });
@@ -1,16 +1,12 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { useTranslation } from "react-i18next";
3
-
4
- import { ListingFormPage } from "@/components/features/marketplace/ListingFormPage";
5
- import { AppHead } from "@/components/shared/head/AppHead";
1
+ import { Navigate, createFileRoute } from "@tanstack/react-router";
6
2
 
7
3
  function PageComponent() {
8
- const { t } = useTranslation();
9
4
  return (
10
- <>
11
- <AppHead title={t(($) => $.listingForm.createTitle)} />
12
- <ListingFormPage />
13
- </>
5
+ <Navigate
6
+ to="/my-listings"
7
+ search={{ create: true }}
8
+ replace
9
+ />
14
10
  );
15
11
  }
16
12
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "info": {
3
3
  "title": "Marketplace API",
4
- "description": "OpenAPI spec for the Tiny marketplace backend and reserved /api/user auth.",
4
+ "description": "OpenAPI spec for the Marketplace backend and reserved /api/user auth.",
5
5
  "version": "1.0.0"
6
6
  },
7
7
  "servers": [
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.6.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.24.0",
14
+ "robodev": "^0.26.0",
15
15
  "rulesync": "^8.18.0",
16
16
  "typescript": "^5.9.2"
17
17
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.6.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.24.0",
14
+ "robodev": "^0.26.0",
15
15
  "rulesync": "^8.18.0",
16
16
  "typescript": "^5.9.2"
17
17
  }