Add React editor project

This commit is contained in:
Rami Bitar
2026-08-09 16:00:10 -04:00
parent 909d99251a
commit 63ecc5e284
212 changed files with 20655 additions and 11571 deletions
+7
View File
@@ -0,0 +1,7 @@
import PageEditor from '@/components/page-editor';
// Editor for /collections/[handle]. Sitting under the route it edits means the preview
// resolves the same params the public page gets.
export default function EditorPage() {
return <PageEditor routeKey="/collections/[handle]" />;
}
+16 -172
View File
@@ -1,194 +1,38 @@
{
"root": {
"props": {
"title": "Collection — Pulse",
"headerFont": "Archivo Black",
"headerFontWeight": "400",
"bodyFont": "Inter",
"primaryColor": "#111111",
"secondaryColor": "#707072",
"accentColor": "#f5f5f5",
"bgColor": "#ffffff",
"fgColor": "#111111",
"mutedColor": "#f5f5f5",
"radius": "xl",
"buttonRadius": "full",
"shadow": "none",
"maxWidth": "2xl"
"title": "Collection",
"description": "Shop the collection.",
"ogImage": ""
}
},
"content": [
{
"type": "header",
"props": {
"id": "nav-collection",
"brand": "PULSE",
"links": [
{
"label": "Run",
"href": "/collections/run"
},
{
"label": "Train",
"href": "/collections/train"
},
{
"label": "Recover",
"href": "/collections/recover"
},
{
"label": "Sale",
"href": "/collections/sale"
},
{
"label": "About",
"href": "/about"
}
],
"showSearch": "yes",
"showAccount": "yes",
"showCart": "yes",
"sticky": "yes",
"tone": "default"
}
"id": "header"
},
"synced": true
},
{
"type": "collection",
"type": "collection-detail",
"props": {
"id": "collection-detail",
"collection": null,
"showDescription": "yes",
"showCoverImage": "yes",
"customCoverImage": "",
"columns": "4",
"limit": 24,
"defaultSort": "BEST_SELLING",
"showAvailability": "yes",
"showPriceRange": "yes",
"showProductType": "yes",
"productTypeOptions": [
{
"label": "Tops"
},
{
"label": "Shorts"
},
{
"label": "Tights"
},
{
"label": "Outerwear"
},
{
"label": "Shoes"
},
{
"label": "Accessories"
}
],
"showVendor": "no",
"vendorOptions": [],
"showTags": "no",
"tagOptions": [],
"showColor": "yes",
"colorOptions": [
{
"label": "Black",
"color": "#111111"
},
{
"label": "White",
"color": "#ffffff"
},
{
"label": "Grey",
"color": "#707072"
},
{
"label": "Volt",
"color": "#d6ff3f"
},
{
"label": "Sodium",
"color": "#fa5400"
}
],
"showStyle": "no",
"styleOptions": [],
"showSize": "yes",
"sizeOptions": [
{
"label": "XS"
},
{
"label": "S"
},
{
"label": "M"
},
{
"label": "L"
},
{
"label": "XL"
},
{
"label": "XXL"
}
],
"showMaterial": "no",
"materialOptions": [],
"metafieldFilters": []
"title": ""
}
},
{
"type": "footer",
"props": {
"id": "footer-collection",
"brand": "PULSE",
"tagline": "Performance gear for athletes who break their kit before they break themselves.",
"columns": [
{
"title": "Shop",
"links": [
{
"label": "All gear",
"href": "/search"
},
{
"label": "New",
"href": "/collections/new"
}
]
},
{
"title": "Help",
"links": [
{
"label": "Shipping",
"href": "/help/shipping"
},
{
"label": "Returns",
"href": "/help/returns"
}
]
}
],
"social": [
{
"label": "Instagram",
"href": "#"
},
{
"label": "Strava",
"href": "#"
}
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
"id": "footer"
},
"synced": true
},
{
"type": "store-assistant",
"props": {
"id": "store-assistant"
}
}
]
+33 -2
View File
@@ -1,5 +1,36 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
import type { Metadata } from 'next';
import PageRender from '@/components/page-render';
import { pageMetadata } from '@/lib/page-metadata';
import { getCollectionProductsPage } from '@/services/shopify/catalog';
import { truncate } from '@/lib/utils';
import page from './page.json';
export async function generateMetadata({
params,
}: {
params: Promise<{ handle: string }>;
}): Promise<Metadata> {
const { handle } = await params;
// No collection-only query exists, so ask for the smallest page of products
// and use just the collection node off it.
const { collection } = await getCollectionProductsPage(handle, {
first: 1,
}).catch(() => ({ collection: null }));
// The blocks fetch client-side and render their own empty state, so a failed
// lookup falls back to the page.json tags rather than a 500.
if (!collection) return pageMetadata(page);
return pageMetadata(page, {
title: collection.title,
description: collection.description
? truncate(collection.description, 160)
: `Shop the ${collection.title} collection.`,
image: collection.image?.url,
path: `/collections/${collection.handle}`,
});
}
export default function Page() {
return <PageRender page={page} />;
+7
View File
@@ -0,0 +1,7 @@
import PageEditor from '@/components/page-editor';
// Editor for /collections. Sitting under the route it edits means the preview
// resolves the same params the public page gets.
export default function EditorPage() {
return <PageEditor routeKey="/collections" />;
}
+39
View File
@@ -0,0 +1,39 @@
{
"root": {
"props": {
"title": "Collections",
"description": "Browse every collection in the store.",
"ogImage": ""
}
},
"content": [
{
"type": "header",
"props": {
"id": "header"
},
"synced": true
},
{
"type": "collections",
"props": {
"id": "collections-grid",
"title": "Our Collections",
"subtitle": "Discover our carefully crafted worlds"
}
},
{
"type": "footer",
"props": {
"id": "footer"
},
"synced": true
},
{
"type": "store-assistant",
"props": {
"id": "store-assistant"
}
}
]
}
+9
View File
@@ -0,0 +1,9 @@
import PageRender from '@/components/page-render';
import { pageMetadata } from '@/lib/page-metadata';
import page from './page.json';
export const metadata = pageMetadata(page, { path: '/collections' });
export default function Page() {
return <PageRender page={page} />;
}