robodev 0.24.0 → 0.25.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 (24) 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/api/_lib.seed.test.ts +10 -0
  6. package/templates/marketplace/api/_lib.ts +114 -57
  7. package/templates/marketplace/api/categories.ts +1 -1
  8. package/templates/marketplace/api/listings/[id].ts +1 -1
  9. package/templates/marketplace/api/listings/paginate.ts +1 -1
  10. package/templates/marketplace/api/listings.ts +1 -1
  11. package/templates/marketplace/apps/fe/src/assets/locales/en/translation.json +1 -0
  12. package/templates/marketplace/apps/fe/src/assets/locales/sl/translation.json +1 -0
  13. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingDetailsPage.tsx +91 -49
  14. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingForm.tsx +278 -0
  15. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingFormPage.tsx +6 -222
  16. package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingGallery.tsx +54 -0
  17. package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx +61 -8
  18. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderStatusTag.tsx +24 -0
  19. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrderSummaryCard.tsx +39 -0
  20. package/templates/marketplace/apps/fe/src/components/features/marketplace/OrdersPage.tsx +57 -45
  21. package/templates/marketplace/apps/fe/src/pages/(private)/my-listings.tsx +11 -1
  22. package/templates/marketplace/apps/fe/src/pages/(private)/sell/index.tsx +6 -10
  23. package/templates/marketplace/package.json +1 -1
  24. package/templates/space/package.json +1 -1
@@ -0,0 +1,278 @@
1
+ import {
2
+ Button,
3
+ FileUpload,
4
+ TextArea,
5
+ TextInput,
6
+ Typography,
7
+ useForm,
8
+ useFormValue,
9
+ useToast,
10
+ } from "@povio/ui/tanstack";
11
+ import { useQueryClient } from "@tanstack/react-query";
12
+ import { useNavigate } from "@tanstack/react-router";
13
+ import { useState } from "react";
14
+ import { useTranslation } from "react-i18next";
15
+ import { z } from "zod";
16
+
17
+ import { RowInputWrapper } from "@/components/shared/forms/RowInputWrapper";
18
+ import { CategoryQueries } from "@/openapi/category/category.queries";
19
+ import { ListingQueries } from "@/openapi/listing/listing.queries";
20
+ import type { ListingModels } from "@/openapi/listing/listing.models";
21
+ import { bindLocalListingImages, centsToDollarsInput, dollarsToCents } from "@/utils/listing-form";
22
+ import { createListing, updateListing } from "@/utils/listing-submit";
23
+
24
+ const listingFormSchema = z.object({
25
+ title: z.string().min(1),
26
+ description: z.string().optional(),
27
+ categoryId: z.string().min(1),
28
+ price: z.string().min(1),
29
+ quantity: z.string().min(1),
30
+ shipping: z.string().min(1),
31
+ shippingDays: z.string().min(1),
32
+ });
33
+
34
+ export interface ListingFormProps {
35
+ listing?: ListingModels.ListingDto;
36
+ onSuccess?: () => void | Promise<void>;
37
+ onCancel?: () => void;
38
+ }
39
+
40
+ export function ListingForm({ listing, onSuccess, onCancel }: ListingFormProps) {
41
+ const { t } = useTranslation();
42
+ const navigate = useNavigate();
43
+ const queryClient = useQueryClient();
44
+ const { successToast, errorToast } = useToast();
45
+ const { data: categories } = CategoryQueries.useGetAll();
46
+ const [pickedFiles, setPickedFiles] = useState<File[]>([]);
47
+ const [removedIds, setRemovedIds] = useState<string[]>([]);
48
+ const [isPending, setIsPending] = useState(false);
49
+ const remainingImages = (listing?.images ?? []).filter((image) => !removedIds.includes(image.id));
50
+
51
+ const form = useForm({
52
+ zodSchema: listingFormSchema,
53
+ defaultValues: {
54
+ title: listing?.title ?? "",
55
+ description: listing?.description ?? "",
56
+ categoryId: listing?.categoryId ?? "",
57
+ price: listing ? centsToDollarsInput(listing.priceCents) : "",
58
+ quantity: listing ? String(listing.quantity) : "1",
59
+ shipping: listing ? centsToDollarsInput(listing.shippingCents) : "",
60
+ shippingDays: listing ? String(listing.shippingDays) : "3",
61
+ },
62
+ });
63
+ const categoryId = useFormValue(form, (values) => values.categoryId);
64
+
65
+ const goToMyListings = () => {
66
+ void navigate({ to: "/my-listings" });
67
+ };
68
+
69
+ const onSubmit = async (data: z.infer<typeof listingFormSchema>) => {
70
+ try {
71
+ setIsPending(true);
72
+ const fields = {
73
+ title: data.title,
74
+ description: data.description,
75
+ categoryId: data.categoryId,
76
+ priceCents: dollarsToCents(data.price),
77
+ quantity: Number(data.quantity),
78
+ shippingCents: dollarsToCents(data.shipping),
79
+ shippingDays: Number(data.shippingDays),
80
+ files: pickedFiles,
81
+ removeImageIds: removedIds,
82
+ };
83
+ if (listing) {
84
+ await updateListing(listing.id, fields);
85
+ successToast({ text: t(($) => $.listingForm.updateSuccess) });
86
+ } else {
87
+ await createListing(fields);
88
+ successToast({ text: t(($) => $.listingForm.createSuccess) });
89
+ }
90
+ await queryClient.invalidateQueries({ queryKey: ListingQueries.keys.all });
91
+ if (onSuccess) {
92
+ await onSuccess();
93
+ return;
94
+ }
95
+ goToMyListings();
96
+ } catch {
97
+ errorToast({
98
+ text: listing ? t(($) => $.listingForm.updateError) : t(($) => $.listingForm.createError),
99
+ });
100
+ } finally {
101
+ setIsPending(false);
102
+ }
103
+ };
104
+
105
+ return (
106
+ <form
107
+ onSubmit={form.handleSubmit(onSubmit)}
108
+ className="flex w-full flex-col gap-4"
109
+ >
110
+ <RowInputWrapper
111
+ label={t(($) => $.listingForm.title)}
112
+ isRequired
113
+ >
114
+ <TextInput
115
+ variant="filled"
116
+ size="extra-small"
117
+ field={{ form, name: "title" }}
118
+ label={t(($) => $.listingForm.title)}
119
+ placeholder={t(($) => $.listingForm.titlePlaceholder)}
120
+ hideLabel
121
+ isRequired
122
+ />
123
+ </RowInputWrapper>
124
+
125
+ <RowInputWrapper label={t(($) => $.listingForm.description)}>
126
+ <TextArea
127
+ variant="filled"
128
+ size="extra-small"
129
+ field={{ form, name: "description" }}
130
+ label={t(($) => $.listingForm.description)}
131
+ placeholder={t(($) => $.listingForm.descriptionPlaceholder)}
132
+ hideLabel
133
+ />
134
+ </RowInputWrapper>
135
+
136
+ <RowInputWrapper
137
+ label={t(($) => $.listingForm.category)}
138
+ isRequired
139
+ >
140
+ <div className="flex flex-wrap gap-1">
141
+ {(categories ?? []).map((category) => (
142
+ <Button
143
+ key={category.id}
144
+ type="button"
145
+ size="xs"
146
+ variant={categoryId === category.id ? "contained" : "outlined"}
147
+ color={categoryId === category.id ? "primary" : "secondary"}
148
+ onPress={() => form.setFieldValue("categoryId", category.id)}
149
+ >
150
+ {category.name}
151
+ </Button>
152
+ ))}
153
+ </div>
154
+ </RowInputWrapper>
155
+
156
+ <RowInputWrapper
157
+ label={t(($) => $.listingForm.price)}
158
+ isRequired
159
+ >
160
+ <TextInput
161
+ variant="filled"
162
+ size="extra-small"
163
+ field={{ form, name: "price" }}
164
+ label={t(($) => $.listingForm.price)}
165
+ hideLabel
166
+ isRequired
167
+ />
168
+ </RowInputWrapper>
169
+
170
+ <RowInputWrapper
171
+ label={t(($) => $.listingForm.quantity)}
172
+ isRequired
173
+ >
174
+ <TextInput
175
+ variant="filled"
176
+ size="extra-small"
177
+ field={{ form, name: "quantity" }}
178
+ label={t(($) => $.listingForm.quantity)}
179
+ hideLabel
180
+ isRequired
181
+ />
182
+ </RowInputWrapper>
183
+
184
+ <RowInputWrapper
185
+ label={t(($) => $.listingForm.shipping)}
186
+ isRequired
187
+ >
188
+ <TextInput
189
+ variant="filled"
190
+ size="extra-small"
191
+ field={{ form, name: "shipping" }}
192
+ label={t(($) => $.listingForm.shipping)}
193
+ hideLabel
194
+ isRequired
195
+ />
196
+ </RowInputWrapper>
197
+
198
+ <RowInputWrapper
199
+ label={t(($) => $.listingForm.shippingDays)}
200
+ isRequired
201
+ >
202
+ <TextInput
203
+ variant="filled"
204
+ size="extra-small"
205
+ field={{ form, name: "shippingDays" }}
206
+ label={t(($) => $.listingForm.shippingDays)}
207
+ hideLabel
208
+ isRequired
209
+ />
210
+ </RowInputWrapper>
211
+
212
+ {remainingImages.length > 0 ? (
213
+ <div className="flex flex-wrap gap-2">
214
+ {remainingImages.map((image) => (
215
+ <div
216
+ key={image.id}
217
+ className="flex flex-col gap-1"
218
+ >
219
+ <img
220
+ src={image.url}
221
+ alt=""
222
+ className="h-20 w-20 object-cover"
223
+ />
224
+ <Button
225
+ type="button"
226
+ size="xs"
227
+ variant="outlined"
228
+ color="secondary"
229
+ onPress={() => setRemovedIds((current) => [...current, image.id])}
230
+ >
231
+ {t(($) => $.listingForm.removeImage)}
232
+ </Button>
233
+ </div>
234
+ ))}
235
+ </div>
236
+ ) : null}
237
+
238
+ {pickedFiles.length > 0 ? (
239
+ <Typography
240
+ size="label-3"
241
+ className="text-text-default-2"
242
+ >
243
+ {pickedFiles.map((file) => file.name).join(", ")}
244
+ </Typography>
245
+ ) : null}
246
+
247
+ <FileUpload
248
+ label={t(($) => $.listingForm.images)}
249
+ emptyText={t(($) => $.listingForm.imageEmptyText)}
250
+ uploadText={t(($) => $.listingForm.imageUploadText)}
251
+ browseText={t(($) => $.listingForm.imageBrowseText)}
252
+ acceptedFileTypes={["image/jpeg", "image/png", "image/webp"]}
253
+ allowsMultiple
254
+ {...bindLocalListingImages((file) => setPickedFiles((current) => [...current, file].slice(0, 8)))}
255
+ />
256
+
257
+ <div className="flex justify-end gap-2">
258
+ <Button
259
+ type="button"
260
+ variant="outlined"
261
+ color="secondary"
262
+ size="xs"
263
+ onPress={() => (onCancel ? onCancel() : goToMyListings())}
264
+ >
265
+ {t(($) => $.listingForm.cancel)}
266
+ </Button>
267
+ <Button
268
+ type="submit"
269
+ size="xs"
270
+ isLoading={isPending}
271
+ isDisabled={isPending}
272
+ >
273
+ {listing ? t(($) => $.listingForm.submitEdit) : t(($) => $.listingForm.submitCreate)}
274
+ </Button>
275
+ </div>
276
+ </form>
277
+ );
278
+ }
@@ -1,243 +1,27 @@
1
- import {
2
- Button,
3
- FileUpload,
4
- TextArea,
5
- TextInput,
6
- Typography,
7
- useForm,
8
- useFormValue,
9
- useToast,
10
- } from "@povio/ui/tanstack";
11
- import { useQueryClient } from "@tanstack/react-query";
12
- import { useNavigate } from "@tanstack/react-router";
13
- import { useState } from "react";
14
1
  import { useTranslation } from "react-i18next";
15
- import { z } from "zod";
16
2
 
17
- import { RowInputWrapper } from "@/components/shared/forms/RowInputWrapper";
18
3
  import { PageHeader } from "@/components/shared/page/PageHeader";
19
- import { CategoryQueries } from "@/openapi/category/category.queries";
20
- import { ListingQueries } from "@/openapi/listing/listing.queries";
21
4
  import type { ListingModels } from "@/openapi/listing/listing.models";
22
- import { bindLocalListingImages, centsToDollarsInput, dollarsToCents } from "@/utils/listing-form";
23
- import { createListing, updateListing } from "@/utils/listing-submit";
24
5
 
25
- const listingFormSchema = z.object({
26
- title: z.string().min(1),
27
- description: z.string().optional(),
28
- categoryId: z.string().min(1),
29
- price: z.string().min(1),
30
- quantity: z.string().min(1),
31
- shipping: z.string().min(1),
32
- shippingDays: z.string().min(1),
33
- });
6
+ import { ListingForm } from "./ListingForm";
34
7
 
35
8
  interface ListingFormPageProps {
36
- listing?: ListingModels.ListingDto;
9
+ listing: ListingModels.ListingDto;
37
10
  }
38
11
 
39
12
  export function ListingFormPage({ listing }: ListingFormPageProps) {
40
13
  const { t } = useTranslation();
41
- const navigate = useNavigate();
42
- const queryClient = useQueryClient();
43
- const { successToast, errorToast } = useToast();
44
- const { data: categories } = CategoryQueries.useGetAll();
45
- const [pickedFiles, setPickedFiles] = useState<File[]>([]);
46
- const [removedIds, setRemovedIds] = useState<string[]>([]);
47
- const [isPending, setIsPending] = useState(false);
48
- const remainingImages = (listing?.images ?? []).filter((image) => !removedIds.includes(image.id));
49
-
50
- const form = useForm({
51
- zodSchema: listingFormSchema,
52
- defaultValues: {
53
- title: listing?.title ?? "",
54
- description: listing?.description ?? "",
55
- categoryId: listing?.categoryId ?? "",
56
- price: listing ? centsToDollarsInput(listing.priceCents) : "",
57
- quantity: listing ? String(listing.quantity) : "1",
58
- shipping: listing ? centsToDollarsInput(listing.shippingCents) : "",
59
- shippingDays: listing ? String(listing.shippingDays) : "3",
60
- },
61
- });
62
- const categoryId = useFormValue(form, (values) => values.categoryId);
63
-
64
- const onSubmit = async (data: z.infer<typeof listingFormSchema>) => {
65
- try {
66
- setIsPending(true);
67
- const fields = {
68
- title: data.title,
69
- description: data.description,
70
- categoryId: data.categoryId,
71
- priceCents: dollarsToCents(data.price),
72
- quantity: Number(data.quantity),
73
- shippingCents: dollarsToCents(data.shipping),
74
- shippingDays: Number(data.shippingDays),
75
- files: pickedFiles,
76
- removeImageIds: removedIds,
77
- };
78
- if (listing) {
79
- await updateListing(listing.id, fields);
80
- successToast({ text: t(($) => $.listingForm.updateSuccess) });
81
- } else {
82
- await createListing(fields);
83
- successToast({ text: t(($) => $.listingForm.createSuccess) });
84
- }
85
- await queryClient.invalidateQueries({ queryKey: ListingQueries.keys.all });
86
- await navigate({ to: "/my-listings" });
87
- } catch {
88
- errorToast({
89
- text: listing ? t(($) => $.listingForm.updateError) : t(($) => $.listingForm.createError),
90
- });
91
- } finally {
92
- setIsPending(false);
93
- }
94
- };
95
14
 
96
15
  return (
97
16
  <div className="flex flex-col gap-6">
98
17
  <PageHeader
99
18
  enableBack
100
19
  backProps={{ backLink: { to: "/my-listings" } }}
101
- title={listing ? t(($) => $.listingForm.editTitle) : t(($) => $.listingForm.createTitle)}
20
+ title={t(($) => $.listingForm.editTitle)}
102
21
  />
103
-
104
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex max-w-xl flex-col gap-4">
105
- <RowInputWrapper label={t(($) => $.listingForm.title)} isRequired>
106
- <TextInput
107
- variant="filled"
108
- size="extra-small"
109
- field={{ form, name: "title" }}
110
- label={t(($) => $.listingForm.title)}
111
- placeholder={t(($) => $.listingForm.titlePlaceholder)}
112
- hideLabel
113
- isRequired
114
- />
115
- </RowInputWrapper>
116
-
117
- <RowInputWrapper label={t(($) => $.listingForm.description)}>
118
- <TextArea
119
- variant="filled"
120
- size="extra-small"
121
- field={{ form, name: "description" }}
122
- label={t(($) => $.listingForm.description)}
123
- placeholder={t(($) => $.listingForm.descriptionPlaceholder)}
124
- hideLabel
125
- />
126
- </RowInputWrapper>
127
-
128
- <RowInputWrapper label={t(($) => $.listingForm.category)} isRequired>
129
- <div className="flex flex-wrap gap-1">
130
- {(categories ?? []).map((category) => (
131
- <Button
132
- key={category.id}
133
- type="button"
134
- size="xs"
135
- variant={categoryId === category.id ? "contained" : "outlined"}
136
- color={categoryId === category.id ? "primary" : "secondary"}
137
- onPress={() => form.setFieldValue("categoryId", category.id)}
138
- >
139
- {category.name}
140
- </Button>
141
- ))}
142
- </div>
143
- </RowInputWrapper>
144
-
145
- <RowInputWrapper label={t(($) => $.listingForm.price)} isRequired>
146
- <TextInput
147
- variant="filled"
148
- size="extra-small"
149
- field={{ form, name: "price" }}
150
- label={t(($) => $.listingForm.price)}
151
- hideLabel
152
- isRequired
153
- />
154
- </RowInputWrapper>
155
-
156
- <RowInputWrapper label={t(($) => $.listingForm.quantity)} isRequired>
157
- <TextInput
158
- variant="filled"
159
- size="extra-small"
160
- field={{ form, name: "quantity" }}
161
- label={t(($) => $.listingForm.quantity)}
162
- hideLabel
163
- isRequired
164
- />
165
- </RowInputWrapper>
166
-
167
- <RowInputWrapper label={t(($) => $.listingForm.shipping)} isRequired>
168
- <TextInput
169
- variant="filled"
170
- size="extra-small"
171
- field={{ form, name: "shipping" }}
172
- label={t(($) => $.listingForm.shipping)}
173
- hideLabel
174
- isRequired
175
- />
176
- </RowInputWrapper>
177
-
178
- <RowInputWrapper label={t(($) => $.listingForm.shippingDays)} isRequired>
179
- <TextInput
180
- variant="filled"
181
- size="extra-small"
182
- field={{ form, name: "shippingDays" }}
183
- label={t(($) => $.listingForm.shippingDays)}
184
- hideLabel
185
- isRequired
186
- />
187
- </RowInputWrapper>
188
-
189
- {remainingImages.length > 0 ? (
190
- <div className="flex flex-wrap gap-2">
191
- {remainingImages.map((image) => (
192
- <div key={image.id} className="flex flex-col gap-1">
193
- <img src={image.url} alt="" className="h-20 w-20 object-cover" />
194
- <Button
195
- type="button"
196
- size="xs"
197
- variant="outlined"
198
- color="secondary"
199
- onPress={() => setRemovedIds((current) => [...current, image.id])}
200
- >
201
- {t(($) => $.listingForm.removeImage)}
202
- </Button>
203
- </div>
204
- ))}
205
- </div>
206
- ) : null}
207
-
208
- {pickedFiles.length > 0 ? (
209
- <Typography size="label-3" className="text-text-default-2">
210
- {pickedFiles.map((file) => file.name).join(", ")}
211
- </Typography>
212
- ) : null}
213
-
214
- <FileUpload
215
- label={t(($) => $.listingForm.images)}
216
- emptyText={t(($) => $.listingForm.imageEmptyText)}
217
- uploadText={t(($) => $.listingForm.imageUploadText)}
218
- browseText={t(($) => $.listingForm.imageBrowseText)}
219
- acceptedFileTypes={["image/jpeg", "image/png", "image/webp"]}
220
- allowsMultiple
221
- {...bindLocalListingImages((file) =>
222
- setPickedFiles((current) => [...current, file].slice(0, 8)),
223
- )}
224
- />
225
-
226
- <div className="flex justify-end gap-2">
227
- <Button
228
- type="button"
229
- variant="outlined"
230
- color="secondary"
231
- size="xs"
232
- onPress={() => navigate({ to: "/my-listings" })}
233
- >
234
- {t(($) => $.listingForm.cancel)}
235
- </Button>
236
- <Button type="submit" size="xs" isLoading={isPending} isDisabled={isPending}>
237
- {listing ? t(($) => $.listingForm.submitEdit) : t(($) => $.listingForm.submitCreate)}
238
- </Button>
239
- </div>
240
- </form>
22
+ <div className="max-w-xl">
23
+ <ListingForm listing={listing} />
24
+ </div>
241
25
  </div>
242
26
  );
243
27
  }
@@ -0,0 +1,54 @@
1
+ import { Button } from "@povio/ui";
2
+ import { useState } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import type { ListingModels } from "@/openapi/listing/listing.models";
6
+ import { getFallbackImageUrl } from "@/utils/image-fallback";
7
+
8
+ interface ListingGalleryProps {
9
+ images: ListingModels.ListingDto["images"];
10
+ }
11
+
12
+ export function ListingGallery({ images }: ListingGalleryProps) {
13
+ const { t } = useTranslation();
14
+ const [activeIndex, setActiveIndex] = useState(0);
15
+ const fallback = getFallbackImageUrl({ width: 800, height: 600 });
16
+ const active = images[activeIndex] ?? images[0];
17
+ const imageSrc = active?.url ?? fallback;
18
+
19
+ return (
20
+ <div className="flex flex-col gap-3">
21
+ <div className="aspect-[4/3] w-full overflow-hidden rounded-m border border-elevation-outline-default-1 bg-elevation-fill-default-2">
22
+ <img
23
+ src={imageSrc}
24
+ alt=""
25
+ className="h-full w-full object-cover"
26
+ />
27
+ </div>
28
+ {images.length > 1 ? (
29
+ <div
30
+ className="flex flex-wrap gap-2"
31
+ aria-label={t(($) => $.listing.images)}
32
+ >
33
+ {images.map((image, index) => (
34
+ <Button
35
+ key={image.id}
36
+ type="button"
37
+ size="xs"
38
+ variant={index === activeIndex ? "contained" : "outlined"}
39
+ color={index === activeIndex ? "primary" : "secondary"}
40
+ onPress={() => setActiveIndex(index)}
41
+ className="h-16 min-h-16 w-16 min-w-16 overflow-hidden p-0"
42
+ >
43
+ <img
44
+ src={image.url}
45
+ alt=""
46
+ className="h-full w-full object-cover"
47
+ />
48
+ </Button>
49
+ ))}
50
+ </div>
51
+ ) : null}
52
+ </div>
53
+ );
54
+ }
@@ -1,6 +1,9 @@
1
- import { Button, Typography, useToast } from "@povio/ui/tanstack";
1
+ import { Drawer, Typography } from "@povio/ui";
2
+ import { Button, useToast } from "@povio/ui/tanstack";
2
3
  import { useQueryClient } from "@tanstack/react-query";
4
+ import { useNavigate } from "@tanstack/react-router";
3
5
  import { Plus } from "lucide-react";
6
+ import { useEffect, useState } from "react";
4
7
  import { useTranslation } from "react-i18next";
5
8
 
6
9
  import { LoadingState } from "@/components/shared/layout/LoadingState";
@@ -9,14 +12,29 @@ import { ListingQueries } from "@/openapi/listing/listing.queries";
9
12
  import { formatCents } from "@/utils/listing-form";
10
13
 
11
14
  import { ListingCard } from "./ListingCard";
15
+ import { ListingForm } from "./ListingForm";
12
16
 
13
- export function MyListingsPage() {
17
+ interface MyListingsPageProps {
18
+ openCreate?: boolean;
19
+ }
20
+
21
+ export function MyListingsPage({ openCreate = false }: MyListingsPageProps) {
14
22
  const { t } = useTranslation();
23
+ const navigate = useNavigate();
15
24
  const queryClient = useQueryClient();
16
25
  const { successToast, errorToast } = useToast();
17
26
  const { data, isLoading } = ListingQueries.useGetAll({ mine: "true" });
18
27
  const deactivate = ListingQueries.useDeleteApiListingsById();
19
28
  const listings = data ?? [];
29
+ const [isCreateOpen, setIsCreateOpen] = useState(openCreate);
30
+
31
+ useEffect(() => {
32
+ if (!openCreate) {
33
+ return;
34
+ }
35
+ setIsCreateOpen(true);
36
+ void navigate({ to: "/my-listings", search: {}, replace: true });
37
+ }, [openCreate, navigate]);
20
38
 
21
39
  const onDeactivate = async (id: string) => {
22
40
  try {
@@ -33,25 +51,60 @@ export function MyListingsPage() {
33
51
  <PageHeader
34
52
  title={t(($) => $.sell.title)}
35
53
  actions={
36
- <Button size="xs" icon={Plus} link={{ to: "/sell" }}>
37
- {t(($) => $.sell.create)}
38
- </Button>
54
+ <Drawer
55
+ label={t(($) => $.listingForm.createTitle)}
56
+ isOpen={isCreateOpen}
57
+ onOpenChange={setIsCreateOpen}
58
+ className="w-full max-w-xl overflow-y-auto"
59
+ trigger={
60
+ <Button
61
+ size="xs"
62
+ icon={Plus}
63
+ >
64
+ {t(($) => $.sell.create)}
65
+ </Button>
66
+ }
67
+ >
68
+ {(close) => (
69
+ <div className="flex flex-col gap-4 p-6">
70
+ <Typography
71
+ size="title-5"
72
+ variant="prominent-1"
73
+ >
74
+ {t(($) => $.listingForm.createTitle)}
75
+ </Typography>
76
+ <ListingForm
77
+ onSuccess={() => close()}
78
+ onCancel={close}
79
+ />
80
+ </div>
81
+ )}
82
+ </Drawer>
39
83
  }
40
84
  />
41
85
 
42
86
  {isLoading ? (
43
87
  <LoadingState />
44
88
  ) : listings.length === 0 ? (
45
- <Typography size="body-3" className="text-text-default-2">
89
+ <Typography
90
+ size="body-3"
91
+ className="text-text-default-2"
92
+ >
46
93
  {t(($) => $.sell.empty)}
47
94
  </Typography>
48
95
  ) : (
49
96
  <div className="flex flex-col gap-4">
50
97
  {listings.map((listing) => (
51
- <div key={listing.id} className="flex flex-col gap-2">
98
+ <div
99
+ key={listing.id}
100
+ className="flex flex-col gap-2"
101
+ >
52
102
  <ListingCard listing={listing} />
53
103
  <div className="flex flex-wrap items-center gap-2">
54
- <Typography size="label-3" className="text-text-default-2">
104
+ <Typography
105
+ size="label-3"
106
+ className="text-text-default-2"
107
+ >
55
108
  {formatCents(listing.priceCents)}
56
109
  {listing.quantity <= 0 ? ` · ${t(($) => $.sell.soldOut)}` : ""}
57
110
  {listing.status === "inactive" ? ` · ${t(($) => $.sell.inactive)}` : ""}