@lodashventure/medusa-campaign 1.3.12 → 1.4.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 (54) hide show
  1. package/.medusa/server/src/admin/index.js +1238 -16
  2. package/.medusa/server/src/admin/index.mjs +1240 -18
  3. package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
  4. package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
  5. package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
  6. package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
  7. package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
  8. package/.medusa/server/src/api/middlewares.js +24 -1
  9. package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
  10. package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
  11. package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
  12. package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
  13. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
  14. package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
  15. package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
  16. package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
  17. package/.medusa/server/src/subscribers/order-placed.js +2 -2
  18. package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
  19. package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
  20. package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
  21. package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
  22. package/package.json +7 -5
  23. package/src/admin/components/campaign-detail-form.tsx +407 -0
  24. package/src/admin/components/campaign-image-uploader.tsx +313 -0
  25. package/src/admin/components/markdown-editor.tsx +298 -0
  26. package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
  27. package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
  28. package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
  29. package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
  30. package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
  31. package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
  32. package/src/api/admin/campaigns/sync/route.ts +53 -28
  33. package/src/api/admin/flash-sales/[id]/route.ts +50 -19
  34. package/src/api/middlewares.ts +21 -0
  35. package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
  36. package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
  37. package/src/api/store/buy-x-get-y/route.ts +12 -5
  38. package/src/api/store/campaigns/[id]/route.ts +54 -24
  39. package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
  40. package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
  41. package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
  42. package/src/modules/custom-campaigns/service.ts +2 -0
  43. package/src/subscribers/order-placed.ts +0 -2
  44. package/src/types/index.d.ts +46 -0
  45. package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
  46. package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
  47. package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
  48. package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
  49. package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
  50. package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
  51. package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
  52. package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
  53. package/src/api/admin/force-fix/route.ts +0 -184
  54. package/src/api/admin/test-campaign/route.ts +0 -141
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodashventure/medusa-campaign",
3
- "version": "1.3.12",
3
+ "version": "1.4.0",
4
4
  "description": "A starter for Medusa plugins.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "build": "medusa plugin:build",
29
- "dev": "medusa plugin:develop"
29
+ "dev": "medusa plugin:develop",
30
+ "prepublishOnly": "medusa plugin:build"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@medusajs/admin-sdk": "2.11.0",
@@ -61,16 +62,16 @@
61
62
  "@medusajs/cli": "2.11.0",
62
63
  "@medusajs/framework": "2.11.0",
63
64
  "@medusajs/icons": "^2.11.0",
65
+ "@medusajs/js-sdk": "*",
64
66
  "@medusajs/medusa": "2.11.0",
65
67
  "@medusajs/test-utils": "2.11.0",
66
68
  "@medusajs/ui": "^4.0.17",
67
- "@medusajs/js-sdk": "*",
68
- "@tanstack/react-query": "*",
69
69
  "@mikro-orm/cli": "6.4.3",
70
70
  "@mikro-orm/core": "6.4.3",
71
71
  "@mikro-orm/knex": "6.4.3",
72
72
  "@mikro-orm/migrations": "6.4.3",
73
73
  "@mikro-orm/postgresql": "6.4.3",
74
+ "@tanstack/react-query": "*",
74
75
  "awilix": "^8.0.1",
75
76
  "pg": "^8.13.0"
76
77
  },
@@ -81,6 +82,7 @@
81
82
  "axios": "^1.12.2",
82
83
  "dayjs": "^1.11.13",
83
84
  "lodash": "^4.17.21",
85
+ "multer": "^2.0.2",
84
86
  "zod": "^4.1.5"
85
87
  }
86
- }
88
+ }
@@ -0,0 +1,407 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ Container,
4
+ Heading,
5
+ Button,
6
+ Input,
7
+ Label,
8
+ toast,
9
+ clx,
10
+ } from "@medusajs/ui";
11
+ import { PhotoSolid, PencilSquare } from "@medusajs/icons";
12
+ import { MarkdownEditor } from "./markdown-editor";
13
+ import { CampaignImageUploader } from "./campaign-image-uploader";
14
+
15
+ interface CampaignDetail {
16
+ id: string;
17
+ campaign_id: string;
18
+ image_url?: string | null;
19
+ image_file_id?: string | null;
20
+ thumbnail_url?: string | null;
21
+ thumbnail_file_id?: string | null;
22
+ detail_content?: string | null;
23
+ terms_and_conditions?: string | null;
24
+ meta_title?: string | null;
25
+ meta_description?: string | null;
26
+ meta_keywords?: string | null;
27
+ link_url?: string | null;
28
+ link_text?: string | null;
29
+ display_order?: number;
30
+ }
31
+
32
+ interface CampaignDetailFormProps {
33
+ campaignId: string;
34
+ campaignName: string;
35
+ }
36
+
37
+ export const CampaignDetailForm = ({
38
+ campaignId,
39
+ campaignName,
40
+ }: CampaignDetailFormProps) => {
41
+ const [campaignDetail, setCampaignDetail] = useState<CampaignDetail | null>(
42
+ null,
43
+ );
44
+ const [isLoading, setIsLoading] = useState(true);
45
+ const [isSaving, setIsSaving] = useState(false);
46
+ const [showImageUploader, setShowImageUploader] = useState<
47
+ "image" | "thumbnail" | null
48
+ >(null);
49
+
50
+ const [formData, setFormData] = useState({
51
+ detail_content: "",
52
+ terms_and_conditions: "",
53
+ meta_title: "",
54
+ meta_description: "",
55
+ meta_keywords: "",
56
+ link_url: "",
57
+ link_text: "",
58
+ display_order: 0,
59
+ });
60
+
61
+ useEffect(() => {
62
+ fetchCampaignDetail();
63
+ }, [campaignId]);
64
+
65
+ const fetchCampaignDetail = async () => {
66
+ setIsLoading(true);
67
+ try {
68
+ const response = await fetch(`/admin/campaigns/${campaignId}/detail`, {
69
+ credentials: "include",
70
+ });
71
+
72
+ if (response.ok) {
73
+ const data = await response.json();
74
+ if (data.campaign_detail) {
75
+ console.log("Campaign detail loaded:", {
76
+ image_url: data.campaign_detail.image_url,
77
+ thumbnail_url: data.campaign_detail.thumbnail_url,
78
+ decoded_image: data.campaign_detail.image_url
79
+ ? decodeURIComponent(data.campaign_detail.image_url)
80
+ : null,
81
+ decoded_thumbnail: data.campaign_detail.thumbnail_url
82
+ ? decodeURIComponent(data.campaign_detail.thumbnail_url)
83
+ : null,
84
+ });
85
+ setCampaignDetail(data.campaign_detail);
86
+ setFormData({
87
+ detail_content: data.campaign_detail.detail_content || "",
88
+ terms_and_conditions:
89
+ data.campaign_detail.terms_and_conditions || "",
90
+ meta_title: data.campaign_detail.meta_title || "",
91
+ meta_description: data.campaign_detail.meta_description || "",
92
+ meta_keywords: data.campaign_detail.meta_keywords || "",
93
+ link_url: data.campaign_detail.link_url || "",
94
+ link_text: data.campaign_detail.link_text || "",
95
+ display_order: data.campaign_detail.display_order || 0,
96
+ });
97
+ }
98
+ } else if (response.status !== 404) {
99
+ console.error("Failed to fetch campaign detail");
100
+ }
101
+ } catch (error) {
102
+ console.error("Error fetching campaign detail:", error);
103
+ } finally {
104
+ setIsLoading(false);
105
+ }
106
+ };
107
+
108
+ const handleSave = async () => {
109
+ setIsSaving(true);
110
+ try {
111
+ const response = await fetch(`/admin/campaigns/${campaignId}/detail`, {
112
+ method: "POST",
113
+ headers: {
114
+ "Content-Type": "application/json",
115
+ },
116
+ body: JSON.stringify(formData),
117
+ credentials: "include",
118
+ });
119
+
120
+ if (response.ok) {
121
+ const data = await response.json();
122
+ setCampaignDetail(data.campaign_detail);
123
+ toast.success("Campaign details saved successfully");
124
+ } else {
125
+ const errorData = await response.json();
126
+ toast.error(errorData.error || "Failed to save campaign details");
127
+ }
128
+ } catch (error) {
129
+ console.error("Error saving campaign detail:", error);
130
+ toast.error("Error saving campaign details");
131
+ } finally {
132
+ setIsSaving(false);
133
+ }
134
+ };
135
+
136
+ if (isLoading) {
137
+ return (
138
+ <Container>
139
+ <div className="py-8 text-center">Loading campaign details...</div>
140
+ </Container>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <>
146
+ <Container className="space-y-6">
147
+ <div className="flex items-center justify-between">
148
+ <div>
149
+ <Heading level="h2">Campaign Details</Heading>
150
+ <p className="text-sm text-ui-fg-subtle">{campaignName}</p>
151
+ </div>
152
+ <Button onClick={handleSave} isLoading={isSaving}>
153
+ Save Details
154
+ </Button>
155
+ </div>
156
+
157
+ {/* Images Section */}
158
+ <div className="space-y-4">
159
+ <Heading level="h3">Images</Heading>
160
+ <div className="grid grid-cols-2 gap-4">
161
+ {/* Main Image */}
162
+ <div className="space-y-2">
163
+ <Label>Main Campaign Image</Label>
164
+ <div
165
+ className={clx(
166
+ "relative flex h-64 items-center justify-center overflow-hidden rounded-lg border-2 border-dashed",
167
+ campaignDetail?.image_url
168
+ ? "border-ui-border-base"
169
+ : "border-ui-border-base bg-ui-bg-subtle",
170
+ )}
171
+ >
172
+ {campaignDetail?.image_url ? (
173
+ <>
174
+ <img
175
+ src={campaignDetail.image_url}
176
+ alt="Campaign"
177
+ className="h-full w-full object-cover"
178
+ onError={(e) => {
179
+ console.error(
180
+ "Failed to load image:",
181
+ campaignDetail.image_url,
182
+ );
183
+ e.currentTarget.style.display = "none";
184
+ }}
185
+ />
186
+ <Button
187
+ variant="secondary"
188
+ size="small"
189
+ className="absolute bottom-2 right-2"
190
+ onClick={() => setShowImageUploader("image")}
191
+ >
192
+ <PencilSquare className="mr-1" />
193
+ Change
194
+ </Button>
195
+ </>
196
+ ) : (
197
+ <Button
198
+ variant="secondary"
199
+ onClick={() => setShowImageUploader("image")}
200
+ >
201
+ <PhotoSolid className="mr-2" />
202
+ Upload Image
203
+ </Button>
204
+ )}
205
+ </div>
206
+ </div>
207
+
208
+ {/* Thumbnail */}
209
+ <div className="space-y-2">
210
+ <Label>Thumbnail</Label>
211
+ <div
212
+ className={clx(
213
+ "relative flex h-64 items-center justify-center overflow-hidden rounded-lg border-2 border-dashed",
214
+ campaignDetail?.thumbnail_url
215
+ ? "border-ui-border-base"
216
+ : "border-ui-border-base bg-ui-bg-subtle",
217
+ )}
218
+ >
219
+ {campaignDetail?.thumbnail_url ? (
220
+ <>
221
+ <img
222
+ src={campaignDetail.thumbnail_url}
223
+ alt="Thumbnail"
224
+ className="h-full w-full object-cover"
225
+ onError={(e) => {
226
+ console.error(
227
+ "Failed to load thumbnail:",
228
+ campaignDetail.thumbnail_url,
229
+ );
230
+ e.currentTarget.style.display = "none";
231
+ }}
232
+ />
233
+ <Button
234
+ variant="secondary"
235
+ size="small"
236
+ className="absolute bottom-2 right-2"
237
+ onClick={() => setShowImageUploader("thumbnail")}
238
+ >
239
+ <PencilSquare className="mr-1" />
240
+ Change
241
+ </Button>
242
+ </>
243
+ ) : (
244
+ <Button
245
+ variant="secondary"
246
+ onClick={() => setShowImageUploader("thumbnail")}
247
+ >
248
+ <PhotoSolid className="mr-2" />
249
+ Upload Thumbnail
250
+ </Button>
251
+ )}
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ {/* Rich Text Content */}
258
+ <div className="space-y-4">
259
+ <MarkdownEditor
260
+ label="Campaign Detail Content"
261
+ value={formData.detail_content}
262
+ onChange={(value) =>
263
+ setFormData({ ...formData, detail_content: value })
264
+ }
265
+ placeholder="Enter campaign details in Markdown format. You can include images, links, code blocks, and more..."
266
+ helpText="Supports Markdown and code syntax"
267
+ rows={12}
268
+ showPreview={true}
269
+ />
270
+ </div>
271
+
272
+ {/* Terms and Conditions */}
273
+ <div className="space-y-4">
274
+ <MarkdownEditor
275
+ label="Terms and Conditions"
276
+ value={formData.terms_and_conditions}
277
+ onChange={(value) =>
278
+ setFormData({ ...formData, terms_and_conditions: value })
279
+ }
280
+ placeholder="Enter terms and conditions for this campaign..."
281
+ helpText="Optional - Campaign specific terms"
282
+ rows={8}
283
+ showPreview={true}
284
+ />
285
+ </div>
286
+
287
+ {/* Link Section */}
288
+ <div className="space-y-4">
289
+ <Heading level="h3">Call to Action Link</Heading>
290
+ <div className="grid grid-cols-2 gap-4">
291
+ <div className="space-y-2">
292
+ <Label htmlFor="link-url">Link URL</Label>
293
+ <Input
294
+ id="link-url"
295
+ type="url"
296
+ value={formData.link_url}
297
+ onChange={(e) =>
298
+ setFormData({ ...formData, link_url: e.target.value })
299
+ }
300
+ placeholder="https://example.com/campaign"
301
+ />
302
+ </div>
303
+ <div className="space-y-2">
304
+ <Label htmlFor="link-text">Link Text</Label>
305
+ <Input
306
+ id="link-text"
307
+ value={formData.link_text}
308
+ onChange={(e) =>
309
+ setFormData({ ...formData, link_text: e.target.value })
310
+ }
311
+ placeholder="Shop Now"
312
+ />
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ {/* SEO Section */}
318
+ <div className="space-y-4">
319
+ <Heading level="h3">SEO & Metadata</Heading>
320
+ <div className="space-y-4">
321
+ <div className="space-y-2">
322
+ <Label htmlFor="meta-title">Meta Title</Label>
323
+ <Input
324
+ id="meta-title"
325
+ value={formData.meta_title}
326
+ onChange={(e) =>
327
+ setFormData({ ...formData, meta_title: e.target.value })
328
+ }
329
+ placeholder="Campaign meta title for SEO"
330
+ />
331
+ </div>
332
+ <div className="space-y-2">
333
+ <Label htmlFor="meta-description">Meta Description</Label>
334
+ <Input
335
+ id="meta-description"
336
+ value={formData.meta_description}
337
+ onChange={(e) =>
338
+ setFormData({
339
+ ...formData,
340
+ meta_description: e.target.value,
341
+ })
342
+ }
343
+ placeholder="Campaign meta description for SEO"
344
+ />
345
+ </div>
346
+ <div className="space-y-2">
347
+ <Label htmlFor="meta-keywords">Meta Keywords</Label>
348
+ <Input
349
+ id="meta-keywords"
350
+ value={formData.meta_keywords}
351
+ onChange={(e) =>
352
+ setFormData({
353
+ ...formData,
354
+ meta_keywords: e.target.value,
355
+ })
356
+ }
357
+ placeholder="keyword1, keyword2, keyword3"
358
+ />
359
+ <p className="text-xs text-ui-fg-subtle">
360
+ Comma-separated keywords for SEO
361
+ </p>
362
+ </div>
363
+ <div className="space-y-2">
364
+ <Label htmlFor="display-order">Display Order</Label>
365
+ <Input
366
+ id="display-order"
367
+ type="number"
368
+ value={formData.display_order}
369
+ onChange={(e) =>
370
+ setFormData({
371
+ ...formData,
372
+ display_order: parseInt(e.target.value) || 0,
373
+ })
374
+ }
375
+ placeholder="0"
376
+ />
377
+ <p className="text-xs text-ui-fg-subtle">
378
+ Lower numbers appear first in listings
379
+ </p>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <div className="flex justify-end border-t pt-4">
385
+ <Button onClick={handleSave} isLoading={isSaving}>
386
+ Save Campaign Details
387
+ </Button>
388
+ </div>
389
+ </Container>
390
+
391
+ {/* Image Uploader Modal */}
392
+ {showImageUploader && (
393
+ <CampaignImageUploader
394
+ campaignId={campaignId}
395
+ imageType={showImageUploader}
396
+ currentImageUrl={
397
+ showImageUploader === "image"
398
+ ? campaignDetail?.image_url
399
+ : campaignDetail?.thumbnail_url
400
+ }
401
+ onClose={() => setShowImageUploader(null)}
402
+ onSuccess={fetchCampaignDetail}
403
+ />
404
+ )}
405
+ </>
406
+ );
407
+ };