This commit is contained in:
Your Name committed 2026-10-03 08:15:21 +07:00
1 parent 91dacd20ce
commit 27cc56abaf
14 files changed
+1757 -35

No files matched your search

@@ -0,0 +1,517 @@
import { useState } from 'react'
import {
Sparkles,
Shield,
Gauge,
Wrench,
Zap,
Award,
Flame,
Plus,
Trash2,
Image as ImageIcon,
CheckCircle2,
Tag
} from 'lucide-react'
import type { CategoryFeaturedSeries, CategoryUspBenefit, CategoryFilterConfig } from '@homaroma/types'
interface CategoryHybridCmsEditorProps {
featuredSeries: CategoryFeaturedSeries
onChangeFeaturedSeries: (val: CategoryFeaturedSeries) => void
uspBenefits: CategoryUspBenefit[]
onChangeUspBenefits: (val: CategoryUspBenefit[]) => void
filterConfig: CategoryFilterConfig
onChangeFilterConfig: (val: CategoryFilterConfig) => void
onOpenMediaModal: (target: 'featured_image' | 'featured_action') => void
}
const AVAILABLE_ICONS = [
{ id: 'shield', label: 'Shield (Protection)', icon: Shield },
{ id: 'gauge', label: 'Gauge (Performance)', icon: Gauge },
{ id: 'wrench', label: 'Wrench (Durability)', icon: Wrench },
{ id: 'zap', label: 'Zap (Power/Speed)', icon: Zap },
{ id: 'award', label: 'Award (Quality)', icon: Award },
{ id: 'flame', label: 'Flame (Racing)', icon: Flame },
]
export default function CategoryHybridCmsEditor({
featuredSeries,
onChangeFeaturedSeries,
uspBenefits,
onChangeUspBenefits,
filterConfig,
onChangeFilterConfig,
onOpenMediaModal,
}: CategoryHybridCmsEditorProps) {
const [newFeatureTag, setNewFeatureTag] = useState('')
const [activeTab, setActiveTab] = useState<'featured' | 'benefits' | 'filters'>('featured')
// Feature Tags handlers
const handleAddFeatureTag = () => {
if (!newFeatureTag.trim()) return
const current = featuredSeries.features || []
onChangeFeaturedSeries({
...featuredSeries,
features: [...current, newFeatureTag.trim()]
})
setNewFeatureTag('')
}
const handleRemoveFeatureTag = (idx: number) => {
const current = featuredSeries.features || []
onChangeFeaturedSeries({
...featuredSeries,
features: current.filter((_, i) => i !== idx)
})
}
// USP Benefits handlers
const handleAddBenefit = () => {
if (uspBenefits.length >= 6) return
onChangeUspBenefits([
...uspBenefits,
{
icon: 'shield',
title: 'New Benefit',
description: 'Describe key performance or quality benefit'
}
])
}
const handleUpdateBenefit = (idx: number, patch: Partial<CategoryUspBenefit>) => {
const updated = uspBenefits.map((b, i) => i === idx ? { ...b, ...patch } : b)
onChangeUspBenefits(updated)
}
const handleRemoveBenefit = (idx: number) => {
onChangeUspBenefits(uspBenefits.filter((_, i) => i !== idx))
}
return (
<div className="admin-card p-6 md:p-8 space-y-6">
{/* Header & Tabs */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/5 pb-5">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-orange-500/10 text-orange-400 border border-orange-500/20">
<Sparkles className="w-5 h-5" />
</div>
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
Category Presentation & CMS Blocks
<span className="text-[10px] uppercase font-mono font-bold tracking-wider px-2 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/30">
Hybrid CMS
</span>
</h2>
<p className="text-xs text-slate-400">Configure Featured Series Banner, Benefits USP, and Filter System</p>
</div>
</div>
{/* Tab Buttons */}
<div className="flex items-center gap-1.5 p-1 rounded-xl bg-night-950 border border-white/10 self-start sm:self-auto">
<button
type="button"
onClick={() => setActiveTab('featured')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'featured'
? 'bg-orange-500 text-night-950 font-bold shadow'
: 'text-slate-400 hover:text-white'
}`}
>
Featured Series
</button>
<button
type="button"
onClick={() => setActiveTab('benefits')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'benefits'
? 'bg-orange-500 text-night-950 font-bold shadow'
: 'text-slate-400 hover:text-white'
}`}
>
Benefits USP
</button>
<button
type="button"
onClick={() => setActiveTab('filters')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
activeTab === 'filters'
? 'bg-orange-500 text-night-950 font-bold shadow'
: 'text-slate-400 hover:text-white'
}`}
>
Filter System
</button>
</div>
</div>
{/* ── TAB 1: Featured Series Highlight ────────────────────────────── */}
{activeTab === 'featured' && (
<div className="space-y-6 animate-fade-in">
{/* Toggle Enable */}
<div
onClick={() => onChangeFeaturedSeries({ ...featuredSeries, enabled: !featuredSeries.enabled })}
className={`flex items-center justify-between p-4 rounded-xl border transition-all cursor-pointer select-none ${
featuredSeries.enabled
? 'bg-orange-500/10 border-orange-500/40 shadow-sm'
: 'bg-white/[0.02] border-white/10 hover:border-white/20'
}`}
>
<div className="space-y-0.5">
<div className="text-sm font-bold text-white flex items-center gap-2">
<span>Enable Featured Series Banner</span>
{featuredSeries.enabled ? (
<span className="text-[10px] text-emerald-400 font-bold bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
Active
</span>
) : (
<span className="text-[10px] text-slate-400 font-bold bg-white/5 border border-white/10 px-2 py-0.5 rounded-full">
Disabled
</span>
)}
</div>
<p className="text-xs text-slate-400">
Displays a prominent showcase banner (Title, Specs, Cutout Product & Lifestyle Action Shot)
</p>
</div>
<button
type="button"
role="switch"
aria-checked={featuredSeries.enabled ?? false}
onClick={(e) => {
e.stopPropagation();
onChangeFeaturedSeries({ ...featuredSeries, enabled: !featuredSeries.enabled });
}}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
featuredSeries.enabled ? 'bg-orange-500' : 'bg-night-950 border border-white/20'
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
featuredSeries.enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
{featuredSeries.enabled && (
<div className="space-y-5 pt-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Badge Kicker */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">Kicker / Badge Text</label>
<input
type="text"
value={featuredSeries.badge || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, badge: e.target.value })}
placeholder="e.g. FEATURED SERIES /"
className="admin-input w-full text-sm py-2.5"
/>
</div>
{/* Series Title */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">Featured Title</label>
<input
type="text"
value={featuredSeries.title || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, title: e.target.value })}
placeholder="e.g. ABSORBER PD SERIES"
className="admin-input w-full text-sm py-2.5 font-bold"
/>
</div>
</div>
{/* Subtitle */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">Subtitle / Tagline</label>
<input
type="text"
value={featuredSeries.subtitle || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, subtitle: e.target.value })}
placeholder="e.g. RACE PROVEN SUSPENSION SYSTEM"
className="admin-input w-full text-sm py-2.5 font-medium"
/>
</div>
{/* Description */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">Description</label>
<textarea
rows={3}
value={featuredSeries.description || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, description: e.target.value })}
placeholder="e.g. Maximum traction. Superior stability. Engineered for reliable performance on & all off the track."
className="admin-input w-full text-xs py-2.5 resize-y"
/>
</div>
{/* CTA Button Text & URL */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">CTA Button Text</label>
<input
type="text"
value={featuredSeries.ctaText || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, ctaText: e.target.value })}
placeholder="e.g. EXPLORE PD SERIES →"
className="admin-input w-full text-sm py-2.5"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-semibold text-slate-300">CTA Destination Link</label>
<input
type="text"
value={featuredSeries.ctaUrl || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, ctaUrl: e.target.value })}
placeholder="e.g. /products/absorber-pd-series or #products"
className="admin-input w-full text-sm py-2.5 font-mono text-cyan-300"
/>
</div>
</div>
{/* Images */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 pt-2">
{/* Product Cutout Image */}
<div className="p-4 rounded-xl bg-night-950 border border-white/10 space-y-3">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-white flex items-center gap-1.5">
<ImageIcon className="w-3.5 h-3.5 text-orange-400" />
<span>Product Featured Cutout Image</span>
</label>
<button
type="button"
onClick={() => onOpenMediaModal('featured_image')}
className="text-xs text-orange-400 hover:text-orange-300 font-semibold cursor-pointer"
>
Browse Library
</button>
</div>
<input
type="text"
value={featuredSeries.imageUrl || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, imageUrl: e.target.value })}
placeholder="https://..."
className="admin-input w-full text-xs py-2 font-mono"
/>
{featuredSeries.imageUrl && (
<div className="relative h-28 w-full rounded-lg overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center">
<img src={featuredSeries.imageUrl} alt="Featured Product" className="max-h-full max-w-full object-contain p-2" />
</div>
)}
</div>
{/* Lifestyle / Action Rider Image */}
<div className="p-4 rounded-xl bg-night-950 border border-white/10 space-y-3">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-white flex items-center gap-1.5">
<ImageIcon className="w-3.5 h-3.5 text-orange-400" />
<span>Action / Lifestyle Background Image</span>
</label>
<button
type="button"
onClick={() => onOpenMediaModal('featured_action')}
className="text-xs text-orange-400 hover:text-orange-300 font-semibold cursor-pointer"
>
Browse Library
</button>
</div>
<input
type="text"
value={featuredSeries.actionImageUrl || ''}
onChange={(e) => onChangeFeaturedSeries({ ...featuredSeries, actionImageUrl: e.target.value })}
placeholder="https://..."
className="admin-input w-full text-xs py-2 font-mono"
/>
{featuredSeries.actionImageUrl && (
<div className="relative h-28 w-full rounded-lg overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center">
<img src={featuredSeries.actionImageUrl} alt="Action Lifestyle" className="h-full w-full object-cover" />
</div>
)}
</div>
</div>
{/* Feature Pills / Badges */}
<div className="space-y-2 pt-2">
<label className="text-xs font-semibold text-slate-300 flex items-center gap-1.5">
<Tag className="w-3.5 h-3.5 text-orange-400" />
<span>Feature Badges / Specs Pills</span>
</label>
<div className="flex flex-wrap gap-2 items-center">
{(featuredSeries.features || []).map((feat, idx) => (
<span
key={idx}
className="px-3 py-1 rounded-full text-xs font-bold bg-orange-500/10 text-orange-400 border border-orange-500/20 flex items-center gap-2"
>
<span>{feat}</span>
<button
type="button"
onClick={() => handleRemoveFeatureTag(idx)}
className="hover:text-rose-400 cursor-pointer"
>
×
</button>
</span>
))}
<div className="flex items-center gap-2">
<input
type="text"
value={newFeatureTag}
onChange={(e) => setNewFeatureTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddFeatureTag()
}
}}
placeholder="Add spec pill (e.g. ADJUSTABLE DAMPING)"
className="admin-input text-xs py-1.5 px-3 rounded-full w-60"
/>
<button
type="button"
onClick={handleAddFeatureTag}
className="px-3 py-1.5 rounded-full bg-white/5 hover:bg-white/10 text-xs font-semibold text-white border border-white/10 cursor-pointer"
>
Add
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* ── TAB 2: Benefits & USP Blocks ─────────────────────────────────── */}
{activeTab === 'benefits' && (
<div className="space-y-6 animate-fade-in">
<div className="flex items-center justify-between">
<p className="text-xs text-slate-400">
Configure 3-column benefits highlight cards (communicates engineering superiority, durability, etc.)
</p>
{uspBenefits.length < 6 && (
<button
type="button"
onClick={handleAddBenefit}
className="px-3.5 py-1.5 rounded-xl bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 text-xs font-bold flex items-center gap-1.5 cursor-pointer transition-all"
>
<Plus className="w-3.5 h-3.5" />
<span>Add Benefit</span>
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{uspBenefits.map((benefit, idx) => {
const matchedIcon = AVAILABLE_ICONS.find(i => i.id === benefit.icon) || AVAILABLE_ICONS[0]
const IconComp = matchedIcon.icon
return (
<div key={idx} className="p-4 rounded-xl bg-white/[0.02] border border-white/10 space-y-3 relative group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-400 border border-orange-500/20">
<IconComp className="w-4 h-4" />
</div>
<select
value={benefit.icon || 'shield'}
onChange={(e) => handleUpdateBenefit(idx, { icon: e.target.value })}
className="bg-night-950 border border-white/10 rounded-lg text-xs py-1 px-2 text-slate-300 cursor-pointer outline-none"
>
{AVAILABLE_ICONS.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.label}
</option>
))}
</select>
</div>
<button
type="button"
onClick={() => handleRemoveBenefit(idx)}
className="p-1.5 text-slate-500 hover:text-rose-400 opacity-60 group-hover:opacity-100 transition-opacity cursor-pointer"
title="Remove Benefit"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div className="space-y-1">
<label className="text-[11px] font-semibold text-slate-400">Title</label>
<input
type="text"
value={benefit.title}
onChange={(e) => handleUpdateBenefit(idx, { title: e.target.value })}
placeholder="e.g. Superior Damping"
className="admin-input w-full text-xs py-2 font-bold"
/>
</div>
<div className="space-y-1">
<label className="text-[11px] font-semibold text-slate-400">Description</label>
<textarea
rows={2}
value={benefit.description || ''}
onChange={(e) => handleUpdateBenefit(idx, { description: e.target.value })}
placeholder="e.g. Key category benefits to communicate performance value"
className="admin-input w-full text-[11px] py-1.5 resize-y"
/>
</div>
</div>
)
})}
</div>
</div>
)}
{/* ── TAB 3: Filter System Configuration ──────────────────────────── */}
{activeTab === 'filters' && (
<div className="space-y-5 animate-fade-in">
<p className="text-xs text-slate-400">
Select which filter facets should be visible in the Category Filter Sidebar on the website:
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{[
{ key: 'showCategoryNav', label: 'Category Pills Navigation', desc: 'Display top category switch pills bar above the content' },
{ key: 'showSeries', label: 'Suspension Series', desc: 'Allow shoppers to filter by series (PD, AB, GP, FX)' },
{ key: 'showType', label: 'Product Type', desc: 'Rear Shock, Front Fork, Steering Damper checkboxes' },
{ key: 'showColor', label: 'Color Swatches', desc: 'Visual color dots (Red, Orange, Blue, Yellow, Silver, Black)' },
{ key: 'showFitment', label: 'Fitment Usage', desc: 'Street Use, Racing Use, Off-road Use tags' },
{ key: 'showPrice', label: 'Price Range Slider', desc: 'Interactive min & max price range slider' },
{ key: 'showStock', label: 'Stock Status', desc: 'In Stock & Pre-order checkboxes' },
].map(({ key, label, desc }) => {
const checked = (filterConfig as any)[key] ?? true
return (
<div
key={key}
onClick={() => onChangeFilterConfig({ ...filterConfig, [key]: !checked })}
className={`p-4 rounded-xl border-2 transition-all cursor-pointer flex flex-col justify-between ${
checked
? 'bg-orange-500/10 border-orange-500/40 text-white'
: 'bg-white/[0.01] border-white/10 text-slate-400 hover:border-white/20'
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="space-y-1">
<span className="text-xs font-bold block">{label}</span>
<p className="text-[11px] text-slate-400 leading-snug">{desc}</p>
</div>
<div className={`w-4 h-4 rounded mt-0.5 flex items-center justify-center border ${
checked ? 'bg-orange-500 border-orange-500 text-night-950 font-bold' : 'border-white/20'
}`}>
{checked && <CheckCircle2 className="w-3.5 h-3.5" />}
</div>
</div>
</div>
)
})}
</div>
</div>
)}
</div>
)
}
+112 -6
View File
@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { API_BASE, type Category, type CategoryMedia, type CategoryMediaRole } from '@homaroma/types'
import { API_BASE, type Category, type CategoryMedia, type CategoryMediaRole, type CategoryFeaturedSeries, type CategoryUspBenefit, type CategoryFilterConfig } from '@homaroma/types'
import CategoryHybridCmsEditor from '../components/category/CategoryHybridCmsEditor'
import {
ArrowLeft,
Save,
@@ -23,7 +24,8 @@ import {
Sparkles,
LayoutTemplate,
Columns,
Grid3X3
Grid3X3,
EyeOff
} from 'lucide-react'
import MediaLibraryModal from '../components/MediaLibraryModal'
import CategoryMediaEditor from '../components/CategoryMediaEditor'
@@ -81,6 +83,38 @@ export default function CategoryEditPage() {
const [layoutType, setLayoutType] = useState<string>('split_showcase')
const [categoryMeta, setCategoryMeta] = useState<Category | null>(null)
// Hybrid CMS States
const [featuredSeries, setFeaturedSeries] = useState<CategoryFeaturedSeries>({
enabled: false,
badge: 'FEATURED SERIES /',
title: '',
subtitle: '',
description: '',
ctaText: 'EXPLORE SERIES →',
ctaUrl: '#products',
imageUrl: '',
actionImageUrl: '',
features: ['ADJUSTABLE DAMPING', 'SUPERIOR RELIABILITY', 'RACE PROVEN'],
})
const [uspBenefits, setUspBenefits] = useState<CategoryUspBenefit[]>([
{ icon: 'shield', title: 'Superior Damping', description: 'Engineered for maximum stability on and off the track' },
{ icon: 'gauge', title: 'Race Proven Performance', description: 'Tested and proven by champions in extreme conditions' },
{ icon: 'wrench', title: 'Durable For Extreme Use', description: 'Premium grade components built to last' },
])
const [filterConfig, setFilterConfig] = useState<CategoryFilterConfig>({
showCategoryNav: true,
showSeries: true,
showType: true,
showColor: true,
showFitment: true,
showPrice: true,
showStock: true,
})
const [mediaTarget, setMediaTarget] = useState<'role' | 'featured_image' | 'featured_action'>('role')
// Media Library Modal
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
const [activeMediaRole, setActiveMediaRole] = useState<CategoryMediaRole>('thumbnail')
@@ -99,6 +133,9 @@ export default function CategoryEditPage() {
? data.media
: (data.imageUrl ? [{ role: 'thumbnail', url: data.imageUrl, focalX: 0.5, focalY: 0.5 }] : []))
setLayoutType(data.layoutType || 'split_showcase')
if (data.featuredSeries) setFeaturedSeries(data.featuredSeries)
if (Array.isArray(data.uspBenefits) && data.uspBenefits.length > 0) setUspBenefits(data.uspBenefits)
if (data.filterConfig) setFilterConfig(data.filterConfig)
if (data.name && typeof data.name === 'object') {
setName(prev => ({ ...prev, ...data.name }))
@@ -304,6 +341,9 @@ export default function CategoryEditPage() {
name: masterName,
imageUrl: thumbnail.url,
layoutType: layoutType || 'split_showcase',
featuredSeries,
uspBenefits,
filterConfig,
media: media.map(({ role, url, width, height, focalX, focalY, sortOrder }) => ({
role,
url,
@@ -703,12 +743,16 @@ export default function CategoryEditPage() {
<p className="text-xs text-slate-400">Choose how this category displays on Storefront & Website</p>
</div>
</div>
<span className="px-2.5 py-1 rounded-lg text-xs font-mono font-semibold bg-white/5 text-slate-300 border border-white/10">
{layoutType}
<span className={`px-2.5 py-1 rounded-lg text-xs font-mono font-semibold border ${
layoutType === 'none'
? 'bg-rose-500/10 text-rose-300 border-rose-500/30'
: 'bg-white/5 text-slate-300 border-white/10'
}`}>
{layoutType === 'none' ? 'none (ปิดส่วนหัว)' : layoutType}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{/* Option 1: Split Showcase */}
<div
onClick={() => setLayoutType('split_showcase')}
@@ -834,9 +878,64 @@ export default function CategoryEditPage() {
</div>
</div>
</div>
{/* Option 4: None / ปิดส่วนหัว */}
<div
onClick={() => setLayoutType('none')}
className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
layoutType === 'none'
? 'bg-rose-500/10 border-rose-400 shadow-lg shadow-rose-500/10'
: 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
}`}
>
<div className="space-y-3">
{/* Visual Layout Mockup */}
<div className="w-full h-24 rounded-xl bg-night-950/80 border border-white/10 p-2.5 flex flex-col justify-between overflow-hidden">
<div className="w-full py-1 px-2 border-b border-dashed border-rose-500/30 flex items-center justify-between">
<div className="h-1.5 w-12 bg-white/20 rounded" />
<span className="text-[9px] font-mono uppercase text-rose-400 font-bold">No Header</span>
</div>
<div className="grid grid-cols-3 gap-1 pt-1 flex-1 items-center">
<div className="h-8 rounded bg-white/10 border border-white/5" />
<div className="h-8 rounded bg-white/10 border border-white/5" />
<div className="h-8 rounded bg-white/10 border border-white/5" />
</div>
</div>
<div>
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-white flex items-center gap-1.5">
<EyeOff className="w-4 h-4 text-rose-400" />
ปิด (No Header)
</h4>
{layoutType === 'none' && (
<CheckCircle2 className="w-4 h-4 text-rose-400" />
)}
</div>
<p className="text-[11px] text-rose-400 font-medium mt-0.5">Clean & Streamlined</p>
<p className="text-xs text-slate-400 mt-2 leading-relaxed">
ไม่แสดง Archetype Header ด้านบน เพื่อเริ่มแสดง Featured Series หรือสินค้าทันที ลดความซ้ำซ้อน
</p>
</div>
</div>
</div>
</div>
</div>
{/* Hybrid CMS Presentation Editor */}
<CategoryHybridCmsEditor
featuredSeries={featuredSeries}
onChangeFeaturedSeries={setFeaturedSeries}
uspBenefits={uspBenefits}
onChangeUspBenefits={setUspBenefits}
filterConfig={filterConfig}
onChangeFilterConfig={setFilterConfig}
onOpenMediaModal={(target) => {
setMediaTarget(target)
setIsMediaModalOpen(true)
}}
/>
{/* Card 3: Associated Products & Quick Link */}
{!isNew && categoryMeta && (
<div className="admin-card p-6 flex items-center justify-between gap-4">
@@ -873,6 +972,7 @@ export default function CategoryEditPage() {
uploadingRole={uploadingRole}
onChange={setMedia}
onBrowse={(role) => {
setMediaTarget('role')
setActiveMediaRole(role)
setIsMediaModalOpen(true)
}}
@@ -911,7 +1011,13 @@ export default function CategoryEditPage() {
isOpen={isMediaModalOpen}
onClose={() => setIsMediaModalOpen(false)}
onSelect={(url) => {
setMediaUrl(activeMediaRole, url)
if (mediaTarget === 'featured_image') {
setFeaturedSeries(prev => ({ ...prev, imageUrl: url }))
} else if (mediaTarget === 'featured_action') {
setFeaturedSeries(prev => ({ ...prev, actionImageUrl: url }))
} else {
setMediaUrl(activeMediaRole, url)
}
setIsMediaModalOpen(false)
}}
/>