@numueg/theme-cli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.js +825 -405
- package/package.json +2 -1
- package/templates/scaffold/index.html +13 -0
- package/templates/scaffold/package.json +27 -0
- package/templates/scaffold/schemas/sections/about_section.json +23 -0
- package/templates/scaffold/schemas/sections/account.json +8 -0
- package/templates/scaffold/schemas/sections/cart_summary.json +12 -0
- package/templates/scaffold/schemas/sections/categories.json +9 -0
- package/templates/scaffold/schemas/sections/featured_collection.json +14 -0
- package/templates/scaffold/schemas/sections/footer.json +14 -0
- package/templates/scaffold/schemas/sections/frequently_bought.json +10 -0
- package/templates/scaffold/schemas/sections/header.json +14 -0
- package/templates/scaffold/schemas/sections/hero.json +15 -0
- package/templates/scaffold/schemas/sections/image_with_text.json +19 -0
- package/templates/scaffold/schemas/sections/marquee.json +9 -0
- package/templates/scaffold/schemas/sections/newsletter.json +11 -0
- package/templates/scaffold/schemas/sections/not_found.json +12 -0
- package/templates/scaffold/schemas/sections/order_confirmation.json +9 -0
- package/templates/scaffold/schemas/sections/product_details.json +12 -0
- package/templates/scaffold/schemas/sections/product_grid.json +12 -0
- package/templates/scaffold/schemas/sections/promo_banner.json +13 -0
- package/templates/scaffold/schemas/sections/rich_text.json +17 -0
- package/templates/scaffold/schemas/sections/search_results.json +11 -0
- package/templates/scaffold/schemas/sections/size_chart.json +9 -0
- package/templates/scaffold/schemas/sections/testimonials.json +22 -0
- package/templates/scaffold/settings_schema.json +35 -0
- package/templates/scaffold/src/dev-entry.tsx +244 -0
- package/templates/scaffold/src/lib/CouponForm.tsx +90 -0
- package/templates/scaffold/src/lib/EditableText.tsx +178 -0
- package/templates/scaffold/src/lib/ProductCard.tsx +99 -0
- package/templates/scaffold/src/lib/cartUI.ts +43 -0
- package/templates/scaffold/src/lib/i18n.ts +17 -0
- package/templates/scaffold/src/lib/section.ts +12 -0
- package/templates/scaffold/src/main.tsx +230 -0
- package/templates/scaffold/src/sections/Footer.tsx +161 -0
- package/templates/scaffold/src/sections/Header.tsx +453 -0
- package/templates/scaffold/src/sections/about_section.tsx +104 -0
- package/templates/scaffold/src/sections/account.tsx +422 -0
- package/templates/scaffold/src/sections/cart_summary.tsx +169 -0
- package/templates/scaffold/src/sections/categories.tsx +57 -0
- package/templates/scaffold/src/sections/featured_collection.tsx +109 -0
- package/templates/scaffold/src/sections/frequently_bought.tsx +187 -0
- package/templates/scaffold/src/sections/hero.tsx +133 -0
- package/templates/scaffold/src/sections/image_with_text.tsx +105 -0
- package/templates/scaffold/src/sections/marquee.tsx +45 -0
- package/templates/scaffold/src/sections/newsletter.tsx +79 -0
- package/templates/scaffold/src/sections/not_found.tsx +56 -0
- package/templates/scaffold/src/sections/order_confirmation.tsx +127 -0
- package/templates/scaffold/src/sections/product_details.tsx +541 -0
- package/templates/scaffold/src/sections/product_grid.tsx +147 -0
- package/templates/scaffold/src/sections/promo_banner.tsx +80 -0
- package/templates/scaffold/src/sections/rich_text.tsx +51 -0
- package/templates/scaffold/src/sections/search_results.tsx +93 -0
- package/templates/scaffold/src/sections/size_chart.tsx +109 -0
- package/templates/scaffold/src/sections/testimonials.tsx +112 -0
- package/templates/scaffold/styles.css +2404 -0
- package/templates/scaffold/templates/error.html +13 -0
- package/templates/scaffold/templates/loading.html +11 -0
- package/templates/scaffold/theme.json +224 -0
- package/templates/scaffold/tsconfig.json +22 -0
- package/templates/scaffold/vite.config.ts +16 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import {
|
|
3
|
+
defineThemeEntry,
|
|
4
|
+
Section,
|
|
5
|
+
useDirection,
|
|
6
|
+
type ThemeSettingsV3,
|
|
7
|
+
} from "@numueg/theme-sdk";
|
|
8
|
+
import manifest from "../theme.json";
|
|
9
|
+
|
|
10
|
+
import Header from "./sections/Header";
|
|
11
|
+
import Footer from "./sections/Footer";
|
|
12
|
+
import Hero from "./sections/hero";
|
|
13
|
+
import Marquee from "./sections/marquee";
|
|
14
|
+
import FeaturedCollection from "./sections/featured_collection";
|
|
15
|
+
import Categories from "./sections/categories";
|
|
16
|
+
import PromoBanner from "./sections/promo_banner";
|
|
17
|
+
import Testimonials from "./sections/testimonials";
|
|
18
|
+
import Newsletter from "./sections/newsletter";
|
|
19
|
+
import ProductDetails from "./sections/product_details";
|
|
20
|
+
import SizeChart from "./sections/size_chart";
|
|
21
|
+
import FrequentlyBought from "./sections/frequently_bought";
|
|
22
|
+
import ProductGrid from "./sections/product_grid";
|
|
23
|
+
import CartSummary from "./sections/cart_summary";
|
|
24
|
+
import OrderConfirmation from "./sections/order_confirmation";
|
|
25
|
+
import ImageWithText from "./sections/image_with_text";
|
|
26
|
+
import RichTextSection from "./sections/rich_text";
|
|
27
|
+
import NotFound from "./sections/not_found";
|
|
28
|
+
import SearchResults from "./sections/search_results";
|
|
29
|
+
import Account from "./sections/account";
|
|
30
|
+
import AboutSection from "./sections/about_section";
|
|
31
|
+
|
|
32
|
+
const SECTION_REGISTRY: Record<string, ComponentType<any>> = {
|
|
33
|
+
header: Header,
|
|
34
|
+
footer: Footer,
|
|
35
|
+
hero: Hero,
|
|
36
|
+
marquee: Marquee,
|
|
37
|
+
featured_collection: FeaturedCollection,
|
|
38
|
+
categories: Categories,
|
|
39
|
+
promo_banner: PromoBanner,
|
|
40
|
+
testimonials: Testimonials,
|
|
41
|
+
newsletter: Newsletter,
|
|
42
|
+
product_details: ProductDetails,
|
|
43
|
+
size_chart: SizeChart,
|
|
44
|
+
frequently_bought: FrequentlyBought,
|
|
45
|
+
product_grid: ProductGrid,
|
|
46
|
+
cart_summary: CartSummary,
|
|
47
|
+
order_confirmation: OrderConfirmation,
|
|
48
|
+
image_with_text: ImageWithText,
|
|
49
|
+
rich_text: RichTextSection,
|
|
50
|
+
not_found: NotFound,
|
|
51
|
+
search_results: SearchResults,
|
|
52
|
+
account: Account,
|
|
53
|
+
about_section: AboutSection,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const isKnown = (type: string) => Boolean(SECTION_REGISTRY[type]);
|
|
57
|
+
|
|
58
|
+
interface SectionLike {
|
|
59
|
+
type: string;
|
|
60
|
+
settings?: Record<string, any>;
|
|
61
|
+
blocks?: Record<string, any> | any[];
|
|
62
|
+
block_order?: string[];
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
interface GroupLike {
|
|
66
|
+
sections?: Record<string, SectionLike> | SectionLike[];
|
|
67
|
+
order?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The theme's own default templates/groups, baked into the bundle. The host
|
|
71
|
+
// passes EMPTY templates for a marketplace PREVIEW (before install / before the
|
|
72
|
+
// merchant customizes), expecting the bundle to fall back to these.
|
|
73
|
+
const PRESETS = (manifest as any).presets ?? {};
|
|
74
|
+
const BUILTIN_TEMPLATES: Record<string, GroupLike> = PRESETS.templates ?? {};
|
|
75
|
+
const BUILTIN_GROUPS: Record<string, GroupLike> = PRESETS.section_groups ?? {};
|
|
76
|
+
|
|
77
|
+
/** Normalise a section instance so blocks are always `{map}` + `order[]`
|
|
78
|
+
* (presets store blocks as an array; resolved host data stores a map). */
|
|
79
|
+
function normaliseInstance(instance: SectionLike): SectionLike {
|
|
80
|
+
if (Array.isArray(instance.blocks)) {
|
|
81
|
+
const map: Record<string, any> = {};
|
|
82
|
+
const order: string[] = [];
|
|
83
|
+
instance.blocks.forEach((b: any, i: number) => {
|
|
84
|
+
const id = `${b?.type ?? "block"}-${i}`;
|
|
85
|
+
map[id] = b;
|
|
86
|
+
order.push(id);
|
|
87
|
+
});
|
|
88
|
+
return { ...instance, blocks: map, block_order: order };
|
|
89
|
+
}
|
|
90
|
+
return instance;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Normalise a template/group (array OR map+order) → ordered instance list. */
|
|
94
|
+
function resolveSections(
|
|
95
|
+
group: GroupLike | undefined,
|
|
96
|
+
): Array<{ id: string; instance: SectionLike }> {
|
|
97
|
+
if (!group || !group.sections) return [];
|
|
98
|
+
if (Array.isArray(group.sections)) {
|
|
99
|
+
return group.sections.map((instance, idx) => ({
|
|
100
|
+
id: `${instance.type}-${idx}`,
|
|
101
|
+
instance: normaliseInstance(instance),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
const map = group.sections as Record<string, SectionLike>;
|
|
105
|
+
const order = group.order ?? Object.keys(map);
|
|
106
|
+
const out: Array<{ id: string; instance: SectionLike }> = [];
|
|
107
|
+
for (const id of order) {
|
|
108
|
+
const instance = map[id];
|
|
109
|
+
if (instance) out.push({ id, instance: normaliseInstance(instance) });
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Prefer the host's customisation; fall back to bundled presets (preview). */
|
|
115
|
+
function selectSections(
|
|
116
|
+
host: GroupLike | undefined,
|
|
117
|
+
builtin: GroupLike | undefined,
|
|
118
|
+
): Array<{ id: string; instance: SectionLike }> {
|
|
119
|
+
const hostList = resolveSections(host).filter((s) => isKnown(s.instance.type));
|
|
120
|
+
if (hostList.length > 0) return hostList;
|
|
121
|
+
return resolveSections(builtin).filter((s) => isKnown(s.instance.type));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function styleVars(global: Record<string, any>): React.CSSProperties {
|
|
125
|
+
const vars: Record<string, string> = {};
|
|
126
|
+
if (global.accent_color) vars["--nt-accent"] = global.accent_color;
|
|
127
|
+
if (global.foreground_color) vars["--nt-fg"] = global.foreground_color;
|
|
128
|
+
if (global.background_color) vars["--nt-bg"] = global.background_color;
|
|
129
|
+
if (global.font_family) {
|
|
130
|
+
const stack = `"${global.font_family}", "Inter", system-ui, sans-serif`;
|
|
131
|
+
vars["--nt-font-body"] = stack;
|
|
132
|
+
vars["--nt-font-display"] = stack;
|
|
133
|
+
}
|
|
134
|
+
return vars as React.CSSProperties;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderList(
|
|
138
|
+
list: Array<{ id: string; instance: SectionLike }>,
|
|
139
|
+
keyPrefix: string,
|
|
140
|
+
groupId?: string,
|
|
141
|
+
extra?: Record<string, unknown>,
|
|
142
|
+
) {
|
|
143
|
+
return list.map(({ id, instance }) => {
|
|
144
|
+
if (instance.disabled) return null;
|
|
145
|
+
const Component = SECTION_REGISTRY[instance.type];
|
|
146
|
+
if (!Component) return null;
|
|
147
|
+
// <Section> emits the data-section-id the customizer's PreviewBridge reads
|
|
148
|
+
// for click-to-select; passing the id down lets each component wire
|
|
149
|
+
// <EditableText>/<EditableImage> for inline field editing.
|
|
150
|
+
return (
|
|
151
|
+
<Section
|
|
152
|
+
key={`${keyPrefix}-${id}`}
|
|
153
|
+
id={id}
|
|
154
|
+
type={instance.type}
|
|
155
|
+
groupId={groupId}
|
|
156
|
+
>
|
|
157
|
+
<Component
|
|
158
|
+
id={id}
|
|
159
|
+
type={instance.type}
|
|
160
|
+
settings={instance.settings}
|
|
161
|
+
blocks={instance.blocks}
|
|
162
|
+
blockOrder={instance.block_order}
|
|
163
|
+
{...extra}
|
|
164
|
+
/>
|
|
165
|
+
</Section>
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ThemeProps {
|
|
171
|
+
themeSettings: ThemeSettingsV3;
|
|
172
|
+
currentTemplate: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default function Theme({ themeSettings, currentTemplate }: ThemeProps) {
|
|
176
|
+
const pageType = currentTemplate || "home";
|
|
177
|
+
const global = themeSettings.global_settings || {};
|
|
178
|
+
const dir = useDirection();
|
|
179
|
+
|
|
180
|
+
const hostTemplates = (themeSettings.templates ?? {}) as Record<
|
|
181
|
+
string,
|
|
182
|
+
GroupLike
|
|
183
|
+
>;
|
|
184
|
+
const hostTemplate =
|
|
185
|
+
hostTemplates[pageType] ?? hostTemplates.page ?? hostTemplates.home;
|
|
186
|
+
const builtinTemplate =
|
|
187
|
+
BUILTIN_TEMPLATES[pageType] ??
|
|
188
|
+
BUILTIN_TEMPLATES.page ??
|
|
189
|
+
BUILTIN_TEMPLATES.home;
|
|
190
|
+
|
|
191
|
+
const hostGroups = (themeSettings.section_groups ?? {}) as Record<
|
|
192
|
+
string,
|
|
193
|
+
GroupLike
|
|
194
|
+
>;
|
|
195
|
+
const headerSections = selectSections(hostGroups.header, BUILTIN_GROUPS.header);
|
|
196
|
+
const footerSections = selectSections(hostGroups.footer, BUILTIN_GROUPS.footer);
|
|
197
|
+
const bodySections = selectSections(hostTemplate, builtinTemplate);
|
|
198
|
+
|
|
199
|
+
// The header is fixed/overlay; pages that don't open with a full-bleed hero
|
|
200
|
+
// need top padding so their first section clears it.
|
|
201
|
+
const firstType = bodySections[0]?.instance.type;
|
|
202
|
+
const bleedTop = firstType === "hero";
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="nt" dir={dir} style={styleVars(global)}>
|
|
206
|
+
{renderList(headerSections, "hg", "header", { solidHeader: !bleedTop })}
|
|
207
|
+
{!bleedTop && headerSections.length > 0 ? (
|
|
208
|
+
<div className="nt-spacer-top" aria-hidden="true" />
|
|
209
|
+
) : null}
|
|
210
|
+
{bodySections.length > 0 ? (
|
|
211
|
+
renderList(bodySections, pageType)
|
|
212
|
+
) : (
|
|
213
|
+
<section className="nt-page nt-container">
|
|
214
|
+
<p className="nt-placeholder">
|
|
215
|
+
No template configured for "{pageType}".
|
|
216
|
+
</p>
|
|
217
|
+
</section>
|
|
218
|
+
)}
|
|
219
|
+
{renderList(footerSections, "fg", "footer")}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Entry: ONE definition → mount (client/hydrate) + createApp (SSR) ────────
|
|
225
|
+
const entry = defineThemeEntry(({ themeSettings, currentTemplate }) => (
|
|
226
|
+
<Theme themeSettings={themeSettings} currentTemplate={currentTemplate} />
|
|
227
|
+
));
|
|
228
|
+
|
|
229
|
+
export const mount = entry.mount;
|
|
230
|
+
export const createApp = entry.createApp;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState } from "react";
|
|
4
|
+
import { useShop,
|
|
5
|
+
useCollections,
|
|
6
|
+
} from "@numueg/theme-sdk";
|
|
7
|
+
import { EditableText } from "../lib/EditableText";
|
|
8
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
9
|
+
import { useT } from "../lib/i18n";
|
|
10
|
+
|
|
11
|
+
interface FooterSettings {
|
|
12
|
+
brand_name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
ticker_text?: string;
|
|
15
|
+
instagram?: string;
|
|
16
|
+
facebook?: string;
|
|
17
|
+
twitter?: string;
|
|
18
|
+
copyright?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function Footer({ id, settings }: EmpSectionProps) {
|
|
22
|
+
const s = settings as FooterSettings;
|
|
23
|
+
const t = useT();
|
|
24
|
+
const shop = useShop();
|
|
25
|
+
const { collections } = useCollections({ limit: 5 });
|
|
26
|
+
// Prefer the real store name; the theme placeholder ("STORE") is treated as
|
|
27
|
+
// unset so it never overrides the store name baked in at activation.
|
|
28
|
+
const brand =
|
|
29
|
+
s.brand_name && s.brand_name !== "STORE"
|
|
30
|
+
? s.brand_name
|
|
31
|
+
: shop?.name || s.brand_name || "STORE";
|
|
32
|
+
const ticker = s.ticker_text || "100% مستقل";
|
|
33
|
+
|
|
34
|
+
// Year is computed client-side to keep the SSR render path deterministic.
|
|
35
|
+
const [year, setYear] = useState<number | null>(null);
|
|
36
|
+
useEffect(() => setYear(new Date().getFullYear()), []);
|
|
37
|
+
|
|
38
|
+
const socials = [
|
|
39
|
+
{ name: "Instagram", url: s.instagram, icon: <IconInstagram /> },
|
|
40
|
+
{ name: "X", url: s.twitter, icon: <IconX /> },
|
|
41
|
+
{ name: "Facebook", url: s.facebook, icon: <IconFacebook /> },
|
|
42
|
+
].filter((x) => x.url);
|
|
43
|
+
|
|
44
|
+
const tickerItems = Array.from({ length: 10 });
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<footer className="nt-footer">
|
|
48
|
+
{/* Ticker */}
|
|
49
|
+
<div className="nt-footer__ticker">
|
|
50
|
+
<div className="nt-marquee__track">
|
|
51
|
+
{tickerItems.concat(tickerItems).map((_, i) => (
|
|
52
|
+
<span className="nt-marquee__item" key={i}>
|
|
53
|
+
<span className="nt-marquee__text">{ticker}</span>
|
|
54
|
+
<span className="nt-marquee__dot">●</span>
|
|
55
|
+
<span className="nt-marquee__sub">{brand}</span>
|
|
56
|
+
<span className="nt-marquee__dot">●</span>
|
|
57
|
+
</span>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="nt-container">
|
|
63
|
+
<div className="nt-footer__grid">
|
|
64
|
+
{/* Brand */}
|
|
65
|
+
<div className="nt-footer__brand">
|
|
66
|
+
<EditableText
|
|
67
|
+
as="h3"
|
|
68
|
+
sectionId={id}
|
|
69
|
+
settingId="brand_name"
|
|
70
|
+
value={brand}
|
|
71
|
+
/>
|
|
72
|
+
<EditableText
|
|
73
|
+
as="p"
|
|
74
|
+
className="nt-footer__desc"
|
|
75
|
+
sectionId={id}
|
|
76
|
+
settingId="description"
|
|
77
|
+
value={
|
|
78
|
+
s.description ||
|
|
79
|
+
"متجر مستقل يقدّم تشكيلة مختارة بعناية. تصميم نظيف، جودة تدوم."
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
<div className="nt-footer__social">
|
|
83
|
+
{socials.length > 0
|
|
84
|
+
? socials.map((so) => (
|
|
85
|
+
<a
|
|
86
|
+
key={so.name}
|
|
87
|
+
href={so.url}
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noopener noreferrer"
|
|
90
|
+
aria-label={so.name}
|
|
91
|
+
>
|
|
92
|
+
{so.icon}
|
|
93
|
+
</a>
|
|
94
|
+
))
|
|
95
|
+
: [<IconInstagram key="i" />, <IconX key="x" />, <IconFacebook key="f" />].map(
|
|
96
|
+
(ic, i) => (
|
|
97
|
+
<span key={i} style={{ opacity: 0.3 }}>
|
|
98
|
+
{ic}
|
|
99
|
+
</span>
|
|
100
|
+
),
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Shop links */}
|
|
106
|
+
<div>
|
|
107
|
+
<p className="nt-footer__heading">{t("Shop", "المتجر")}</p>
|
|
108
|
+
<div className="nt-footer__links">
|
|
109
|
+
<a href="/products">{t("All products", "كل المنتجات")}</a>
|
|
110
|
+
{collections.slice(0, 5).map((c) => (
|
|
111
|
+
<a key={c.id} href={`/collections/${c.slug}`}>
|
|
112
|
+
{c.name}
|
|
113
|
+
</a>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Help links */}
|
|
119
|
+
<div>
|
|
120
|
+
<p className="nt-footer__heading">{t("Help", "المساعدة")}</p>
|
|
121
|
+
<div className="nt-footer__links">
|
|
122
|
+
<a href="/pages/contact">{t("Contact us", "تواصل معنا")}</a>
|
|
123
|
+
<a href="/pages/shipping">{t("Shipping", "الشحن")}</a>
|
|
124
|
+
<a href="/pages/returns">{t("Returns", "الإرجاع")}</a>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="nt-footer__bottom">
|
|
130
|
+
<p className="nt-footer__copy">
|
|
131
|
+
{s.copyright || `© ${year ?? ""} ${brand}`.trim()}
|
|
132
|
+
</p>
|
|
133
|
+
<div className="nt-paybadges">
|
|
134
|
+
<span className="nt-paybadge">VISA</span>
|
|
135
|
+
<span className="nt-paybadge">Mastercard</span>
|
|
136
|
+
<span className="nt-paybadge">mada</span>
|
|
137
|
+
<span className="nt-paybadge">Apple Pay</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</footer>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const IconInstagram = () => (
|
|
146
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
147
|
+
<rect x="2" y="2" width="20" height="20" rx="5" />
|
|
148
|
+
<circle cx="12" cy="12" r="5" />
|
|
149
|
+
<circle cx="17.5" cy="6.5" r="1.5" fill="currentColor" stroke="none" />
|
|
150
|
+
</svg>
|
|
151
|
+
);
|
|
152
|
+
const IconX = () => (
|
|
153
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
154
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
const IconFacebook = () => (
|
|
158
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
159
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
|
160
|
+
</svg>
|
|
161
|
+
);
|