Initial commit
This commit is contained in:
108
config/configs.ts
Normal file
108
config/configs.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
createFieldShopifyProduct,
|
||||
createFieldShopifyCollection,
|
||||
} from "@reacteditor/field-shopify";
|
||||
|
||||
import { navigationEditor } from "@/components/navigation/navigation.editor";
|
||||
import { footerEditor } from "@/components/footer/footer.editor";
|
||||
|
||||
import { heroEditor } from "@/components/hero/hero.editor";
|
||||
import { bannerEditor } from "@/components/landing/banner.editor";
|
||||
|
||||
import { createFeaturedProductEditor } from "@/components/commerce/featured-product.editor";
|
||||
import { createProductsGridEditor } from "@/components/commerce/products-grid.editor";
|
||||
import { createProductsCarouselEditor } from "@/components/commerce/products-carousel.editor";
|
||||
import { collectionGridEditor } from "@/components/commerce/collection-grid.editor";
|
||||
import { createCollectionEditor } from "@/components/commerce/collection.editor";
|
||||
import { createProductDetailsEditor } from "@/components/commerce/product-details.editor";
|
||||
import { createRecommendedProductsEditor } from "@/components/commerce/recommended-products.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 Root from "@/config/root";
|
||||
import type { UserConfig } from "@/config/types";
|
||||
|
||||
const SHOPIFY_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN ?? "mock.shop";
|
||||
const STOREFRONT_TOKEN =
|
||||
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN ?? "";
|
||||
|
||||
const productField = createFieldShopifyProduct({
|
||||
storeDomain: SHOPIFY_DOMAIN,
|
||||
storefrontAccessToken: STOREFRONT_TOKEN || undefined,
|
||||
}) as any;
|
||||
|
||||
const collectionField = createFieldShopifyCollection({
|
||||
storeDomain: SHOPIFY_DOMAIN,
|
||||
storefrontAccessToken: STOREFRONT_TOKEN || undefined,
|
||||
}) as any;
|
||||
|
||||
const categories = {
|
||||
navigation: { title: "Navigation" },
|
||||
hero: { title: "Hero & Banners" },
|
||||
commerce: { title: "Commerce" },
|
||||
content: { title: "Content" },
|
||||
footer: { title: "Footer" },
|
||||
};
|
||||
|
||||
const sharedComponents = {
|
||||
navigation: navigationEditor,
|
||||
footer: footerEditor,
|
||||
};
|
||||
|
||||
export const homeConfig: UserConfig = {
|
||||
root: Root,
|
||||
categories,
|
||||
components: {
|
||||
...sharedComponents,
|
||||
hero: heroEditor,
|
||||
banner: bannerEditor,
|
||||
"featured-product": createFeaturedProductEditor({ productField }),
|
||||
"products-grid": createProductsGridEditor({ collectionField }),
|
||||
"products-carousel": createProductsCarouselEditor({ collectionField }),
|
||||
"collection-grid": collectionGridEditor,
|
||||
features: featuresEditor,
|
||||
testimonials: testimonialsEditor,
|
||||
"image-gallery": imageGalleryEditor,
|
||||
"newsletter-cta": newsletterCtaEditor,
|
||||
logos: logosEditor,
|
||||
cta: ctaEditor,
|
||||
faq: faqEditor,
|
||||
} as any,
|
||||
};
|
||||
|
||||
export const productConfig: UserConfig = {
|
||||
root: Root,
|
||||
categories,
|
||||
components: {
|
||||
...sharedComponents,
|
||||
"product-details": createProductDetailsEditor({ productField }),
|
||||
"recommended-products": createRecommendedProductsEditor({ productField }),
|
||||
"products-carousel": createProductsCarouselEditor({ collectionField }),
|
||||
} as any,
|
||||
};
|
||||
|
||||
export const collectionsConfig: UserConfig = {
|
||||
root: Root,
|
||||
categories,
|
||||
components: {
|
||||
...sharedComponents,
|
||||
collection: createCollectionEditor({ collectionField }),
|
||||
} as any,
|
||||
};
|
||||
|
||||
export const searchConfig: UserConfig = {
|
||||
root: Root,
|
||||
categories,
|
||||
components: {
|
||||
...sharedComponents,
|
||||
"search-products": searchProductsEditor,
|
||||
} as any,
|
||||
};
|
||||
51
config/icons.tsx
Normal file
51
config/icons.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Download,
|
||||
ExternalLink,
|
||||
|
||||
Play,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Star,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const iconMap: Record<string, LucideIcon> = {
|
||||
"arrow-right": ArrowRight,
|
||||
"arrow-up-right": ArrowUpRight,
|
||||
check: Check,
|
||||
"chevron-right": ChevronRight,
|
||||
download: Download,
|
||||
"external-link": ExternalLink,
|
||||
|
||||
play: Play,
|
||||
rocket: Rocket,
|
||||
sparkles: Sparkles,
|
||||
star: Star,
|
||||
zap: Zap,
|
||||
};
|
||||
|
||||
export const iconOptions = [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Arrow right", value: "arrow-right" },
|
||||
{ label: "Arrow up right", value: "arrow-up-right" },
|
||||
{ label: "Check", value: "check" },
|
||||
{ label: "Chevron right", value: "chevron-right" },
|
||||
{ label: "Download", value: "download" },
|
||||
{ label: "External link", value: "external-link" },
|
||||
|
||||
{ label: "Play", value: "play" },
|
||||
{ label: "Rocket", value: "rocket" },
|
||||
{ label: "Sparkles", value: "sparkles" },
|
||||
{ label: "Star", value: "star" },
|
||||
{ label: "Zap", value: "zap" },
|
||||
];
|
||||
|
||||
export const resolveIcon = (name: string): LucideIcon | null => {
|
||||
if (!name || name === "none") return null;
|
||||
return iconMap[name] ?? null;
|
||||
};
|
||||
206
config/initial-data.ts
Normal file
206
config/initial-data.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { UserData } from "./types";
|
||||
|
||||
export const getInitialData = (path: string): Partial<UserData> =>
|
||||
initialData[path] ?? { content: [], root: { props: { title: "Untitled" } } };
|
||||
|
||||
export const initialData: Record<string, UserData> = {
|
||||
"/": {
|
||||
root: {
|
||||
props: {
|
||||
title: "Maison — Considered essentials",
|
||||
headerFont: "Inter",
|
||||
bodyFont: "Inter",
|
||||
},
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "navigation",
|
||||
props: {
|
||||
id: "nav-home",
|
||||
brand: "Maison",
|
||||
links: [
|
||||
{ label: "Shop", href: "/collections" },
|
||||
{ label: "Lookbook", href: "/lookbook" },
|
||||
{ label: "Journal", href: "/journal" },
|
||||
{ label: "About", href: "/about" },
|
||||
],
|
||||
cta: { label: "", href: "" },
|
||||
showSearch: "yes",
|
||||
showCart: "yes",
|
||||
sticky: "yes",
|
||||
tone: "default",
|
||||
bannerText: "",
|
||||
bannerTone: "accent",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "hero",
|
||||
props: {
|
||||
id: "hero-home",
|
||||
tagline: "Spring 2026",
|
||||
heading: "Made for the way you move",
|
||||
subheading:
|
||||
"A considered wardrobe of essentials, cut from natural fibers and designed to last.",
|
||||
primaryCta: { label: "Shop the collection", href: "/collections" },
|
||||
secondaryCta: { label: "Our story", href: "/about" },
|
||||
imageUrl:
|
||||
"https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=2400&q=80",
|
||||
align: "left",
|
||||
height: "lg",
|
||||
tone: "dark",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "products-carousel",
|
||||
props: {
|
||||
id: "carousel-home",
|
||||
tagline: "New",
|
||||
heading: "Just dropped",
|
||||
subheading: "Fresh additions to the lineup.",
|
||||
limit: 12,
|
||||
slidesPerView: "4",
|
||||
ctaLabel: "Shop new",
|
||||
ctaHref: "/collections/new",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "featured-product",
|
||||
props: {
|
||||
id: "featured-home",
|
||||
handle: "",
|
||||
tagline: "Featured",
|
||||
ctaLabel: "Add to bag",
|
||||
align: "left",
|
||||
tone: "muted",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "collection-grid",
|
||||
props: {
|
||||
id: "collections-home",
|
||||
tagline: "Shop by collection",
|
||||
heading: "Curated edits",
|
||||
subheading: "Bundles built around the way you actually live.",
|
||||
layout: "tiles",
|
||||
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: {
|
||||
id: "newsletter-home",
|
||||
tagline: "Stay in the loop",
|
||||
heading: "Letters from the studio",
|
||||
subheading:
|
||||
"New collections, mill stories, and the occasional invitation. Twice a month.",
|
||||
buttonLabel: "Subscribe",
|
||||
endpoint: "",
|
||||
imageUrl:
|
||||
"https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1800&q=80",
|
||||
layout: "split",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "footer",
|
||||
props: {
|
||||
id: "footer-home",
|
||||
brand: "Maison",
|
||||
tagline:
|
||||
"Considered essentials, made in small batches and built to last beyond the season.",
|
||||
columns: [
|
||||
{
|
||||
title: "Shop",
|
||||
links: [
|
||||
{ label: "All", href: "/collections" },
|
||||
{ label: "New", href: "/collections/new" },
|
||||
{ label: "Best sellers", href: "/collections/best" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
links: [
|
||||
{ label: "Our story", href: "/about" },
|
||||
{ label: "Materials", href: "/materials" },
|
||||
{ label: "Journal", href: "/journal" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Help",
|
||||
links: [
|
||||
{ label: "Shipping", href: "/help/shipping" },
|
||||
{ label: "Returns", href: "/help/returns" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Legal",
|
||||
links: [
|
||||
{ label: "Terms", href: "/terms" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
],
|
||||
social: [
|
||||
{ label: "Instagram", href: "#" },
|
||||
{ label: "Pinterest", href: "#" },
|
||||
{ label: "TikTok", href: "#" },
|
||||
],
|
||||
showNewsletter: "no",
|
||||
newsletterHeading: "Stay in touch",
|
||||
newsletterEndpoint: "",
|
||||
copyright: "© 2026 Maison. All rights reserved.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
22
config/options.ts
Normal file
22
config/options.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const spacingOptions = [
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
{ label: "24px", value: "24px" },
|
||||
{ label: "32px", value: "32px" },
|
||||
{ label: "40px", value: "40px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "56px", value: "56px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "72px", value: "72px" },
|
||||
{ label: "80px", value: "80px" },
|
||||
{ label: "88px", value: "88px" },
|
||||
{ label: "96px", value: "96px" },
|
||||
{ label: "104px", value: "104px" },
|
||||
{ label: "112px", value: "112px" },
|
||||
{ label: "120px", value: "120px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "136px", value: "136px" },
|
||||
{ label: "144px", value: "144px" },
|
||||
{ label: "152px", value: "152px" },
|
||||
{ label: "160px", value: "160px" },
|
||||
];
|
||||
134
config/root.tsx
Normal file
134
config/root.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { DefaultRootProps, RootConfig } from "@reacteditor/core";
|
||||
import { createFieldGoogleFonts } from "@reacteditor/field-google-fonts";
|
||||
import { imageField } from "@reacteditor/plugin-media/field";
|
||||
import { ThemeProvider, ThemeProps } from "@/components/ThemeProvider";
|
||||
import { frontendAiMediaAdapter } from "@/services/media-adapter";
|
||||
|
||||
export type RootProps = DefaultRootProps &
|
||||
ThemeProps & {
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
};
|
||||
|
||||
const headerFontField = createFieldGoogleFonts() as any;
|
||||
const bodyFontField = createFieldGoogleFonts() as any;
|
||||
|
||||
export const Root: RootConfig<{
|
||||
props: RootProps;
|
||||
fields: {
|
||||
userField: { type: "userField"; option: boolean };
|
||||
};
|
||||
}> = {
|
||||
defaultProps: {
|
||||
title: "Untitled",
|
||||
headerFont: "Inter",
|
||||
headerFontWeight: "600",
|
||||
bodyFont: "Inter",
|
||||
// Hex defaults so the color picker reads them and any non-picker
|
||||
// input (typed hex, AI-set value, etc.) is round-trip compatible.
|
||||
primaryColor: "#0a0a0a",
|
||||
secondaryColor: "#64748B",
|
||||
accentColor: "#f5f5f5",
|
||||
bgColor: "#ffffff",
|
||||
fgColor: "#0a0a0a",
|
||||
mutedColor: "#f5f5f5",
|
||||
radius: "md",
|
||||
shadow: "sm",
|
||||
maxWidth: "lg",
|
||||
},
|
||||
fields: {
|
||||
title: { label: "Page title", type: "text" },
|
||||
description: { label: "Description", type: "textarea" },
|
||||
ogImage: { label: "OG image", ...imageField({ adapter: frontendAiMediaAdapter }) },
|
||||
headerFont: { label: "Header font", ...headerFontField },
|
||||
headerFontWeight: {
|
||||
label: "Header font weight",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "300 — Light", value: "300" },
|
||||
{ label: "400 — Regular", value: "400" },
|
||||
{ label: "500 — Medium", value: "500" },
|
||||
{ label: "600 — Semibold", value: "600" },
|
||||
{ label: "700 — Bold", value: "700" },
|
||||
{ label: "800 — Extrabold", value: "800" },
|
||||
{ label: "900 — Black", value: "900" },
|
||||
],
|
||||
},
|
||||
bodyFont: { label: "Body font", ...bodyFontField },
|
||||
primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" },
|
||||
secondaryColor: { label: "Secondary color", type: "color", placeholder: "#64748B" },
|
||||
accentColor: { label: "Accent color", type: "color", placeholder: "#f5f5f5" },
|
||||
bgColor: { label: "Background color", type: "color", placeholder: "#ffffff" },
|
||||
fgColor: { label: "Foreground color", type: "color", placeholder: "#0a0a0a" },
|
||||
mutedColor: { label: "Muted color", type: "color", placeholder: "#f5f5f5" },
|
||||
radius: {
|
||||
label: "Radius",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "None (square)", value: "none" },
|
||||
{ label: "Small", value: "sm" },
|
||||
{ label: "Medium", value: "md" },
|
||||
{ label: "Large", value: "lg" },
|
||||
{ label: "Extra large", value: "xl" },
|
||||
],
|
||||
},
|
||||
shadow: {
|
||||
label: "Shadow",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Small", value: "sm" },
|
||||
{ label: "Medium", value: "md" },
|
||||
{ label: "Large", value: "lg" },
|
||||
{ label: "Extra large", value: "xl" },
|
||||
],
|
||||
},
|
||||
maxWidth: {
|
||||
label: "Max width",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Small", value: "sm" },
|
||||
{ label: "Medium", value: "md" },
|
||||
{ label: "Large", value: "lg" },
|
||||
{ label: "Extra large", value: "xl" },
|
||||
{ label: "Full bleed", value: "full" },
|
||||
],
|
||||
},
|
||||
},
|
||||
render: ({
|
||||
children,
|
||||
headerFont,
|
||||
headerFontWeight,
|
||||
bodyFont,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
accentColor,
|
||||
bgColor,
|
||||
fgColor,
|
||||
mutedColor,
|
||||
radius,
|
||||
shadow,
|
||||
maxWidth,
|
||||
}) => {
|
||||
return (
|
||||
<ThemeProvider
|
||||
headerFont={headerFont}
|
||||
headerFontWeight={headerFontWeight}
|
||||
bodyFont={bodyFont}
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
accentColor={accentColor}
|
||||
bgColor={bgColor}
|
||||
fgColor={fgColor}
|
||||
mutedColor={mutedColor}
|
||||
radius={radius}
|
||||
shadow={shadow}
|
||||
maxWidth={maxWidth}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default Root;
|
||||
61
config/types.ts
Normal file
61
config/types.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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 { 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 { BannerProps } from "@/components/landing/banner";
|
||||
import { NewsletterCtaProps } from "@/components/landing/newsletter-cta";
|
||||
import { ImageGalleryProps } from "@/components/landing/image-gallery";
|
||||
|
||||
import { RootProps } from "./root";
|
||||
|
||||
export type { RootProps } from "./root";
|
||||
|
||||
export type Components = {
|
||||
navigation: NavigationProps;
|
||||
hero: HeroProps;
|
||||
banner: BannerProps;
|
||||
"featured-product": FeaturedProductProps;
|
||||
"products-grid": ProductsGridProps;
|
||||
"products-carousel": ProductsCarouselProps;
|
||||
"collection-grid": CollectionGridProps;
|
||||
collection: CollectionProps;
|
||||
"product-details": ProductDetailsProps;
|
||||
"recommended-products": RecommendedProductsProps;
|
||||
features: FeaturesProps;
|
||||
testimonials: TestimonialsProps;
|
||||
"image-gallery": ImageGalleryProps;
|
||||
"newsletter-cta": NewsletterCtaProps;
|
||||
logos: LogosProps;
|
||||
cta: CTAProps;
|
||||
faq: FAQProps;
|
||||
footer: FooterProps;
|
||||
};
|
||||
|
||||
export type UserConfig = Config<{
|
||||
components: Components;
|
||||
root: RootProps;
|
||||
categories: ["navigation", "hero", "commerce", "content", "footer"];
|
||||
fields: {
|
||||
userField: {
|
||||
type: "userField";
|
||||
option: boolean;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export type UserData = Data<Components, RootProps>;
|
||||
Reference in New Issue
Block a user