@lodashventure/medusa-campaign 1.1.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.
- package/.medusa/server/src/admin/index.js +1 -67
- package/.medusa/server/src/admin/index.mjs +1 -67
- package/.medusa/server/src/workflows/index.js +10 -0
- package/package.json +4 -4
- package/src/admin/README.md +31 -0
- package/src/admin/components/FlashSaleForm.tsx +379 -0
- package/src/admin/components/FlashSalePage.tsx +113 -0
- package/src/admin/components/ProductSelector.tsx +88 -0
- package/src/admin/hooks/useFlashSaleById.ts +21 -0
- package/src/admin/hooks/useFlashSales.ts +25 -0
- package/src/admin/lib/sdk.ts +10 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
- package/src/admin/routes/flash-sales/create/page.tsx +51 -0
- package/src/admin/routes/flash-sales/page.tsx +15 -0
- package/src/admin/tsconfig.json +24 -0
- package/src/admin/types/campaign.ts +25 -0
- package/src/admin/vite-env.d.ts +1 -0
- package/src/api/README.md +133 -0
- package/src/api/admin/flash-sales/[id]/route.ts +164 -0
- package/src/api/admin/flash-sales/route.ts +87 -0
- package/src/api/middlewares.ts +32 -0
- package/src/api/store/campaigns/[id]/route.ts +133 -0
- package/src/api/store/campaigns/route.ts +36 -0
- package/src/jobs/README.md +36 -0
- package/src/links/README.md +26 -0
- package/src/links/campaign-type.ts +8 -0
- package/src/modules/README.md +116 -0
- package/src/modules/custom-campaigns/index.ts +8 -0
- package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
- package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
- package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
- package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
- package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
- package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
- package/src/modules/custom-campaigns/service.ts +10 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
- package/src/providers/README.md +30 -0
- package/src/subscribers/README.md +59 -0
- package/src/subscribers/order-placed.ts +17 -0
- package/src/workflows/README.md +79 -0
- package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
- package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
- package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
- package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
- package/src/workflows/index.ts +3 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
|
+
import { Sparkles } from "@medusajs/icons";
|
|
3
|
+
import { Button, Container, toast } from "@medusajs/ui";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import { FC, useState } from "react";
|
|
7
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
8
|
+
import {
|
|
9
|
+
CampaignData,
|
|
10
|
+
CustomCampaign,
|
|
11
|
+
FlashSaleForm,
|
|
12
|
+
} from "../../../components/FlashSaleForm";
|
|
13
|
+
import { useFlashSaleById } from "../../../hooks/useFlashSaleById";
|
|
14
|
+
|
|
15
|
+
const FlashSaleDetail: FC = () => {
|
|
16
|
+
const { id } = useParams<{ id: string }>();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const { data, isLoading } = useFlashSaleById(id || "");
|
|
20
|
+
|
|
21
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
22
|
+
|
|
23
|
+
async function handleSubmit(campaignData: CampaignData) {
|
|
24
|
+
try {
|
|
25
|
+
await axios.put(`/admin/flash-sales/${id}`, {
|
|
26
|
+
...campaignData,
|
|
27
|
+
starts_at: new Date(campaignData.starts_at).toUTCString(),
|
|
28
|
+
ends_at: new Date(campaignData.ends_at).toUTCString(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
toast.success("Flash sale updated successfully");
|
|
32
|
+
queryClient.invalidateQueries({
|
|
33
|
+
exact: false,
|
|
34
|
+
predicate(query) {
|
|
35
|
+
return ["flash-sales", "flash-sale"].includes(
|
|
36
|
+
query.queryKey[0] as string
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
navigate("/flash-sales");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
let message: string;
|
|
43
|
+
|
|
44
|
+
if (axios.isAxiosError(error)) {
|
|
45
|
+
console.log(error);
|
|
46
|
+
message = error.response?.data.message;
|
|
47
|
+
} else if (error instanceof Error) {
|
|
48
|
+
message = error.message;
|
|
49
|
+
} else {
|
|
50
|
+
message = "Failed to update flash sale";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toast.error("Failed to update flash sale", {
|
|
54
|
+
description: message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isLoading) {
|
|
60
|
+
return (
|
|
61
|
+
<Container className="flex items-center justify-center h-screen">
|
|
62
|
+
<div className="animate-spin h-10 w-10 border-4 border-primary rounded-full border-t-transparent"></div>
|
|
63
|
+
</Container>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!data) {
|
|
68
|
+
return (
|
|
69
|
+
<Container>
|
|
70
|
+
<p>Flash sale not found</p>
|
|
71
|
+
</Container>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Convert campaign data to the format expected by the form
|
|
76
|
+
const campaignData: CustomCampaign = {
|
|
77
|
+
name: data.name || "",
|
|
78
|
+
description: data.description || "",
|
|
79
|
+
type: "flash-sale",
|
|
80
|
+
starts_at: data.starts_at,
|
|
81
|
+
ends_at: data.ends_at,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<FlashSaleForm
|
|
87
|
+
initialData={campaignData}
|
|
88
|
+
initialProducts={
|
|
89
|
+
new Map(data.products.map((product) => [product.product.id, product]))
|
|
90
|
+
}
|
|
91
|
+
onSubmit={handleSubmit}
|
|
92
|
+
onCancel={() => navigate("/flash-sales")}
|
|
93
|
+
disabled={!isEditing}
|
|
94
|
+
/>
|
|
95
|
+
<Button onClick={() => setIsEditing((prev) => !prev)}>Toggle Edit</Button>
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const config = defineRouteConfig({
|
|
101
|
+
label: "Flash Sale Detail",
|
|
102
|
+
icon: Sparkles,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export default FlashSaleDetail;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { toast } from "@medusajs/ui";
|
|
2
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { FC } from "react";
|
|
5
|
+
import { useNavigate } from "react-router-dom";
|
|
6
|
+
import { CampaignData, FlashSaleForm } from "../../../components/FlashSaleForm";
|
|
7
|
+
|
|
8
|
+
const FlashSaleCreate: FC = () => {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const queryClient = useQueryClient();
|
|
11
|
+
|
|
12
|
+
async function handleSubmit(campaignData: CampaignData) {
|
|
13
|
+
try {
|
|
14
|
+
await axios.post("/admin/flash-sales", {
|
|
15
|
+
...campaignData,
|
|
16
|
+
starts_at: new Date(campaignData.starts_at).toUTCString(),
|
|
17
|
+
ends_at: new Date(campaignData.ends_at).toUTCString(),
|
|
18
|
+
});
|
|
19
|
+
toast.success("Flash sale created successfully");
|
|
20
|
+
queryClient.invalidateQueries({
|
|
21
|
+
exact: false,
|
|
22
|
+
queryKey: ["flash-sales"],
|
|
23
|
+
});
|
|
24
|
+
navigate("/flash-sales");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
let message: string;
|
|
27
|
+
|
|
28
|
+
if (axios.isAxiosError(error)) {
|
|
29
|
+
console.log(error);
|
|
30
|
+
message = error.response?.data.message;
|
|
31
|
+
} else if (error instanceof Error) {
|
|
32
|
+
message = error.message;
|
|
33
|
+
} else {
|
|
34
|
+
message = "Failed to create flash sale";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toast.error("Failed to create flash sale", {
|
|
38
|
+
description: message,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<FlashSaleForm
|
|
45
|
+
onSubmit={handleSubmit}
|
|
46
|
+
onCancel={() => navigate("/flash-sales")}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default FlashSaleCreate;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
2
|
+
import { Sparkles } from "@medusajs/icons";
|
|
3
|
+
import { FC } from "react";
|
|
4
|
+
import { FlashSalePage } from "../../components/FlashSalePage";
|
|
5
|
+
|
|
6
|
+
const FlashSale: FC = () => {
|
|
7
|
+
return <FlashSalePage />;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const config = defineRouteConfig({
|
|
11
|
+
label: "Flash Sale",
|
|
12
|
+
icon: Sparkles,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default FlashSale;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["."]
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Campaign {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
start_date: string;
|
|
6
|
+
end_date: string;
|
|
7
|
+
status: "draft" | "active" | "completed" | "cancelled";
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CampaignFilters {
|
|
13
|
+
status?: string[];
|
|
14
|
+
q?: string;
|
|
15
|
+
start_date?: string;
|
|
16
|
+
end_date?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CampaignFormValues {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
start_date: string;
|
|
23
|
+
end_date: string;
|
|
24
|
+
status: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Custom API Routes
|
|
2
|
+
|
|
3
|
+
An API Route is a REST API endpoint.
|
|
4
|
+
|
|
5
|
+
An API Route is created in a TypeScript or JavaScript file under the `/src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.
|
|
6
|
+
|
|
7
|
+
For example, to create a `GET` API Route at `/store/hello-world`, create the file `src/api/store/hello-world/route.ts` with the following content:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
|
|
11
|
+
|
|
12
|
+
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
13
|
+
res.json({
|
|
14
|
+
message: "Hello world!",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Supported HTTP methods
|
|
20
|
+
|
|
21
|
+
The file based routing supports the following HTTP methods:
|
|
22
|
+
|
|
23
|
+
- GET
|
|
24
|
+
- POST
|
|
25
|
+
- PUT
|
|
26
|
+
- PATCH
|
|
27
|
+
- DELETE
|
|
28
|
+
- OPTIONS
|
|
29
|
+
- HEAD
|
|
30
|
+
|
|
31
|
+
You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file.
|
|
32
|
+
|
|
33
|
+
For example:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
|
|
37
|
+
|
|
38
|
+
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
39
|
+
// Handle GET requests
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
43
|
+
// Handle POST requests
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function PUT(req: MedusaRequest, res: MedusaResponse) {
|
|
47
|
+
// Handle PUT requests
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Parameters
|
|
52
|
+
|
|
53
|
+
To create an API route that accepts a path parameter, create a directory within the route's path whose name is of the format `[param]`.
|
|
54
|
+
|
|
55
|
+
For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type {
|
|
59
|
+
MedusaRequest,
|
|
60
|
+
MedusaResponse,
|
|
61
|
+
} from "@medusajs/framework/http"
|
|
62
|
+
|
|
63
|
+
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
64
|
+
const { productId } = req.params;
|
|
65
|
+
|
|
66
|
+
res.json({
|
|
67
|
+
message: `You're looking for product ${productId}`
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
|
|
73
|
+
|
|
74
|
+
For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`.
|
|
75
|
+
|
|
76
|
+
## Using the container
|
|
77
|
+
|
|
78
|
+
The Medusa container is available on `req.scope`. Use it to access modules' main services and other registered resources:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type {
|
|
82
|
+
MedusaRequest,
|
|
83
|
+
MedusaResponse,
|
|
84
|
+
} from "@medusajs/framework/http"
|
|
85
|
+
|
|
86
|
+
export const GET = async (
|
|
87
|
+
req: MedusaRequest,
|
|
88
|
+
res: MedusaResponse
|
|
89
|
+
) => {
|
|
90
|
+
const productModuleService = req.scope.resolve("product")
|
|
91
|
+
|
|
92
|
+
const [, count] = await productModuleService.listAndCount()
|
|
93
|
+
|
|
94
|
+
res.json({
|
|
95
|
+
count,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Middleware
|
|
101
|
+
|
|
102
|
+
You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file must export a configuration object with what middleware you want to apply to which routes.
|
|
103
|
+
|
|
104
|
+
For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineMiddlewares } from "@medusajs/framework/http"
|
|
108
|
+
import type {
|
|
109
|
+
MedusaRequest,
|
|
110
|
+
MedusaResponse,
|
|
111
|
+
MedusaNextFunction,
|
|
112
|
+
} from "@medusajs/framework/http";
|
|
113
|
+
|
|
114
|
+
async function logger(
|
|
115
|
+
req: MedusaRequest,
|
|
116
|
+
res: MedusaResponse,
|
|
117
|
+
next: MedusaNextFunction
|
|
118
|
+
) {
|
|
119
|
+
console.log("Request received");
|
|
120
|
+
next();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default defineMiddlewares({
|
|
124
|
+
routes: [
|
|
125
|
+
{
|
|
126
|
+
matcher: "/store/custom",
|
|
127
|
+
middlewares: [logger],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions.
|