229 lines
7.2 KiB
TypeScript
229 lines
7.2 KiB
TypeScript
import { ComponentConfig } from "@reacteditor/core";
|
|
import { useState } from "react";
|
|
import { LayoutGrid } from "lucide-react";
|
|
import { Typography } from "~/editor/theme/Typography";
|
|
|
|
export type FooterProps = {
|
|
brand: string;
|
|
tagline: string;
|
|
columns: Array<{
|
|
title: string;
|
|
links: Array<{ label: string; href: string }>;
|
|
}>;
|
|
social: Array<{ label: string; href: string }>;
|
|
showNewsletter: "yes" | "no";
|
|
newsletterHeading: string;
|
|
newsletterEndpoint: string;
|
|
copyright: string;
|
|
};
|
|
|
|
function Footer({
|
|
brand,
|
|
tagline,
|
|
columns,
|
|
social,
|
|
showNewsletter,
|
|
newsletterHeading,
|
|
newsletterEndpoint,
|
|
copyright,
|
|
}: FooterProps) {
|
|
const [email, setEmail] = useState("");
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!email) return;
|
|
if (newsletterEndpoint) {
|
|
try {
|
|
await fetch(newsletterEndpoint, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
} catch {}
|
|
}
|
|
setSubmitted(true);
|
|
};
|
|
|
|
return (
|
|
<footer className="border-t border-border bg-background">
|
|
<div className="container mx-auto max-w-7xl px-6 py-20 md:py-24">
|
|
<div className="grid grid-cols-1 gap-12 md:grid-cols-12">
|
|
<div className="md:col-span-4">
|
|
<Typography variant="h5" as="p">
|
|
{brand}
|
|
</Typography>
|
|
{tagline ? (
|
|
<Typography variant="body2" className="mt-3 max-w-sm text-muted-foreground">
|
|
{tagline}
|
|
</Typography>
|
|
) : null}
|
|
|
|
{showNewsletter === "yes" ? (
|
|
<form onSubmit={submit} className="mt-8 max-w-sm">
|
|
<p className="text-sm font-medium">{newsletterHeading}</p>
|
|
{submitted ? (
|
|
<p className="mt-3 text-sm text-muted-foreground">
|
|
Thanks — we'll be in touch.
|
|
</p>
|
|
) : (
|
|
<div className="mt-3 flex border-b border-border focus-within:border-foreground">
|
|
<input
|
|
type="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="you@example.com"
|
|
className="flex-1 bg-transparent py-2 text-sm placeholder:text-muted-foreground focus:outline-none"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="ml-3 text-sm font-medium tracking-wide hover:opacity-70"
|
|
>
|
|
Subscribe →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</form>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-8 md:col-span-8 md:grid-cols-4">
|
|
{columns.map((col, i) => (
|
|
<div key={i}>
|
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
{col.title}
|
|
</p>
|
|
<ul className="mt-4 space-y-2.5">
|
|
{col.links.map((l, j) => (
|
|
<li key={j}>
|
|
<a
|
|
href={l.href}
|
|
className="text-sm text-foreground/80 hover:text-foreground"
|
|
>
|
|
{l.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-16 flex flex-col items-start justify-between gap-4 border-t border-border pt-8 md:flex-row md:items-center">
|
|
<p className="text-xs text-muted-foreground">{copyright}</p>
|
|
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
|
{social.map((s, i) => (
|
|
<a
|
|
key={i}
|
|
href={s.href}
|
|
className="text-xs uppercase tracking-[0.18em] text-foreground/70 hover:text-foreground"
|
|
>
|
|
{s.label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
);
|
|
}
|
|
|
|
export const footerEditor: ComponentConfig<FooterProps> = {
|
|
label: "Footer",
|
|
icon: <LayoutGrid size={16} />,
|
|
category: "footer",
|
|
defaultProps: {
|
|
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: "yes",
|
|
newsletterHeading: "Stay in touch",
|
|
newsletterEndpoint: "",
|
|
copyright: "© 2026 Maison. All rights reserved.",
|
|
},
|
|
fields: {
|
|
brand: { label: "Brand", type: "text", contentEditable: true },
|
|
tagline: { label: "Tagline", type: "textarea", contentEditable: true },
|
|
columns: {
|
|
label: "Columns",
|
|
type: "array",
|
|
defaultItemProps: { title: "Column", links: [] },
|
|
getItemSummary: (it) => it?.title || "Column",
|
|
arrayFields: {
|
|
title: { label: "Title", type: "text", contentEditable: true },
|
|
links: {
|
|
label: "Links",
|
|
type: "array",
|
|
defaultItemProps: { label: "Link", href: "/" },
|
|
getItemSummary: (it) => it?.label || "Link",
|
|
arrayFields: {
|
|
label: { label: "Label", type: "text", contentEditable: true },
|
|
href: { label: "Link", type: "text" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
social: {
|
|
label: "Social links",
|
|
type: "array",
|
|
defaultItemProps: { label: "Instagram", href: "#" },
|
|
getItemSummary: (it) => it?.label || "Social",
|
|
arrayFields: {
|
|
label: { label: "Label", type: "text", contentEditable: true },
|
|
href: { label: "Link", type: "text" },
|
|
},
|
|
},
|
|
showNewsletter: {
|
|
label: "Newsletter form",
|
|
type: "radio",
|
|
options: [
|
|
{ label: "Show", value: "yes" },
|
|
{ label: "Hide", value: "no" },
|
|
],
|
|
},
|
|
newsletterHeading: { label: "Newsletter heading", type: "text", contentEditable: true },
|
|
newsletterEndpoint: { label: "Newsletter endpoint", type: "text" },
|
|
copyright: { label: "Copyright", type: "text", contentEditable: true },
|
|
},
|
|
render: (props) => <Footer {...props} />,
|
|
};
|