add nextjs style routes

This commit is contained in:
Rami Bitar
2026-06-06 13:46:56 -04:00
parent ef550e9b55
commit aa58af410d
23 changed files with 1183 additions and 855 deletions

View File

@@ -1,705 +0,0 @@
{
"/": {
"root": {
"props": {
"title": "Pulse — Made to Move",
"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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-home",
"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"
}
},
{
"type": "hero",
"props": {
"id": "hero-home",
"tagline": "Spring 2026",
"heading": "Made to move.",
"subheading": "Performance gear engineered for athletes who train like they mean it. Built light. Built honest. Built for the next mile.",
"buttons": [
{ "label": "Shop the kit", "href": "/search", "variant": "primary" },
{ "label": "Our story", "href": "/about", "variant": "secondary" }
],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/athletic-assortment.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "products-carousel",
"props": {
"id": "carousel-home",
"tagline": "Just dropped",
"heading": "New arrivals",
"subheading": "Fresh kit for the season ahead.",
"limit": 12,
"slidesPerView": "4",
"ctaLabel": "Shop new",
"ctaHref": "/collections/new"
}
},
{
"type": "hero",
"props": {
"id": "cover-home-performance",
"tagline": "Performance Series",
"heading": "Engineered to outlast you.",
"subheading": "Tested on track, trail, and treadmill. Every piece earns its place.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "features",
"props": {
"id": "features-home",
"tagline": "Why Pulse",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after."
},
{
"title": "Field tested",
"body": "Twelve weeks. Two race blocks. One full season. Nothing ships without that on the record."
},
{
"title": "Guaranteed",
"body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it."
}
]
}
},
{
"type": "hero",
"props": {
"id": "cover-home-discipline",
"tagline": "Discipline",
"heading": "The work happens before sunrise.",
"subheading": "We make the kit. You do the work. Five-thirty isn't early — it's the only honest hour.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-discipline.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "collection-grid",
"props": {
"id": "collections-home",
"tagline": "Shop by sport",
"heading": "Built for what you actually do.",
"subheading": "",
"layout": "tiles",
"limit": 6
}
},
{
"type": "testimonials",
"props": {
"id": "testimonials-home",
"tagline": "From the field",
"heading": "Athletes on Pulse",
"items": [
{
"quote": "Took the shell to Chamonix and back. Still smells, still works, still cheaper than therapy.",
"author": "Mateo S.",
"role": "Sub-3 marathoner / Boulder",
"avatar": ""
},
{
"quote": "I have one drawer of running gear. The Pulse half is the half I actually wear.",
"author": "Priya N.",
"role": "Ultra runner / Portland",
"avatar": ""
},
{
"quote": "Wore the shorts for an entire 16-week block. They held. That's the whole review.",
"author": "Jonas R.",
"role": "Track coach / Berlin",
"avatar": ""
}
]
}
},
{
"type": "newsletter-cta",
"props": {
"id": "newsletter-home",
"tagline": "Field notes",
"heading": "Weekly notes from the lab.",
"subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.",
"buttonLabel": "Subscribe",
"endpoint": "",
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png",
"layout": "split"
}
},
{
"type": "footer",
"props": {
"id": "footer-home",
"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" },
{ "label": "Run", "href": "/collections/run" },
{ "label": "Train", "href": "/collections/train" },
{ "label": "Sale", "href": "/collections/sale" }
]
},
{
"title": "About",
"links": [
{ "label": "Our story", "href": "/about" },
{ "label": "The lab", "href": "/lab" },
{ "label": "Athletes", "href": "/athletes" }
]
},
{
"title": "Help",
"links": [
{ "label": "Shipping", "href": "/help/shipping" },
{ "label": "Returns", "href": "/help/returns" },
{ "label": "Lifetime guarantee", "href": "/help/guarantee" },
{ "label": "Contact", "href": "/contact" }
]
},
{
"title": "Legal",
"links": [
{ "label": "Terms", "href": "/terms" },
{ "label": "Privacy", "href": "/privacy" }
]
}
],
"social": [
{ "label": "Instagram", "href": "#" },
{ "label": "Strava", "href": "#" },
{ "label": "YouTube", "href": "#" }
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
},
"/products/:handle": {
"root": {
"props": {
"title": "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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-product",
"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"
}
},
{
"type": "product-details",
"props": {
"id": "product-details",
"product": null
}
},
{
"type": "products-carousel",
"props": {
"id": "carousel-related",
"tagline": "Pairs well with",
"heading": "Complete the kit",
"limit": 8,
"slidesPerView": "4",
"ctaLabel": "Shop all",
"ctaHref": "/collections"
}
},
{
"type": "footer",
"props": {
"id": "footer-product",
"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" },
{ "label": "Lifetime guarantee", "href": "/help/guarantee" }
]
}
],
"social": [
{ "label": "Instagram", "href": "#" },
{ "label": "Strava", "href": "#" }
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
},
"/collections/:handle": {
"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"
}
},
"content": [
{
"type": "navigation",
"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"
}
},
{
"type": "collection",
"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": []
}
},
{
"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."
}
}
]
},
"/search": {
"root": {
"props": {
"title": "Shop — 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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-search",
"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"
}
},
{
"type": "search-products",
"props": {
"id": "search-products",
"heading": "All gear.",
"subheading": "Filter by sport, fabric, fit, color. Or sort by what's been beaten the hardest.",
"columns": "4",
"limit": 24,
"showAvailability": "yes",
"showPriceRange": "yes",
"showProductType": "yes",
"showVendor": "no",
"showTags": "yes",
"metafieldFilters": [],
"defaultSort": "BEST_SELLING",
"productTypeOptions": [
{ "label": "Tops" },
{ "label": "Shorts" },
{ "label": "Tights" },
{ "label": "Outerwear" },
{ "label": "Shoes" },
{ "label": "Accessories" }
],
"vendorOptions": [],
"tagOptions": [
{ "label": "New" },
{ "label": "Race day" },
{ "label": "Field tested" },
{ "label": "Sale" }
]
}
},
{
"type": "footer",
"props": {
"id": "footer-search",
"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": "Run", "href": "/collections/run" },
{ "label": "Train", "href": "/collections/train" }
]
},
{
"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."
}
}
]
},
"/about": {
"root": {
"props": {
"title": "About — 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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-about",
"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"
}
},
{
"type": "hero",
"props": {
"id": "hero-about",
"tagline": "The lab",
"heading": "We don't make activewear.",
"subheading": "Pulse started in a converted warehouse with three coaches, two athletes, and a whiteboard full of things they wished worked better. Eight years later, the whiteboard is still there. So is the standard.",
"buttons": [
{ "label": "Shop the kit", "href": "/search", "variant": "primary" }
],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/hero-about.png",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "features",
"props": {
"id": "features-about",
"tagline": "What we believe",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after, if at all."
},
{
"title": "Field tested",
"body": "Twelve weeks of training. Two race blocks. One full season. Nothing leaves the lab without that on the record."
},
{
"title": "Guaranteed",
"body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it. That's the deal."
}
]
}
},
{
"type": "hero",
"props": {
"id": "cover-about-performance",
"tagline": "Used up is the goal",
"heading": "Designed to be worn out, not preserved.",
"subheading": "If you're babying your kit, we did something wrong. Bring it back when it's done — we'll send the next one.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "newsletter-cta",
"props": {
"id": "newsletter-about",
"tagline": "Field notes",
"heading": "Weekly notes from the lab.",
"subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.",
"buttonLabel": "Subscribe",
"endpoint": "",
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png",
"layout": "split"
}
},
{
"type": "footer",
"props": {
"id": "footer-about",
"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" },
{ "label": "Run", "href": "/collections/run" },
{ "label": "Train", "href": "/collections/train" }
]
},
{
"title": "About",
"links": [
{ "label": "Our story", "href": "/about" },
{ "label": "The lab", "href": "/lab" },
{ "label": "Athletes", "href": "/athletes" }
]
},
{
"title": "Help",
"links": [
{ "label": "Shipping", "href": "/help/shipping" },
{ "label": "Returns", "href": "/help/returns" },
{ "label": "Lifetime guarantee", "href": "/help/guarantee" },
{ "label": "Contact", "href": "/contact" }
]
}
],
"social": [
{ "label": "Instagram", "href": "#" },
{ "label": "Strava", "href": "#" },
{ "label": "YouTube", "href": "#" }
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
}
}

View File

@@ -1,17 +0,0 @@
"use client";
import { useParams } from "next/navigation";
import { Render } from "@reacteditor/core";
import { appConfig } from "@/editor.config";
import resolveRoute from "@/lib/resolve-route";
import schema from "@/app.schema.json";
import globals from "@/app.globals.json";
export default function Page() {
const params = useParams();
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
const { key } = resolveRoute(segments);
const { root, content } = (schema as any)[key] ?? {};
const data = { root, content, globals };
return <Render config={appConfig as any} data={data} />;
}

View File

@@ -0,0 +1,6 @@
import PageEditor from "@/components/page-editor";
import page from "../page.json";
export default function EditorPage() {
return <PageEditor page={page} routeKey="/app/about" />;
}

214
app/about/page.json Normal file
View File

@@ -0,0 +1,214 @@
{
"root": {
"props": {
"title": "About — 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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-about",
"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"
}
},
{
"type": "hero",
"props": {
"id": "hero-about",
"tagline": "The lab",
"heading": "We don't make activewear.",
"subheading": "Pulse started in a converted warehouse with three coaches, two athletes, and a whiteboard full of things they wished worked better. Eight years later, the whiteboard is still there. So is the standard.",
"buttons": [
{
"label": "Shop the kit",
"href": "/search",
"variant": "primary"
}
],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/hero-about.png",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "features",
"props": {
"id": "features-about",
"tagline": "What we believe",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after, if at all."
},
{
"title": "Field tested",
"body": "Twelve weeks of training. Two race blocks. One full season. Nothing leaves the lab without that on the record."
},
{
"title": "Guaranteed",
"body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it. That's the deal."
}
]
}
},
{
"type": "hero",
"props": {
"id": "cover-about-performance",
"tagline": "Used up is the goal",
"heading": "Designed to be worn out, not preserved.",
"subheading": "If you're babying your kit, we did something wrong. Bring it back when it's done — we'll send the next one.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "newsletter-cta",
"props": {
"id": "newsletter-about",
"tagline": "Field notes",
"heading": "Weekly notes from the lab.",
"subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.",
"buttonLabel": "Subscribe",
"endpoint": "",
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png",
"layout": "split"
}
},
{
"type": "footer",
"props": {
"id": "footer-about",
"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"
},
{
"label": "Run",
"href": "/collections/run"
},
{
"label": "Train",
"href": "/collections/train"
}
]
},
{
"title": "About",
"links": [
{
"label": "Our story",
"href": "/about"
},
{
"label": "The lab",
"href": "/lab"
},
{
"label": "Athletes",
"href": "/athletes"
}
]
},
{
"title": "Help",
"links": [
{
"label": "Shipping",
"href": "/help/shipping"
},
{
"label": "Returns",
"href": "/help/returns"
},
{
"label": "Lifetime guarantee",
"href": "/help/guarantee"
},
{
"label": "Contact",
"href": "/contact"
}
]
}
],
"social": [
{
"label": "Instagram",
"href": "#"
},
{
"label": "Strava",
"href": "#"
},
{
"label": "YouTube",
"href": "#"
}
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
}

6
app/about/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
export default function Page() {
return <PageRender page={page} />;
}

View File

@@ -0,0 +1,6 @@
import PageEditor from "@/components/page-editor";
import page from "../page.json";
export default function EditorPage() {
return <PageEditor page={page} routeKey="/app/collections/[handle]" />;
}

View File

@@ -0,0 +1,195 @@
{
"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"
}
},
"content": [
{
"type": "navigation",
"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"
}
},
{
"type": "collection",
"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": []
}
},
{
"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."
}
}
]
}

View File

@@ -0,0 +1,6 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
export default function Page() {
return <PageRender page={page} />;
}

View File

@@ -1,61 +0,0 @@
"use client";
import { useMemo } from "react";
import { useParams } from "next/navigation";
import { Editor, blocksPlugin, outlinePlugin } from "@reacteditor/core";
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
import { createShopifyPlugin } from "@reacteditor/plugin-shopify";
import { mediaPlugin } from "@reacteditor/plugin-media";
import { mediaAdapter } from "@/lib/adapters/media-adapter";
import { appConfig } from "@/editor.config";
import resolveRoute from "@/lib/resolve-route";
import schema from "@/app.schema.json";
import globals from "@/app.globals.json";
export default function EditorPage() {
const params = useParams();
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
const { key, path, params: routeParams } = resolveRoute(segments);
const { root, content } = (schema as any)[key] ?? {};
const data = { root, content, globals };
const plugins = useMemo(
() => [
blocksPlugin(),
outlinePlugin(),
mediaPlugin({
adapter: mediaAdapter,
}),
createTailwindCdnPlugin(),
createShopifyPlugin({
storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN ?? "mock.shop",
publicAccessToken:
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN,
}),
],
[],
);
const handlePublish = async (nextData: any, route?: any) => {
const resolved = route ?? { key };
if (typeof window !== "undefined" && window.parent !== window) {
window.parent.postMessage(
{ type: "PUBLISH", data: { data: nextData, route: resolved } },
"*",
);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
};
return (
<div className="h-screen w-screen">
<Editor
config={appConfig as any}
data={data}
route={{ key, path, params: routeParams }}
plugins={plugins}
onPublish={handlePublish}
/>
</div>
);
}

6
app/editor/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
import PageEditor from "@/components/page-editor";
import page from "../page.json";
export default function EditorPage() {
return <PageEditor page={page} routeKey="/app" />;
}

302
app/page.json Normal file
View File

@@ -0,0 +1,302 @@
{
"root": {
"props": {
"title": "Pulse — Made to Move",
"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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-home",
"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"
}
},
{
"type": "hero",
"props": {
"id": "hero-home",
"tagline": "Spring 2026",
"heading": "Made to move.",
"subheading": "Performance gear engineered for athletes who train like they mean it. Built light. Built honest. Built for the next mile.",
"buttons": [
{
"label": "Shop the kit",
"href": "/search",
"variant": "primary"
},
{
"label": "Our story",
"href": "/about",
"variant": "secondary"
}
],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/athletic-assortment.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "products-carousel",
"props": {
"id": "carousel-home",
"tagline": "Just dropped",
"heading": "New arrivals",
"subheading": "Fresh kit for the season ahead.",
"limit": 12,
"slidesPerView": "4",
"ctaLabel": "Shop new",
"ctaHref": "/collections/new"
}
},
{
"type": "hero",
"props": {
"id": "cover-home-performance",
"tagline": "Performance Series",
"heading": "Engineered to outlast you.",
"subheading": "Tested on track, trail, and treadmill. Every piece earns its place.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "features",
"props": {
"id": "features-home",
"tagline": "Why Pulse",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after."
},
{
"title": "Field tested",
"body": "Twelve weeks. Two race blocks. One full season. Nothing ships without that on the record."
},
{
"title": "Guaranteed",
"body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it."
}
]
}
},
{
"type": "hero",
"props": {
"id": "cover-home-discipline",
"tagline": "Discipline",
"heading": "The work happens before sunrise.",
"subheading": "We make the kit. You do the work. Five-thirty isn't early — it's the only honest hour.",
"buttons": [],
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-discipline.jpeg",
"align": "left",
"height": "lg",
"tone": "light"
}
},
{
"type": "collection-grid",
"props": {
"id": "collections-home",
"tagline": "Shop by sport",
"heading": "Built for what you actually do.",
"subheading": "",
"layout": "tiles",
"limit": 6
}
},
{
"type": "testimonials",
"props": {
"id": "testimonials-home",
"tagline": "From the field",
"heading": "Athletes on Pulse",
"items": [
{
"quote": "Took the shell to Chamonix and back. Still smells, still works, still cheaper than therapy.",
"author": "Mateo S.",
"role": "Sub-3 marathoner / Boulder",
"avatar": ""
},
{
"quote": "I have one drawer of running gear. The Pulse half is the half I actually wear.",
"author": "Priya N.",
"role": "Ultra runner / Portland",
"avatar": ""
},
{
"quote": "Wore the shorts for an entire 16-week block. They held. That's the whole review.",
"author": "Jonas R.",
"role": "Track coach / Berlin",
"avatar": ""
}
]
}
},
{
"type": "newsletter-cta",
"props": {
"id": "newsletter-home",
"tagline": "Field notes",
"heading": "Weekly notes from the lab.",
"subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.",
"buttonLabel": "Subscribe",
"endpoint": "",
"imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png",
"layout": "split"
}
},
{
"type": "footer",
"props": {
"id": "footer-home",
"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"
},
{
"label": "Run",
"href": "/collections/run"
},
{
"label": "Train",
"href": "/collections/train"
},
{
"label": "Sale",
"href": "/collections/sale"
}
]
},
{
"title": "About",
"links": [
{
"label": "Our story",
"href": "/about"
},
{
"label": "The lab",
"href": "/lab"
},
{
"label": "Athletes",
"href": "/athletes"
}
]
},
{
"title": "Help",
"links": [
{
"label": "Shipping",
"href": "/help/shipping"
},
{
"label": "Returns",
"href": "/help/returns"
},
{
"label": "Lifetime guarantee",
"href": "/help/guarantee"
},
{
"label": "Contact",
"href": "/contact"
}
]
},
{
"title": "Legal",
"links": [
{
"label": "Terms",
"href": "/terms"
},
{
"label": "Privacy",
"href": "/privacy"
}
]
}
],
"social": [
{
"label": "Instagram",
"href": "#"
},
{
"label": "Strava",
"href": "#"
},
{
"label": "YouTube",
"href": "#"
}
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
}

6
app/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
export default function Page() {
return <PageRender page={page} />;
}

View File

@@ -0,0 +1,6 @@
import PageEditor from "@/components/page-editor";
import page from "../page.json";
export default function EditorPage() {
return <PageEditor page={page} routeKey="/app/products/[handle]" />;
}

View File

@@ -0,0 +1,129 @@
{
"root": {
"props": {
"title": "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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-product",
"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"
}
},
{
"type": "product-details",
"props": {
"id": "product-details",
"product": null
}
},
{
"type": "products-carousel",
"props": {
"id": "carousel-related",
"tagline": "Pairs well with",
"heading": "Complete the kit",
"limit": 8,
"slidesPerView": "4",
"ctaLabel": "Shop all",
"ctaHref": "/collections"
}
},
{
"type": "footer",
"props": {
"id": "footer-product",
"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"
},
{
"label": "Lifetime guarantee",
"href": "/help/guarantee"
}
]
}
],
"social": [
{
"label": "Instagram",
"href": "#"
},
{
"label": "Strava",
"href": "#"
}
],
"showNewsletter": "no",
"newsletterHeading": "",
"newsletterEndpoint": "",
"copyright": "© 2026 Pulse. Built to be replaced when used up."
}
}
]
}

View File

@@ -0,0 +1,6 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
export default function Page() {
return <PageRender page={page} />;
}

View File

@@ -0,0 +1,6 @@
import PageEditor from "@/components/page-editor";
import page from "../page.json";
export default function EditorPage() {
return <PageEditor page={page} routeKey="/app/search" />;
}

162
app/search/page.json Normal file
View File

@@ -0,0 +1,162 @@
{
"root": {
"props": {
"title": "Shop — 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"
}
},
"content": [
{
"type": "navigation",
"props": {
"id": "nav-search",
"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"
}
},
{
"type": "search-products",
"props": {
"id": "search-products",
"heading": "All gear.",
"subheading": "Filter by sport, fabric, fit, color. Or sort by what's been beaten the hardest.",
"columns": "4",
"limit": 24,
"showAvailability": "yes",
"showPriceRange": "yes",
"showProductType": "yes",
"showVendor": "no",
"showTags": "yes",
"metafieldFilters": [],
"defaultSort": "BEST_SELLING",
"productTypeOptions": [
{
"label": "Tops"
},
{
"label": "Shorts"
},
{
"label": "Tights"
},
{
"label": "Outerwear"
},
{
"label": "Shoes"
},
{
"label": "Accessories"
}
],
"vendorOptions": [],
"tagOptions": [
{
"label": "New"
},
{
"label": "Race day"
},
{
"label": "Field tested"
},
{
"label": "Sale"
}
]
}
},
{
"type": "footer",
"props": {
"id": "footer-search",
"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": "Run",
"href": "/collections/run"
},
{
"label": "Train",
"href": "/collections/train"
}
]
},
{
"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."
}
}
]
}

6
app/search/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
import PageRender from "@/components/page-render";
import page from "./page.json";
export default function Page() {
return <PageRender page={page} />;
}

View File

@@ -0,0 +1,92 @@
"use client";
import { useMemo } from "react";
import { usePathname, useParams } from "next/navigation";
import { Editor, blocksPlugin, outlinePlugin } from "@reacteditor/core";
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
import { createShopifyPlugin } from "@reacteditor/plugin-shopify";
import { mediaPlugin } from "@reacteditor/plugin-media";
import { mediaAdapter } from "@/lib/adapters/media-adapter";
import { appConfig } from "@/editor.config";
import globals from "@/app.globals.json";
import type { PageData } from "@/components/page-render";
/**
* Shared editor for a route. In an editor route's `page.tsx`, import the
* sibling public route's `page.json` and pass it through with the route key:
*
* import page from "../page.json";
* export default () => <PageEditor page={page} routeKey="/app/products/[handle]" />;
*
* `routeKey` is the Next.js app-directory path of the route (e.g.
* "/app/products/[handle]"); the host resolves it to `<routeKey>/page.json`
* to know which file to save. The concrete public path and params are derived
* separately from the live URL.
*/
export default function PageEditor({
page,
routeKey,
}: {
page: PageData;
routeKey: string;
}) {
const pathname = usePathname() ?? "/editor";
const params = useParams();
// Each editor route is a `.../editor` child of its public route; the
// published path drops that trailing segment.
const path = pathname.replace(/\/editor$/, "") || "/";
const routeParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params ?? {})) {
if (typeof value === "string") routeParams[key] = value;
}
const data = { root: page.root, content: page.content, globals };
const plugins = useMemo(
() => [
blocksPlugin(),
outlinePlugin(),
mediaPlugin({
adapter: mediaAdapter,
}),
createTailwindCdnPlugin(),
createShopifyPlugin({
storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN ?? "mock.shop",
publicAccessToken:
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN,
}),
],
[],
);
const handlePublish = async (nextData: any, route?: any) => {
// The page we save is always the public route — i.e. the live path with
// the trailing `/editor` segment stripped, never the `.../editor` URL.
const resolved = {
key: routeKey,
params: routeParams,
...(route ?? {}),
path,
};
if (typeof window !== "undefined" && window.parent !== window) {
window.parent.postMessage(
{ type: "PUBLISH", data: { data: nextData, route: resolved } },
"*",
);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
};
return (
<div className="h-screen w-screen">
<Editor
config={appConfig as any}
data={data as any}
route={{ key: routeKey, path, params: routeParams }}
plugins={plugins}
onPublish={handlePublish}
/>
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Render } from "@reacteditor/core";
import { appConfig } from "@/editor.config";
import globals from "@/app.globals.json";
export type PageData = {
root?: unknown;
content?: unknown;
};
/**
* Shared renderer for a route. Drop a `page.json` next to a route's
* `page.tsx`, import it, and hand it here:
*
* import page from "./page.json";
* export default () => <PageRender page={page} />;
*/
export default function PageRender({ page }: { page: PageData }) {
const data = { root: page.root, content: page.content, globals };
return <Render config={appConfig as any} data={data as any} />;
}

View File

@@ -3,15 +3,13 @@
import { useParams } from "next/navigation";
/**
* Returns the last segment of the current catch-all slug route, e.g. the
* `cool-shirt` in `/products/cool-shirt`. Components use this to derive the
* resource they should load from the Next.js route segments directly.
* Returns the dynamic handle of the current route, e.g. the `cool-shirt` in
* `/products/cool-shirt` (or its `/products/cool-shirt/editor` editor view).
* Both the public and editor routes share the same `[handle]` segment, so the
* value is identical in either context.
*/
export function useRouteSegment(): string | undefined {
const params = useParams();
const segments = Array.isArray(params?.slug) ? (params.slug as string[]) : [];
// Editor routes are served under `/editor/*`; ignore that prefix so the
// resolved segment matches the public route.
const routeSegments = segments[0] === "editor" ? segments.slice(1) : segments;
return routeSegments[routeSegments.length - 1];
const handle = params?.handle;
return typeof handle === "string" ? handle : undefined;
}

View File

@@ -1,64 +0,0 @@
import schema from "@/app.schema.json";
export type ResolvedRoute = {
key: string;
path: string;
params: Record<string, string>;
};
const ROUTE_KEYS = Object.keys(schema as Record<string, unknown>);
/**
* Matches a concrete path's segments against a single express-style pattern
* (e.g. `/products/:handle`). Returns the captured params, or null on mismatch.
*/
const matchPattern = (
pattern: string,
segments: string[],
): Record<string, string> | null => {
const patternSegments = pattern === "/" ? [] : pattern.slice(1).split("/");
if (patternSegments.length !== segments.length) return null;
const params: Record<string, string> = {};
for (let i = 0; i < patternSegments.length; i++) {
const part = patternSegments[i];
if (part.startsWith(":")) {
params[part.slice(1)] = segments[i];
} else if (part !== segments[i]) {
return null;
}
}
return params;
};
/**
* Resolves catch-all slug segments to a route key defined in the schema,
* supporting any express-style pattern (`/products/:handle`, `/blog/:slug`,
* etc.). Static segments are preferred over dynamic ones when both match.
*/
const resolveRoute = (segments: string[] = []): ResolvedRoute => {
// Editor routes live under `/editor/*`; the `editor` prefix is not part of
// the schema route keys, so strip it before matching.
const routeSegments =
segments[0] === "editor" ? segments.slice(1) : segments;
const path =
routeSegments.length === 0 ? "/" : `/${routeSegments.join("/")}`;
let best: ResolvedRoute | null = null;
let bestDynamicCount = Infinity;
for (const key of ROUTE_KEYS) {
const params = matchPattern(key, routeSegments);
if (!params) continue;
const dynamicCount = Object.keys(params).length;
if (dynamicCount < bestDynamicCount) {
best = { key, path, params };
bestDynamicCount = dynamicCount;
}
}
return best ?? { key: path, path, params: {} };
};
export default resolveRoute;

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long