diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 243488e..a9ba6e6 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -4,6 +4,7 @@ import DashboardPage from './pages/DashboardPage' import ProductsPage from './pages/ProductsPage' import ProductEditPage from './pages/ProductEditPage' import ProductBomPage from './pages/ProductBomPage' +import ProductCmsPage from './pages/ProductCmsPage' import CategoriesPage from './pages/CategoriesPage' import CustomersPage from './pages/CustomersPage' import PageBuilderPage from './pages/PageBuilderPage' @@ -57,6 +58,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin/src/components/page-builder/ProductLayoutBlockForm.tsx b/apps/admin/src/components/page-builder/ProductLayoutBlockForm.tsx new file mode 100644 index 0000000..a1d8267 --- /dev/null +++ b/apps/admin/src/components/page-builder/ProductLayoutBlockForm.tsx @@ -0,0 +1,87 @@ +import { type PageSection } from '@homaroma/types'; + +interface ProductLayoutBlockFormProps { + editingSection: Partial; + getLocalizedValue: (payload: unknown, field: string) => string; + updateLocalizedValue: (field: string, value: string) => void; + updatePayloadValue: (field: string, value: unknown) => void; +} + +const BLOCK_HELP: Record = { + PRODUCT_MAIN: 'Renders the product gallery and purchase panel from live product data.', + PRODUCT_DETAILS: 'Renders the product details markdown from the selected product.', + PRODUCT_SPECS: 'Renders product attributes and specifications when available.', + PRODUCT_REVIEWS: 'Renders customer reviews for the selected product.', + RELATED_PRODUCTS: 'Renders products from the same category as the current product.', +}; + +export default function ProductLayoutBlockForm({ + editingSection, + getLocalizedValue, + updateLocalizedValue, + updatePayloadValue, +}: ProductLayoutBlockFormProps) { + const type = editingSection.type?.toUpperCase() || ''; + const canShowViewAll = type === 'RELATED_PRODUCTS'; + + return ( +
+
+

+ Product Detail Native Block +

+

+ {BLOCK_HELP[type] || 'This block renders live product detail content on the website.'} +

+
+ +
+
+ + updateLocalizedValue('title', event.target.value)} + className="admin-input w-full" + placeholder="Leave blank to use the default label" + /> +
+ +
+ + updateLocalizedValue('subtitle', event.target.value)} + className="admin-input w-full" + placeholder="Optional supporting text" + /> +
+
+ +
+ + + {canShowViewAll && ( + + )} +
+
+ ); +} diff --git a/apps/admin/src/pages/PageBuilderPage.tsx b/apps/admin/src/pages/PageBuilderPage.tsx index 51934fb..d04c907 100644 --- a/apps/admin/src/pages/PageBuilderPage.tsx +++ b/apps/admin/src/pages/PageBuilderPage.tsx @@ -39,6 +39,15 @@ import ContactFormForm from '../components/page-builder/ContactFormForm'; import HeroV2Form from '../components/page-builder/HeroV2Form'; import UspBarForm from '../components/page-builder/UspBarForm'; import CustomBlockForm from '../components/page-builder/CustomBlockForm'; +import ProductLayoutBlockForm from '../components/page-builder/ProductLayoutBlockForm'; + +const PRODUCT_LAYOUT_BLOCK_TYPES = new Set([ + 'PRODUCT_MAIN', + 'PRODUCT_DETAILS', + 'PRODUCT_SPECS', + 'PRODUCT_REVIEWS', + 'RELATED_PRODUCTS', +]); interface Brand { id: number; @@ -650,6 +659,34 @@ export default function PageBuilderPage() { showAddToCart: true, }; break; + case 'PRODUCT_MAIN': + defaultPayload = {}; + break; + case 'PRODUCT_DETAILS': + defaultPayload = { + title: 'Product Details', + showHeading: true, + }; + break; + case 'PRODUCT_SPECS': + defaultPayload = { + title: 'Specifications', + showHeading: true, + }; + break; + case 'PRODUCT_REVIEWS': + defaultPayload = { + title: 'Reviews', + showHeading: true, + }; + break; + case 'RELATED_PRODUCTS': + defaultPayload = { + title: 'Related Products', + showHeading: true, + showViewAllLink: true, + }; + break; case 'ACCORDION': defaultPayload = { title: 'Frequently Asked Questions', @@ -1040,6 +1077,11 @@ export default function PageBuilderPage() { + + + + + @@ -1268,6 +1310,15 @@ export default function PageBuilderPage() { /> )} + {PRODUCT_LAYOUT_BLOCK_TYPES.has(editingSection.type?.toUpperCase() || '') && ( + + )} +
+ +
+ + + {success && ( +
+ +

Product CMS updated successfully.

+
+ )} + + {error && ( +
+ +

{error}

+
+ )} + +
+
+

Total Blocks

+

{detailSections.length}

+
+
+

Native Blocks

+

{nativeSectionCount}

+
+
+

Hidden

+

{inactiveSectionCount}

+
+
+

Mode

+

{selectedSectionId === 'new' ? 'Create block' : 'Edit selected block'}

+
+
+ +
+
+
+ +
+

Product Detail Page Builder

+

Start with native blocks. Use content blocks only when this product needs extra storytelling.

+
+
+
+
+ {languages.map((lang) => ( + + ))} +
+ {detailSections.length === 0 && ( + + )} + +
+
+ +
+
+
+

+ + Native Product Blocks +

+

These blocks render live product data and are the recommended starting point.

+
+
+ {NATIVE_BLOCKS.map(({ type, icon: Icon }) => ( + + ))} +
+
+
+ +
+
+
+
+

Section Order

+

Move blocks to control the product detail page layout.

+
+ +
+ + {detailSections.length === 0 && ( +
+ +

No product CMS blocks yet.

+

Seed the standard layout or choose a native block above.

+
+ )} + + {detailSections.map((section, index) => ( +
+ + +
+ + + + +
+
+ ))} +
+ +
+
+
+
+

+ {selectedSectionId === 'new' ? 'New Block' : 'Editing Block'} +

+

{SECTION_LABELS[sectionType] || sectionType}

+

+ {SECTION_HELP[sectionType] || 'Configure the section payload for this product detail page.'} +

+
+ {selectedSection?.isLocked && ( +
+ + Locked +
+ )} +
+ +
+
+ + +
+ + +
+
+ +
+ {PRODUCT_NATIVE_SECTION_TYPES.has(sectionType) ? ( +
+
+

Native Settings

+

These controls affect headings and presentation. Product data still comes from the product record.

+
+
+
+ + syncNativeLocalizedPayload('title', e.target.value)} + className="w-full 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" + placeholder="Leave blank to use default" + /> +
+
+ + syncNativeLocalizedPayload('subtitle', e.target.value)} + className="w-full 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" + placeholder="Optional" + /> +
+
+
+ + {sectionType === 'RELATED_PRODUCTS' && ( + + )} + {sectionType === 'PRODUCT_MAIN' && ( +
+
+ +
+

Product Main Behavior

+

Product data is managed on the Edit Product page. These controls only change the purchase experience.

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
+ )} + {sectionType === 'PRODUCT_REVIEWS' && ( +
+ + Reviews are pulled from published product reviews. +
+ )} +
+
+ ) : ( +
+
+

+ + Product Content Block +

+

These forms follow the Page Builder block design, but stay scoped to this product.

+
+ {sectionType === 'HERO' && ( +
+
+ + +
+ + {typeof sectionPayload.imageUrl === 'string' && sectionPayload.imageUrl && ( +
+ updatePayloadValue('focalPoint', focalPoint)} + /> +
+ )} + +
+ + updateLocalizedValue('tagline', event.target.value)} className="admin-input w-full" /> +
+
+ + updateLocalizedValue('title', event.target.value)} className="admin-input w-full" /> +

Use [brackets] for accent serif text.

+
+
+ +