@lodashventure/medusa-campaign 1.4.1 → 1.4.3

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 (46) hide show
  1. package/.medusa/server/src/admin/index.js +741 -306
  2. package/.medusa/server/src/admin/index.mjs +742 -307
  3. package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +2 -6
  4. package/.medusa/server/src/api/admin/coupons/[id]/route.js +76 -0
  5. package/.medusa/server/src/api/admin/coupons/route.js +88 -0
  6. package/.medusa/server/src/api/middlewares.js +32 -1
  7. package/.medusa/server/src/api/store/campaigns/route.js +78 -7
  8. package/.medusa/server/src/api/store/coupons/public/route.js +110 -0
  9. package/.medusa/server/src/api/store/customers/me/coupons/route.js +148 -0
  10. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +2 -2
  11. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +2 -2
  12. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251101000000.js +16 -0
  13. package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
  14. package/.medusa/server/src/workflows/custom-campaign/createCouponCampaignWorkflow.js +105 -0
  15. package/.medusa/server/src/workflows/custom-campaign/updateCouponCampaignWorkflow.js +59 -0
  16. package/.medusa/server/src/workflows/index.js +6 -2
  17. package/package.json +15 -30
  18. package/src/admin/components/BuyXGetYForm.tsx +24 -13
  19. package/src/admin/components/CouponForm.tsx +352 -0
  20. package/src/admin/components/CouponPage.tsx +104 -0
  21. package/src/admin/components/ProductSelector.tsx +22 -11
  22. package/src/admin/hooks/useCouponById.ts +36 -0
  23. package/src/admin/hooks/useCoupons.ts +46 -0
  24. package/src/admin/hooks/useFlashSaleById.ts +36 -10
  25. package/src/admin/hooks/useFlashSales.ts +36 -10
  26. package/src/admin/routes/coupons/[id]/page.tsx +147 -0
  27. package/src/admin/routes/coupons/create/page.tsx +49 -0
  28. package/src/admin/routes/coupons/page.tsx +15 -0
  29. package/src/admin/routes/flash-sales/[id]/page.tsx +2 -11
  30. package/src/admin/routes/flash-sales/create/page.tsx +0 -6
  31. package/src/admin/widgets/campaign-detail-widget.tsx +33 -26
  32. package/src/api/admin/buy-x-get-y/[id]/route.ts +11 -15
  33. package/src/api/admin/coupons/[id]/route.ts +98 -0
  34. package/src/api/admin/coupons/route.ts +109 -0
  35. package/src/api/middlewares.ts +34 -0
  36. package/src/api/store/campaigns/route.ts +107 -24
  37. package/src/api/store/coupons/public/route.ts +165 -0
  38. package/src/api/store/customers/me/coupons/route.ts +244 -0
  39. package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +1 -1
  40. package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +1 -1
  41. package/src/modules/custom-campaigns/migrations/Migration20251101000000.ts +21 -0
  42. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
  43. package/src/workflows/custom-campaign/createCouponCampaignWorkflow.ts +176 -0
  44. package/src/workflows/custom-campaign/updateCouponCampaignWorkflow.ts +105 -0
  45. package/src/workflows/index.ts +3 -1
  46. package/src/admin/widgets/campaign-stats-widget.tsx +0 -238
@@ -0,0 +1,352 @@
1
+ import { zodResolver } from "@hookform/resolvers/zod";
2
+ import {
3
+ Button,
4
+ Container,
5
+ Input,
6
+ Label,
7
+ Select,
8
+ Textarea,
9
+ toast,
10
+ } from "@medusajs/ui";
11
+ import dayjs from "dayjs";
12
+ import { FC } from "react";
13
+ import { Controller, useForm } from "react-hook-form";
14
+ import z from "zod";
15
+
16
+ const couponSchema = z
17
+ .object({
18
+ name: z.string().min(1, "Name is required"),
19
+ description: z.string().min(1, "Description is required"),
20
+ type: z.literal("coupon"),
21
+ code: z.string().min(1, "Coupon code is required"),
22
+ discount_type: z.enum(["percentage", "fixed"]),
23
+ discount_value: z.number().positive("Discount must be positive"),
24
+ currency_code: z.string().optional(),
25
+ starts_at: z.string().min(1, "Start date is required"),
26
+ ends_at: z.string().min(1, "End date is required"),
27
+ allocation: z.enum(["each", "total"]).optional(),
28
+ target_type: z.enum(["order", "items"]).optional(),
29
+ })
30
+ .superRefine((data, ctx) => {
31
+ if (new Date(data.ends_at) < new Date(data.starts_at)) {
32
+ ctx.addIssue({
33
+ code: z.ZodIssueCode.custom,
34
+ path: ["ends_at"],
35
+ message: "End date must be after start date",
36
+ });
37
+ }
38
+
39
+ if (data.discount_type === "fixed" && !data.currency_code) {
40
+ ctx.addIssue({
41
+ code: z.ZodIssueCode.custom,
42
+ path: ["currency_code"],
43
+ message: "Currency is required for fixed discount",
44
+ });
45
+ }
46
+ });
47
+
48
+ export type CouponFormData = z.infer<typeof couponSchema>;
49
+
50
+ interface CouponFormProps {
51
+ initialData?: Partial<CouponFormData>;
52
+ onSubmit: (data: CouponFormData) => void;
53
+ onCancel: () => void;
54
+ disabled?: boolean;
55
+ }
56
+
57
+ export const CouponForm: FC<CouponFormProps> = ({
58
+ initialData,
59
+ onSubmit,
60
+ onCancel,
61
+ disabled = false,
62
+ }) => {
63
+ const {
64
+ register,
65
+ control,
66
+ handleSubmit,
67
+ watch,
68
+ setValue,
69
+ formState: { errors },
70
+ } = useForm<CouponFormData>({
71
+ resolver: zodResolver(couponSchema),
72
+ defaultValues: {
73
+ name: initialData?.name ?? "",
74
+ description: initialData?.description ?? "",
75
+ type: "coupon",
76
+ code: initialData?.code ?? "",
77
+ discount_type: initialData?.discount_type ?? "percentage",
78
+ discount_value: initialData?.discount_value ?? 0,
79
+ currency_code: initialData?.currency_code ?? "",
80
+ starts_at:
81
+ initialData?.starts_at ??
82
+ dayjs().startOf("day").format("YYYY-MM-DDTHH:mm"),
83
+ ends_at:
84
+ initialData?.ends_at ??
85
+ dayjs().endOf("day").format("YYYY-MM-DDTHH:mm"),
86
+ allocation: initialData?.allocation ?? "total",
87
+ target_type: initialData?.target_type ?? "order",
88
+ },
89
+ });
90
+
91
+ const discountType = watch("discount_type");
92
+ const startsAt = watch("starts_at");
93
+ const endsAt = watch("ends_at");
94
+
95
+ const handleDateTimeChange = (
96
+ field: "starts_at" | "ends_at",
97
+ type: "date" | "time",
98
+ value: string,
99
+ ) => {
100
+ const currentValue = watch(field);
101
+
102
+ if (type === "date") {
103
+ const time = currentValue
104
+ ? dayjs(currentValue).format("HH:mm")
105
+ : field === "starts_at"
106
+ ? "00:00"
107
+ : "23:59";
108
+ setValue(field, `${value}T${time}`);
109
+ } else {
110
+ const date = currentValue
111
+ ? dayjs(currentValue).format("YYYY-MM-DD")
112
+ : dayjs().format("YYYY-MM-DD");
113
+ setValue(field, `${date}T${value}`);
114
+ }
115
+ };
116
+
117
+ const onFormSubmit = (data: CouponFormData) => {
118
+ onSubmit(data);
119
+ };
120
+
121
+ const onFormError = () => {
122
+ const errorMessages = Object.values(errors)
123
+ .map((err) => err?.message)
124
+ .filter(Boolean)
125
+ .join(", ");
126
+
127
+ toast.error("Invalid coupon data", {
128
+ description: errorMessages,
129
+ });
130
+ };
131
+
132
+ return (
133
+ <Container>
134
+ <div className="flex items-center justify-between">
135
+ <h1 className="text-xl font-semibold">Coupon</h1>
136
+ <Button variant="transparent" onClick={onCancel}>
137
+ Cancel
138
+ </Button>
139
+ </div>
140
+
141
+ <form
142
+ onSubmit={handleSubmit(onFormSubmit, onFormError)}
143
+ className="space-y-6 my-8"
144
+ >
145
+ <div className="space-y-2">
146
+ <Label>Name</Label>
147
+ <Input {...register("name")} disabled={disabled} />
148
+ {errors.name && (
149
+ <p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
150
+ )}
151
+ </div>
152
+
153
+ <div className="space-y-2">
154
+ <Label>Description</Label>
155
+ <Textarea {...register("description")} disabled={disabled} />
156
+ {errors.description && (
157
+ <p className="text-red-500 text-sm mt-1">
158
+ {errors.description.message}
159
+ </p>
160
+ )}
161
+ </div>
162
+
163
+ <div className="grid grid-cols-2 gap-4">
164
+ <div className="space-y-2">
165
+ <Label>Start Date</Label>
166
+ <div className="flex gap-2">
167
+ <Input
168
+ type="date"
169
+ value={startsAt ? dayjs(startsAt).format("YYYY-MM-DD") : ""}
170
+ onChange={(e) =>
171
+ handleDateTimeChange("starts_at", "date", e.target.value)
172
+ }
173
+ disabled={disabled}
174
+ className="flex-1"
175
+ />
176
+ <Input
177
+ type="time"
178
+ value={startsAt ? dayjs(startsAt).format("HH:mm") : ""}
179
+ onChange={(e) =>
180
+ handleDateTimeChange("starts_at", "time", e.target.value)
181
+ }
182
+ disabled={disabled}
183
+ className="flex-1"
184
+ />
185
+ </div>
186
+ {errors.starts_at && (
187
+ <p className="text-red-500 text-sm mt-1">
188
+ {errors.starts_at.message}
189
+ </p>
190
+ )}
191
+ </div>
192
+
193
+ <div className="space-y-2">
194
+ <Label>End Date</Label>
195
+ <div className="flex gap-2">
196
+ <Input
197
+ type="date"
198
+ value={endsAt ? dayjs(endsAt).format("YYYY-MM-DD") : ""}
199
+ onChange={(e) =>
200
+ handleDateTimeChange("ends_at", "date", e.target.value)
201
+ }
202
+ disabled={disabled}
203
+ className="flex-1"
204
+ />
205
+ <Input
206
+ type="time"
207
+ value={endsAt ? dayjs(endsAt).format("HH:mm") : ""}
208
+ onChange={(e) =>
209
+ handleDateTimeChange("ends_at", "time", e.target.value)
210
+ }
211
+ disabled={disabled}
212
+ className="flex-1"
213
+ />
214
+ </div>
215
+ {errors.ends_at && (
216
+ <p className="text-red-500 text-sm mt-1">
217
+ {errors.ends_at.message}
218
+ </p>
219
+ )}
220
+ </div>
221
+ </div>
222
+
223
+ <div className="grid grid-cols-2 gap-4">
224
+ <div className="space-y-2">
225
+ <Label>Coupon Code</Label>
226
+ <Input {...register("code")} disabled={disabled} />
227
+ {errors.code && (
228
+ <p className="text-red-500 text-sm mt-1">{errors.code.message}</p>
229
+ )}
230
+ </div>
231
+
232
+ <div className="space-y-2">
233
+ <Label>Discount Type</Label>
234
+ <Controller
235
+ name="discount_type"
236
+ control={control}
237
+ render={({ field }) => (
238
+ <Select
239
+ value={field.value}
240
+ onValueChange={field.onChange}
241
+ disabled={disabled}
242
+ >
243
+ <Select.Trigger>
244
+ <Select.Value placeholder="Select discount type" />
245
+ </Select.Trigger>
246
+ <Select.Content>
247
+ <Select.Item value="percentage">Percentage</Select.Item>
248
+ <Select.Item value="fixed">Fixed Amount</Select.Item>
249
+ </Select.Content>
250
+ </Select>
251
+ )}
252
+ />
253
+ </div>
254
+ </div>
255
+
256
+ <div className="grid grid-cols-2 gap-4">
257
+ <div className="space-y-2">
258
+ <Label>Discount Value</Label>
259
+ <Controller
260
+ name="discount_value"
261
+ control={control}
262
+ render={({ field }) => (
263
+ <Input
264
+ type="number"
265
+ value={field.value}
266
+ min={0}
267
+ step={field.value % 1 === 0 ? 1 : 0.01}
268
+ onChange={(e) => field.onChange(Number(e.target.value))}
269
+ disabled={disabled}
270
+ />
271
+ )}
272
+ />
273
+ {errors.discount_value && (
274
+ <p className="text-red-500 text-sm mt-1">
275
+ {errors.discount_value.message}
276
+ </p>
277
+ )}
278
+ </div>
279
+
280
+ {discountType === "fixed" && (
281
+ <div className="space-y-2">
282
+ <Label>Currency Code</Label>
283
+ <Input {...register("currency_code")} disabled={disabled} />
284
+ {errors.currency_code && (
285
+ <p className="text-red-500 text-sm mt-1">
286
+ {errors.currency_code.message}
287
+ </p>
288
+ )}
289
+ </div>
290
+ )}
291
+ </div>
292
+
293
+ <div className="grid grid-cols-2 gap-4">
294
+ <div className="space-y-2">
295
+ <Label>Allocation</Label>
296
+ <Controller
297
+ name="allocation"
298
+ control={control}
299
+ render={({ field }) => (
300
+ <Select
301
+ value={field.value}
302
+ onValueChange={field.onChange}
303
+ disabled={disabled}
304
+ >
305
+ <Select.Trigger>
306
+ <Select.Value placeholder="Select allocation" />
307
+ </Select.Trigger>
308
+ <Select.Content>
309
+ <Select.Item value="total">Order Total</Select.Item>
310
+ <Select.Item value="each">Each Item</Select.Item>
311
+ </Select.Content>
312
+ </Select>
313
+ )}
314
+ />
315
+ </div>
316
+
317
+ <div className="space-y-2">
318
+ <Label>Target Type</Label>
319
+ <Controller
320
+ name="target_type"
321
+ control={control}
322
+ render={({ field }) => (
323
+ <Select
324
+ value={field.value}
325
+ onValueChange={field.onChange}
326
+ disabled={disabled}
327
+ >
328
+ <Select.Trigger>
329
+ <Select.Value placeholder="Select target" />
330
+ </Select.Trigger>
331
+ <Select.Content>
332
+ <Select.Item value="order">Order</Select.Item>
333
+ <Select.Item value="items">Items</Select.Item>
334
+ </Select.Content>
335
+ </Select>
336
+ )}
337
+ />
338
+ </div>
339
+ </div>
340
+
341
+ <div className="flex justify-end gap-2">
342
+ <Button variant="secondary" type="button" onClick={onCancel}>
343
+ Cancel
344
+ </Button>
345
+ <Button type="submit" disabled={disabled}>
346
+ Save Coupon
347
+ </Button>
348
+ </div>
349
+ </form>
350
+ </Container>
351
+ );
352
+ };
@@ -0,0 +1,104 @@
1
+ import { CampaignDTO, PromotionDTO } 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 { useCoupons } from "../hooks/useCoupons";
15
+
16
+ type CouponCampaign = CampaignDTO & {
17
+ promotions?: PromotionDTO[];
18
+ };
19
+
20
+ const columnHelper = createDataTableColumnHelper<CouponCampaign>();
21
+
22
+ const useCouponColumns = () =>
23
+ useMemo(
24
+ () => [
25
+ columnHelper.accessor("name", {
26
+ header: "Name",
27
+ cell: (info) => info.getValue(),
28
+ }),
29
+ columnHelper.display({
30
+ id: "code",
31
+ header: "Code",
32
+ cell: (info) => info.row.original.promotions?.[0]?.code ?? "-",
33
+ }),
34
+ columnHelper.display({
35
+ id: "status",
36
+ header: "Status",
37
+ cell: (info) => {
38
+ const endsAt = info.row.original.ends_at;
39
+ if (!endsAt) {
40
+ return "";
41
+ }
42
+
43
+ const isActive = dayjs(endsAt).isAfter(dayjs());
44
+ return (
45
+ <Badge color={isActive ? "green" : "grey"}>
46
+ {isActive ? "Active" : "Inactive"}
47
+ </Badge>
48
+ );
49
+ },
50
+ }),
51
+ columnHelper.accessor("starts_at", {
52
+ header: "Start Date",
53
+ cell: (info) =>
54
+ info.getValue() ? dayjs(info.getValue()).format("YYYY-MM-DD HH:mm") : "",
55
+ }),
56
+ columnHelper.accessor("ends_at", {
57
+ header: "End Date",
58
+ cell: (info) =>
59
+ info.getValue() ? dayjs(info.getValue()).format("YYYY-MM-DD HH:mm") : "",
60
+ }),
61
+ ],
62
+ [],
63
+ );
64
+
65
+ export const CouponPage = () => {
66
+ const navigate = useNavigate();
67
+ const limit = 20;
68
+ const [pagination, setPagination] = useState<DataTablePaginationState>({
69
+ pageSize: limit,
70
+ pageIndex: 0,
71
+ });
72
+
73
+ const offset = pagination.pageIndex * limit;
74
+
75
+ const { data } = useCoupons({ limit, offset });
76
+ const columns = useCouponColumns();
77
+
78
+ const table = useDataTable({
79
+ data: (data?.campaigns as CouponCampaign[]) ?? [],
80
+ columns,
81
+ getRowId: (row: CouponCampaign) => row.id,
82
+ pagination: {
83
+ state: pagination,
84
+ onPaginationChange: setPagination,
85
+ },
86
+ rowCount: data?.count ?? 0,
87
+ onRowClick: (_, row) => {
88
+ navigate(`/coupons/${row.id}`);
89
+ },
90
+ });
91
+
92
+ return (
93
+ <Container>
94
+ <div className="flex justify-between items-center">
95
+ <h1 className="text-xl font-semibold">Coupons</h1>
96
+ <Button onClick={() => navigate("/coupons/create")}>Create Coupon</Button>
97
+ </div>
98
+ <DataTable instance={table}>
99
+ <DataTable.Table />
100
+ <DataTable.Pagination />
101
+ </DataTable>
102
+ </Container>
103
+ );
104
+ };
@@ -7,9 +7,8 @@ import {
7
7
  Text,
8
8
  useDataTable,
9
9
  } from "@medusajs/ui";
10
- import { useQuery } from "@tanstack/react-query";
11
10
  import { debounce } from "lodash";
12
- import { useMemo, useState } from "react";
11
+ import { useEffect, useMemo, useState } from "react";
13
12
  import { sdk } from "../lib/sdk";
14
13
 
15
14
  interface ProductSelectorProps {
@@ -48,18 +47,30 @@ export const ProductSelector = ({
48
47
  setSearch(value);
49
48
  }, 500);
50
49
 
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
- });
50
+ const [products, setProducts] = useState<any>(null);
51
+ const [isLoading, setIsLoading] = useState(true);
52
+
53
+ useEffect(() => {
54
+ const fetchProducts = async () => {
55
+ setIsLoading(true);
56
+ try {
57
+ const result = await sdk.admin.product.list({
58
+ q: search,
59
+ limit: 100,
60
+ });
61
+ setProducts(result);
62
+ } catch (error) {
63
+ console.error("Failed to fetch products:", error);
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ };
68
+ fetchProducts();
69
+ }, [selectedProductIds, search]);
59
70
 
60
71
  const selectableProducts = useMemo(() => {
61
72
  return products?.products?.filter(
62
- (product) => !selectedProductIds.includes(product.id)
73
+ (product) => !selectedProductIds.includes(product.id),
63
74
  );
64
75
  }, [products?.products, selectedProductIds]);
65
76
 
@@ -0,0 +1,36 @@
1
+ import axios from "axios";
2
+ import { useEffect, useState } from "react";
3
+
4
+ export const useCouponById = (id: string) => {
5
+ const [data, setData] = useState<any | null>(null);
6
+ const [isLoading, setIsLoading] = useState(true);
7
+ const [error, setError] = useState<Error | null>(null);
8
+
9
+ const fetchCoupon = async () => {
10
+ if (!id) {
11
+ return;
12
+ }
13
+
14
+ setIsLoading(true);
15
+ setError(null);
16
+ try {
17
+ const response = await axios.get(`/admin/coupons/${id}`);
18
+ setData(response.data);
19
+ } catch (err) {
20
+ setError(err instanceof Error ? err : new Error("Failed to fetch coupon"));
21
+ } finally {
22
+ setIsLoading(false);
23
+ }
24
+ };
25
+
26
+ useEffect(() => {
27
+ fetchCoupon();
28
+ }, [id]);
29
+
30
+ return {
31
+ data,
32
+ isLoading,
33
+ error,
34
+ refetch: fetchCoupon,
35
+ };
36
+ };
@@ -0,0 +1,46 @@
1
+ import axios from "axios";
2
+ import { useEffect, useState } from "react";
3
+
4
+ export type CouponCampaignResponse = {
5
+ campaigns: any[];
6
+ count: number;
7
+ limit: number;
8
+ offset: number;
9
+ };
10
+
11
+ interface Pagination {
12
+ limit: number;
13
+ offset: number;
14
+ }
15
+
16
+ export const useCoupons = ({ limit, offset }: Pagination) => {
17
+ const [data, setData] = useState<CouponCampaignResponse | null>(null);
18
+ const [isLoading, setIsLoading] = useState(true);
19
+ const [error, setError] = useState<Error | null>(null);
20
+
21
+ const fetchCoupons = async () => {
22
+ setIsLoading(true);
23
+ setError(null);
24
+ try {
25
+ const response = await axios.get<CouponCampaignResponse>("/admin/coupons", {
26
+ params: { limit, offset },
27
+ });
28
+ setData(response.data);
29
+ } catch (err) {
30
+ setError(err instanceof Error ? err : new Error("Failed to fetch coupons"));
31
+ } finally {
32
+ setIsLoading(false);
33
+ }
34
+ };
35
+
36
+ useEffect(() => {
37
+ fetchCoupons();
38
+ }, [limit, offset]);
39
+
40
+ return {
41
+ data,
42
+ isLoading,
43
+ error,
44
+ refetch: fetchCoupons,
45
+ };
46
+ };
@@ -1,21 +1,47 @@
1
- import { useQuery } from "@tanstack/react-query";
2
1
  import axios from "axios";
2
+ import { useEffect, useState } from "react";
3
3
  import { CampaignProduct, CustomCampaign } from "../components/FlashSaleForm";
4
4
 
5
5
  /**
6
6
  * Hook to fetch a single flash sale by ID
7
7
  */
8
8
  export const useFlashSaleById = (id: string) => {
9
- const query = useQuery({
10
- queryKey: ["flash-sale", id],
11
- queryFn: async () => {
12
- const { data } = await axios.get<
9
+ const [data, setData] = useState<
10
+ (CustomCampaign & { products: CampaignProduct[] }) | null
11
+ >(null);
12
+ const [isLoading, setIsLoading] = useState(true);
13
+ const [error, setError] = useState<Error | null>(null);
14
+
15
+ const fetchFlashSale = async () => {
16
+ if (!id) {
17
+ setIsLoading(false);
18
+ return;
19
+ }
20
+
21
+ setIsLoading(true);
22
+ setError(null);
23
+ try {
24
+ const response = await axios.get<
13
25
  CustomCampaign & { products: CampaignProduct[] }
14
26
  >(`/admin/flash-sales/${id}`);
15
- return data;
16
- },
17
- enabled: !!id,
18
- });
27
+ setData(response.data);
28
+ } catch (err) {
29
+ setError(
30
+ err instanceof Error ? err : new Error("Failed to fetch flash sale"),
31
+ );
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ fetchFlashSale();
39
+ }, [id]);
19
40
 
20
- return query;
41
+ return {
42
+ data,
43
+ isLoading,
44
+ error,
45
+ refetch: fetchFlashSale,
46
+ };
21
47
  };