From 173c5837875830d37f64bb3f72badd0fee9bdcfd Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Jul 2026 06:37:42 -0400 Subject: [PATCH] x --- apps/admin/src/App.tsx | 2 + .../page-builder/ProductLayoutBlockForm.tsx | 87 ++ apps/admin/src/pages/PageBuilderPage.tsx | 51 + apps/admin/src/pages/ProductCmsPage.tsx | 1274 +++++++++++++++++ apps/admin/src/pages/ProductsPage.tsx | 11 +- .../migration.sql | 23 + apps/backend/prisma/schema.prisma | 17 + apps/backend/prisma/seed-pages.js | 41 +- apps/backend/prisma/seed-pages.ts | 39 +- apps/backend/src/routes/pages.ts | 10 +- apps/backend/src/routes/products.ts | 246 ++++ apps/backend/src/routes/settings.ts | 7 +- apps/mcp-server/section-toolkit.ts | 16 + apps/spa/src/pages/ProductDetailPage.tsx | 502 ++++--- apps/website/next.config.ts | 6 + apps/website/src/app/[lang]/layout.tsx | 63 +- .../src/app/[lang]/products/[slug]/page.tsx | 508 +++++-- apps/website/src/app/layout.tsx | 34 + .../src/components/AddToCartSection.tsx | 60 +- .../components/ProductStickyPurchaseBar.tsx | 145 ++ apps/website/src/lib/cms.ts | 22 +- packages/types/index.ts | 47 +- 22 files changed, 2847 insertions(+), 364 deletions(-) create mode 100644 apps/admin/src/components/page-builder/ProductLayoutBlockForm.tsx create mode 100644 apps/admin/src/pages/ProductCmsPage.tsx create mode 100644 apps/backend/prisma/migrations/20260716002554_add_product_detail_sections/migration.sql create mode 100644 apps/website/src/app/layout.tsx create mode 100644 apps/website/src/components/ProductStickyPurchaseBar.tsx 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.

+
+
+ +