@lodashventure/medusa-campaign 1.3.12 → 1.3.13

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
@@ -0,0 +1,313 @@
1
+ import { useState, useCallback } from "react";
2
+ import {
3
+ PhotoSolid,
4
+ Trash,
5
+ CloudArrowUp,
6
+ X,
7
+ } from "@medusajs/icons";
8
+ import { Button, Heading, Text, Alert, clx } from "@medusajs/ui";
9
+
10
+ interface CampaignImageUploaderProps {
11
+ campaignId: string;
12
+ imageType: "image" | "thumbnail";
13
+ currentImageUrl?: string | null;
14
+ onClose: () => void;
15
+ onSuccess?: () => void;
16
+ }
17
+
18
+ export const CampaignImageUploader = ({
19
+ campaignId,
20
+ imageType,
21
+ currentImageUrl,
22
+ onClose,
23
+ onSuccess,
24
+ }: CampaignImageUploaderProps) => {
25
+ const [displayImage, setDisplayImage] = useState(currentImageUrl || null);
26
+ const [isUploading, setIsUploading] = useState(false);
27
+ const [isDragging, setIsDragging] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
30
+
31
+ const isThumbnail = imageType === "thumbnail";
32
+ const maxSize = 5; // 5MB for images
33
+ const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
34
+
35
+ const formatFileTypes = () => {
36
+ return "JPEG, PNG, GIF, or WebP";
37
+ };
38
+
39
+ const validateFile = (file: File): boolean => {
40
+ if (!allowedTypes.includes(file.type)) {
41
+ setError(`Please upload a valid image file (${formatFileTypes()})`);
42
+ return false;
43
+ }
44
+ if (file.size > maxSize * 1024 * 1024) {
45
+ setError(`File size must be less than ${maxSize}MB`);
46
+ return false;
47
+ }
48
+ return true;
49
+ };
50
+
51
+ const handleFileUpload = async (file: File) => {
52
+ if (!validateFile(file)) return;
53
+
54
+ setError(null);
55
+
56
+ // Show preview immediately
57
+ const reader = new FileReader();
58
+ reader.onloadend = () => {
59
+ setPreviewUrl(reader.result as string);
60
+ };
61
+ reader.readAsDataURL(file);
62
+
63
+ // Upload to server
64
+ setIsUploading(true);
65
+ const formData = new FormData();
66
+ formData.append("file", file);
67
+
68
+ try {
69
+ const response = await fetch(
70
+ `/admin/campaigns/${campaignId}/${imageType}`,
71
+ {
72
+ method: "POST",
73
+ body: formData,
74
+ credentials: "include",
75
+ }
76
+ );
77
+
78
+ if (response.ok) {
79
+ const result = await response.json();
80
+ const newImageUrl =
81
+ imageType === "image"
82
+ ? result.campaign_detail?.image_url
83
+ : result.campaign_detail?.thumbnail_url;
84
+ setDisplayImage(newImageUrl);
85
+ setPreviewUrl(null);
86
+
87
+ // Call onSuccess callback if provided
88
+ onSuccess?.();
89
+
90
+ // Show success for a moment before closing
91
+ setTimeout(() => {
92
+ onClose();
93
+ }, 1000);
94
+ } else {
95
+ const errorData = await response.json();
96
+ setError(errorData.error || `Failed to upload ${imageType}`);
97
+ setPreviewUrl(null);
98
+ }
99
+ } catch (err) {
100
+ setError(`Error uploading ${imageType}`);
101
+ setPreviewUrl(null);
102
+ console.error(`Error uploading ${imageType}:`, err);
103
+ } finally {
104
+ setIsUploading(false);
105
+ }
106
+ };
107
+
108
+ const handleDelete = async () => {
109
+ if (!confirm(`Are you sure you want to delete the ${imageType}?`)) return;
110
+
111
+ setIsUploading(true);
112
+ setError(null);
113
+
114
+ try {
115
+ const response = await fetch(
116
+ `/admin/campaigns/${campaignId}/${imageType}`,
117
+ {
118
+ method: "DELETE",
119
+ credentials: "include",
120
+ }
121
+ );
122
+
123
+ if (response.ok) {
124
+ setDisplayImage(null);
125
+ setPreviewUrl(null);
126
+
127
+ // Call onSuccess callback if provided
128
+ onSuccess?.();
129
+
130
+ setTimeout(() => {
131
+ onClose();
132
+ }, 500);
133
+ } else {
134
+ const errorData = await response.json();
135
+ setError(errorData.error || `Failed to delete ${imageType}`);
136
+ }
137
+ } catch (err) {
138
+ setError(`Error deleting ${imageType}`);
139
+ console.error(`Error deleting ${imageType}:`, err);
140
+ } finally {
141
+ setIsUploading(false);
142
+ }
143
+ };
144
+
145
+ const handleDragEnter = useCallback((e: React.DragEvent) => {
146
+ e.preventDefault();
147
+ e.stopPropagation();
148
+ setIsDragging(true);
149
+ }, []);
150
+
151
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+ setIsDragging(false);
155
+ }, []);
156
+
157
+ const handleDragOver = useCallback((e: React.DragEvent) => {
158
+ e.preventDefault();
159
+ e.stopPropagation();
160
+ }, []);
161
+
162
+ const handleDrop = useCallback((e: React.DragEvent) => {
163
+ e.preventDefault();
164
+ e.stopPropagation();
165
+ setIsDragging(false);
166
+
167
+ const file = e.dataTransfer.files?.[0];
168
+ if (file) {
169
+ handleFileUpload(file);
170
+ }
171
+ }, []);
172
+
173
+ const imageToDisplay = previewUrl || displayImage;
174
+
175
+ return (
176
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
177
+ <div className="w-full max-w-2xl rounded-lg bg-ui-bg-base p-6">
178
+ <div className="mb-6 flex items-center justify-between">
179
+ <div>
180
+ <Heading level="h2">
181
+ Upload Campaign {isThumbnail ? "Thumbnail" : "Image"}
182
+ </Heading>
183
+ <Text className="text-ui-fg-subtle">
184
+ {isThumbnail
185
+ ? "Thumbnail for campaign listing"
186
+ : "Main campaign image"}
187
+ </Text>
188
+ </div>
189
+ <Button variant="secondary" size="small" onClick={onClose}>
190
+ <X />
191
+ </Button>
192
+ </div>
193
+
194
+ {error && (
195
+ <Alert variant="error" dismissible className="mb-4">
196
+ {error}
197
+ </Alert>
198
+ )}
199
+
200
+ <div className="space-y-4">
201
+ {imageToDisplay ? (
202
+ <div className="space-y-4">
203
+ <div className="relative overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-subtle">
204
+ <img
205
+ src={imageToDisplay}
206
+ alt={`Campaign ${imageType}`}
207
+ className={clx(
208
+ "w-full object-contain",
209
+ isThumbnail ? "h-32" : "h-64"
210
+ )}
211
+ />
212
+ {isUploading && (
213
+ <div className="absolute inset-0 flex items-center justify-center bg-black/50">
214
+ <div className="text-white">Uploading...</div>
215
+ </div>
216
+ )}
217
+ </div>
218
+ <div className="flex gap-2">
219
+ <Button
220
+ variant="secondary"
221
+ disabled={isUploading}
222
+ onClick={() =>
223
+ document.getElementById(`${imageType}-file-input`)?.click()
224
+ }
225
+ >
226
+ <CloudArrowUp className="mr-2" />
227
+ Replace
228
+ </Button>
229
+ <Button
230
+ variant="danger"
231
+ disabled={isUploading}
232
+ onClick={handleDelete}
233
+ >
234
+ <Trash className="mr-2" />
235
+ Delete
236
+ </Button>
237
+ </div>
238
+ </div>
239
+ ) : (
240
+ <div
241
+ className={clx(
242
+ "flex flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors",
243
+ isThumbnail ? "h-48" : "h-64",
244
+ isDragging
245
+ ? "border-ui-border-interactive bg-ui-bg-highlight"
246
+ : "border-ui-border-base bg-ui-bg-subtle",
247
+ !isUploading && "cursor-pointer"
248
+ )}
249
+ onDragEnter={handleDragEnter}
250
+ onDragLeave={handleDragLeave}
251
+ onDragOver={handleDragOver}
252
+ onDrop={handleDrop}
253
+ onClick={() =>
254
+ !isUploading &&
255
+ document.getElementById(`${imageType}-file-input`)?.click()
256
+ }
257
+ >
258
+ <PhotoSolid className="mb-4 h-12 w-12 text-ui-fg-subtle" />
259
+ <Text className="mb-2 text-lg font-medium">
260
+ {isDragging
261
+ ? `Drop ${imageType} here`
262
+ : `Upload ${isThumbnail ? "Thumbnail" : "Image"}`}
263
+ </Text>
264
+ <Text className="mb-4 text-center text-ui-fg-subtle">
265
+ Drag and drop an image here, or click to select
266
+ </Text>
267
+ <Text className="text-sm text-ui-fg-subtle">
268
+ {formatFileTypes()} • Max {maxSize}MB
269
+ </Text>
270
+ {isThumbnail && (
271
+ <Text className="mt-2 text-xs text-ui-fg-subtle">
272
+ Recommended: 16:9 aspect ratio, minimum 400x225px
273
+ </Text>
274
+ )}
275
+ {!isThumbnail && (
276
+ <Text className="mt-2 text-xs text-ui-fg-subtle">
277
+ Recommended: High resolution, minimum 1200x600px
278
+ </Text>
279
+ )}
280
+ {isUploading && (
281
+ <div className="mt-4">
282
+ <Text>Uploading...</Text>
283
+ </div>
284
+ )}
285
+ </div>
286
+ )}
287
+ <input
288
+ id={`${imageType}-file-input`}
289
+ type="file"
290
+ accept={allowedTypes.join(",")}
291
+ onChange={(e) => {
292
+ const file = e.target.files?.[0];
293
+ if (file) handleFileUpload(file);
294
+ e.target.value = ""; // Reset input
295
+ }}
296
+ className="hidden"
297
+ />
298
+ </div>
299
+
300
+ <div className="mt-6 flex items-center justify-between border-t pt-4">
301
+ <Text className="text-sm text-ui-fg-subtle">
302
+ {isThumbnail
303
+ ? "Thumbnail will be displayed in campaign listings"
304
+ : "Main image for the campaign detail page"}
305
+ </Text>
306
+ <Button variant="secondary" onClick={onClose}>
307
+ Close
308
+ </Button>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ );
313
+ };
@@ -0,0 +1,298 @@
1
+ import { useState } from "react";
2
+ import { Label, Textarea, Button, Tabs, clx } from "@medusajs/ui";
3
+ import { CommandLine, InformationCircle } from "@medusajs/icons";
4
+
5
+ interface MarkdownEditorProps {
6
+ label: string;
7
+ value: string;
8
+ onChange: (value: string) => void;
9
+ placeholder?: string;
10
+ helpText?: string;
11
+ rows?: number;
12
+ showPreview?: boolean;
13
+ }
14
+
15
+ export const MarkdownEditor = ({
16
+ label,
17
+ value,
18
+ onChange,
19
+ placeholder,
20
+ helpText,
21
+ rows = 10,
22
+ showPreview = true,
23
+ }: MarkdownEditorProps) => {
24
+ const [activeTab, setActiveTab] = useState<"write" | "preview">("write");
25
+
26
+ const insertMarkdown = (before: string, after: string = "") => {
27
+ const textarea = document.getElementById(
28
+ `markdown-${label}`,
29
+ ) as HTMLTextAreaElement;
30
+ if (!textarea) return;
31
+
32
+ const start = textarea.selectionStart;
33
+ const end = textarea.selectionEnd;
34
+ const selectedText = value.substring(start, end);
35
+ const newText =
36
+ value.substring(0, start) +
37
+ before +
38
+ selectedText +
39
+ after +
40
+ value.substring(end);
41
+
42
+ onChange(newText);
43
+
44
+ // Set cursor position after inserted text
45
+ setTimeout(() => {
46
+ textarea.focus();
47
+ textarea.setSelectionRange(
48
+ start + before.length,
49
+ start + before.length + selectedText.length,
50
+ );
51
+ }, 0);
52
+ };
53
+
54
+ const renderMarkdownPreview = (markdown: string) => {
55
+ // Simple markdown to HTML conversion for preview
56
+ let html = markdown;
57
+
58
+ // Headers
59
+ html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
60
+ html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
61
+ html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
62
+
63
+ // Bold
64
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
65
+ html = html.replace(/__(.+?)__/g, "<strong>$1</strong>");
66
+
67
+ // Italic
68
+ html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
69
+ html = html.replace(/_(.+?)_/g, "<em>$1</em>");
70
+
71
+ // Code blocks
72
+ html = html.replace(
73
+ /```(\w+)?\n([\s\S]+?)```/g,
74
+ '<pre class="bg-ui-bg-subtle p-4 rounded-lg overflow-x-auto"><code class="language-$1">$2</code></pre>',
75
+ );
76
+
77
+ // Inline code
78
+ html = html.replace(
79
+ /`([^`]+)`/g,
80
+ '<code class="bg-ui-bg-subtle px-1 py-0.5 rounded text-sm">$1</code>',
81
+ );
82
+
83
+ // Links
84
+ html = html.replace(
85
+ /\[([^\]]+)\]\(([^)]+)\)/g,
86
+ '<a href="$2" class="text-ui-fg-interactive underline">$1</a>',
87
+ );
88
+
89
+ // Images
90
+ html = html.replace(
91
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
92
+ '<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg my-2" />',
93
+ );
94
+
95
+ // Lists
96
+ html = html.replace(/^\* (.+)$/gim, "<li>$1</li>");
97
+ html = html.replace(/^\- (.+)$/gim, "<li>$1</li>");
98
+ html = html.replace(
99
+ /(<li>.*<\/li>)/s,
100
+ "<ul class='list-disc ml-6'>$1</ul>",
101
+ );
102
+
103
+ // Paragraphs
104
+ html = html.replace(/\n\n/g, "</p><p>");
105
+ html = `<p>${html}</p>`;
106
+
107
+ return html;
108
+ };
109
+
110
+ return (
111
+ <div className="space-y-2">
112
+ <div className="flex items-center justify-between">
113
+ <Label htmlFor={`markdown-${label}`} className="font-medium">
114
+ {label}
115
+ </Label>
116
+ {helpText && (
117
+ <div className="flex items-center gap-1 text-xs text-ui-fg-subtle">
118
+ <InformationCircle className="h-4 w-4" />
119
+ <span>{helpText}</span>
120
+ </div>
121
+ )}
122
+ </div>
123
+
124
+ {showPreview ? (
125
+ <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as any)}>
126
+ <Tabs.List>
127
+ <Tabs.Trigger value="write">Write</Tabs.Trigger>
128
+ <Tabs.Trigger value="preview">Preview</Tabs.Trigger>
129
+ </Tabs.List>
130
+
131
+ <Tabs.Content value="write" className="mt-4">
132
+ <div className="space-y-2">
133
+ <div className="flex flex-wrap gap-2 border-b pb-2">
134
+ <Button
135
+ type="button"
136
+ variant="secondary"
137
+ size="small"
138
+ onClick={() => insertMarkdown("**", "**")}
139
+ title="Bold"
140
+ >
141
+ <strong>B</strong>
142
+ </Button>
143
+ <Button
144
+ type="button"
145
+ variant="secondary"
146
+ size="small"
147
+ onClick={() => insertMarkdown("*", "*")}
148
+ title="Italic"
149
+ >
150
+ <em>I</em>
151
+ </Button>
152
+ <Button
153
+ type="button"
154
+ variant="secondary"
155
+ size="small"
156
+ onClick={() => insertMarkdown("`", "`")}
157
+ title="Inline Code"
158
+ >
159
+ <CommandLine className="h-4 w-4" />
160
+ </Button>
161
+ <Button
162
+ type="button"
163
+ variant="secondary"
164
+ size="small"
165
+ onClick={() => insertMarkdown("## ")}
166
+ title="Heading"
167
+ >
168
+ H2
169
+ </Button>
170
+ <Button
171
+ type="button"
172
+ variant="secondary"
173
+ size="small"
174
+ onClick={() => insertMarkdown("### ")}
175
+ title="Heading"
176
+ >
177
+ H3
178
+ </Button>
179
+ <Button
180
+ type="button"
181
+ variant="secondary"
182
+ size="small"
183
+ onClick={() => insertMarkdown("[", "](url)")}
184
+ title="Link"
185
+ >
186
+ Link
187
+ </Button>
188
+ <Button
189
+ type="button"
190
+ variant="secondary"
191
+ size="small"
192
+ onClick={() => insertMarkdown("![alt](", ")")}
193
+ title="Image"
194
+ >
195
+ Image
196
+ </Button>
197
+ <Button
198
+ type="button"
199
+ variant="secondary"
200
+ size="small"
201
+ onClick={() => insertMarkdown("```\n", "\n```")}
202
+ title="Code Block"
203
+ >
204
+ Code
205
+ </Button>
206
+ <Button
207
+ type="button"
208
+ variant="secondary"
209
+ size="small"
210
+ onClick={() => insertMarkdown("- ")}
211
+ title="List"
212
+ >
213
+ List
214
+ </Button>
215
+ </div>
216
+ <Textarea
217
+ id={`markdown-${label}`}
218
+ value={value}
219
+ onChange={(e) => onChange(e.target.value)}
220
+ placeholder={placeholder}
221
+ rows={rows}
222
+ className="font-mono text-sm"
223
+ />
224
+ </div>
225
+ </Tabs.Content>
226
+
227
+ <Tabs.Content value="preview" className="mt-4">
228
+ <div
229
+ className={clx(
230
+ "min-h-[200px] rounded-lg border border-ui-border-base bg-ui-bg-subtle p-4",
231
+ "prose prose-sm max-w-none",
232
+ )}
233
+ dangerouslySetInnerHTML={{
234
+ __html: value
235
+ ? renderMarkdownPreview(value)
236
+ : '<p class="text-ui-fg-subtle">Nothing to preview</p>',
237
+ }}
238
+ />
239
+ </Tabs.Content>
240
+ </Tabs>
241
+ ) : (
242
+ <div className="space-y-2">
243
+ <div className="flex flex-wrap gap-2 border-b pb-2">
244
+ <Button
245
+ type="button"
246
+ variant="secondary"
247
+ size="small"
248
+ onClick={() => insertMarkdown("**", "**")}
249
+ title="Bold"
250
+ >
251
+ <strong>B</strong>
252
+ </Button>
253
+ <Button
254
+ type="button"
255
+ variant="secondary"
256
+ size="small"
257
+ onClick={() => insertMarkdown("*", "*")}
258
+ title="Italic"
259
+ >
260
+ <em>I</em>
261
+ </Button>
262
+ <Button
263
+ type="button"
264
+ variant="secondary"
265
+ size="small"
266
+ onClick={() => insertMarkdown("`", "`")}
267
+ title="Inline Code"
268
+ >
269
+ <CommandLine className="h-4 w-4" />
270
+ </Button>
271
+ <Button
272
+ type="button"
273
+ variant="secondary"
274
+ size="small"
275
+ onClick={() => insertMarkdown("```\n", "\n```")}
276
+ title="Code Block"
277
+ >
278
+ Code
279
+ </Button>
280
+ </div>
281
+ <Textarea
282
+ id={`markdown-${label}`}
283
+ value={value}
284
+ onChange={(e) => onChange(e.target.value)}
285
+ placeholder={placeholder}
286
+ rows={rows}
287
+ className="font-mono text-sm"
288
+ />
289
+ </div>
290
+ )}
291
+
292
+ <div className="text-xs text-ui-fg-subtle">
293
+ Supports Markdown formatting: **bold**, *italic*, `code`, ```code
294
+ blocks```, [links](url), ![images](url), and lists
295
+ </div>
296
+ </div>
297
+ );
298
+ };
@@ -1,6 +1,6 @@
1
1
  import { defineRouteConfig } from "@medusajs/admin-sdk";
2
2
  import { Sparkles } from "@medusajs/icons";
3
- import { Button, Container, toast } from "@medusajs/ui";
3
+ import { Button, Container, toast, Tabs } from "@medusajs/ui";
4
4
  import { useQueryClient } from "@tanstack/react-query";
5
5
  import axios from "axios";
6
6
  import { FC, useState } from "react";
@@ -11,6 +11,9 @@ import {
11
11
  FlashSaleForm,
12
12
  } from "../../../components/FlashSaleForm";
13
13
  import { useFlashSaleById } from "../../../hooks/useFlashSaleById";
14
+ // Note: In production, adjust this import path based on your build setup
15
+ // The components should be accessible from the admin-store build
16
+ import { CampaignDetailForm } from "../../../components/campaign-detail-form";
14
17
 
15
18
  const FlashSaleDetail: FC = () => {
16
19
  const { id } = useParams<{ id: string }>();
@@ -33,7 +36,7 @@ const FlashSaleDetail: FC = () => {
33
36
  exact: false,
34
37
  predicate(query) {
35
38
  return ["flash-sales", "flash-sale"].includes(
36
- query.queryKey[0] as string
39
+ query.queryKey[0] as string,
37
40
  );
38
41
  },
39
42
  });
@@ -82,18 +85,52 @@ const FlashSaleDetail: FC = () => {
82
85
  };
83
86
 
84
87
  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
- </>
88
+ <Container>
89
+ <div className="mb-6 flex items-center justify-between">
90
+ <div>
91
+ <h1 className="text-2xl font-semibold mb-1">{data.name}</h1>
92
+ <p className="text-ui-fg-subtle">
93
+ Manage campaign settings and content
94
+ </p>
95
+ </div>
96
+ <div className="flex gap-2">
97
+ <Button variant="secondary" onClick={() => navigate("/flash-sales")}>
98
+ Back to Campaigns
99
+ </Button>
100
+ <Button onClick={() => setIsEditing((prev) => !prev)}>
101
+ {isEditing ? "View Mode" : "Edit Mode"}
102
+ </Button>
103
+ </div>
104
+ </div>
105
+
106
+ <Tabs defaultValue="settings">
107
+ <Tabs.List>
108
+ <Tabs.Trigger value="settings">Campaign Settings</Tabs.Trigger>
109
+ <Tabs.Trigger value="details">Campaign Details</Tabs.Trigger>
110
+ </Tabs.List>
111
+
112
+ <Tabs.Content value="settings" className="mt-6">
113
+ <FlashSaleForm
114
+ initialData={campaignData}
115
+ initialProducts={
116
+ new Map(
117
+ data.products.map((product) => [product.product.id, product]),
118
+ )
119
+ }
120
+ onSubmit={handleSubmit}
121
+ onCancel={() => navigate("/flash-sales")}
122
+ disabled={!isEditing}
123
+ />
124
+ </Tabs.Content>
125
+
126
+ <Tabs.Content value="details" className="mt-6">
127
+ <CampaignDetailForm
128
+ campaignId={id || ""}
129
+ campaignName={data.name || ""}
130
+ />
131
+ </Tabs.Content>
132
+ </Tabs>
133
+ </Container>
97
134
  );
98
135
  };
99
136