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.
- package/package.json +1 -1
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/README.md +3 -3
- package/templates/marketplace/api/_lib.seed.test.ts +10 -0
- package/templates/marketplace/api/_lib.ts +114 -57
- package/templates/marketplace/api/categories.ts +1 -1
- package/templates/marketplace/api/listings/[id].ts +1 -1
- package/templates/marketplace/api/listings/paginate.ts +1 -1
- package/templates/marketplace/api/listings.ts +1 -1
- package/templates/marketplace/apps/fe/src/assets/locales/en/translation.json +11 -5
- package/templates/marketplace/apps/fe/src/assets/locales/sl/translation.json +11 -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 +107 -48
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingForm.tsx +278 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingFormPage.tsx +6 -222
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingGallery.tsx +54 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx +120 -29
- package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderStatusTag.tsx +24 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderSummaryCard.tsx +39 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/OrdersPage.tsx +57 -45
- 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/apps/fe/src/pages/(private)/my-listings.tsx +11 -1
- package/templates/marketplace/apps/fe/src/pages/(private)/sell/index.tsx +6 -10
- package/templates/marketplace/openapi.json +1 -1
- package/templates/marketplace/package.json +1 -1
- 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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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,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
|
-
|
|
12
|
-
|
|
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
|
|
4
|
+
"description": "OpenAPI spec for the Marketplace backend and reserved /api/user auth.",
|
|
5
5
|
"version": "1.0.0"
|
|
6
6
|
},
|
|
7
7
|
"servers": [
|