@lodashventure/medusa-campaign 1.0.0 → 1.1.1

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 (45) hide show
  1. package/.medusa/server/src/admin/index.js +2203 -273
  2. package/.medusa/server/src/admin/index.mjs +2196 -267
  3. package/.medusa/server/src/workflows/index.js +10 -0
  4. package/package.json +4 -4
  5. package/src/admin/README.md +31 -0
  6. package/src/admin/components/FlashSaleForm.tsx +379 -0
  7. package/src/admin/components/FlashSalePage.tsx +113 -0
  8. package/src/admin/components/ProductSelector.tsx +88 -0
  9. package/src/admin/hooks/useFlashSaleById.ts +21 -0
  10. package/src/admin/hooks/useFlashSales.ts +25 -0
  11. package/src/admin/lib/sdk.ts +10 -0
  12. package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
  13. package/src/admin/routes/flash-sales/create/page.tsx +51 -0
  14. package/src/admin/routes/flash-sales/page.tsx +15 -0
  15. package/src/admin/tsconfig.json +24 -0
  16. package/src/admin/types/campaign.ts +25 -0
  17. package/src/admin/vite-env.d.ts +1 -0
  18. package/src/api/README.md +133 -0
  19. package/src/api/admin/flash-sales/[id]/route.ts +164 -0
  20. package/src/api/admin/flash-sales/route.ts +87 -0
  21. package/src/api/middlewares.ts +32 -0
  22. package/src/api/store/campaigns/[id]/route.ts +133 -0
  23. package/src/api/store/campaigns/route.ts +36 -0
  24. package/src/jobs/README.md +36 -0
  25. package/src/links/README.md +26 -0
  26. package/src/links/campaign-type.ts +8 -0
  27. package/src/modules/README.md +116 -0
  28. package/src/modules/custom-campaigns/index.ts +8 -0
  29. package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
  30. package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
  31. package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
  32. package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
  33. package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
  34. package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
  35. package/src/modules/custom-campaigns/service.ts +10 -0
  36. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
  37. package/src/providers/README.md +30 -0
  38. package/src/subscribers/README.md +59 -0
  39. package/src/subscribers/order-placed.ts +17 -0
  40. package/src/workflows/README.md +79 -0
  41. package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
  42. package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
  43. package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
  44. package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
  45. package/src/workflows/index.ts +3 -0
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updatePromotionUsageWorkflow = exports.updateCustomFlashSaleWorkflow = exports.createCustomFlashSaleWorkflow = void 0;
4
+ var createCustomCampaignWorkflow_1 = require("./custom-campaign/createCustomCampaignWorkflow");
5
+ Object.defineProperty(exports, "createCustomFlashSaleWorkflow", { enumerable: true, get: function () { return createCustomCampaignWorkflow_1.createCustomFlashSaleWorkflow; } });
6
+ var updateCustomFlashSaleWorkflow_1 = require("./custom-campaign/updateCustomFlashSaleWorkflow");
7
+ Object.defineProperty(exports, "updateCustomFlashSaleWorkflow", { enumerable: true, get: function () { return updateCustomFlashSaleWorkflow_1.updateCustomFlashSaleWorkflow; } });
8
+ var updatePromotionUsageWorkflow_1 = require("./custom-campaign/updatePromotionUsageWorkflow");
9
+ Object.defineProperty(exports, "updatePromotionUsageWorkflow", { enumerable: true, get: function () { return updatePromotionUsageWorkflow_1.updatePromotionUsageWorkflow; } });
10
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLCtGQUErRjtBQUF0Riw2SUFBQSw2QkFBNkIsT0FBQTtBQUN0QyxpR0FBZ0c7QUFBdkYsOElBQUEsNkJBQTZCLE9BQUE7QUFDdEMsK0ZBQThGO0FBQXJGLDRJQUFBLDRCQUE0QixPQUFBIn0=
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@lodashventure/medusa-campaign",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "A starter for Medusa plugins.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
7
7
  "files": [
8
- ".medusa/server"
8
+ ".medusa/server",
9
+ "src"
9
10
  ],
10
11
  "exports": {
11
12
  "./package.json": "./package.json",
@@ -25,8 +26,7 @@
25
26
  ],
26
27
  "scripts": {
27
28
  "build": "medusa plugin:build",
28
- "dev": "medusa plugin:develop",
29
- "prepublishOnly": "medusa plugin:build"
29
+ "dev": "medusa plugin:develop"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@medusajs/admin-sdk": "2.10.0",
@@ -0,0 +1,31 @@
1
+ # Admin Customizations
2
+
3
+ You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.
4
+
5
+ ## Example: Create a Widget
6
+
7
+ A widget is a React component that can be injected into an existing page in the admin dashboard.
8
+
9
+ For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
10
+
11
+ ```tsx title="src/admin/widgets/product-widget.tsx"
12
+ import { defineWidgetConfig } from "@medusajs/admin-sdk"
13
+
14
+ // The widget
15
+ const ProductWidget = () => {
16
+ return (
17
+ <div>
18
+ <h2>Product Widget</h2>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ // The widget's configurations
24
+ export const config = defineWidgetConfig({
25
+ zone: "product.details.after",
26
+ })
27
+
28
+ export default ProductWidget
29
+ ```
30
+
31
+ This inserts a widget with the text “Product Widget” at the end of a product’s details page.
@@ -0,0 +1,379 @@
1
+ import { Trash } from "@medusajs/icons";
2
+ import {
3
+ Button,
4
+ Container,
5
+ FocusModal,
6
+ Input,
7
+ Label,
8
+ Select,
9
+ Table,
10
+ Textarea,
11
+ toast,
12
+ } from "@medusajs/ui";
13
+ import dayjs from "dayjs";
14
+ import { FC, useState } from "react";
15
+ import { Controller, useFieldArray, useForm } from "react-hook-form";
16
+ import { zodResolver } from "@hookform/resolvers/zod";
17
+ import z from "zod";
18
+ import { ProductSelector } from "./ProductSelector";
19
+
20
+ const customCampaignSchema = z.object({
21
+ name: z.string().min(1, "Name is required"),
22
+ description: z.string().min(1, "Description is required"),
23
+ type: z.literal("flash-sale"),
24
+ starts_at: z.string().min(1, "Start date is required"),
25
+ ends_at: z.string().min(1, "End date is required"),
26
+ });
27
+
28
+ export type CustomCampaign = z.infer<typeof customCampaignSchema>;
29
+
30
+ const campaignProductSchema = z.object({
31
+ product: z.object({
32
+ id: z.string(),
33
+ title: z.string(),
34
+ }),
35
+ discountType: z.enum([
36
+ "percentage",
37
+ // , "fixed"
38
+ ]),
39
+ discountValue: z.number().min(1),
40
+ limit: z.number().min(1),
41
+ maxQty: z.number().min(1),
42
+ });
43
+
44
+ export type CampaignProduct = z.infer<typeof campaignProductSchema>;
45
+
46
+ export const campaignDataSchema = customCampaignSchema
47
+ .extend({
48
+ products: z
49
+ .array(campaignProductSchema)
50
+ .min(1, "At least one product is required"),
51
+ })
52
+ .refine(
53
+ (data) => new Date(data.starts_at) < new Date(data.ends_at),
54
+ "End date must be after start date",
55
+ );
56
+
57
+ export type CampaignData = z.infer<typeof campaignDataSchema>;
58
+
59
+ interface FlashSaleFormProps {
60
+ initialData?: CustomCampaign;
61
+ initialProducts?: Map<string, CampaignProduct>;
62
+ onSubmit: (data: CampaignData) => void;
63
+ onCancel: () => void;
64
+ disabled?: boolean;
65
+ }
66
+
67
+ export const FlashSaleForm: FC<FlashSaleFormProps> = ({
68
+ initialData,
69
+ initialProducts,
70
+ onSubmit,
71
+ onCancel,
72
+ disabled = false,
73
+ }) => {
74
+ const {
75
+ control,
76
+ register,
77
+ handleSubmit,
78
+ watch,
79
+ setValue,
80
+ formState: { errors },
81
+ } = useForm<CampaignData>({
82
+ resolver: zodResolver(campaignDataSchema),
83
+ defaultValues: {
84
+ name: initialData?.name || "",
85
+ description: initialData?.description || "",
86
+ type: "flash-sale",
87
+ starts_at: initialData?.starts_at || "",
88
+ ends_at: initialData?.ends_at || "",
89
+ products: initialProducts ? Array.from(initialProducts.values()) : [],
90
+ },
91
+ });
92
+
93
+ const { fields, append, remove, update } = useFieldArray({
94
+ control,
95
+ name: "products",
96
+ });
97
+
98
+ const [openProductModal, setOpenProductModal] = useState(false);
99
+
100
+ const startsAt = watch("starts_at");
101
+ const endsAt = watch("ends_at");
102
+
103
+ const handleDateTimeChange = (
104
+ field: "starts_at" | "ends_at",
105
+ type: "date" | "time",
106
+ value: string,
107
+ ) => {
108
+ const currentValue = watch(field);
109
+
110
+ if (type === "date") {
111
+ const time = currentValue
112
+ ? dayjs(currentValue).format("HH:mm")
113
+ : field === "starts_at"
114
+ ? "00:00"
115
+ : "23:59";
116
+ setValue(field, `${value}T${time}`);
117
+ } else {
118
+ const date = currentValue
119
+ ? dayjs(currentValue).format("YYYY-MM-DD")
120
+ : dayjs().format("YYYY-MM-DD");
121
+ setValue(field, `${date}T${value}`);
122
+ }
123
+ };
124
+
125
+ const onFormSubmit = (data: CampaignData) => {
126
+ onSubmit(data);
127
+ };
128
+
129
+ const onFormError = () => {
130
+ const errorMessages = Object.entries(errors)
131
+ .map(([key, value]) => {
132
+ if (key === "products" && Array.isArray(value)) {
133
+ return value
134
+ .map((item, index) =>
135
+ item
136
+ ? `Product ${index + 1}: ${Object.values(item).join(", ")}`
137
+ : "",
138
+ )
139
+ .filter(Boolean)
140
+ .join(", ");
141
+ }
142
+ return value?.message || value?.root?.message;
143
+ })
144
+ .filter(Boolean)
145
+ .join(", ");
146
+
147
+ toast.error("Invalid data", {
148
+ description: errorMessages,
149
+ });
150
+ };
151
+
152
+ return (
153
+ <Container>
154
+ <div className="flex items-center justify-between">
155
+ <h1 className="text-xl font-semibold">Flash sale campaign</h1>
156
+ <Button variant="transparent" onClick={onCancel}>
157
+ Cancel
158
+ </Button>
159
+ </div>
160
+ <form
161
+ onSubmit={handleSubmit(onFormSubmit, onFormError)}
162
+ className="space-y-4 my-8"
163
+ >
164
+ <div>
165
+ <Label>Name</Label>
166
+ <Input {...register("name")} disabled={disabled} />
167
+ {errors.name && (
168
+ <p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
169
+ )}
170
+ </div>
171
+
172
+ <div>
173
+ <Label>Description</Label>
174
+ <Textarea {...register("description")} disabled={disabled} />
175
+ {errors.description && (
176
+ <p className="text-red-500 text-sm mt-1">
177
+ {errors.description.message}
178
+ </p>
179
+ )}
180
+ </div>
181
+
182
+ <div className="grid grid-cols-2 gap-4">
183
+ <div>
184
+ <Label>Start Date</Label>
185
+ <div className="flex gap-2">
186
+ <Input
187
+ type="date"
188
+ value={startsAt ? dayjs(startsAt).format("YYYY-MM-DD") : ""}
189
+ onChange={(e) =>
190
+ handleDateTimeChange("starts_at", "date", e.target.value)
191
+ }
192
+ disabled={disabled}
193
+ className="flex-1"
194
+ />
195
+ <Input
196
+ type="time"
197
+ value={startsAt ? dayjs(startsAt).format("HH:mm") : ""}
198
+ onChange={(e) =>
199
+ handleDateTimeChange("starts_at", "time", e.target.value)
200
+ }
201
+ disabled={disabled}
202
+ className="w-32"
203
+ />
204
+ </div>
205
+ {errors.starts_at && (
206
+ <p className="text-red-500 text-sm mt-1">
207
+ {errors.starts_at.message}
208
+ </p>
209
+ )}
210
+ </div>
211
+ <div>
212
+ <Label>End Date</Label>
213
+ <div className="flex gap-2">
214
+ <Input
215
+ type="date"
216
+ value={endsAt ? dayjs(endsAt).format("YYYY-MM-DD") : ""}
217
+ onChange={(e) =>
218
+ handleDateTimeChange("ends_at", "date", e.target.value)
219
+ }
220
+ disabled={disabled}
221
+ className="flex-1"
222
+ />
223
+ <Input
224
+ type="time"
225
+ value={endsAt ? dayjs(endsAt).format("HH:mm") : ""}
226
+ onChange={(e) =>
227
+ handleDateTimeChange("ends_at", "time", e.target.value)
228
+ }
229
+ disabled={disabled}
230
+ className="w-32"
231
+ />
232
+ </div>
233
+ {errors.ends_at && (
234
+ <p className="text-red-500 text-sm mt-1">
235
+ {errors.ends_at.message}
236
+ </p>
237
+ )}
238
+ </div>
239
+ </div>
240
+
241
+ <div className="flex justify-between items-center">
242
+ <Label>Products</Label>
243
+ <Button
244
+ type="button"
245
+ variant="secondary"
246
+ onClick={() => setOpenProductModal(true)}
247
+ disabled={disabled}
248
+ >
249
+ Add Product
250
+ </Button>
251
+ </div>
252
+
253
+ {errors.products?.root && (
254
+ <p className="text-red-500 text-sm">{errors.products.root.message}</p>
255
+ )}
256
+
257
+ <Table>
258
+ <Table.Header>
259
+ <Table.Row>
260
+ <Table.HeaderCell>Product</Table.HeaderCell>
261
+ <Table.HeaderCell>Discount Type</Table.HeaderCell>
262
+ <Table.HeaderCell>Discount Value</Table.HeaderCell>
263
+ <Table.HeaderCell>Limit</Table.HeaderCell>
264
+ <Table.HeaderCell>Max Qty per Order</Table.HeaderCell>
265
+ <Table.HeaderCell>Actions</Table.HeaderCell>
266
+ </Table.Row>
267
+ </Table.Header>
268
+ <Table.Body>
269
+ {fields.map((field, index) => (
270
+ <Table.Row key={field.id}>
271
+ <Table.Cell>{field.product.title}</Table.Cell>
272
+ <Table.Cell>
273
+ <Controller
274
+ name={`products.${index}.discountType`}
275
+ control={control}
276
+ render={({ field }) => (
277
+ <Select
278
+ value={field.value}
279
+ onValueChange={field.onChange}
280
+ disabled={disabled}
281
+ >
282
+ <Select.Trigger>
283
+ <Select.Value placeholder="Select discount type" />
284
+ </Select.Trigger>
285
+ <Select.Content>
286
+ <Select.Item value="percentage">
287
+ Percentage
288
+ </Select.Item>
289
+ {/* <Select.Item value="fixed">Fixed</Select.Item> */}
290
+ </Select.Content>
291
+ </Select>
292
+ )}
293
+ />
294
+ </Table.Cell>
295
+ <Table.Cell>
296
+ <Controller
297
+ name={`products.${index}.discountValue`}
298
+ control={control}
299
+ render={({ field }) => (
300
+ <Input
301
+ type="number"
302
+ value={field.value}
303
+ onChange={(e) => field.onChange(Number(e.target.value))}
304
+ disabled={disabled}
305
+ />
306
+ )}
307
+ />
308
+ </Table.Cell>
309
+ <Table.Cell>
310
+ <Controller
311
+ name={`products.${index}.limit`}
312
+ control={control}
313
+ render={({ field }) => (
314
+ <Input
315
+ type="number"
316
+ value={field.value}
317
+ onChange={(e) => field.onChange(Number(e.target.value))}
318
+ disabled={disabled}
319
+ />
320
+ )}
321
+ />
322
+ </Table.Cell>
323
+ <Table.Cell>
324
+ <Controller
325
+ name={`products.${index}.maxQty`}
326
+ control={control}
327
+ render={({ field }) => (
328
+ <Input
329
+ type="number"
330
+ value={field.value}
331
+ onChange={(e) => field.onChange(Number(e.target.value))}
332
+ disabled={disabled}
333
+ />
334
+ )}
335
+ />
336
+ </Table.Cell>
337
+ <Table.Cell>
338
+ <Button
339
+ type="button"
340
+ variant="danger"
341
+ onClick={() => remove(index)}
342
+ disabled={disabled}
343
+ >
344
+ <Trash />
345
+ </Button>
346
+ </Table.Cell>
347
+ </Table.Row>
348
+ ))}
349
+ </Table.Body>
350
+ </Table>
351
+
352
+ <Button type="submit" disabled={disabled}>
353
+ Save
354
+ </Button>
355
+ </form>
356
+
357
+ <FocusModal open={openProductModal} onOpenChange={setOpenProductModal}>
358
+ <FocusModal.Content>
359
+ <FocusModal.Header title="Add Product" />
360
+ <FocusModal.Body>
361
+ <ProductSelector
362
+ selectedProductIds={fields.map((f) => f.product.id)}
363
+ onSelectProduct={(product) => {
364
+ append({
365
+ product,
366
+ discountType: "percentage",
367
+ discountValue: 10,
368
+ limit: 10,
369
+ maxQty: 1,
370
+ });
371
+ setOpenProductModal(false);
372
+ }}
373
+ />
374
+ </FocusModal.Body>
375
+ </FocusModal.Content>
376
+ </FocusModal>
377
+ </Container>
378
+ );
379
+ };
@@ -0,0 +1,113 @@
1
+ import { CampaignDTO } from "@medusajs/framework/types";
2
+ import {
3
+ Badge,
4
+ Button,
5
+ Container,
6
+ createDataTableColumnHelper,
7
+ DataTable,
8
+ DataTablePaginationState,
9
+ useDataTable,
10
+ } from "@medusajs/ui";
11
+ import dayjs from "dayjs";
12
+ import { useMemo, useState } from "react";
13
+ import { useNavigate } from "react-router-dom";
14
+ import { useFlashSales } from "../hooks/useFlashSales";
15
+
16
+ export const FlashSalePage = () => {
17
+ const navigate = useNavigate();
18
+ const limit = 20;
19
+ const [pagination, setPagination] = useState<DataTablePaginationState>({
20
+ pageSize: limit,
21
+ pageIndex: 0,
22
+ });
23
+ const offset = useMemo(() => {
24
+ return pagination.pageIndex * limit;
25
+ }, [pagination]);
26
+
27
+ const columnHelper = createDataTableColumnHelper<CampaignDTO>();
28
+
29
+ const columns = [
30
+ columnHelper.accessor("name", {
31
+ header: "Name",
32
+ cell: (info) => info.getValue(),
33
+ }),
34
+ columnHelper.accessor("ends_at", {
35
+ header: "Status",
36
+ cell: (info) => {
37
+ const date = info.getValue();
38
+ if (!date) {
39
+ return "";
40
+ }
41
+ const isActive = dayjs(date).isAfter(dayjs());
42
+
43
+ return (
44
+ <Badge color={isActive ? "green" : "grey"}>
45
+ {isActive ? "Active" : "Inactive"}
46
+ </Badge>
47
+ );
48
+ },
49
+ }),
50
+ columnHelper.accessor("starts_at", {
51
+ header: "Start Date",
52
+ cell: (info) => {
53
+ const date = info.getValue();
54
+ if (!date) {
55
+ return "";
56
+ }
57
+ return dayjs(date).format("YYYY-MM-DD HH:mm");
58
+ },
59
+ }),
60
+ columnHelper.accessor("ends_at", {
61
+ header: "End Date",
62
+ cell: (info) => {
63
+ const date = info.getValue();
64
+ if (!date) {
65
+ return "";
66
+ }
67
+ return dayjs(date).format("YYYY-MM-DD HH:mm");
68
+ },
69
+ }),
70
+ ];
71
+
72
+ const { data } = useFlashSales({ limit, offset });
73
+
74
+ const table = useDataTable({
75
+ data: data?.campaigns || [],
76
+ columns,
77
+ getRowId: (campaign: CampaignDTO) => campaign.id,
78
+ pagination: {
79
+ state: pagination,
80
+ onPaginationChange: setPagination,
81
+ },
82
+ rowCount: data?.count || 0,
83
+ onRowClick: (_, row) => {
84
+ navigate(`/flash-sales/${row.id}`);
85
+ },
86
+ });
87
+
88
+ return (
89
+ <Container>
90
+ <div className="flex justify-between items-center">
91
+ <h1 className="text-xl font-semibold">Campaigns</h1>
92
+ <Button onClick={() => navigate("/flash-sales/create")}>
93
+ Create Campaign
94
+ </Button>
95
+ </div>
96
+ <DataTable instance={table}>
97
+ <DataTable.Table />
98
+ <DataTable.Pagination />
99
+ </DataTable>
100
+ </Container>
101
+ );
102
+ };
103
+
104
+ // Campaign
105
+ // limit (global)
106
+ // period
107
+
108
+ // Products
109
+ // limit (per product global)
110
+ //
111
+
112
+ // promotion
113
+ //
@@ -0,0 +1,88 @@
1
+ import { AdminProduct } from "@medusajs/framework/types";
2
+ import {
3
+ Container,
4
+ createDataTableColumnHelper,
5
+ DataTable,
6
+ Input,
7
+ Text,
8
+ useDataTable,
9
+ } from "@medusajs/ui";
10
+ import { useQuery } from "@tanstack/react-query";
11
+ import { debounce } from "lodash";
12
+ import { useMemo, useState } from "react";
13
+ import { sdk } from "../lib/sdk";
14
+
15
+ interface ProductSelectorProps {
16
+ selectedProductIds: string[];
17
+ onSelectProduct?: (product: AdminProduct) => void;
18
+ }
19
+
20
+ const columnHelper = createDataTableColumnHelper<AdminProduct>();
21
+
22
+ const columns = [
23
+ columnHelper.accessor("title", {
24
+ header: "Title",
25
+ cell: (info) => (
26
+ <Text size="large" className="py-2">
27
+ {info.getValue()}
28
+ </Text>
29
+ ),
30
+ }),
31
+ columnHelper.accessor("description", {
32
+ header: "Description",
33
+ cell: (info) => (
34
+ <Text size="large" className="py-2">
35
+ {info.getValue()}
36
+ </Text>
37
+ ),
38
+ }),
39
+ ];
40
+
41
+ export const ProductSelector = ({
42
+ selectedProductIds,
43
+ onSelectProduct,
44
+ }: ProductSelectorProps) => {
45
+ const [search, setSearch] = useState("");
46
+
47
+ const debouncedSearchHandler = debounce((value: string) => {
48
+ setSearch(value);
49
+ }, 500);
50
+
51
+ const { data: products } = useQuery({
52
+ queryKey: ["products", selectedProductIds, search],
53
+ queryFn: () =>
54
+ sdk.admin.product.list({
55
+ q: search,
56
+ limit: 100,
57
+ }),
58
+ });
59
+
60
+ const selectableProducts = useMemo(() => {
61
+ return products?.products?.filter(
62
+ (product) => !selectedProductIds.includes(product.id)
63
+ );
64
+ }, [products?.products, selectedProductIds]);
65
+
66
+ const table = useDataTable({
67
+ data: selectableProducts || [],
68
+ columns,
69
+ getRowId: (product) => product.id,
70
+ onRowClick: (_, row) => {
71
+ // @ts-ignore
72
+ onSelectProduct?.(row.original);
73
+ },
74
+ });
75
+
76
+ return (
77
+ <Container>
78
+ <Input
79
+ className="text-lg py-2"
80
+ placeholder="Search products..."
81
+ onChange={(e) => debouncedSearchHandler(e.target.value)}
82
+ />
83
+ <DataTable instance={table}>
84
+ <DataTable.Table />
85
+ </DataTable>
86
+ </Container>
87
+ );
88
+ };
@@ -0,0 +1,21 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import axios from "axios";
3
+ import { CampaignProduct, CustomCampaign } from "../components/FlashSaleForm";
4
+
5
+ /**
6
+ * Hook to fetch a single flash sale by ID
7
+ */
8
+ export const useFlashSaleById = (id: string) => {
9
+ const query = useQuery({
10
+ queryKey: ["flash-sale", id],
11
+ queryFn: async () => {
12
+ const { data } = await axios.get<
13
+ CustomCampaign & { products: CampaignProduct[] }
14
+ >(`/admin/flash-sales/${id}`);
15
+ return data;
16
+ },
17
+ enabled: !!id,
18
+ });
19
+
20
+ return query;
21
+ };
@@ -0,0 +1,25 @@
1
+ import { CampaignDTO } from "@medusajs/framework/types";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import axios from "axios";
4
+
5
+ export const useFlashSales = (pagination: {
6
+ limit: number;
7
+ offset: number;
8
+ }) => {
9
+ const query = useQuery({
10
+ queryKey: ["flash-sales", pagination],
11
+ queryFn: async () => {
12
+ const { data } = await axios.get<{
13
+ campaigns: CampaignDTO[];
14
+ count: number;
15
+ limit: number;
16
+ offset: number;
17
+ }>("/admin/flash-sales", {
18
+ params: pagination,
19
+ });
20
+ return data;
21
+ },
22
+ });
23
+
24
+ return query;
25
+ };