+
+ {/* Card Presentation & Styling (Parity with Web CMS Product Carousel) */}
+
diff --git a/apps/admin/src/pages/CategoryCmsPage.tsx b/apps/admin/src/pages/CategoryCmsPage.tsx
index da1a233..5f8ec56 100644
--- a/apps/admin/src/pages/CategoryCmsPage.tsx
+++ b/apps/admin/src/pages/CategoryCmsPage.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import { Link, useNavigate, useParams } from 'react-router-dom'
+import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import {
AlertCircle,
ArrowDown,
@@ -25,11 +25,17 @@ import {
Sparkles,
Trash2,
Upload,
+ Columns,
+ Sliders,
+ Maximize2,
+ Palette,
+ LayoutGrid,
} from 'lucide-react'
-import { API_BASE, type Category, type CategorySection } from '@homaroma/types'
+import { API_BASE, type Category, type CategorySection, type CategoryFeaturedSeries, type CategoryUspBenefit, type CategoryFilterConfig } from '@homaroma/types'
import MediaLibraryModal from '../components/MediaLibraryModal'
import CategoryHeroCarouselBlockForm from '../components/category/CategoryHeroCarouselBlockForm'
import CategoryDividerBlockForm from '../components/category/CategoryDividerBlockForm'
+import CategoryHybridCmsEditor from '../components/category/CategoryHybridCmsEditor'
import { validateCategoryHeroCarouselPayload } from '../components/category/categoryHeroCarouselValidation'
import { getAdminDisplayString } from '../utils/localization'
import BlockTranslationModal, { isTranslatableBlock } from '../components/page-builder/BlockTranslationModal'
@@ -173,6 +179,41 @@ export default function CategoryCmsPage() {
const [isTranslationModalOpen, setIsTranslationModalOpen] = useState(false)
const [translationModalTab, setTranslationModalTab] = useState<'export' | 'import'>('export')
+ const [searchParams, setSearchParams] = useSearchParams()
+ const initialTab = searchParams.get('tab') === 'blocks' ? 'page_builder' : 'presentation'
+ const [cmsTab, setCmsTab] = useState<'presentation' | 'page_builder'>(initialTab)
+
+ // Presentation & Hybrid CMS States
+ const [layoutType, setLayoutType] = useState('split_showcase')
+ const [featuredSeries, setFeaturedSeries] = useState({
+ 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([
+ { 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({
+ showCategoryNav: true,
+ showSeries: true,
+ showType: true,
+ showColor: true,
+ showFitment: true,
+ showPrice: true,
+ showStock: true,
+ })
+ const [savingPresentation, setSavingPresentation] = useState(false)
+ const [presentationSuccess, setPresentationSuccess] = useState(false)
+
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
const selectedSection = selectedSectionId === 'new'
@@ -228,6 +269,12 @@ export default function CategoryCmsPage() {
if (!categoryRes.ok) throw new Error('Category not found')
const categoryData = await categoryRes.json()
setCategory(categoryData)
+ if (categoryData.layoutType) setLayoutType(categoryData.layoutType)
+ if (categoryData.featuredSeries) setFeaturedSeries(categoryData.featuredSeries)
+ if (Array.isArray(categoryData.uspBenefits) && categoryData.uspBenefits.length > 0) {
+ setUspBenefits(categoryData.uspBenefits)
+ }
+ if (categoryData.filterConfig) setFilterConfig(categoryData.filterConfig)
await fetchDetailSections(id)
} catch (err: any) {
setError(err.message)
@@ -239,6 +286,34 @@ export default function CategoryCmsPage() {
fetchData()
}, [id])
+ const handleSavePresentation = async () => {
+ if (!id) return
+ setSavingPresentation(true)
+ setError(null)
+ setPresentationSuccess(false)
+ try {
+ const response = await fetch(`${API_BASE}/categories/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ layoutType,
+ featuredSeries,
+ uspBenefits,
+ filterConfig,
+ }),
+ })
+ const data = await response.json().catch(() => null)
+ if (!response.ok) throw new Error(getApiErrorMessage(data, 'Failed to save presentation settings'))
+ setCategory((prev) => (prev ? { ...prev, ...data } : data))
+ setPresentationSuccess(true)
+ setTimeout(() => setPresentationSuccess(false), 3000)
+ } catch (err: any) {
+ setError(err instanceof Error ? err.message : 'Failed to save presentation settings')
+ } finally {
+ setSavingPresentation(false)
+ }
+ }
+
const syncNativePayload = (field: string, value: unknown) => {
setSectionPayload((prev) => {
const next = { ...prev, [field]: value }
@@ -472,462 +547,1096 @@ export default function CategoryCmsPage() {
)}
- {/* Top 4 Stats Cards */}
-
-
-
Total Blocks
-
{detailSections.length}
-
-
-
Native Blocks
-
{nativeSectionCount}
-
-
-
Hidden
-
{inactiveSectionCount}
-
-
-
Mode
-
{selectedSectionId === 'new' ? 'Create block' : 'Edit selected block'}
+ {/* Top Workspace Mode Tabs: Category Presentation & Hybrid CMS (Wide Full) vs Dynamic Page Builder */}
+
+
+
+
+
+ {/* Tab Quick Action */}
+ {cmsTab === 'presentation' && (
+
+ )}
- {/* Main Page Builder Section */}
-
- {/* Section Header with Language Switcher and Add Block Button */}
-
-
-
-
-
Category Detail Page Builder
-
Start with native blocks. Use content blocks only when this category needs extra storytelling.
+ {/* Main Tab Content */}
+ {cmsTab === 'presentation' ? (
+
+ {/* Presentation Feedback */}
+ {presentationSuccess && (
+
+
+
บันทึกการตั้งค่า Category Presentation & Hybrid CMS สำเร็จ!
-
-
-
- {languages.map((lang) => (
-
- ))}
-
- {detailSections.length === 0 && (
-
- )}
-
-
-
+ )}
- {/* Native Category Blocks Quick Bar */}
-
-
-
-
-
- Native Category Blocks
-
-
These blocks render live category data and are the recommended starting point.
-
-
- {NATIVE_BLOCKS.map(({ type, label, icon: Icon }) => (
-
- ))}
-
-
-
-
- {/* 2-Column Section Order & Block Form */}
-
- {/* Left Column: Section Order */}
-
-
-
-
Section Order
-
Move blocks to control the category detail page layout.
-
-
-
-
- {detailSections.length === 0 && (
-
-
-
No category CMS blocks yet.
-
Seed the standard layout or choose a native block above.
-
- )}
-
- {detailSections.map((section, index) => (
-
-
-
-
-
-
-
-
+ {/* Card 1: Category Layout & Presentation Archetypes in Wide View */}
+
+
-
- {/* Right Column: Editing Block */}
-
-
-
-
- {selectedSectionId === 'new' ? 'New Block' : 'Editing Block'}
-
-
{SECTION_LABELS[sectionType] || sectionType}
-
- {SECTION_HELP[sectionType] || 'Configure the section payload for this category detail page.'}
-
+
+ Category Layout & Presentation Archetype
+
+ Wide Full
+
+
+
เลือกโครงสร้างส่วนหัวและการแสดงผลสินค้าบน Storefront & Website
-
- {isTranslatableBlock(sectionType) && (
- <>
-
-
- >
- )}
- {selectedSection?.isLocked && (
-
-
- Locked
+
+
+ {layoutType === 'none' ? 'none (ปิดส่วนหัว)' : layoutType}
+
+
+
+
+ {/* Option 1: Split Showcase */}
+
setLayoutType('split_showcase')}
+ className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
+ layoutType === 'split_showcase'
+ ? 'bg-cyan-glow/10 border-cyan-glow shadow-lg shadow-cyan-glow/10'
+ : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
+ }`}
+ >
+
+
+
+
+
+
+
+ Split Showcase
+
+ {layoutType === 'split_showcase' && (
+
+ )}
+
+
ค่าเริ่มต้น (Balanced)
+
+ หัวข้อแบ่ง 2 ฝั่ง (ข้อความซ้าย + รูปเด่นขวา) พร้อมการจัดแสดงสินค้า 3-4 คอลัมน์มาตรฐาน
+
+
-
-
-
-
-
+ {/* Option 2: Editorial Hero */}
+
setLayoutType('editorial_hero')}
+ className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
+ layoutType === 'editorial_hero'
+ ? 'bg-amber-500/10 border-amber-400 shadow-lg shadow-amber-500/10'
+ : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
+ }`}
+ >
+
+
-
+
+
+
+
+ Editorial Hero
+
+ {layoutType === 'editorial_hero' && (
+
+ )}
+
+
Cinematic & Luxury
+
+ Hero Banner เต็มจอสไตล์นิตยสารหรูหรา พร้อมการ์ดสินค้าขนาดใหญ่ 2-3 คอลัมน์
+
+
+
+
+
+ {/* Option 3: Dense Catalog */}
+
setLayoutType('dense_catalog')}
+ className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
+ layoutType === 'dense_catalog'
+ ? 'bg-purple-500/10 border-purple-400 shadow-lg shadow-purple-500/10'
+ : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
+ }`}
+ >
+
+
+
+
+
+
+
+ Dense Catalog
+
+ {layoutType === 'dense_catalog' && (
+
+ )}
+
+
High Density Shopper
+
+ Header ย่อกะทัดรัด พร้อมตารางสินค้าความหนาแน่นสูง 4-5 คอลัมน์ เหมาะสำหรับสินค้าจำนวนมาก
+
+
+
+
+
+ {/* Option 4: None / ปิดส่วนหัว */}
+
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]'
+ }`}
+ >
+
+
+
+
+
+
+
+ ปิด (No Header)
+
+ {layoutType === 'none' && (
+
+ )}
+
+
Clean & Streamlined
+
+ ไม่แสดง Archetype Header ด้านบน เพื่อเริ่มแสดง Featured Series หรือสินค้าทันที ลดความซ้ำซ้อน
+
+
+
+
+
+
+
+ {/* Card 2: CategoryHybridCmsEditor in Wide Full */}
+
{
+ openMediaPicker((url) => {
+ if (target === 'featured_image') {
+ setFeaturedSeries((prev) => ({ ...prev, imageUrl: url }))
+ } else if (target === 'featured_action') {
+ setFeaturedSeries((prev) => ({ ...prev, actionImageUrl: url }))
+ }
+ })
+ }}
+ categoryId={id ? parseInt(id, 10) : undefined}
+ categoryName={categoryName}
+ />
+
+ {/* Sticky Bottom Save Bar */}
+
+
+
+
+
+
+
Save Presentation & Hybrid CMS Settings
+
บันทึก Layout Archetype, แบนเนอร์ Featured Series, การ์ด USP Benefits และตัวกรอง
- {/* Block Form Container */}
-
- {CATEGORY_NATIVE_SECTION_TYPES.has(sectionType) ? (
-
-
-
Native Category Settings
-
These controls affect headers, navigation, and presentation. Live products come from this category.
-
-
- {sectionType === 'CATEGORY_HEADER' && (
-
- )}
-
- {sectionType === 'CATALOG_GRID' && (
-
- )}
-
- {sectionType === 'USP_BAR' && (
-
-
-
Renders the 3 USP benefit columns configured for this category.
-
- )}
-
- ) : (
-
-
-
- Category Content Block
-
-
These forms follow the Page Builder block design, but stay scoped to this category.
-
-
- {sectionType === 'HERO_CAROUSEL' && (
-
{
- setSectionPayload(nextPayload)
- setSectionJson(JSON.stringify(nextPayload, null, 2))
- }}
- onBrowseMedia={openMediaPicker}
- />
- )}
-
- {sectionType === 'DIVIDER' && (
- {
- setSectionPayload(nextPayload)
- setSectionJson(JSON.stringify(nextPayload, null, 2))
- }}
- />
- )}
-
+
+ {presentationSuccess && (
+
+
+ บันทึกสำเร็จ!
+
)}
+
+
+
+
+ ) : (
+ /* Page Builder Blocks Tab */
+
+ {/* Top 4 Stats Cards */}
+
+
+
Total Blocks
+
{detailSections.length}
+
+
+
Native Blocks
+
{nativeSectionCount}
+
+
+
Hidden
+
{inactiveSectionCount}
+
+
+
Mode
+
{selectedSectionId === 'new' ? 'Create block' : 'Edit selected block'}
+
+
- {/* Bottom Save Bar */}
-
- {sectionSaveError && (
-
+ {/* Main Page Builder Section */}
+
+ {/* Section Header with Language Switcher and Add Block Button */}
+
+
+
+
+
Category Detail Page Builder
+
Start with native blocks. Use content blocks only when this category needs extra storytelling.
+
+
+
+
+ {languages.map((lang) => (
+
+ ))}
+
+ {detailSections.length === 0 && (
+
)}
-
-
+
+ {/* Native Category Blocks Quick Bar */}
+
+
+
+
+
+ Native Category Blocks
+
+
These blocks render live category data and are the recommended starting point.
+
+
+ {NATIVE_BLOCKS.map(({ type, label, icon: Icon }) => (
+
+ ))}
+
+
+
+
+ {/* 2-Column Section Order & Block Form */}
+
+ {/* Left Column: Section Order */}
+
+
+
+
Section Order
+
Move blocks to control the category detail page layout.
+
+
+
+
+ {detailSections.length === 0 && (
+
+
+
No category CMS blocks yet.
+
Seed the standard layout or choose a native block above.
+
+ )}
+
+ {detailSections.map((section, index) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Right Column: Editing Block */}
+
+
+
+
+
+ {selectedSectionId === 'new' ? 'New Block' : 'Editing Block'}
+
+
{SECTION_LABELS[sectionType] || sectionType}
+
+ {SECTION_HELP[sectionType] || 'Configure the section payload for this category detail page.'}
+
+
+
+ {isTranslatableBlock(sectionType) && (
+ <>
+
+
+ >
+ )}
+ {selectedSection?.isLocked && (
+
+
+ Locked
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Block Form Container */}
+
+ {CATEGORY_NATIVE_SECTION_TYPES.has(sectionType) ? (
+
+
+
Native Category Settings
+
These controls affect headers, navigation, and presentation. Live products come from this category.
+
+
+ {sectionType === 'CATEGORY_HEADER' && (
+
+ {/* Layout Archetype banner with shortcut */}
+
+
+
+ Active Layout Archetype:
+
+ {layoutType}
+
+
+
+ เลือก Split Showcase, Editorial Hero, Dense Catalog หรือ ปิดส่วนหัว ได้ที่แท็บ Presentation
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {sectionType === 'CATALOG_GRID' && (
+
+ {/* Facet filter shortcut */}
+
+
+
+
+ Filter Facets & Product Attributes
+
+
+ ปรับแต่งตัวกรองแถบข้าง (Series, Type, Color, Fitment, Price, Stock) และจัดการ Attributes
+
+
+
+
+
+
+
+
+
+
+ {/* Container Width (Standard, Wide, Full) */}
+
+
+
+
+ {(sectionPayload.containerWidth ?? filterConfig.containerWidth) === 'full' || (sectionPayload.fullWidth ?? filterConfig.fullWidth)
+ ? 'Full Width (1920px+)'
+ : (sectionPayload.containerWidth ?? filterConfig.containerWidth) === 'wide'
+ ? 'Wide (1600px)'
+ : 'Standard (1280px)'}
+
+
+
+ {[
+ { val: 'standard', label: 'Standard Boxed', desc: '1280px กึ่งกลาง' },
+ { val: 'wide', label: 'Wide (1600px)', desc: 'ขยายกว้าง 1600px' },
+ { val: 'full', label: 'Full Width Fluid', desc: 'เต็มจอ 100% (1920px+)' },
+ ].map((item) => {
+ const currentWidth = (sectionPayload.containerWidth ?? filterConfig.containerWidth) ||
+ ((sectionPayload.fullWidth ?? filterConfig.fullWidth) ? 'full' : 'standard')
+ const isSelected = currentWidth === item.val
+ return (
+
+ )
+ })}
+
+
+
+ {/* Product Card Presentation (Card Style, Density & Elements) */}
+
+
+
+
+ Product Card Presentation (รูปแบบและการแสดงผลการ์ดสินค้า)
+
+
+ ปรับแต่งสไตล์การ์ด ขนาด และองค์ประกอบปุ่มการสั่งซื้อ (Parity กับ CMS Product Carousel)
+
+
+
+ {/* Card Style Selector */}
+
+
+
+ {[
+ { val: 'standard', label: 'Standard Retail', desc: 'กรอบ 1:1 Boxed พร้อมปุ่มเพิ่มลงตะกร้า' },
+ { val: 'minimal', label: 'Fashion Minimal', desc: 'กรอบแนวตั้ง 3:4 Portrait โหมด Lookbook' },
+ { val: 'swatch', label: 'Editorial Swatch', desc: 'การ์ดโชว์กลิ่นโน้ต/สี Fragrance Swatches' },
+ ].map((item) => {
+ const currentStyle = (sectionPayload.cardStyle ?? filterConfig.cardStyle) || 'standard'
+ const isSelected = currentStyle === item.val
+ return (
+
+ )
+ })}
+
+
+
+ {/* Card Size / Density */}
+
+
+
+ {[
+ { val: 'compact', label: 'Compact Dense', desc: 'กะทัดรัด เห็นของเยอะ' },
+ { val: 'medium', label: 'Medium Standard', desc: 'สมดุลมาตรฐาน' },
+ { val: 'large', label: 'Large Editorial', desc: 'ใหญ่ ชัดเจน โดดเด่น' },
+ ].map((item) => {
+ const currentSize = (sectionPayload.cardSize ?? filterConfig.cardSize) || 'medium'
+ const isSelected = currentSize === item.val
+ return (
+
+ )
+ })}
+
+
+
+ {/* Quick Element Toggles */}
+
+
+
+ {/* Card Columns Selector */}
+
+
+
+ {[
+ { val: undefined, label: 'Auto (ตามธีม)' },
+ { val: 2, label: '2 Cols (Wide การ์ดใหญ่)' },
+ { val: 3, label: '3 Cols (Standard)' },
+ { val: 4, label: '4 Cols (Wide)' },
+ { val: 5, label: '5 Cols (Dense)' },
+ ].map((item) => {
+ const isSelected = (sectionPayload.gridColumns ?? filterConfig.gridColumns) === item.val
+ return (
+
+ )
+ })}
+
+
+
+ {/* Filter Position */}
+
+
+
+ {[
+ { val: 'sidebar', label: 'Sidebar (แถบแนวตั้งด้านซ้าย)' },
+ { val: 'top_bar', label: 'Top Bar (แนวนอนด้านบน Wide Ribbon)' },
+ ].map((item) => {
+ const isSelected = ((sectionPayload.filterPosition ?? filterConfig.filterPosition) || 'sidebar') === item.val
+ return (
+
+ )
+ })}
+
+
+
+
+
+ syncNativePayload('itemsPerPage', parseInt(e.target.value, 10) || 24)}
+ className="w-40 px-4 py-3 bg-night-950 border border-white/10 focus:border-cyan-glow/40 rounded-xl outline-none text-sm text-white transition-all"
+ />
+
+
+ )}
+
+ {sectionType === 'USP_BAR' && (
+
+
+
+ {/* Shortcut to USP cards in presentation */}
+
+
+
+
+ Configured USP Benefits ({uspBenefits.length} รายการ)
+
+
+ แก้ไขไอคอน, หัวข้อ และคำอธิบายการ์ดคุณสมบัติเด่นของหมวดหมู่ได้ที่แท็บ Presentation
+
+
+
+
+
+ )}
+
+ ) : (
+
+
+
+ Category Content Block
+
+
These forms follow the Page Builder block design, but stay scoped to this category.
+
+
+ {sectionType === 'HERO_CAROUSEL' && (
+
{
+ setSectionPayload(nextPayload)
+ setSectionJson(JSON.stringify(nextPayload, null, 2))
+ }}
+ onBrowseMedia={openMediaPicker}
+ />
+ )}
+
+ {sectionType === 'DIVIDER' && (
+ {
+ setSectionPayload(nextPayload)
+ setSectionJson(JSON.stringify(nextPayload, null, 2))
+ }}
+ />
+ )}
+
+ )}
+
+ {/* Bottom Save Bar */}
+
+ {sectionSaveError && (
+
+ )}
+
+
+
+
+
+
+
-
+ )}
{/* Media Library Modal */}
('role')
-
// Media Library Modal
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
const [activeMediaRole, setActiveMediaRole] = useState('thumbnail')
@@ -470,11 +464,11 @@ export default function CategoryEditPage() {
{!isNew && (
- CMS Blocks
+ CMS & Presentation
)}
@@ -803,212 +797,78 @@ export default function CategoryEditPage() {
)}
- {/* Card: Category Layout Archetype */}
-
-
-
-
-
-
-
-
Category Layout & Presentation
-
Choose how this category displays on Storefront & Website
-
-
-
- {layoutType === 'none' ? 'none (ปิดส่วนหัว)' : layoutType}
-
-
-
-
- {/* Option 1: Split Showcase */}
-
setLayoutType('split_showcase')}
- className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
- layoutType === 'split_showcase'
- ? 'bg-cyan-glow/10 border-cyan-glow shadow-lg shadow-cyan-glow/10'
- : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
- }`}
- >
-
- {/* Visual Layout Mockup */}
-
-
-
+ {/* Card: Category Presentation & CMS Blocks Overview */}
+ {!isNew ? (
+
+
+
+
+
-
-
-
-
- Split Showcase
-
- {layoutType === 'split_showcase' && (
-
- )}
-
-
ค่าเริ่มต้น (Balanced)
-
- หัวข้อแบ่ง 2 ฝั่ง (ข้อความซ้าย + รูปเด่นขวา) พร้อมการจัดแสดงสินค้า 3-4 คอลัมน์มาตรฐาน
+
+ Category Presentation & CMS Blocks
+
+ CMS Page
+
+
+
+ ปรับแต่ง Layout Archetype, แบนเนอร์ Featured Series, USP Benefits และ Dynamic Blocks ในหน้า CMS แบบเต็มจอ (Wide Full)
+
+
+
+
จัดการ Presentation & CMS Blocks
+
+
- {/* Option 2: Editorial Hero */}
-
setLayoutType('editorial_hero')}
- className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
- layoutType === 'editorial_hero'
- ? 'bg-amber-500/10 border-amber-400 shadow-lg shadow-amber-500/10'
- : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
- }`}
- >
-
- {/* Visual Layout Mockup */}
-
-
-
-
-
-
- Editorial Hero
-
- {layoutType === 'editorial_hero' && (
-
- )}
-
-
Cinematic & Luxury
-
- Hero Banner เต็มจอสไตล์นิตยสารหรูหรา พร้อมการ์ดสินค้าขนาดใหญ่ 2-3 คอลัมน์
-
-
+ {/* Status Chips */}
+
+
+
Layout
+
+ {categoryMeta?.layoutType || layoutType || 'split_showcase'}
+
-
-
- {/* Option 3: Dense Catalog */}
-
setLayoutType('dense_catalog')}
- className={`relative p-5 rounded-2xl border-2 transition-all cursor-pointer flex flex-col justify-between group ${
- layoutType === 'dense_catalog'
- ? 'bg-purple-500/10 border-purple-400 shadow-lg shadow-purple-500/10'
- : 'bg-white/[0.02] border-white/10 hover:border-white/20 hover:bg-white/[0.04]'
- }`}
- >
-
- {/* Visual Layout Mockup */}
-
-
-
-
-
-
- Dense Catalog
-
- {layoutType === 'dense_catalog' && (
-
- )}
-
-
High Density Shopper
-
- Header ย่อกะทัดรัด พร้อมตารางสินค้าความหนาแน่นสูง 4-5 คอลัมน์ เหมาะสำหรับสินค้าจำนวนมาก
-
-
+
+
Featured Series
+
+ {categoryMeta?.featuredSeries?.enabled ? 'เปิดใช้งาน' : 'ปิดอยู่'}
+
-
-
- {/* Option 4: None / ปิดส่วนหัว */}
-
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]'
- }`}
- >
-
- {/* Visual Layout Mockup */}
-
-
-
-
-
-
- ปิด (No Header)
-
- {layoutType === 'none' && (
-
- )}
-
-
Clean & Streamlined
-
- ไม่แสดง Archetype Header ด้านบน เพื่อเริ่มแสดง Featured Series หรือสินค้าทันที ลดความซ้ำซ้อน
-
-
+
+
USP Benefits
+
+ {(categoryMeta?.uspBenefits as any[])?.length || (uspBenefits as any[])?.length || 0} รายการ
+
+
+
+
CMS Builder
+
+
Page Blocks
+
+
-
-
- {/* Hybrid CMS Presentation Editor */}
-
{
- setMediaTarget(target)
- setIsMediaModalOpen(true)
- }}
- categoryId={!isNew && id ? parseInt(id) : undefined}
- categoryName={name.th || name.en || 'Category'}
- />
+ ) : (
+
+
+
Category Presentation & CMS Blocks
+
+ บันทึกสร้างหมวดหมู่นี้ก่อน จากนั้นจะสามารถไปปรับแต่ง Presentation & Dynamic CMS Blocks ได้ที่หน้า CMS
+
+
+ )}
{/* Card 3: Associated Products & Quick Link */}
{!isNew && categoryMeta && (
@@ -1046,7 +906,6 @@ export default function CategoryEditPage() {
uploadingRole={uploadingRole}
onChange={setMedia}
onBrowse={(role) => {
- setMediaTarget('role')
setActiveMediaRole(role)
setIsMediaModalOpen(true)
}}
@@ -1085,13 +944,7 @@ export default function CategoryEditPage() {
isOpen={isMediaModalOpen}
onClose={() => setIsMediaModalOpen(false)}
onSelect={(url) => {
- if (mediaTarget === 'featured_image') {
- setFeaturedSeries(prev => ({ ...prev, imageUrl: url }))
- } else if (mediaTarget === 'featured_action') {
- setFeaturedSeries(prev => ({ ...prev, actionImageUrl: url }))
- } else {
- setMediaUrl(activeMediaRole, url)
- }
+ setMediaUrl(activeMediaRole, url)
setIsMediaModalOpen(false)
}}
/>
diff --git a/apps/website/src/app/[lang]/category/[id]/page.tsx b/apps/website/src/app/[lang]/category/[id]/page.tsx
index a79fde6..ae40575 100644
--- a/apps/website/src/app/[lang]/category/[id]/page.tsx
+++ b/apps/website/src/app/[lang]/category/[id]/page.tsx
@@ -3,7 +3,7 @@ import Image from 'next/image'
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import { generateSeoMetadata } from '@/lib/seo'
-import type { Category, Product, AttributeDefinition, CategorySection, PageSection } from '@homaroma/types'
+import type { Category, Product, AttributeDefinition, CategorySection, PageSection, CategoryFilterConfig } from '@homaroma/types'
import { getCategoryImage, getCategoryMedia, resolveCategoryMediaUrl } from '@homaroma/types'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
@@ -14,6 +14,9 @@ import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult,
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { resolveLocalized } from '@/lib/localization'
+export const dynamic = 'force-dynamic'
+export const revalidate = 0
+
function toPageSection(section: CategorySection): PageSection {
return {
id: section.id,
@@ -126,22 +129,65 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
const hasArtDirectedHero = Boolean(category.media?.some((item) => item.role === 'hero_desktop' || item.role === 'hero_mobile'))
const layoutType = category.layoutType || 'split_showcase'
- // Determine Product Grid Class based on layout archetype
- const getProductGridClass = () => {
+ // Determine Product Grid Class based on layout archetype, cardSize, and width mode
+ const getProductGridClass = (config?: CategoryFilterConfig, isFullWidth?: boolean) => {
+ // 1. Explicit grid columns on section block
+ const explicitCols = config?.gridColumns
+ if (explicitCols === 2) {
+ return 'grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8'
+ }
+ if (explicitCols === 3) {
+ return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8'
+ }
+ if (explicitCols === 4) {
+ return 'grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-6'
+ }
+ if (explicitCols === 5) {
+ return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-5'
+ }
+
+ // 2. Card Size / Density setting on section block (large, compact, medium)
+ const cardSize = config?.cardSize ?? category.filterConfig?.cardSize
+ if (cardSize === 'compact') {
+ return isFullWidth
+ ? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 md:gap-4'
+ : 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 md:gap-4'
+ }
+ if (cardSize === 'large') {
+ return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 md:gap-8'
+ }
+
+ // 3. Fallback to category-level grid columns
+ const categoryCols = category.filterConfig?.gridColumns
+ if (categoryCols === 2) return 'grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8'
+ if (categoryCols === 3) return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8'
+ if (categoryCols === 4) return 'grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-6'
+ if (categoryCols === 5) return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-5'
+
switch (layoutType) {
case 'editorial_hero':
- return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10'
+ return isFullWidth
+ ? 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-6 md:gap-8'
+ : 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10'
case 'dense_catalog':
- return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 md:gap-6'
+ return isFullWidth
+ ? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 md:gap-5'
+ : 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 md:gap-6'
case 'split_showcase':
default:
- return 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8'
+ return isFullWidth
+ ? 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 md:gap-6'
+ : 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8'
}
}
const renderHeaderSection = (payload?: Record) => {
const showPills = payload?.showCategoryNav !== false && category.filterConfig?.showCategoryNav !== false
const showBreadcrumb = payload?.showBreadcrumbs !== false
+ const isFullWidth = Boolean(category.filterConfig?.fullWidth || category.featuredSeries?.fullWidth)
+ const headerContainerClass = isFullWidth
+ ? 'mx-auto w-full max-w-[1920px] px-4 sm:px-6 lg:px-8'
+ : 'mx-auto max-w-7xl px-6'
return (
@@ -201,7 +247,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
) : layoutType === 'dense_catalog' ? (
/* ── Archetype 3: Dense Catalog Layout ── */
-
+
{showBreadcrumb && (
@@ -229,7 +275,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
) : (
/* ── Archetype 1: Split Showcase Layout (Default) ── */
-
+
{showBreadcrumb && (
@@ -261,7 +307,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
)}
{layoutType === 'none' && showBreadcrumb && (
-
+