x
This commit is contained in:
1 parent
0ceee3dbff
commit
2d39edbd57
18 files changed
+804
-153
No files matched your search
@@ -75,6 +75,7 @@ export default function CategoryBulkJsonModal({
|
||||
id: cat.id,
|
||||
name: catBaseName || transMap['th'] || 'Unnamed Category',
|
||||
imageUrl: cat.imageUrl || null,
|
||||
media: cat.media || [],
|
||||
layoutType: cat.layoutType || 'split_showcase',
|
||||
translations: transMap,
|
||||
};
|
||||
@@ -89,8 +90,9 @@ export default function CategoryBulkJsonModal({
|
||||
const aiTranslatePrompt = useMemo(() => {
|
||||
const sampleItems = exportData.slice(0, 5);
|
||||
return `Please translate and optimize our E-commerce Category Names into all 8 supported languages (th, en, ja, zh, vi, ko, lo, it).
|
||||
Ensure the tone is elegant, accurate for luxury aromatherapy & lifestyle products.
|
||||
Preserve the tenant's catalog terminology and brand tone.
|
||||
Allowed layoutType values: "split_showcase" (default), "editorial_hero", "dense_catalog".
|
||||
Allowed media roles: "thumbnail", "hero_desktop", "hero_mobile", "card_portrait", "social_share".
|
||||
|
||||
Return ONLY a valid JSON Array conforming to this structure:
|
||||
\`\`\`json
|
||||
@@ -101,6 +103,11 @@ ${JSON.stringify(
|
||||
{
|
||||
name: 'น้ำมันหอมระเหย',
|
||||
imageUrl: '/images/categories/essential-oils.jpg',
|
||||
media: [
|
||||
{ role: 'thumbnail', url: '/images/categories/essential-oils-square.jpg' },
|
||||
{ role: 'hero_desktop', url: '/images/categories/essential-oils-wide.jpg' },
|
||||
{ role: 'hero_mobile', url: '/images/categories/essential-oils-mobile.jpg' },
|
||||
],
|
||||
layoutType: 'split_showcase',
|
||||
translations: {
|
||||
th: 'น้ำมันหอมระเหย',
|
||||
@@ -116,6 +123,11 @@ ${JSON.stringify(
|
||||
{
|
||||
name: 'ก้านไม้หอมปรับอากาศ',
|
||||
imageUrl: '/images/categories/reed-diffusers.jpg',
|
||||
media: [
|
||||
{ role: 'thumbnail', url: '/images/categories/reed-diffusers-square.jpg' },
|
||||
{ role: 'hero_desktop', url: '/images/categories/reed-diffusers-wide.jpg' },
|
||||
{ role: 'hero_mobile', url: '/images/categories/reed-diffusers-mobile.jpg' },
|
||||
],
|
||||
layoutType: 'editorial_hero',
|
||||
translations: {
|
||||
th: 'ก้านไม้หอมปรับอากาศ',
|
||||
@@ -169,6 +181,7 @@ ${JSON.stringify(
|
||||
id: typeof item.id === 'number' ? item.id : undefined,
|
||||
name: baseName,
|
||||
imageUrl: item.imageUrl || null,
|
||||
media: Array.isArray(item.media) ? item.media : [],
|
||||
layoutType: typeof item.layoutType === 'string' ? item.layoutType : 'split_showcase',
|
||||
translations: trans,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { AlertCircle, ClipboardPaste, Image as ImageIcon, Loader2, Trash2 } from 'lucide-react'
|
||||
import type { CategoryMedia, CategoryMediaRole } from '@homaroma/types'
|
||||
import FocalPointPicker from './page-builder/FocalPointPicker'
|
||||
|
||||
type CategoryMediaEditorProps = {
|
||||
media: CategoryMedia[]
|
||||
layoutType: string
|
||||
displayName: string
|
||||
uploadingRole: CategoryMediaRole | null
|
||||
onChange: Dispatch<SetStateAction<CategoryMedia[]>>
|
||||
onBrowse: (role: CategoryMediaRole) => void
|
||||
onPaste: (role: CategoryMediaRole) => void
|
||||
}
|
||||
|
||||
const MEDIA_SLOTS: Array<{
|
||||
role: CategoryMediaRole
|
||||
label: string
|
||||
ratio: string
|
||||
dimensions: string
|
||||
previewClass: string
|
||||
expectedRatio: number
|
||||
}> = [
|
||||
{ role: 'thumbnail', label: 'Thumbnail', ratio: '1:1', dimensions: '1200 × 1200 px', previewClass: 'aspect-square', expectedRatio: 1 },
|
||||
{ role: 'hero_desktop', label: 'Desktop Hero', ratio: '21:9', dimensions: '2400 × 1028 px', previewClass: 'aspect-[21/9]', expectedRatio: 21 / 9 },
|
||||
{ role: 'hero_mobile', label: 'Mobile Hero', ratio: '4:5', dimensions: '1200 × 1500 px', previewClass: 'aspect-[4/5]', expectedRatio: 4 / 5 },
|
||||
{ role: 'card_portrait', label: 'Portrait Card', ratio: '4:5', dimensions: '1200 × 1500 px', previewClass: 'aspect-[4/5]', expectedRatio: 4 / 5 },
|
||||
{ role: 'social_share', label: 'Social Share', ratio: '1.91:1', dimensions: '1200 × 630 px', previewClass: 'aspect-[1.91/1]', expectedRatio: 1200 / 630 },
|
||||
]
|
||||
|
||||
function isRecommendedForLayout(role: CategoryMediaRole, layoutType: string) {
|
||||
return layoutType === 'editorial_hero' && (role === 'hero_desktop' || role === 'hero_mobile')
|
||||
}
|
||||
|
||||
export default function CategoryMediaEditor({
|
||||
media,
|
||||
layoutType,
|
||||
displayName,
|
||||
uploadingRole,
|
||||
onChange,
|
||||
onBrowse,
|
||||
onPaste,
|
||||
}: CategoryMediaEditorProps) {
|
||||
const findMedia = (role: CategoryMediaRole) => media.find((item) => item.role === role)
|
||||
|
||||
const updateMedia = (role: CategoryMediaRole, update: Partial<CategoryMedia>) => {
|
||||
onChange((current) => {
|
||||
const existing = current.find((item) => item.role === role)
|
||||
if (existing) return current.map((item) => item.role === role ? { ...item, ...update } : item)
|
||||
if (!update.url) return current
|
||||
return [...current, { role, url: update.url, focalX: 0.5, focalY: 0.5, sortOrder: current.length, ...update }]
|
||||
})
|
||||
}
|
||||
|
||||
const removeMedia = (role: CategoryMediaRole) => {
|
||||
onChange((current) => current.filter((item) => item.role !== role))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-card space-y-5 p-6">
|
||||
<div className="border-b border-white/5 pb-4">
|
||||
<h2 className="text-base font-bold text-white">Category Media</h2>
|
||||
<p className="mt-1 text-xs text-slate-400">Art-directed assets for cards, responsive heroes, and social sharing.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{MEDIA_SLOTS.map((slot) => {
|
||||
const item = findMedia(slot.role)
|
||||
const required = slot.role === 'thumbnail'
|
||||
const recommended = isRecommendedForLayout(slot.role, layoutType)
|
||||
const isUploading = uploadingRole === slot.role
|
||||
const actualRatio = item?.width && item?.height ? item.width / item.height : null
|
||||
const ratioMismatch = actualRatio !== null && Math.abs(actualRatio - slot.expectedRatio) / slot.expectedRatio > 0.08
|
||||
|
||||
return (
|
||||
<section key={slot.role} className="space-y-3 rounded-2xl border border-white/10 bg-white/[0.02] p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-bold text-white">{slot.label}</h3>
|
||||
{(required || recommended) && (
|
||||
<span className="rounded-md border border-cyan-glow/20 bg-cyan-glow/10 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider text-cyan-glow">
|
||||
{required ? 'Required' : 'Recommended'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-slate-500">{slot.ratio} · {slot.dimensions}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPaste(slot.role)}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-semibold text-slate-300 transition hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<ClipboardPaste className="h-3 w-3" />
|
||||
Paste
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onBrowse(slot.role)}
|
||||
className={`relative flex w-full items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-white/10 bg-night-950/60 transition hover:border-cyan-glow/40 ${slot.previewClass}`}
|
||||
>
|
||||
{item?.url ? (
|
||||
<img
|
||||
src={item.url}
|
||||
alt={`${displayName || 'Category'} ${slot.label}`}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
style={{ objectPosition: `${(item.focalX ?? 0.5) * 100}% ${(item.focalY ?? 0.5) * 100}%` }}
|
||||
onLoad={(event) => {
|
||||
const { naturalWidth, naturalHeight } = event.currentTarget
|
||||
if (naturalWidth !== item.width || naturalHeight !== item.height) {
|
||||
updateMedia(slot.role, { width: naturalWidth, height: naturalHeight })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2 p-5 text-center">
|
||||
<ImageIcon className="mx-auto h-6 w-6 text-slate-500" />
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Select {slot.label}</p>
|
||||
</div>
|
||||
)}
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-night-950/80">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-cyan-glow" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{item?.url ? (
|
||||
<div className="space-y-3">
|
||||
<p className={`text-[11px] ${ratioMismatch ? 'text-amber-400' : 'text-slate-500'}`}>
|
||||
Actual: {item.width && item.height ? `${item.width} × ${item.height} px` : 'detecting dimensions…'}
|
||||
{ratioMismatch ? ` · recommended ${slot.ratio}` : ''}
|
||||
</p>
|
||||
<FocalPointPicker
|
||||
imageUrl={item.url}
|
||||
focalPoint={{
|
||||
x: `${(item.focalX ?? 0.5) * 100}%`,
|
||||
y: `${(item.focalY ?? 0.5) * 100}%`,
|
||||
}}
|
||||
onChange={(point) => updateMedia(slot.role, {
|
||||
focalX: Number.parseFloat(point.x) / 100,
|
||||
focalY: Number.parseFloat(point.y) / 100,
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeMedia(slot.role)}
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-rose-400 hover:underline"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Remove asset
|
||||
</button>
|
||||
</div>
|
||||
) : (required || recommended) ? (
|
||||
<div className="flex items-start gap-2 rounded-xl border border-amber-500/10 bg-amber-500/5 p-3 text-xs text-amber-400">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{required ? 'This asset is required before saving.' : 'This layout will use the documented fallback until this asset is supplied.'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { API_BASE, getCategoryImage, type Category } from '@homaroma/types'
|
||||
import { API_BASE, getCategoryImage, resolveCategoryMediaUrl, type Category } from '@homaroma/types'
|
||||
import {
|
||||
Edit3,
|
||||
Package,
|
||||
@@ -237,7 +237,7 @@ export default function CategoriesPage() {
|
||||
) : (
|
||||
filteredCategories.map((cat) => {
|
||||
const fallbackName = typeof cat.name === 'string' ? cat.name : (cat.name?.en || cat.name?.th || '')
|
||||
const imageUrl = cat.imageUrl || getCategoryImage(fallbackName)
|
||||
const imageUrl = resolveCategoryMediaUrl(cat, 'thumbnail', getCategoryImage(fallbackName))
|
||||
const langsCount = getAvailableLangsCount(cat)
|
||||
const thaiName = getThaiName(cat)
|
||||
|
||||
@@ -339,7 +339,7 @@ export default function CategoriesPage() {
|
||||
) : (
|
||||
filteredCategories.map((cat) => {
|
||||
const fallbackName = typeof cat.name === 'string' ? cat.name : (cat.name?.en || cat.name?.th || '')
|
||||
const imageUrl = cat.imageUrl || getCategoryImage(fallbackName)
|
||||
const imageUrl = resolveCategoryMediaUrl(cat, 'thumbnail', getCategoryImage(fallbackName))
|
||||
const thaiName = getThaiName(cat)
|
||||
const englishName = getEnglishName(cat)
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { API_BASE, type Category } from '@homaroma/types'
|
||||
import { API_BASE, type Category, type CategoryMedia, type CategoryMediaRole } from '@homaroma/types'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Trash2,
|
||||
Image as ImageIcon,
|
||||
UploadCloud,
|
||||
Loader2,
|
||||
ClipboardPaste,
|
||||
Globe,
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
Grid3X3
|
||||
} from 'lucide-react'
|
||||
import MediaLibraryModal from '../components/MediaLibraryModal'
|
||||
import CategoryMediaEditor from '../components/CategoryMediaEditor'
|
||||
|
||||
const ADDITIONAL_LANGS = [
|
||||
{ code: 'ja', label: 'Japanese (日本語)', placeholder: '例: アロマキャンドル' },
|
||||
@@ -78,12 +77,14 @@ export default function CategoryEditPage() {
|
||||
lo: '',
|
||||
it: '',
|
||||
})
|
||||
const [imageUrl, setImageUrl] = useState<string>('')
|
||||
const [media, setMedia] = useState<CategoryMedia[]>([])
|
||||
const [layoutType, setLayoutType] = useState<string>('split_showcase')
|
||||
const [categoryMeta, setCategoryMeta] = useState<Category | null>(null)
|
||||
|
||||
// Media Library Modal
|
||||
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
|
||||
const [activeMediaRole, setActiveMediaRole] = useState<CategoryMediaRole>('thumbnail')
|
||||
const [uploadingRole, setUploadingRole] = useState<CategoryMediaRole | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isNew) return
|
||||
@@ -94,7 +95,9 @@ export default function CategoryEditPage() {
|
||||
if (!res.ok) throw new Error('Category not found')
|
||||
const data = await res.json()
|
||||
setCategoryMeta(data)
|
||||
setImageUrl(data.imageUrl || '')
|
||||
setMedia(Array.isArray(data.media) && data.media.length > 0
|
||||
? data.media
|
||||
: (data.imageUrl ? [{ role: 'thumbnail', url: data.imageUrl, focalX: 0.5, focalY: 0.5 }] : []))
|
||||
setLayoutType(data.layoutType || 'split_showcase')
|
||||
|
||||
if (data.name && typeof data.name === 'object') {
|
||||
@@ -223,8 +226,17 @@ export default function CategoryEditPage() {
|
||||
}
|
||||
|
||||
// ── Image Upload Handlers ─────────────────────────────────────────────────
|
||||
const handleUpload = async (file: File) => {
|
||||
const setMediaUrl = (role: CategoryMediaRole, url: string) => {
|
||||
setMedia((current) => {
|
||||
const existing = current.find((item) => item.role === role)
|
||||
if (existing) return current.map((item) => item.role === role ? { ...item, url } : item)
|
||||
return [...current, { role, url, focalX: 0.5, focalY: 0.5, sortOrder: current.length }]
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File, role: CategoryMediaRole) => {
|
||||
setIsUploading(true)
|
||||
setUploadingRole(role)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
@@ -235,17 +247,18 @@ export default function CategoryEditPage() {
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
setImageUrl(data.url)
|
||||
setMediaUrl(role, data.url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err)
|
||||
alert('Failed to upload image')
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
setUploadingRole(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteFromClipboard = async () => {
|
||||
const handlePasteFromClipboard = async (role: CategoryMediaRole) => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read()
|
||||
for (const item of items) {
|
||||
@@ -253,7 +266,7 @@ export default function CategoryEditPage() {
|
||||
for (const type of imageTypes) {
|
||||
const blob = await item.getType(type)
|
||||
const file = new File([blob], 'pasted-category-image.png', { type })
|
||||
handleUpload(file)
|
||||
void handleUpload(file, role)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -266,6 +279,12 @@ export default function CategoryEditPage() {
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const thumbnail = media.find((item) => item.role === 'thumbnail' && item.url.trim())
|
||||
if (!thumbnail) {
|
||||
alert('A 1:1 category thumbnail is required before saving.')
|
||||
return
|
||||
}
|
||||
if (!name.th?.trim()) {
|
||||
alert('Please enter Thai category name')
|
||||
return
|
||||
@@ -283,8 +302,17 @@ export default function CategoryEditPage() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: masterName,
|
||||
imageUrl: imageUrl || null,
|
||||
layoutType: layoutType || 'split_showcase'
|
||||
imageUrl: thumbnail.url,
|
||||
layoutType: layoutType || 'split_showcase',
|
||||
media: media.map(({ role, url, width, height, focalX, focalY, sortOrder }) => ({
|
||||
role,
|
||||
url,
|
||||
width: width ?? null,
|
||||
height: height ?? null,
|
||||
focalX: focalX ?? 0.5,
|
||||
focalY: focalY ?? 0.5,
|
||||
sortOrder: sortOrder ?? 0,
|
||||
})),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -838,85 +866,18 @@ export default function CategoryEditPage() {
|
||||
{/* Right Column (1 Col): Image & Metadata */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Category Image Card */}
|
||||
<div className="admin-card p-6 space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-4">
|
||||
<h2 className="text-base font-bold text-white">Category Image</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePasteFromClipboard}
|
||||
className="flex items-center gap-1 px-2.5 py-1 rounded-lg bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow text-[10px] font-bold uppercase tracking-wider transition-all border border-cyan-glow/20 cursor-pointer"
|
||||
title="Paste image directly from clipboard"
|
||||
>
|
||||
<ClipboardPaste className="w-3 h-3" />
|
||||
Paste Image
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Preview Area */}
|
||||
<div
|
||||
className="relative w-full aspect-square rounded-2xl overflow-hidden border-2 border-dashed border-white/10 bg-night-950/60 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/40 transition-all shadow-inner"
|
||||
onClick={() => setIsMediaModalOpen(true)}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<>
|
||||
<img
|
||||
src={imageUrl}
|
||||
className="w-full h-full object-cover"
|
||||
alt={displayName}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-night-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-2 backdrop-blur-xs text-white">
|
||||
<UploadCloud className="w-8 h-8 text-cyan-glow" />
|
||||
<span className="text-xs font-bold">Change Image</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center space-y-2 p-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-white/5 flex items-center justify-center mx-auto text-slate-500 group-hover:text-cyan-glow transition-colors">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 font-bold uppercase tracking-wider">Select Image</p>
|
||||
<p className="text-[10px] text-slate-600">Click to browse Media Library</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 bg-night-950/80 flex flex-col items-center justify-center gap-2">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
<span className="text-xs text-slate-300 font-medium">Uploading image...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image Actions & Helpers */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMediaModalOpen(true)}
|
||||
className="text-cyan-glow hover:underline cursor-pointer font-medium"
|
||||
>
|
||||
Browse Media Library
|
||||
</button>
|
||||
{imageUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setImageUrl('')}
|
||||
className="text-rose-400 hover:underline cursor-pointer"
|
||||
>
|
||||
Remove Image
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!imageUrl && (
|
||||
<div className="p-3 rounded-xl bg-amber-500/5 border border-amber-500/10 text-amber-400 text-xs flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<span>If no image is uploaded, a default placeholder graphic will be used.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CategoryMediaEditor
|
||||
media={media}
|
||||
layoutType={layoutType}
|
||||
displayName={displayName}
|
||||
uploadingRole={uploadingRole}
|
||||
onChange={setMedia}
|
||||
onBrowse={(role) => {
|
||||
setActiveMediaRole(role)
|
||||
setIsMediaModalOpen(true)
|
||||
}}
|
||||
onPaste={(role) => void handlePasteFromClipboard(role)}
|
||||
/>
|
||||
|
||||
{/* Quick Info & Tips Card */}
|
||||
<div className="admin-card p-6 space-y-4">
|
||||
@@ -950,7 +911,7 @@ export default function CategoryEditPage() {
|
||||
isOpen={isMediaModalOpen}
|
||||
onClose={() => setIsMediaModalOpen(false)}
|
||||
onSelect={(url) => {
|
||||
setImageUrl(url)
|
||||
setMediaUrl(activeMediaRole, url)
|
||||
setIsMediaModalOpen(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user