Initial commit
This commit is contained in:
102
editor/components/footer/footer.editor.tsx
Normal file
102
editor/components/footer/footer.editor.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ComponentConfig } from "@reacteditor/core";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import { Footer, type FooterProps } from "@/editor/components/footer/footer";
|
||||
|
||||
export const footerEditor: ComponentConfig<FooterProps> = {
|
||||
label: "Footer",
|
||||
icon: <LayoutGrid size={16} />,
|
||||
category: "footer",
|
||||
global: true,
|
||||
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} />,
|
||||
};
|
||||
129
editor/components/footer/footer.tsx
Normal file
129
editor/components/footer/footer.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
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;
|
||||
};
|
||||
|
||||
export 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}>
|
||||
<Link
|
||||
to={l.href}
|
||||
className="text-sm text-foreground/80 hover:text-foreground"
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user