x
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user