update components
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "nav-about",
|
||||
"brand": "PULSE",
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hero",
|
||||
"type": "cover",
|
||||
"props": {
|
||||
"id": "hero-about",
|
||||
"tagline": "The lab",
|
||||
@@ -73,31 +73,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"type": "cover",
|
||||
"props": {
|
||||
"id": "cover-about-performance",
|
||||
"tagline": "Used up is the goal",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "nav-collection",
|
||||
"brand": "PULSE",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "nav-home",
|
||||
"brand": "PULSE",
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hero",
|
||||
"type": "cover",
|
||||
"props": {
|
||||
"id": "hero-home",
|
||||
"tagline": "Spring 2026",
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hero",
|
||||
"type": "cover",
|
||||
"props": {
|
||||
"id": "cover-home-performance",
|
||||
"tagline": "Performance Series",
|
||||
@@ -105,31 +105,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"type": "cover",
|
||||
"props": {
|
||||
"id": "cover-home-discipline",
|
||||
"tagline": "Discipline",
|
||||
@@ -153,34 +129,6 @@
|
||||
"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": {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "nav-product",
|
||||
"brand": "PULSE",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "nav-search",
|
||||
"brand": "PULSE",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { LayoutTemplate } from "lucide-react";
|
||||
import { Hero, type HeroProps } from "@/components/hero/hero";
|
||||
import { Cover, type CoverProps } from "@/components/cover/cover";
|
||||
|
||||
const heroEditor: ComponentConfig<HeroProps> = {
|
||||
label: "Hero",
|
||||
const coverEditor: ComponentConfig<CoverProps> = {
|
||||
label: "Cover",
|
||||
icon: <LayoutTemplate size={16} />,
|
||||
category: "hero",
|
||||
defaultProps: {
|
||||
@@ -76,7 +76,7 @@ const heroEditor: ComponentConfig<HeroProps> = {
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (props) => <Hero {...props} />,
|
||||
render: (props) => <Cover {...props} />,
|
||||
};
|
||||
|
||||
export default heroEditor;
|
||||
export default coverEditor;
|
||||
@@ -2,32 +2,32 @@ import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Typography } from "@/components/Typography";
|
||||
|
||||
export type HeroButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
||||
export type CoverButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
||||
|
||||
export type HeroButton = {
|
||||
export type CoverButton = {
|
||||
label: string;
|
||||
href: string;
|
||||
variant: HeroButtonVariant;
|
||||
variant: CoverButtonVariant;
|
||||
};
|
||||
|
||||
export type HeroProps = {
|
||||
export type CoverProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
buttons: HeroButton[];
|
||||
buttons: CoverButton[];
|
||||
imageUrl: string;
|
||||
align: "left" | "center";
|
||||
height: "md" | "lg" | "full";
|
||||
tone: "light" | "dark";
|
||||
};
|
||||
|
||||
const heightClass: Record<HeroProps["height"], string> = {
|
||||
const heightClass: Record<CoverProps["height"], string> = {
|
||||
md: "min-h-[60vh]",
|
||||
lg: "min-h-[80vh]",
|
||||
full: "min-h-screen",
|
||||
};
|
||||
|
||||
function buttonClass(variant: HeroButtonVariant, isDark: boolean): string {
|
||||
function buttonClass(variant: CoverButtonVariant, isDark: boolean): string {
|
||||
switch (variant) {
|
||||
case "primary":
|
||||
return cn(
|
||||
@@ -48,7 +48,7 @@ function buttonClass(variant: HeroButtonVariant, isDark: boolean): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function Hero({
|
||||
export function Cover({
|
||||
tagline,
|
||||
heading,
|
||||
subheading,
|
||||
@@ -57,7 +57,7 @@ export function Hero({
|
||||
align,
|
||||
height,
|
||||
tone,
|
||||
}: HeroProps) {
|
||||
}: CoverProps) {
|
||||
const isDark = tone === "dark";
|
||||
const visibleButtons = (buttons ?? []).filter((b) => b?.label);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { CTA, type CTAProps } from "@/components/cta/cta";
|
||||
|
||||
const ctaEditor: ComponentConfig<CTAProps> = {
|
||||
label: "Call to action",
|
||||
icon: <Megaphone size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "",
|
||||
heading: "Designed once. Worn for years.",
|
||||
subheading:
|
||||
"Join 40,000 people building a wardrobe they actually reach for.",
|
||||
primaryCta: { label: "Shop now", href: "/collections" },
|
||||
secondaryCta: { label: "Read our story", href: "/about" },
|
||||
imageUrl:
|
||||
"https://images.unsplash.com/photo-1483985988355-763728e1935b?auto=format&fit=crop&w=2400&q=80",
|
||||
align: "center",
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
heading: { label: "Heading", type: "textarea", contentEditable: true },
|
||||
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
||||
primaryCta: {
|
||||
label: "Primary CTA",
|
||||
type: "object",
|
||||
objectFields: {
|
||||
label: { label: "Label", type: "text", contentEditable: true },
|
||||
href: { label: "Link", type: "text" },
|
||||
},
|
||||
},
|
||||
secondaryCta: {
|
||||
label: "Secondary CTA",
|
||||
type: "object",
|
||||
objectFields: {
|
||||
label: { label: "Label", type: "text", contentEditable: true },
|
||||
href: { label: "Link", type: "text" },
|
||||
},
|
||||
},
|
||||
imageUrl: { label: "Background image", type: "image" },
|
||||
align: {
|
||||
label: "Alignment",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ label: "Left", value: "left" },
|
||||
{ label: "Center", value: "center" },
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (props) => <CTA {...props} />,
|
||||
};
|
||||
|
||||
export default ctaEditor;
|
||||
@@ -1,81 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Heading } from "@/components/Heading";
|
||||
|
||||
export type CTAProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
primaryCta: { label: string; href: string };
|
||||
secondaryCta: { label: string; href: string };
|
||||
imageUrl: string;
|
||||
align: "left" | "center";
|
||||
};
|
||||
|
||||
export function CTA({
|
||||
tagline,
|
||||
heading,
|
||||
subheading,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
imageUrl,
|
||||
align,
|
||||
}: CTAProps) {
|
||||
return (
|
||||
<section className="relative overflow-hidden py-24 md:py-32">
|
||||
<div className="absolute inset-0 -z-10">
|
||||
{imageUrl ? (
|
||||
<>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/45" />
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full w-full bg-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"container mx-auto flex max-w-4xl flex-col px-6 text-white",
|
||||
align === "center" ? "items-center text-center" : "items-start",
|
||||
)}
|
||||
>
|
||||
<Heading
|
||||
tagline={tagline}
|
||||
title={heading}
|
||||
subtitle={subheading}
|
||||
align={align === "center" ? "center" : "left"}
|
||||
size="lg"
|
||||
tone="light"
|
||||
subtitleClassName="max-w-xl"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-10 flex flex-wrap gap-3",
|
||||
align === "center" && "justify-center",
|
||||
)}
|
||||
>
|
||||
{primaryCta?.label ? (
|
||||
<Link
|
||||
href={primaryCta.href || "#"}
|
||||
className="inline-flex items-center justify-center rounded-md bg-white px-6 py-3 text-sm font-medium tracking-wide text-black hover:opacity-90"
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Link>
|
||||
) : null}
|
||||
{secondaryCta?.label ? (
|
||||
<Link
|
||||
href={secondaryCta.href || "#"}
|
||||
className="inline-flex items-center justify-center rounded-md border border-white px-6 py-3 text-sm font-medium tracking-wide text-white hover:bg-white/10"
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { FAQ, type FAQProps } from "@/components/faq/faq";
|
||||
|
||||
const faqEditor: ComponentConfig<FAQProps> = {
|
||||
label: "FAQ",
|
||||
icon: <HelpCircle size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "Help",
|
||||
heading: "Common questions",
|
||||
subheading: "",
|
||||
items: [
|
||||
{
|
||||
question: "What's your return policy?",
|
||||
answer:
|
||||
"Free returns within 30 days of delivery. Items should be unworn with original tags attached.",
|
||||
},
|
||||
{
|
||||
question: "Where do you ship?",
|
||||
answer:
|
||||
"We ship worldwide. Free standard shipping on orders over $150 in the US, $250 international.",
|
||||
},
|
||||
{
|
||||
question: "How are your products made?",
|
||||
answer:
|
||||
"In small batches at family-run mills in Portugal, Italy, and Japan. Every piece is sampled and approved by our team.",
|
||||
},
|
||||
{
|
||||
question: "How do I care for my pieces?",
|
||||
answer:
|
||||
"Cold wash, lay flat to dry, iron when damp. Care details are on every product page and on the inner label.",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
heading: { label: "Heading", type: "text", contentEditable: true },
|
||||
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
||||
items: {
|
||||
label: "Items",
|
||||
type: "array",
|
||||
defaultItemProps: { question: "", answer: "" },
|
||||
getItemSummary: (it) => it?.question || "Question",
|
||||
arrayFields: {
|
||||
question: { label: "Question", type: "text", contentEditable: true },
|
||||
answer: { label: "Answer", type: "textarea", contentEditable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => <FAQ {...props} />,
|
||||
};
|
||||
|
||||
export default faqEditor;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, Minus } from "lucide-react";
|
||||
import { Heading } from "@/components/Heading";
|
||||
|
||||
export type FAQProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
items: Array<{ question: string; answer: string }>;
|
||||
};
|
||||
|
||||
export function FAQ({ tagline, heading, subheading, items }: FAQProps) {
|
||||
const [open, setOpen] = useState<number | null>(0);
|
||||
return (
|
||||
<section className="bg-background py-20 md:py-28">
|
||||
<div className="container mx-auto max-w-3xl px-6">
|
||||
<Heading
|
||||
tagline={tagline}
|
||||
title={heading}
|
||||
subtitle={subheading}
|
||||
align="center"
|
||||
size="lg"
|
||||
className="mb-12"
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border border-y border-border">
|
||||
{items.map((item, i) => {
|
||||
const isOpen = open === i;
|
||||
return (
|
||||
<div key={i}>
|
||||
<button
|
||||
onClick={() => setOpen(isOpen ? null : i)}
|
||||
className="flex w-full items-center justify-between py-6 text-left"
|
||||
>
|
||||
<span className="text-base font-medium tracking-tight md:text-lg">
|
||||
{item.question}
|
||||
</span>
|
||||
{isOpen ? (
|
||||
<Minus size={18} strokeWidth={1.5} className="flex-shrink-0" />
|
||||
) : (
|
||||
<Plus size={18} strokeWidth={1.5} className="flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
{isOpen ? (
|
||||
<p className="pb-6 pr-8 text-sm leading-relaxed text-muted-foreground md:text-base">
|
||||
{item.answer}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { Features, type FeaturesProps } from "@/components/features/features";
|
||||
|
||||
const featuresEditor: ComponentConfig<FeaturesProps> = {
|
||||
label: "Features",
|
||||
icon: <Sparkles size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "Why us",
|
||||
heading: "Built with intention",
|
||||
subheading: "A small set of values that shape every piece we make.",
|
||||
columns: "3",
|
||||
items: [
|
||||
{
|
||||
title: "Natural fibers",
|
||||
body: "Linen, organic cotton, and merino — sourced from mills with traceable supply chains.",
|
||||
},
|
||||
{
|
||||
title: "Small batches",
|
||||
body: "Made in considered quantities so nothing goes to waste — and nothing gets discounted into the bin.",
|
||||
},
|
||||
{
|
||||
title: "Built to last",
|
||||
body: "Reinforced seams, double-stitched edges, and finishes that age into something better.",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
heading: { label: "Heading", type: "text", contentEditable: true },
|
||||
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
||||
columns: {
|
||||
label: "Columns",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "2 columns", value: "2" },
|
||||
{ label: "3 columns", value: "3" },
|
||||
{ label: "4 columns", value: "4" },
|
||||
],
|
||||
},
|
||||
items: {
|
||||
label: "Items",
|
||||
type: "array",
|
||||
defaultItemProps: { title: "Feature", body: "Description." },
|
||||
getItemSummary: (it) => it?.title || "Feature",
|
||||
arrayFields: {
|
||||
title: { label: "Title", type: "text", contentEditable: true },
|
||||
body: { label: "Body", type: "textarea", contentEditable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => <Features {...props} />,
|
||||
};
|
||||
|
||||
export default featuresEditor;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Typography } from "@/components/Typography";
|
||||
import { Container } from "@/components/layout/Container";
|
||||
import { Heading } from "@/components/Heading";
|
||||
|
||||
export type FeaturesProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
columns: "2" | "3" | "4";
|
||||
items: Array<{ title: string; body: string }>;
|
||||
};
|
||||
|
||||
const colClass: Record<FeaturesProps["columns"], string> = {
|
||||
"2": "md:grid-cols-2",
|
||||
"3": "md:grid-cols-3",
|
||||
"4": "md:grid-cols-2 lg:grid-cols-4",
|
||||
};
|
||||
|
||||
export function Features({ tagline, heading, subheading, columns, items }: FeaturesProps) {
|
||||
return (
|
||||
<section className="bg-background py-20 md:py-28">
|
||||
<Container>
|
||||
<Heading
|
||||
tagline={tagline}
|
||||
title={heading}
|
||||
subtitle={subheading}
|
||||
align="center"
|
||||
size="lg"
|
||||
className="mx-auto mb-16"
|
||||
maxWidth="max-w-2xl"
|
||||
/>
|
||||
|
||||
<div className={`grid grid-cols-1 gap-x-10 gap-y-12 ${colClass[columns]}`}>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="flex flex-col gap-3">
|
||||
<Typography variant="caption">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</Typography>
|
||||
<Typography variant="h5">{item.title}</Typography>
|
||||
<Typography variant="body2" className="text-muted-foreground">
|
||||
{item.body}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Menu as MenuIcon } from "lucide-react";
|
||||
import { Navigation, type NavigationProps } from "@/components/navigation/navigation";
|
||||
import { Header, type HeaderProps } from "@/components/header/header";
|
||||
|
||||
const navigationEditor: ComponentConfig<NavigationProps> = {
|
||||
label: "Navigation",
|
||||
const headerEditor: ComponentConfig<HeaderProps> = {
|
||||
label: "Header",
|
||||
icon: <MenuIcon size={16} />,
|
||||
category: "navigation",
|
||||
global: true,
|
||||
@@ -68,7 +68,7 @@ const navigationEditor: ComponentConfig<NavigationProps> = {
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (props) => <Navigation {...props} />,
|
||||
render: (props) => <Header {...props} />,
|
||||
};
|
||||
|
||||
export default navigationEditor;
|
||||
export default headerEditor;
|
||||
@@ -7,7 +7,7 @@ import { Container } from "@/components/layout/Container";
|
||||
import { Typography } from "@/components/Typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type NavigationProps = {
|
||||
export type HeaderProps = {
|
||||
brand: string;
|
||||
logo?: string;
|
||||
links: Array<{ label: string; href: string }>;
|
||||
@@ -17,7 +17,7 @@ export type NavigationProps = {
|
||||
tone: "default" | "muted" | "inverse";
|
||||
};
|
||||
|
||||
export function Navigation({
|
||||
export function Header({
|
||||
brand,
|
||||
logo,
|
||||
links,
|
||||
@@ -25,12 +25,12 @@ export function Navigation({
|
||||
showCart,
|
||||
sticky,
|
||||
tone,
|
||||
}: NavigationProps) {
|
||||
}: HeaderProps) {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const cart = useShopifyCart();
|
||||
const itemCount = cart?.itemCount ?? 0;
|
||||
|
||||
const toneClass: Record<NavigationProps["tone"], string> = {
|
||||
const toneClass: Record<HeaderProps["tone"], string> = {
|
||||
default: "bg-background text-foreground border-b border-border",
|
||||
muted: "bg-muted/40 text-foreground border-b border-border",
|
||||
inverse: "bg-foreground text-background",
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { Banner, type BannerProps } from "@/components/landing/banner";
|
||||
|
||||
const bannerEditor: ComponentConfig<BannerProps> = {
|
||||
label: "Announcement bar",
|
||||
icon: <Megaphone size={16} />,
|
||||
category: "hero",
|
||||
defaultProps: {
|
||||
text: "Free shipping on orders over $150",
|
||||
ctaLabel: "Shop new",
|
||||
ctaHref: "/collections/new",
|
||||
tone: "default",
|
||||
},
|
||||
fields: {
|
||||
text: { label: "Text", type: "text", contentEditable: true },
|
||||
ctaLabel: { label: "CTA label", type: "text", contentEditable: true },
|
||||
ctaHref: { label: "CTA link", type: "text" },
|
||||
tone: {
|
||||
label: "Tone",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Default (dark)", value: "default" },
|
||||
{ label: "Inverse (light)", value: "inverse" },
|
||||
{ label: "Muted", value: "muted" },
|
||||
],
|
||||
},
|
||||
},
|
||||
render: (props) => <Banner {...props} />,
|
||||
};
|
||||
|
||||
export default bannerEditor;
|
||||
@@ -1,32 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type BannerProps = {
|
||||
text: string;
|
||||
ctaLabel: string;
|
||||
ctaHref: string;
|
||||
tone: "default" | "inverse" | "muted";
|
||||
};
|
||||
|
||||
export function Banner({ text, ctaLabel, ctaHref, tone }: BannerProps) {
|
||||
const toneClass: Record<BannerProps["tone"], string> = {
|
||||
default: "bg-foreground text-background",
|
||||
inverse: "bg-background text-foreground border-y border-border",
|
||||
muted: "bg-muted text-foreground border-y border-border",
|
||||
};
|
||||
return (
|
||||
<div className={cn("w-full py-2 text-center text-xs tracking-[0.18em] uppercase", toneClass[tone])}>
|
||||
<div className="container mx-auto flex flex-col items-center justify-center gap-2 px-6 sm:flex-row">
|
||||
<span>{text}</span>
|
||||
{ctaLabel ? (
|
||||
<Link
|
||||
href={ctaHref || "#"}
|
||||
className="underline-offset-4 hover:underline"
|
||||
>
|
||||
{ctaLabel} →
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Images } from "lucide-react";
|
||||
import { ImageGallery, type ImageGalleryProps } from "@/components/landing/image-gallery";
|
||||
|
||||
const imageGalleryEditor: ComponentConfig<ImageGalleryProps> = {
|
||||
label: "Image gallery",
|
||||
icon: <Images size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "Lookbook",
|
||||
heading: "Spring in the studio",
|
||||
subheading: "",
|
||||
layout: "editorial",
|
||||
items: [
|
||||
{ src: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=1600&q=80", alt: "" },
|
||||
{ src: "https://images.unsplash.com/photo-1483985988355-763728e1935b?auto=format&fit=crop&w=1200&q=80", alt: "" },
|
||||
{ src: "https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1200&q=80", alt: "" },
|
||||
{ src: "https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=1200&q=80", alt: "" },
|
||||
{ src: "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?auto=format&fit=crop&w=1200&q=80", alt: "" },
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
heading: { label: "Heading", type: "text", contentEditable: true },
|
||||
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
||||
layout: {
|
||||
label: "Layout",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Grid", value: "grid" },
|
||||
{ label: "Masonry", value: "masonry" },
|
||||
{ label: "Editorial (mosaic)", value: "editorial" },
|
||||
],
|
||||
},
|
||||
items: {
|
||||
label: "Images",
|
||||
type: "array",
|
||||
defaultItemProps: { src: "", alt: "" },
|
||||
getItemSummary: (it) => it?.caption || "Image",
|
||||
arrayFields: {
|
||||
src: { label: "Image", type: "image" },
|
||||
alt: { label: "Alt text", type: "text", contentEditable: true },
|
||||
caption: { label: "Caption", type: "text", contentEditable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => <ImageGallery {...props} />,
|
||||
};
|
||||
|
||||
export default imageGalleryEditor;
|
||||
@@ -1,87 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Container } from "@/components/layout/Container";
|
||||
import { Heading } from "@/components/Heading";
|
||||
|
||||
export type ImageGalleryProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
layout: "grid" | "masonry" | "editorial";
|
||||
items: Array<{ src: string; alt: string; caption?: string }>;
|
||||
};
|
||||
|
||||
export function ImageGallery({ tagline, heading, subheading, layout, items }: ImageGalleryProps) {
|
||||
return (
|
||||
<section className="bg-background py-20 md:py-28">
|
||||
<Container>
|
||||
<Heading
|
||||
tagline={tagline}
|
||||
title={heading}
|
||||
subtitle={subheading}
|
||||
align="center"
|
||||
size="lg"
|
||||
className="mx-auto mb-12"
|
||||
maxWidth="max-w-2xl"
|
||||
/>
|
||||
|
||||
{layout === "masonry" ? (
|
||||
<div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
|
||||
{items.map((it, i) => (
|
||||
<figure key={i} className="mb-4 break-inside-avoid">
|
||||
<img
|
||||
src={it.src}
|
||||
alt={it.alt}
|
||||
className="w-full rounded-md object-cover"
|
||||
/>
|
||||
{it.caption ? (
|
||||
<figcaption className="mt-2 text-xs text-muted-foreground">
|
||||
{it.caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
) : layout === "editorial" ? (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-12">
|
||||
{items.slice(0, 5).map((it, i) => (
|
||||
<figure
|
||||
key={i}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-md bg-muted",
|
||||
i === 0 && "md:col-span-7 md:row-span-2",
|
||||
i === 1 && "md:col-span-5",
|
||||
i === 2 && "md:col-span-5",
|
||||
i === 3 && "md:col-span-6",
|
||||
i === 4 && "md:col-span-6",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={it.src}
|
||||
alt={it.alt}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((it, i) => (
|
||||
<figure key={i}>
|
||||
<img
|
||||
src={it.src}
|
||||
alt={it.alt}
|
||||
className="aspect-[4/5] w-full rounded-md object-cover"
|
||||
/>
|
||||
{it.caption ? (
|
||||
<figcaption className="mt-2 text-xs text-muted-foreground">
|
||||
{it.caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Award } from "lucide-react";
|
||||
import { Logos, type LogosProps } from "@/components/logos/logos";
|
||||
|
||||
const logosEditor: ComponentConfig<LogosProps> = {
|
||||
label: "Press / Logos",
|
||||
icon: <Award size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "As seen in",
|
||||
layout: "row",
|
||||
items: [
|
||||
{ src: "https://logo.clearbit.com/vogue.com", alt: "Vogue" },
|
||||
{ src: "https://logo.clearbit.com/highsnobiety.com", alt: "Highsnobiety" },
|
||||
{ src: "https://logo.clearbit.com/gq.com", alt: "GQ" },
|
||||
{ src: "https://logo.clearbit.com/dezeen.com", alt: "Dezeen" },
|
||||
{ src: "https://logo.clearbit.com/wallpaper.com", alt: "Wallpaper*" },
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
layout: {
|
||||
label: "Layout",
|
||||
type: "radio",
|
||||
options: [
|
||||
{ label: "Row", value: "row" },
|
||||
{ label: "Marquee", value: "marquee" },
|
||||
],
|
||||
},
|
||||
items: {
|
||||
label: "Logos",
|
||||
type: "array",
|
||||
defaultItemProps: { src: "", alt: "" },
|
||||
getItemSummary: (it) => it?.alt || "Logo",
|
||||
arrayFields: {
|
||||
src: { label: "Image", type: "image" },
|
||||
alt: { label: "Alt text", type: "text", contentEditable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => <Logos {...props} />,
|
||||
};
|
||||
|
||||
export default logosEditor;
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Container } from "@/components/layout/Container";
|
||||
import { Typography } from "@/components/Typography";
|
||||
|
||||
export type LogosProps = {
|
||||
tagline: string;
|
||||
items: Array<{ src: string; alt: string }>;
|
||||
layout: "row" | "marquee";
|
||||
};
|
||||
|
||||
export function Logos({ tagline, items, layout }: LogosProps) {
|
||||
return (
|
||||
<section className="border-y border-border bg-muted/40 py-12">
|
||||
<Container>
|
||||
{tagline ? (
|
||||
<Typography variant="caption" className="mb-8 text-center">
|
||||
{tagline}
|
||||
</Typography>
|
||||
) : null}
|
||||
{layout === "marquee" ? (
|
||||
<div className="overflow-hidden">
|
||||
<div className="flex animate-[marquee_30s_linear_infinite] gap-16 [--gap:4rem]">
|
||||
{[...items, ...items].map((it, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={it.src}
|
||||
alt={it.alt}
|
||||
className="h-7 w-auto opacity-60 grayscale"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
|
||||
{items.map((it, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={it.src}
|
||||
alt={it.alt}
|
||||
className="h-7 w-auto opacity-60 grayscale"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig, Fields } from "@reacteditor/core";
|
||||
import { Mail } from "lucide-react";
|
||||
import { NewsletterCta, type NewsletterCtaProps } from "@/components/landing/newsletter-cta";
|
||||
import { NewsletterCta, type NewsletterCtaProps } from "@/components/newsletter-cta/newsletter-cta";
|
||||
|
||||
const baseFields: Fields<NewsletterCtaProps> = {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { CollectionGrid, type CollectionGridProps } from "@/components/commerce/collection-grid";
|
||||
import { CollectionGrid, type CollectionGridProps } from "@/components/shopify/collection-grid";
|
||||
|
||||
const collectionGridEditor: ComponentConfig<CollectionGridProps> = {
|
||||
label: "Collections",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { CollectionView, type CollectionProps } from "@/components/commerce/collection";
|
||||
import { CollectionView, type CollectionProps } from "@/components/shopify/collection";
|
||||
|
||||
const collectionEditor: ComponentConfig<CollectionProps> = {
|
||||
label: "Collection page",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Star } from "lucide-react";
|
||||
import { FeaturedProductView, type FeaturedProductProps } from "@/components/commerce/featured-product";
|
||||
import { FeaturedProductView, type FeaturedProductProps } from "@/components/shopify/featured-product";
|
||||
|
||||
const featuredProductEditor: ComponentConfig<FeaturedProductProps> = {
|
||||
label: "Featured product",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Package } from "lucide-react";
|
||||
import { ProductDetailsView, type ProductDetailsProps } from "@/components/commerce/product-details";
|
||||
import { ProductDetailsView, type ProductDetailsProps } from "@/components/shopify/product-details";
|
||||
|
||||
const productDetailsEditor: ComponentConfig<ProductDetailsProps> = {
|
||||
label: "Product details",
|
||||
@@ -3,7 +3,7 @@ import { LayoutGrid } from "lucide-react";
|
||||
import {
|
||||
ProductRecommendationsView,
|
||||
type ProductRecommendationsProps,
|
||||
} from "@/components/commerce/product-recommendations";
|
||||
} from "@/components/shopify/product-recommendations";
|
||||
|
||||
const productRecommendationsEditor: ComponentConfig<ProductRecommendationsProps> = {
|
||||
label: "Product recommendations",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { GalleryHorizontalEnd } from "lucide-react";
|
||||
import { ProductsCarousel, type ProductsCarouselProps } from "@/components/commerce/products-carousel";
|
||||
import { ProductsCarousel, type ProductsCarouselProps } from "@/components/shopify/products-carousel";
|
||||
|
||||
const productsCarouselEditor: ComponentConfig<ProductsCarouselProps> = {
|
||||
label: "Products carousel",
|
||||
@@ -113,8 +113,8 @@ export function ProductsCarousel({
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="left-2 md:-left-12" />
|
||||
<CarouselNext className="right-2 md:-right-12" />
|
||||
<CarouselPrevious className="left-2 md:left-4" />
|
||||
<CarouselNext className="right-2 md:right-4" />
|
||||
</Carousel>
|
||||
</Container>
|
||||
</section>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import { ProductsGrid, type ProductsGridProps } from "@/components/commerce/products-grid";
|
||||
import { ProductsGrid, type ProductsGridProps } from "@/components/shopify/products-grid";
|
||||
|
||||
const productsGridEditor: ComponentConfig<ProductsGridProps> = {
|
||||
label: "Products grid",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { RecommendedProductsView, type RecommendedProductsProps } from "@/components/commerce/recommended-products";
|
||||
import { RecommendedProductsView, type RecommendedProductsProps } from "@/components/shopify/recommended-products";
|
||||
|
||||
const recommendedProductsEditor: ComponentConfig<RecommendedProductsProps> = {
|
||||
label: "Recommended products",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentConfig } from '@reacteditor/core';
|
||||
import { Search } from 'lucide-react';
|
||||
import { SearchProductsView, type SearchProductsProps } from '@/components/commerce/search-products';
|
||||
import { SearchProductsView, type SearchProductsProps } from '@/components/shopify/search-products';
|
||||
|
||||
const searchProductsEditor: ComponentConfig<SearchProductsProps> = {
|
||||
label: 'Search & filter',
|
||||
@@ -1,58 +0,0 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { Quote } from "lucide-react";
|
||||
import { Testimonials, type TestimonialsProps } from "@/components/testimonials/testimonials";
|
||||
|
||||
const testimonialsEditor: ComponentConfig<TestimonialsProps> = {
|
||||
label: "Testimonials",
|
||||
icon: <Quote size={16} />,
|
||||
category: "content",
|
||||
defaultProps: {
|
||||
tagline: "Reviews",
|
||||
heading: "What our customers say",
|
||||
items: [
|
||||
{
|
||||
quote:
|
||||
"I've been wearing the same linen shirt for two summers now and it's somehow gotten better with every wash.",
|
||||
author: "Mara K.",
|
||||
role: "Berlin",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"Considered cuts, neutral palette, real fabric. Exactly what I want when I'm getting dressed in the dark.",
|
||||
author: "Theo R.",
|
||||
role: "Brooklyn",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=200&q=80",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"The shipping was thoughtful, the packaging was minimal, and the trousers fit on the first try. Rare combination.",
|
||||
author: "Ines P.",
|
||||
role: "Paris",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&w=200&q=80",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||
heading: { label: "Heading", type: "text", contentEditable: true },
|
||||
items: {
|
||||
label: "Items",
|
||||
type: "array",
|
||||
defaultItemProps: { quote: "", author: "", role: "" },
|
||||
getItemSummary: (it) => it?.author || "Testimonial",
|
||||
arrayFields: {
|
||||
quote: { label: "Quote", type: "textarea", contentEditable: true },
|
||||
author: { label: "Author", type: "text", contentEditable: true },
|
||||
role: { label: "Role", type: "text", contentEditable: true },
|
||||
avatar: { label: "Avatar", type: "image" },
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => <Testimonials {...props} />,
|
||||
};
|
||||
|
||||
export default testimonialsEditor;
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import { Heading } from "@/components/Heading";
|
||||
|
||||
export type TestimonialsProps = {
|
||||
tagline: string;
|
||||
heading: string;
|
||||
items: Array<{
|
||||
quote: string;
|
||||
author: string;
|
||||
role: string;
|
||||
avatar?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export function Testimonials({ tagline, heading, items }: TestimonialsProps) {
|
||||
const [i, setI] = useState(0);
|
||||
const total = items.length;
|
||||
const item = items[i];
|
||||
|
||||
return (
|
||||
<section className="bg-muted/40 py-20 md:py-28">
|
||||
<div className="container mx-auto max-w-4xl px-6 text-center">
|
||||
<Heading
|
||||
tagline={tagline}
|
||||
title={heading}
|
||||
align="center"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{item ? (
|
||||
<div className="relative mt-12">
|
||||
{total > 1 ? (
|
||||
<button
|
||||
onClick={() => setI((p) => (p - 1 + total) % total)}
|
||||
className="absolute left-0 top-1/2 hidden -translate-y-1/2 items-center justify-center rounded-full border h-10 w-10 hover:bg-background md:inline-flex"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<figure className="mx-auto flex max-w-2xl flex-col items-center px-12 md:px-16">
|
||||
<blockquote
|
||||
className="text-balance text-foreground"
|
||||
style={{ fontSize: "clamp(1.25rem, 2.4vw, 1.75rem)", lineHeight: 1.4 }}
|
||||
>
|
||||
{item.quote}
|
||||
</blockquote>
|
||||
<figcaption className="mt-8 flex items-center gap-3">
|
||||
{item.avatar ? (
|
||||
<img
|
||||
src={item.avatar}
|
||||
alt={item.author}
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-medium">{item.author}</p>
|
||||
{item.role ? (
|
||||
<p className="text-xs text-muted-foreground">{item.role}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
{total > 1 ? (
|
||||
<button
|
||||
onClick={() => setI((p) => (p + 1) % total)}
|
||||
className="absolute right-0 top-1/2 hidden -translate-y-1/2 items-center justify-center rounded-full border h-10 w-10 hover:bg-background md:inline-flex"
|
||||
aria-label="Next"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{total > 1 ? (
|
||||
<div className="mt-8 flex items-center justify-center gap-3 md:hidden">
|
||||
<button
|
||||
onClick={() => setI((p) => (p - 1 + total) % total)}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border hover:bg-background"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setI((p) => (p + 1) % total)}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border hover:bg-background"
|
||||
aria-label="Next"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const initialData: Record<string, UserData> = {
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "navigation",
|
||||
type: "header",
|
||||
props: {
|
||||
id: "nav-home",
|
||||
brand: "PULSE",
|
||||
@@ -34,7 +34,7 @@ export const initialData: Record<string, UserData> = {
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "hero",
|
||||
type: "cover",
|
||||
props: {
|
||||
id: "hero-home",
|
||||
tagline: "Spring 2026",
|
||||
@@ -85,56 +85,6 @@ export const initialData: Record<string, UserData> = {
|
||||
limit: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "features",
|
||||
props: {
|
||||
id: "features-home",
|
||||
tagline: "Why us",
|
||||
heading: "Built with intention",
|
||||
subheading: "A small set of values that shape every piece we make.",
|
||||
columns: "3",
|
||||
items: [
|
||||
{
|
||||
title: "Natural fibers",
|
||||
body: "Linen, organic cotton, and merino — sourced from mills with traceable supply chains.",
|
||||
},
|
||||
{
|
||||
title: "Small batches",
|
||||
body: "Made in considered quantities so nothing goes to waste.",
|
||||
},
|
||||
{
|
||||
title: "Built to last",
|
||||
body: "Reinforced seams and finishes that age into something better.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "testimonials",
|
||||
props: {
|
||||
id: "testimonials-home",
|
||||
tagline: "Reviews",
|
||||
heading: "What our customers say",
|
||||
items: [
|
||||
{
|
||||
quote:
|
||||
"I've been wearing the same linen shirt for two summers now and it's somehow gotten better with every wash.",
|
||||
author: "Mara K.",
|
||||
role: "Berlin",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"Considered cuts, neutral palette, real fabric. Exactly what I want when I'm getting dressed in the dark.",
|
||||
author: "Theo R.",
|
||||
role: "Brooklyn",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=200&q=80",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "newsletter-cta",
|
||||
props: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DefaultRootProps, RootConfig } from "@reacteditor/core";
|
||||
import { createFieldGoogleFonts } from "@reacteditor/field-google-fonts";
|
||||
import { ThemeProvider, ThemeProps } from "@/components/ThemeProvider";
|
||||
import { ThemeProvider, ThemeProps } from "@/components/theme-provider";
|
||||
|
||||
export type RootProps = DefaultRootProps &
|
||||
ThemeProps & {
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { Config, Data } from "@reacteditor/core";
|
||||
|
||||
import { HeroProps } from "@/components/hero/hero";
|
||||
import { LogosProps } from "@/components/logos/logos";
|
||||
import { FeaturesProps } from "@/components/features/features";
|
||||
import { TestimonialsProps } from "@/components/testimonials/testimonials";
|
||||
import { CTAProps } from "@/components/cta/cta";
|
||||
import { FAQProps } from "@/components/faq/faq";
|
||||
import { NavigationProps } from "@/components/navigation/navigation";
|
||||
import { CoverProps } from "@/components/cover/cover";
|
||||
import { HeaderProps } from "@/components/header/header";
|
||||
import { FooterProps } from "@/components/footer/footer";
|
||||
|
||||
import { ProductsGridProps } from "@/components/commerce/products-grid";
|
||||
import { ProductsCarouselProps } from "@/components/commerce/products-carousel";
|
||||
import { CollectionGridProps } from "@/components/commerce/collection-grid";
|
||||
import { CollectionProps } from "@/components/commerce/collection";
|
||||
import { ProductDetailsProps } from "@/components/commerce/product-details";
|
||||
import { RecommendedProductsProps } from "@/components/commerce/recommended-products";
|
||||
import { FeaturedProductProps } from "@/components/commerce/featured-product";
|
||||
import { ProductsGridProps } from "@/components/shopify/products-grid";
|
||||
import { ProductsCarouselProps } from "@/components/shopify/products-carousel";
|
||||
import { CollectionGridProps } from "@/components/shopify/collection-grid";
|
||||
import { CollectionProps } from "@/components/shopify/collection";
|
||||
import { ProductDetailsProps } from "@/components/shopify/product-details";
|
||||
import { RecommendedProductsProps } from "@/components/shopify/recommended-products";
|
||||
import { ProductRecommendationsProps } from "@/components/shopify/product-recommendations";
|
||||
import { SearchProductsProps } from "@/components/shopify/search-products";
|
||||
import { FeaturedProductProps } from "@/components/shopify/featured-product";
|
||||
|
||||
import { BannerProps } from "@/components/landing/banner";
|
||||
import { NewsletterCtaProps } from "@/components/landing/newsletter-cta";
|
||||
import { ImageGalleryProps } from "@/components/landing/image-gallery";
|
||||
import { NewsletterCtaProps } from "@/components/newsletter-cta/newsletter-cta";
|
||||
|
||||
import { RootProps } from "./root";
|
||||
|
||||
export type { RootProps } from "./root";
|
||||
|
||||
export type Components = {
|
||||
navigation: NavigationProps;
|
||||
hero: HeroProps;
|
||||
banner: BannerProps;
|
||||
header: HeaderProps;
|
||||
cover: CoverProps;
|
||||
"newsletter-cta": NewsletterCtaProps;
|
||||
"featured-product": FeaturedProductProps;
|
||||
"products-grid": ProductsGridProps;
|
||||
"products-carousel": ProductsCarouselProps;
|
||||
@@ -36,20 +31,15 @@ export type Components = {
|
||||
collection: CollectionProps;
|
||||
"product-details": ProductDetailsProps;
|
||||
"recommended-products": RecommendedProductsProps;
|
||||
features: FeaturesProps;
|
||||
testimonials: TestimonialsProps;
|
||||
"image-gallery": ImageGalleryProps;
|
||||
"newsletter-cta": NewsletterCtaProps;
|
||||
logos: LogosProps;
|
||||
cta: CTAProps;
|
||||
faq: FAQProps;
|
||||
"product-recommendations": ProductRecommendationsProps;
|
||||
"search-products": SearchProductsProps;
|
||||
footer: FooterProps;
|
||||
};
|
||||
|
||||
export type UserConfig = Config<{
|
||||
components: Components;
|
||||
root: RootProps;
|
||||
categories: ["navigation", "hero", "commerce", "content", "footer"];
|
||||
categories: ["header", "cover", "commerce", "content", "footer"];
|
||||
fields: {
|
||||
userField: {
|
||||
type: "userField";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
updateCartLines,
|
||||
} from '@/hooks/use-shopify-cart';
|
||||
import { setShopifyCredentials } from '@/services/shopify/client';
|
||||
import CartDrawer from '@/components/commerce/cart-drawer';
|
||||
import CartDrawer from '@/components/shopify/cart-drawer';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
|
||||
const CART_ID_KEY = 'cartId';
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import navigationEditor from "@/components/navigation/navigation.editor";
|
||||
import headerEditor from "@/components/header/header.editor";
|
||||
import footerEditor from "@/components/footer/footer.editor";
|
||||
|
||||
import heroEditor from "@/components/hero/hero.editor";
|
||||
import bannerEditor from "@/components/landing/banner.editor";
|
||||
import coverEditor from "@/components/cover/cover.editor";
|
||||
import newsletterCtaEditor from "@/components/newsletter-cta/newsletter-cta.editor";
|
||||
|
||||
import featuredProductEditor from "@/components/commerce/featured-product.editor";
|
||||
import productsGridEditor from "@/components/commerce/products-grid.editor";
|
||||
import productsCarouselEditor from "@/components/commerce/products-carousel.editor";
|
||||
import collectionGridEditor from "@/components/commerce/collection-grid.editor";
|
||||
import collectionEditor from "@/components/commerce/collection.editor";
|
||||
import productDetailsEditor from "@/components/commerce/product-details.editor";
|
||||
import recommendedProductsEditor from "@/components/commerce/recommended-products.editor";
|
||||
import productRecommendationsEditor from "@/components/commerce/product-recommendations.editor";
|
||||
import searchProductsEditor from "@/components/commerce/search-products.editor";
|
||||
|
||||
import featuresEditor from "@/components/features/features.editor";
|
||||
import testimonialsEditor from "@/components/testimonials/testimonials.editor";
|
||||
import imageGalleryEditor from "@/components/landing/image-gallery.editor";
|
||||
import newsletterCtaEditor from "@/components/landing/newsletter-cta.editor";
|
||||
import logosEditor from "@/components/logos/logos.editor";
|
||||
import ctaEditor from "@/components/cta/cta.editor";
|
||||
import faqEditor from "@/components/faq/faq.editor";
|
||||
import featuredProductEditor from "@/components/shopify/featured-product.editor";
|
||||
import productsGridEditor from "@/components/shopify/products-grid.editor";
|
||||
import productsCarouselEditor from "@/components/shopify/products-carousel.editor";
|
||||
import collectionGridEditor from "@/components/shopify/collection-grid.editor";
|
||||
import collectionEditor from "@/components/shopify/collection.editor";
|
||||
import productDetailsEditor from "@/components/shopify/product-details.editor";
|
||||
import recommendedProductsEditor from "@/components/shopify/recommended-products.editor";
|
||||
import productRecommendationsEditor from "@/components/shopify/product-recommendations.editor";
|
||||
import searchProductsEditor from "@/components/shopify/search-products.editor";
|
||||
|
||||
import Root from "@/config/root";
|
||||
import type { UserConfig } from "@/config/types";
|
||||
|
||||
const categories = {
|
||||
navigation: { title: "Navigation" },
|
||||
hero: { title: "Hero & Banners" },
|
||||
header: { title: "Header" },
|
||||
cover: { title: "Cover" },
|
||||
commerce: { title: "Commerce" },
|
||||
content: { title: "Content" },
|
||||
footer: { title: "Footer" },
|
||||
@@ -37,10 +29,10 @@ export const appConfig: UserConfig = {
|
||||
root: Root,
|
||||
categories,
|
||||
components: {
|
||||
navigation: navigationEditor,
|
||||
header: headerEditor,
|
||||
footer: footerEditor,
|
||||
hero: heroEditor,
|
||||
banner: bannerEditor,
|
||||
cover: coverEditor,
|
||||
"newsletter-cta": newsletterCtaEditor,
|
||||
"featured-product": featuredProductEditor,
|
||||
"products-grid": productsGridEditor,
|
||||
"products-carousel": productsCarouselEditor,
|
||||
@@ -50,12 +42,5 @@ export const appConfig: UserConfig = {
|
||||
"recommended-products": recommendedProductsEditor,
|
||||
"product-recommendations": productRecommendationsEditor,
|
||||
"search-products": searchProductsEditor,
|
||||
features: featuresEditor,
|
||||
testimonials: testimonialsEditor,
|
||||
"image-gallery": imageGalleryEditor,
|
||||
"newsletter-cta": newsletterCtaEditor,
|
||||
logos: logosEditor,
|
||||
cta: ctaEditor,
|
||||
faq: faqEditor,
|
||||
} as any,
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user