@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.
- package/.medusa/server/src/admin/index.js +1238 -16
- package/.medusa/server/src/admin/index.mjs +1240 -18
- package/.medusa/server/src/api/admin/campaigns/[id]/detail/route.js +67 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/image/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/[id]/thumbnail/route.js +80 -0
- package/.medusa/server/src/api/admin/campaigns/sync/route.js +8 -6
- package/.medusa/server/src/api/admin/flash-sales/[id]/route.js +22 -4
- package/.medusa/server/src/api/middlewares.js +24 -1
- package/.medusa/server/src/api/store/buy-x-get-y/[id]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/products/[productId]/route.js +1 -1
- package/.medusa/server/src/api/store/buy-x-get-y/route.js +3 -1
- package/.medusa/server/src/api/store/campaigns/[id]/route.js +40 -12
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +53 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +22 -0
- package/.medusa/server/src/modules/custom-campaigns/models/campaign-detail.js +26 -0
- package/.medusa/server/src/modules/custom-campaigns/service.js +3 -1
- package/.medusa/server/src/subscribers/order-placed.js +2 -2
- package/.medusa/server/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.js +16 -4
- package/.medusa/server/src/workflows/campaign-detail/update-campaign-detail.js +55 -0
- package/.medusa/server/src/workflows/campaign-detail/upload-campaign-images.js +120 -0
- package/.medusa/server/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.js +13 -14
- package/package.json +7 -5
- package/src/admin/components/campaign-detail-form.tsx +407 -0
- package/src/admin/components/campaign-image-uploader.tsx +313 -0
- package/src/admin/components/markdown-editor.tsx +298 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +51 -14
- package/src/admin/widgets/campaign-detail-widget.tsx +299 -0
- package/src/admin/widgets/campaign-stats-widget.tsx +238 -0
- package/src/api/admin/campaigns/[id]/detail/route.ts +77 -0
- package/src/api/admin/campaigns/[id]/image/route.ts +87 -0
- package/src/api/admin/campaigns/[id]/thumbnail/route.ts +87 -0
- package/src/api/admin/campaigns/sync/route.ts +53 -28
- package/src/api/admin/flash-sales/[id]/route.ts +50 -19
- package/src/api/middlewares.ts +21 -0
- package/src/api/store/buy-x-get-y/[id]/route.ts +10 -10
- package/src/api/store/buy-x-get-y/products/[productId]/route.ts +11 -12
- package/src/api/store/buy-x-get-y/route.ts +12 -5
- package/src/api/store/campaigns/[id]/route.ts +54 -24
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +53 -0
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +19 -0
- package/src/modules/custom-campaigns/models/campaign-detail.ts +25 -0
- package/src/modules/custom-campaigns/service.ts +2 -0
- package/src/subscribers/order-placed.ts +0 -2
- package/src/types/index.d.ts +46 -0
- package/src/workflows/buy-x-get-y/applyBuyXGetYToCartWorkflow.ts +41 -18
- package/src/workflows/campaign-detail/update-campaign-detail.ts +85 -0
- package/src/workflows/campaign-detail/upload-campaign-images.ts +163 -0
- package/src/workflows/custom-campaign/createBuyXGetYCampaignWorkflow.ts +23 -22
- package/.medusa/server/src/api/admin/campaigns/fix-dates/route.js +0 -103
- package/.medusa/server/src/api/admin/force-fix/route.js +0 -176
- package/.medusa/server/src/api/admin/test-campaign/route.js +0 -132
- package/src/api/admin/campaigns/fix-dates/route.ts +0 -107
- package/src/api/admin/force-fix/route.ts +0 -184
- 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
|
+
"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
|
+
};
|