@hectasquare/medusa-plugin-category-tree 1.0.2 → 1.0.6

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.
@@ -1,7 +1,17 @@
1
1
  import React, { useState, useEffect } from "react"
2
- import { Button, Drawer, Input, Label, Switch, Text, Textarea, toast } from "@medusajs/ui"
3
- import { Trash } from "@medusajs/icons"
2
+ import {
3
+ Button,
4
+ Drawer,
5
+ Input,
6
+ Label,
7
+ Switch,
8
+ Text,
9
+ Textarea,
10
+ toast,
11
+ } from "@medusajs/ui"
12
+ import { Trash, Plus, ArrowUpRightMicro, ArrowsPointingOut } from "@medusajs/icons"
4
13
  import { CategoryTreeNode, SalesChannelInfo } from "./types"
14
+ import { CategoryProductsManager } from "./category-products-manager"
5
15
  import { sdk } from "../../lib/sdk"
6
16
 
7
17
  export interface EditCategoryDrawerProps {
@@ -13,6 +23,23 @@ export interface EditCategoryDrawerProps {
13
23
  onDeleteSuccess: (deletedId: string) => void
14
24
  }
15
25
 
26
+ function extractYouTubeVideoId(url: string): string | null {
27
+ if (!url || typeof url !== "string") return null
28
+ const trimmed = url.trim()
29
+ const patterns = [
30
+ /(?:youtube\.com\/shorts\/)([^?&\s]+)/i,
31
+ /(?:youtube\.com\/watch\?.*v=)([^?&\s]+)/i,
32
+ /(?:youtu\.be\/)([^?&\s]+)/i,
33
+ /(?:youtube\.com\/embed\/)([^?&\s]+)/i,
34
+ ]
35
+ for (const pattern of patterns) {
36
+ const match = trimmed.match(pattern)
37
+ if (match && match[1]) return match[1]
38
+ }
39
+ if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed)) return trimmed
40
+ return null
41
+ }
42
+
16
43
  export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
17
44
  open,
18
45
  onOpenChange,
@@ -21,6 +48,8 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
21
48
  onSuccess,
22
49
  onDeleteSuccess,
23
50
  }) => {
51
+ const [activeTab, setActiveTab] = useState<"general" | "products" | "channels" | "youtube">("general")
52
+
24
53
  const [name, setName] = useState("")
25
54
  const [handle, setHandle] = useState("")
26
55
  const [description, setDescription] = useState("")
@@ -29,6 +58,11 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
29
58
  const [isActive, setIsActive] = useState(true)
30
59
  const [isInternal, setIsInternal] = useState(false)
31
60
  const [selectedChannels, setSelectedChannels] = useState<string[]>([])
61
+ const [youtubeUrls, setYoutubeUrls] = useState<string[]>([])
62
+ const [newYoutubeInput, setNewYoutubeInput] = useState("")
63
+
64
+ const [currentMetadata, setCurrentMetadata] = useState<Record<string, any>>({})
65
+ const [isFullScreen, setIsFullScreen] = useState(false)
32
66
  const [isSubmitting, setIsSubmitting] = useState(false)
33
67
  const [isDeleting, setIsDeleting] = useState(false)
34
68
 
@@ -42,6 +76,18 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
42
76
  setIsActive(Boolean(category.is_active))
43
77
  setIsInternal(Boolean(category.is_internal))
44
78
  setSelectedChannels(category.sales_channels || salesChannels.map((s) => s.id))
79
+ setCurrentMetadata((category.metadata as Record<string, any>) || {})
80
+
81
+ // Parse YouTube URLs from metadata
82
+ const meta = category.metadata as Record<string, any> | null
83
+ const urls: string[] = []
84
+ if (meta?.youtube_urls && Array.isArray(meta.youtube_urls)) {
85
+ meta.youtube_urls.forEach((u) => typeof u === "string" && u.trim() && urls.push(u.trim()))
86
+ } else if (meta?.youtube_url && typeof meta.youtube_url === "string") {
87
+ urls.push(meta.youtube_url.trim())
88
+ }
89
+ setYoutubeUrls(urls)
90
+ setActiveTab("general")
45
91
  }
46
92
  }, [category, salesChannels])
47
93
 
@@ -55,6 +101,28 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
55
101
  )
56
102
  }
57
103
 
104
+ const handleAddYoutube = () => {
105
+ const raw = newYoutubeInput.trim()
106
+ if (!raw) return
107
+ const id = extractYouTubeVideoId(raw)
108
+ if (!id) {
109
+ toast.error("Please enter a valid YouTube URL or video ID")
110
+ return
111
+ }
112
+ const cleanUrl = `https://www.youtube.com/watch?v=${id}`
113
+ if (youtubeUrls.includes(cleanUrl)) {
114
+ toast.error("Video already added")
115
+ return
116
+ }
117
+ setYoutubeUrls((prev) => [...prev, cleanUrl])
118
+ setNewYoutubeInput("")
119
+ toast.success("YouTube video added")
120
+ }
121
+
122
+ const handleRemoveYoutube = (urlToRemove: string) => {
123
+ setYoutubeUrls((prev) => prev.filter((u) => u !== urlToRemove))
124
+ }
125
+
58
126
  const handleSubmit = async (e: React.FormEvent) => {
59
127
  e.preventDefault()
60
128
  if (!name.trim()) {
@@ -65,8 +133,9 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
65
133
  setIsSubmitting(true)
66
134
  try {
67
135
  const updatedMetadata: Record<string, any> = {
68
- ...(category.metadata || {}),
136
+ ...(currentMetadata || {}),
69
137
  sales_channels: selectedChannels,
138
+ youtube_urls: youtubeUrls,
70
139
  }
71
140
  if (imageUrl.trim()) {
72
141
  updatedMetadata.thumbnail = imageUrl.trim()
@@ -126,185 +195,377 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
126
195
 
127
196
  return (
128
197
  <Drawer open={open} onOpenChange={onOpenChange}>
129
- <Drawer.Content className="max-w-md">
198
+ <Drawer.Content
199
+ className={`transition-all duration-300 w-full z-50 ${
200
+ isFullScreen
201
+ ? "!max-w-full !w-screen !h-screen !inset-0 !rounded-none"
202
+ : "max-w-5xl xl:max-w-6xl w-[92vw]"
203
+ }`}
204
+ >
130
205
  <form onSubmit={handleSubmit} className="flex flex-col h-full">
131
- <Drawer.Header>
132
- <Drawer.Title>Edit Category: {category.name}</Drawer.Title>
133
- <Drawer.Description>
134
- Tier {category.tier} • ID: {category.id}
135
- </Drawer.Description>
136
- </Drawer.Header>
206
+ <Drawer.Header className="border-b border-ui-border-base pb-3">
207
+ <div className="flex items-center justify-between w-full">
208
+ <div>
209
+ <Drawer.Title className="text-base font-semibold text-ui-fg-base">
210
+ {category.name}
211
+ </Drawer.Title>
212
+ <Drawer.Description className="text-xs text-ui-fg-muted mt-0.5">
213
+ Tier {category.tier} • ID: {category.id}
214
+ </Drawer.Description>
215
+ </div>
137
216
 
138
- <Drawer.Body className="flex flex-col gap-4 overflow-y-auto px-6 py-4 flex-1">
139
- <div className="flex flex-col gap-1.5">
140
- <Label htmlFor="edit-name" className="text-xs font-semibold">
141
- Category Name *
142
- </Label>
143
- <Input
144
- id="edit-name"
145
- size="small"
146
- value={name}
147
- onChange={(e) => setName(e.target.value)}
148
- />
217
+ <div className="flex items-center gap-2">
218
+ {/* Full Screen / Wide Mode Toggle */}
219
+ <button
220
+ type="button"
221
+ onClick={() => setIsFullScreen((prev) => !prev)}
222
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors"
223
+ title={isFullScreen ? "Exit Fullscreen" : "Expand to Full Page Modal"}
224
+ >
225
+ <ArrowsPointingOut className="w-3.5 h-3.5" />
226
+ <span>{isFullScreen ? "Exit Fullscreen" : "Full Page"}</span>
227
+ </button>
228
+
229
+ {/* Direct Open Legacy Page Link */}
230
+ <button
231
+ type="button"
232
+ onClick={() => window.open(`/app/categories/${category.id}`, "_blank")}
233
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors"
234
+ title="Open full category details page in a new tab"
235
+ >
236
+ <span>Open Full Page</span>
237
+ <ArrowUpRightMicro className="w-3.5 h-3.5" />
238
+ </button>
239
+ </div>
149
240
  </div>
150
241
 
151
- <div className="flex flex-col gap-1.5">
152
- <Label htmlFor="edit-handle" className="text-xs font-semibold">
153
- Handle (URL slug)
154
- </Label>
155
- <Input
156
- id="edit-handle"
157
- size="small"
158
- value={handle}
159
- onChange={(e) => setHandle(e.target.value)}
160
- />
242
+ {/* Tab Navigation Bar */}
243
+ <div className="flex items-center gap-1 mt-3 border-b border-ui-border-base/50 -mb-3 pb-1">
244
+ <button
245
+ type="button"
246
+ onClick={() => setActiveTab("general")}
247
+ className={`px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${
248
+ activeTab === "general"
249
+ ? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
250
+ : "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
251
+ }`}
252
+ >
253
+ General & Ranking
254
+ </button>
255
+
256
+ <button
257
+ type="button"
258
+ onClick={() => setActiveTab("products")}
259
+ className={`px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${
260
+ activeTab === "products"
261
+ ? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
262
+ : "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
263
+ }`}
264
+ >
265
+ Products Management
266
+ </button>
267
+
268
+ <button
269
+ type="button"
270
+ onClick={() => setActiveTab("channels")}
271
+ className={`px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${
272
+ activeTab === "channels"
273
+ ? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
274
+ : "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
275
+ }`}
276
+ >
277
+ Storefronts & Media
278
+ </button>
279
+
280
+ <button
281
+ type="button"
282
+ onClick={() => setActiveTab("youtube")}
283
+ className={`px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${
284
+ activeTab === "youtube"
285
+ ? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
286
+ : "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
287
+ }`}
288
+ >
289
+ YouTube Videos ({youtubeUrls.length})
290
+ </button>
161
291
  </div>
292
+ </Drawer.Header>
162
293
 
163
- <div className="flex flex-col gap-1.5 p-3 rounded-lg bg-ui-bg-subtle border border-ui-border-base">
164
- <div className="flex items-center justify-between">
165
- <Label htmlFor="edit-rank" className="text-xs font-semibold">
166
- Storefront Ranking (Sort Order)
167
- </Label>
168
- <span className="text-[10px] font-mono text-ui-fg-muted">
169
- Controls Storefront 2 & Monfil
170
- </span>
171
- </div>
172
- <div className="flex items-center gap-2">
173
- <Input
174
- id="edit-rank"
175
- type="number"
176
- size="small"
177
- value={rank}
178
- onChange={(e) => setRank(Number(e.target.value))}
179
- className="w-24"
180
- />
181
- <div className="flex items-center gap-1">
182
- <button
183
- type="button"
184
- onClick={() => setRank((r) => Math.max(0, r - 1))}
185
- className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
186
- title="Move Earlier in Storefront"
187
- >
188
- ↑ Earlier
189
- </button>
190
- <button
191
- type="button"
192
- onClick={() => setRank((r) => r + 1)}
193
- className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
194
- title="Move Later in Storefront"
195
- >
196
- ↓ Later
197
- </button>
198
- <button
199
- type="button"
200
- onClick={() => setRank(0)}
201
- className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
202
- title="Set as First Category"
203
- >
204
- Top (#0)
205
- </button>
294
+ <Drawer.Body className="flex flex-col gap-4 overflow-y-auto px-6 py-4 flex-1">
295
+ {/* TAB 1: GENERAL & RANKING */}
296
+ {activeTab === "general" && (
297
+ <div className="flex flex-col gap-4">
298
+ <div className="flex flex-col gap-1.5">
299
+ <Label htmlFor="edit-name" className="text-xs font-semibold">
300
+ Category Name *
301
+ </Label>
302
+ <Input
303
+ id="edit-name"
304
+ size="small"
305
+ value={name}
306
+ onChange={(e) => setName(e.target.value)}
307
+ />
206
308
  </div>
207
- </div>
208
- <p className="text-[11px] text-ui-fg-muted leading-relaxed">
209
- Categories are sorted by rank ASC (0, 1, 2...). Lower numbers appear first in your storefront navigation.
210
- </p>
211
- </div>
212
309
 
213
- <div className="flex flex-col gap-1.5">
214
- <Label htmlFor="edit-image" className="text-xs font-semibold">
215
- Category Image URL
216
- </Label>
217
- <Input
218
- id="edit-image"
219
- size="small"
220
- value={imageUrl}
221
- onChange={(e) => setImageUrl(e.target.value)}
222
- />
223
- {imageUrl && (
224
- <div className="mt-1 w-16 h-16 rounded-lg overflow-hidden border border-ui-border-base bg-ui-bg-subtle">
225
- <img
226
- src={imageUrl}
227
- alt="Category"
228
- className="w-full h-full object-cover"
229
- onError={(e) => {
230
- ;(e.target as any).style.display = "none"
231
- }}
310
+ <div className="flex flex-col gap-1.5">
311
+ <Label htmlFor="edit-handle" className="text-xs font-semibold">
312
+ Handle (URL slug)
313
+ </Label>
314
+ <Input
315
+ id="edit-handle"
316
+ size="small"
317
+ value={handle}
318
+ onChange={(e) => setHandle(e.target.value)}
232
319
  />
233
320
  </div>
234
- )}
235
- </div>
236
321
 
237
- <div className="flex flex-col gap-1.5">
238
- <Label htmlFor="edit-desc" className="text-xs font-semibold">
239
- Description
240
- </Label>
241
- <Textarea
242
- id="edit-desc"
243
- value={description}
244
- onChange={(e) => setDescription(e.target.value)}
245
- rows={2}
246
- />
247
- </div>
322
+ <div className="flex flex-col gap-1.5 p-3 rounded-lg bg-ui-bg-subtle border border-ui-border-base">
323
+ <div className="flex items-center justify-between">
324
+ <Label htmlFor="edit-rank" className="text-xs font-semibold">
325
+ Storefront Ranking (Sort Order)
326
+ </Label>
327
+ <span className="text-[10px] font-mono text-ui-fg-muted">
328
+ Controls Storefront 2 & Monfil
329
+ </span>
330
+ </div>
331
+ <div className="flex items-center gap-2">
332
+ <Input
333
+ id="edit-rank"
334
+ type="number"
335
+ size="small"
336
+ value={rank}
337
+ onChange={(e) => setRank(Number(e.target.value))}
338
+ className="w-24"
339
+ />
340
+ <div className="flex items-center gap-1">
341
+ <button
342
+ type="button"
343
+ onClick={() => setRank((r) => Math.max(0, r - 1))}
344
+ className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
345
+ title="Move Earlier in Storefront"
346
+ >
347
+ ↑ Earlier
348
+ </button>
349
+ <button
350
+ type="button"
351
+ onClick={() => setRank((r) => r + 1)}
352
+ className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
353
+ title="Move Later in Storefront"
354
+ >
355
+ ↓ Later
356
+ </button>
357
+ <button
358
+ type="button"
359
+ onClick={() => setRank(0)}
360
+ className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
361
+ title="Set as First Category"
362
+ >
363
+ Top (#0)
364
+ </button>
365
+ </div>
366
+ </div>
367
+ <p className="text-[11px] text-ui-fg-muted leading-relaxed">
368
+ Categories are sorted by rank ASC (0, 1, 2...). Lower numbers appear first on your storefront navigation menu.
369
+ </p>
370
+ </div>
248
371
 
249
- <div className="flex flex-col gap-2 pt-2 border-t border-ui-border-base">
250
- <Label className="text-xs font-semibold">Storefront Visibility</Label>
251
- <Text size="xsmall" className="text-ui-fg-muted">
252
- Designate which storefronts can display this category.
253
- </Text>
254
- <div className="flex flex-col gap-2 mt-1">
255
- {salesChannels.map((channel) => {
256
- const isChecked = selectedChannels.includes(channel.id)
257
- return (
258
- <label
259
- key={channel.id}
260
- className="flex items-center gap-2.5 text-xs text-ui-fg-base cursor-pointer select-none"
261
- >
262
- <input
263
- type="checkbox"
264
- checked={isChecked}
265
- onChange={() => toggleChannel(channel.id)}
266
- className="rounded border-ui-border-base text-blue-600 focus:ring-blue-500 cursor-pointer"
267
- />
268
- <span className="font-medium">{channel.name}</span>
269
- </label>
270
- )
271
- })}
372
+ <div className="flex flex-col gap-1.5">
373
+ <Label htmlFor="edit-desc" className="text-xs font-semibold">
374
+ Description
375
+ </Label>
376
+ <Textarea
377
+ id="edit-desc"
378
+ value={description}
379
+ onChange={(e) => setDescription(e.target.value)}
380
+ rows={3}
381
+ />
382
+ </div>
383
+
384
+ <div className="flex items-center justify-between pt-2 border-t border-ui-border-base">
385
+ <div>
386
+ <Label className="text-xs font-semibold">Active Status</Label>
387
+ <Text size="xsmall" className="text-ui-fg-muted">
388
+ Visible to shoppers on enabled storefronts
389
+ </Text>
390
+ </div>
391
+ <Switch checked={isActive} onCheckedChange={setIsActive} />
392
+ </div>
393
+
394
+ <div className="flex items-center justify-between">
395
+ <div>
396
+ <Label className="text-xs font-semibold">Internal Only</Label>
397
+ <Text size="xsmall" className="text-ui-fg-muted">
398
+ Hidden from public storefront navigation
399
+ </Text>
400
+ </div>
401
+ <Switch checked={isInternal} onCheckedChange={setIsInternal} />
402
+ </div>
403
+
404
+ <div className="pt-3 border-t border-ui-border-base">
405
+ <Button
406
+ type="button"
407
+ variant="danger"
408
+ size="small"
409
+ onClick={handleDelete}
410
+ isLoading={isDeleting}
411
+ className="w-full flex items-center justify-center gap-1.5"
412
+ >
413
+ <Trash className="w-3.5 h-3.5" />
414
+ <span>Delete Category</span>
415
+ </Button>
416
+ </div>
272
417
  </div>
273
- </div>
418
+ )}
274
419
 
275
- <div className="flex items-center justify-between pt-2 border-t border-ui-border-base">
276
- <div>
277
- <Label className="text-xs font-semibold">Active Status</Label>
278
- <Text size="xsmall" className="text-ui-fg-muted">
279
- Visible to shoppers on enabled storefronts
280
- </Text>
420
+ {/* TAB 2: PRODUCTS MANAGEMENT */}
421
+ {activeTab === "products" && (
422
+ <div className="flex flex-col gap-3">
423
+ <CategoryProductsManager
424
+ categoryId={category.id}
425
+ categoryName={category.name}
426
+ initialMetadata={currentMetadata}
427
+ onMetadataUpdate={(meta) => {
428
+ setCurrentMetadata(meta)
429
+ }}
430
+ />
281
431
  </div>
282
- <Switch checked={isActive} onCheckedChange={setIsActive} />
283
- </div>
432
+ )}
284
433
 
285
- <div className="flex items-center justify-between">
286
- <div>
287
- <Label className="text-xs font-semibold">Internal Only</Label>
288
- <Text size="xsmall" className="text-ui-fg-muted">
289
- Hidden from public storefront navigation
290
- </Text>
434
+ {/* TAB 3: STOREFRONTS & MEDIA */}
435
+ {activeTab === "channels" && (
436
+ <div className="flex flex-col gap-4">
437
+ <div className="flex flex-col gap-2">
438
+ <Label className="text-xs font-semibold">Storefront Visibility</Label>
439
+ <Text size="xsmall" className="text-ui-fg-muted">
440
+ Designate which storefront sales channels display this category.
441
+ </Text>
442
+ <div className="flex flex-col gap-2 mt-1">
443
+ {salesChannels.map((channel) => {
444
+ const isChecked = selectedChannels.includes(channel.id)
445
+ return (
446
+ <label
447
+ key={channel.id}
448
+ className="flex items-center gap-2.5 text-xs text-ui-fg-base cursor-pointer select-none p-2 rounded-lg border border-ui-border-base bg-ui-bg-subtle/40 hover:bg-ui-bg-subtle"
449
+ >
450
+ <input
451
+ type="checkbox"
452
+ checked={isChecked}
453
+ onChange={() => toggleChannel(channel.id)}
454
+ className="rounded border-ui-border-base text-blue-600 focus:ring-blue-500 cursor-pointer"
455
+ />
456
+ <span className="font-medium">{channel.name}</span>
457
+ </label>
458
+ )
459
+ })}
460
+ </div>
461
+ </div>
462
+
463
+ <div className="flex flex-col gap-1.5 pt-3 border-t border-ui-border-base">
464
+ <Label htmlFor="edit-image" className="text-xs font-semibold">
465
+ Category Thumbnail / Image URL
466
+ </Label>
467
+ <Input
468
+ id="edit-image"
469
+ size="small"
470
+ value={imageUrl}
471
+ onChange={(e) => setImageUrl(e.target.value)}
472
+ placeholder="https://example.com/category-image.jpg"
473
+ />
474
+ {imageUrl && (
475
+ <div className="mt-2 w-24 h-24 rounded-xl overflow-hidden border border-ui-border-base bg-ui-bg-subtle shadow-xs">
476
+ <img
477
+ src={imageUrl}
478
+ alt="Category Preview"
479
+ className="w-full h-full object-cover"
480
+ onError={(e) => {
481
+ ;(e.target as any).style.display = "none"
482
+ }}
483
+ />
484
+ </div>
485
+ )}
486
+ </div>
291
487
  </div>
292
- <Switch checked={isInternal} onCheckedChange={setIsInternal} />
293
- </div>
488
+ )}
294
489
 
295
- <div className="pt-3 border-t border-ui-border-base">
296
- <Button
297
- type="button"
298
- variant="danger"
299
- size="small"
300
- onClick={handleDelete}
301
- isLoading={isDeleting}
302
- className="w-full flex items-center justify-center gap-1.5"
303
- >
304
- <Trash className="w-3.5 h-3.5" />
305
- <span>Delete Category</span>
306
- </Button>
307
- </div>
490
+ {/* TAB 4: YOUTUBE VIDEOS */}
491
+ {activeTab === "youtube" && (
492
+ <div className="flex flex-col gap-4">
493
+ <div>
494
+ <h4 className="text-xs font-semibold text-ui-fg-base">
495
+ Category Demonstration Videos (YouTube)
496
+ </h4>
497
+ <p className="text-xs text-ui-fg-muted mt-0.5">
498
+ Attach YouTube video demonstration links to show interactive video carousels on the storefront category page.
499
+ </p>
500
+ </div>
501
+
502
+ <div className="flex items-center gap-2">
503
+ <Input
504
+ size="small"
505
+ placeholder="Paste YouTube watch URL, shorts, or video ID…"
506
+ value={newYoutubeInput}
507
+ onChange={(e) => setNewYoutubeInput(e.target.value)}
508
+ className="flex-1 text-xs"
509
+ onKeyDown={(e) => {
510
+ if (e.key === "Enter") {
511
+ e.preventDefault()
512
+ handleAddYoutube()
513
+ }
514
+ }}
515
+ />
516
+ <Button
517
+ size="small"
518
+ type="button"
519
+ variant="secondary"
520
+ onClick={handleAddYoutube}
521
+ className="flex items-center gap-1 text-xs flex-shrink-0"
522
+ >
523
+ <Plus className="w-3.5 h-3.5" />
524
+ <span>Add Video</span>
525
+ </Button>
526
+ </div>
527
+
528
+ {youtubeUrls.length === 0 ? (
529
+ <div className="p-8 text-center text-xs text-ui-fg-muted border border-dashed border-ui-border-base rounded-xl">
530
+ No demonstration videos attached yet.
531
+ </div>
532
+ ) : (
533
+ <div className="grid grid-cols-2 gap-3">
534
+ {youtubeUrls.map((url) => {
535
+ const id = extractYouTubeVideoId(url)
536
+ const thumb = id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null
537
+ return (
538
+ <div
539
+ key={url}
540
+ className="flex flex-col border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-subtle/40 shadow-xs"
541
+ >
542
+ <div className="relative aspect-video bg-black/80 overflow-hidden flex items-center justify-center">
543
+ {thumb ? (
544
+ <img src={thumb} alt="YouTube Preview" className="w-full h-full object-cover" />
545
+ ) : (
546
+ <span className="text-xs text-white">Video</span>
547
+ )}
548
+ <button
549
+ type="button"
550
+ onClick={() => handleRemoveYoutube(url)}
551
+ className="absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors"
552
+ title="Remove video"
553
+ >
554
+ <Trash className="w-3 h-3" />
555
+ </button>
556
+ </div>
557
+ <div className="p-2">
558
+ <span className="text-[10px] text-ui-fg-muted truncate block font-mono" title={url}>
559
+ {url}
560
+ </span>
561
+ </div>
562
+ </div>
563
+ )
564
+ })}
565
+ </div>
566
+ )}
567
+ </div>
568
+ )}
308
569
  </Drawer.Body>
309
570
 
310
571
  <Drawer.Footer className="flex justify-end gap-2 px-6 py-4 border-t border-ui-border-base">
@@ -315,7 +576,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
315
576
  onClick={() => onOpenChange(false)}
316
577
  disabled={isSubmitting || isDeleting}
317
578
  >
318
- Cancel
579
+ Close
319
580
  </Button>
320
581
  <Button
321
582
  type="submit"
@@ -323,7 +584,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
323
584
  isLoading={isSubmitting}
324
585
  disabled={isSubmitting || !name.trim() || isDeleting}
325
586
  >
326
- Save Changes
587
+ Save Category
327
588
  </Button>
328
589
  </Drawer.Footer>
329
590
  </form>