@numueg/theme-cli 0.5.0 → 0.6.0
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 +817 -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 +517 -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,80 @@
|
|
|
1
|
+
import { EditableImage } from "@numueg/theme-sdk";
|
|
2
|
+
import { EditableText } from "../lib/EditableText";
|
|
3
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
4
|
+
import { useT } from "../lib/i18n";
|
|
5
|
+
|
|
6
|
+
interface PromoSettings {
|
|
7
|
+
badge_text?: string;
|
|
8
|
+
headline?: string;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
cta_text?: string;
|
|
11
|
+
cta_link?: string;
|
|
12
|
+
image_url?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Full-width black promo banner — square image beside a badge + headline +
|
|
17
|
+
* subtitle and a light pill CTA. All text + the image are inline-editable.
|
|
18
|
+
*/
|
|
19
|
+
export default function PromoBanner({ id, settings }: EmpSectionProps) {
|
|
20
|
+
const s = settings as PromoSettings;
|
|
21
|
+
const t = useT();
|
|
22
|
+
const badge = s.badge_text ?? t("Limited offer", "عرض محدود");
|
|
23
|
+
const headline = s.headline ?? t("25% off all accessories", "خصم ٢٥٪ على كل الإكسسوارات");
|
|
24
|
+
const subtitle = s.subtitle ?? t("Ends this month — don't miss out!", "العرض ساري لنهاية الشهر. متفوتش الفرصة!");
|
|
25
|
+
const ctaText = s.cta_text ?? t("Shop now", "تسوق الآن");
|
|
26
|
+
const ctaLink = s.cta_link ?? "/products";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section style={{ paddingBlock: "2rem" }}>
|
|
30
|
+
<div className="nt-container">
|
|
31
|
+
<div className="nt-promo">
|
|
32
|
+
<div className="nt-promo__img">
|
|
33
|
+
<EditableImage
|
|
34
|
+
sectionId={id}
|
|
35
|
+
settingId="image_url"
|
|
36
|
+
src={
|
|
37
|
+
s.image_url ||
|
|
38
|
+
"https://images.unsplash.com/photo-1611085583191-a3b181a88401?w=600"
|
|
39
|
+
}
|
|
40
|
+
alt=""
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="nt-promo__body">
|
|
44
|
+
{badge ? (
|
|
45
|
+
<EditableText
|
|
46
|
+
as="span"
|
|
47
|
+
className="nt-promo__badge"
|
|
48
|
+
sectionId={id}
|
|
49
|
+
settingId="badge_text"
|
|
50
|
+
value={badge}
|
|
51
|
+
/>
|
|
52
|
+
) : null}
|
|
53
|
+
<EditableText
|
|
54
|
+
as="h3"
|
|
55
|
+
className="nt-promo__title"
|
|
56
|
+
sectionId={id}
|
|
57
|
+
settingId="headline"
|
|
58
|
+
value={headline}
|
|
59
|
+
/>
|
|
60
|
+
<EditableText
|
|
61
|
+
as="p"
|
|
62
|
+
className="nt-promo__sub"
|
|
63
|
+
sectionId={id}
|
|
64
|
+
settingId="subtitle"
|
|
65
|
+
value={subtitle}
|
|
66
|
+
/>
|
|
67
|
+
<a className="nt-btn-light" href={ctaLink}>
|
|
68
|
+
<EditableText
|
|
69
|
+
as="span"
|
|
70
|
+
sectionId={id}
|
|
71
|
+
settingId="cta_text"
|
|
72
|
+
value={ctaText}
|
|
73
|
+
/>
|
|
74
|
+
</a>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { RichText } from "@numueg/theme-sdk";
|
|
2
|
+
import { EditableText } from "../lib/EditableText";
|
|
3
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
4
|
+
|
|
5
|
+
interface RichTextSettings {
|
|
6
|
+
title?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
align?: "start" | "center";
|
|
9
|
+
width?: "narrow" | "wide";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rich-text content block — body for the `page` template (About / Shipping /
|
|
14
|
+
* Returns / Terms CMS pages). The merchant's HTML lives in `content` and is
|
|
15
|
+
* rendered through the SDK's sanitising `<RichText>`; the optional title is
|
|
16
|
+
* inline-editable.
|
|
17
|
+
*/
|
|
18
|
+
export default function RichTextSection({ id, settings }: EmpSectionProps) {
|
|
19
|
+
const s = settings as RichTextSettings;
|
|
20
|
+
const align = s.align === "center" ? "center" : "start";
|
|
21
|
+
const narrow = s.width !== "wide";
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="nt-container" style={{ paddingBlock: "3rem" }}>
|
|
25
|
+
<div
|
|
26
|
+
className="nt-richtext"
|
|
27
|
+
style={{
|
|
28
|
+
textAlign: align,
|
|
29
|
+
maxWidth: narrow ? "44rem" : "64rem",
|
|
30
|
+
marginInline: align === "center" ? "auto" : undefined,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{s.title ? (
|
|
34
|
+
<EditableText
|
|
35
|
+
as="h1"
|
|
36
|
+
className="nt-display-sm"
|
|
37
|
+
sectionId={id}
|
|
38
|
+
settingId="title"
|
|
39
|
+
value={s.title}
|
|
40
|
+
style={{ marginBottom: "1.5rem" }}
|
|
41
|
+
/>
|
|
42
|
+
) : null}
|
|
43
|
+
{s.content ? (
|
|
44
|
+
<RichText className="nt-prose" html={s.content} />
|
|
45
|
+
) : (
|
|
46
|
+
<p className="nt-placeholder">أضف المحتوى من إعدادات القسم.</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
usePage,
|
|
6
|
+
useProducts,
|
|
7
|
+
type Product,
|
|
8
|
+
} from "@numueg/theme-sdk";
|
|
9
|
+
import { EditableText } from "../lib/EditableText";
|
|
10
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
11
|
+
import { ProductCard } from "../lib/ProductCard";
|
|
12
|
+
|
|
13
|
+
interface SearchSettings {
|
|
14
|
+
title?: string;
|
|
15
|
+
search_placeholder?: string;
|
|
16
|
+
no_results_text?: string;
|
|
17
|
+
columns_desktop?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Search results — body for the `search` template. Seeds the query from
|
|
22
|
+
* `page.data.query` (the storefront /search route stashes it) and lets the
|
|
23
|
+
* visitor refine in a live box. Uses the host's pre-fetched `page.data.results`
|
|
24
|
+
* when present, else filters `useProducts()` client-side (customizer preview).
|
|
25
|
+
*/
|
|
26
|
+
export default function SearchResults({ id, settings }: EmpSectionProps) {
|
|
27
|
+
const s = settings as SearchSettings;
|
|
28
|
+
const cols = Math.max(2, Math.min(5, s.columns_desktop ?? 4));
|
|
29
|
+
const placeholder = s.search_placeholder ?? "ابحث عن المنتجات…";
|
|
30
|
+
|
|
31
|
+
const page = usePage();
|
|
32
|
+
const pd = page?.data as Record<string, unknown> | undefined;
|
|
33
|
+
const initialQuery =
|
|
34
|
+
(pd?.query as string | undefined) ?? (pd?.q as string | undefined) ?? "";
|
|
35
|
+
const preFetched = (pd?.results as Product[] | undefined) ?? [];
|
|
36
|
+
|
|
37
|
+
const { products } = useProducts({ fetchIfMissing: true });
|
|
38
|
+
const [query, setQuery] = useState(initialQuery);
|
|
39
|
+
|
|
40
|
+
const matches = useMemo(() => {
|
|
41
|
+
const needle = query.trim().toLowerCase();
|
|
42
|
+
if (!needle) return preFetched;
|
|
43
|
+
const pool = preFetched.length > 0 ? preFetched : products;
|
|
44
|
+
return pool.filter(
|
|
45
|
+
(p) =>
|
|
46
|
+
p.name?.toLowerCase().includes(needle) ||
|
|
47
|
+
p.description?.toLowerCase().includes(needle),
|
|
48
|
+
);
|
|
49
|
+
}, [query, preFetched, products]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<section className="nt-container" style={{ paddingBlock: "2.5rem" }}>
|
|
53
|
+
<EditableText
|
|
54
|
+
as="h1"
|
|
55
|
+
className="nt-display-sm"
|
|
56
|
+
sectionId={id}
|
|
57
|
+
settingId="title"
|
|
58
|
+
value={s.title ?? "البحث"}
|
|
59
|
+
style={{ marginBottom: "1.25rem" }}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<input
|
|
63
|
+
className="nt-input"
|
|
64
|
+
type="search"
|
|
65
|
+
value={query}
|
|
66
|
+
placeholder={placeholder}
|
|
67
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
68
|
+
style={{ marginBottom: "1.5rem" }}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
{query.trim() ? (
|
|
72
|
+
<p className="nt-label" style={{ marginBottom: "1.5rem" }}>
|
|
73
|
+
{matches.length} نتيجة لـ "{query.trim()}"
|
|
74
|
+
</p>
|
|
75
|
+
) : null}
|
|
76
|
+
|
|
77
|
+
{matches.length > 0 ? (
|
|
78
|
+
<div className="nt-grid" style={{ ["--cols" as string]: cols }}>
|
|
79
|
+
{matches.map((p) => (
|
|
80
|
+
<ProductCard key={p.id} product={p} />
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<p className="nt-placeholder" style={{ paddingBlock: "3rem" }}>
|
|
85
|
+
{query.trim()
|
|
86
|
+
? s.no_results_text ||
|
|
87
|
+
"لا توجد نتائج. جرّب كلمة أخرى أو تصفّح كل التشكيلة."
|
|
88
|
+
: "اكتب كلمة للبحث في المتجر."}
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
</section>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState } from "react";
|
|
3
|
+
import { useProductSizeChart,
|
|
4
|
+
} from "@numueg/theme-sdk";
|
|
5
|
+
import { EditableText } from "../lib/EditableText";
|
|
6
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
7
|
+
|
|
8
|
+
interface SizeChartSettings {
|
|
9
|
+
trigger_label?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Size guide. Resolution (product `custom` → store `default` → `off`) is owned
|
|
15
|
+
* by the SDK's `useProductSizeChart()` so every theme resolves it identically
|
|
16
|
+
* to the merchant hub + backend validator. Renders a trigger pill + modal
|
|
17
|
+
* table; returns null when there's nothing to show.
|
|
18
|
+
*/
|
|
19
|
+
export default function SizeChart({ id, settings }: EmpSectionProps) {
|
|
20
|
+
const s = settings as SizeChartSettings;
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const chart = useProductSizeChart();
|
|
23
|
+
|
|
24
|
+
if (!chart) return null;
|
|
25
|
+
const unit = chart.unit ? ` (${chart.unit})` : "";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="nt-container" style={{ paddingBottom: "1rem" }}>
|
|
29
|
+
<button
|
|
30
|
+
className="nt-sizeguide__trigger"
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => setOpen(true)}
|
|
33
|
+
>
|
|
34
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
35
|
+
<path d="M3 7h18v10H3z" />
|
|
36
|
+
<path d="M7 7v4M11 7v6M15 7v4M19 7v6" />
|
|
37
|
+
</svg>
|
|
38
|
+
<EditableText
|
|
39
|
+
as="span"
|
|
40
|
+
sectionId={id}
|
|
41
|
+
settingId="trigger_label"
|
|
42
|
+
value={s.trigger_label || "دليل المقاسات"}
|
|
43
|
+
/>
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
{open ? (
|
|
47
|
+
<div
|
|
48
|
+
className="nt-modal"
|
|
49
|
+
role="dialog"
|
|
50
|
+
aria-modal="true"
|
|
51
|
+
aria-label={s.title || "دليل المقاسات"}
|
|
52
|
+
>
|
|
53
|
+
<div className="nt-modal__overlay" onClick={() => setOpen(false)} />
|
|
54
|
+
<div className="nt-modal__panel">
|
|
55
|
+
<div className="nt-modal__head">
|
|
56
|
+
<h2 className="nt-modal__title">
|
|
57
|
+
{(s.title || "دليل المقاسات") + unit}
|
|
58
|
+
</h2>
|
|
59
|
+
<button
|
|
60
|
+
className="nt-drawer__close"
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => setOpen(false)}
|
|
63
|
+
aria-label="إغلاق"
|
|
64
|
+
>
|
|
65
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
66
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
67
|
+
</svg>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="nt-modal__body">
|
|
71
|
+
{chart.image_url ? (
|
|
72
|
+
<img
|
|
73
|
+
className="nt-sizeguide__img"
|
|
74
|
+
src={chart.image_url}
|
|
75
|
+
alt=""
|
|
76
|
+
/>
|
|
77
|
+
) : null}
|
|
78
|
+
<table className="nt-sizetable">
|
|
79
|
+
<thead>
|
|
80
|
+
<tr>
|
|
81
|
+
<th>المقاس</th>
|
|
82
|
+
{chart.column_headers.map((h, i) => (
|
|
83
|
+
<th key={i}>{h}</th>
|
|
84
|
+
))}
|
|
85
|
+
</tr>
|
|
86
|
+
</thead>
|
|
87
|
+
<tbody>
|
|
88
|
+
{chart.rows.map((row, ri) => (
|
|
89
|
+
<tr key={ri}>
|
|
90
|
+
<th scope="row">{row.size}</th>
|
|
91
|
+
{chart.column_headers.map((_, ci) => (
|
|
92
|
+
<td key={ci}>{row.values[ci] ?? "—"}</td>
|
|
93
|
+
))}
|
|
94
|
+
</tr>
|
|
95
|
+
))}
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
{chart.notes ? (
|
|
99
|
+
<p className="nt-muted" style={{ fontSize: "0.8125rem", marginTop: "1rem" }}>
|
|
100
|
+
{chart.notes}
|
|
101
|
+
</p>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type BlockInstance } from "@numueg/theme-sdk";
|
|
2
|
+
import { EditableText } from "../lib/EditableText";
|
|
3
|
+
import type { EmpSectionProps } from "../lib/section";
|
|
4
|
+
import { useT } from "../lib/i18n";
|
|
5
|
+
|
|
6
|
+
interface TestimonialsSettings {
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Customer reviews — three bordered cards on the off-white canvas, each with
|
|
11
|
+
* black star rating, quote and an avatar initial. Cards come from `review`
|
|
12
|
+
* blocks the merchant arranges in the customizer. */
|
|
13
|
+
export default function Testimonials({
|
|
14
|
+
id,
|
|
15
|
+
settings,
|
|
16
|
+
blocks,
|
|
17
|
+
blockOrder,
|
|
18
|
+
}: EmpSectionProps) {
|
|
19
|
+
const s = settings as TestimonialsSettings;
|
|
20
|
+
const t = useT();
|
|
21
|
+
const title = s.title ?? t("What our customers say", "رأي عملائنا");
|
|
22
|
+
|
|
23
|
+
const reviews = (blockOrder ?? [])
|
|
24
|
+
.map((bid) => ({ bid, block: blocks?.[bid] }))
|
|
25
|
+
.filter(
|
|
26
|
+
(x): x is { bid: string; block: BlockInstance } =>
|
|
27
|
+
!!x.block && !x.block.disabled && x.block.type === "review",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (reviews.length === 0) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section className="nt-section nt-bg-white">
|
|
34
|
+
<div className="nt-container">
|
|
35
|
+
<EditableText
|
|
36
|
+
as="h2"
|
|
37
|
+
className="nt-display-sm"
|
|
38
|
+
sectionId={id}
|
|
39
|
+
settingId="title"
|
|
40
|
+
value={title}
|
|
41
|
+
style={{ textAlign: "center", marginBottom: "2rem" }}
|
|
42
|
+
/>
|
|
43
|
+
<div className="nt-tgrid">
|
|
44
|
+
{reviews.map(({ bid, block }) => {
|
|
45
|
+
const rating = Math.max(
|
|
46
|
+
0,
|
|
47
|
+
Math.min(5, Number(block.settings.rating ?? 5)),
|
|
48
|
+
);
|
|
49
|
+
const author = (block.settings.author as string) || "عميل";
|
|
50
|
+
const city = block.settings.city as string | undefined;
|
|
51
|
+
const text = (block.settings.text as string) || "";
|
|
52
|
+
return (
|
|
53
|
+
<div className="nt-tcard" key={bid}>
|
|
54
|
+
<div className="nt-stars" aria-label={`${rating} من 5`}>
|
|
55
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
56
|
+
<Star key={i} filled={i < rating} />
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
<EditableText
|
|
60
|
+
as="p"
|
|
61
|
+
className="nt-tquote"
|
|
62
|
+
sectionId={id}
|
|
63
|
+
blockId={bid}
|
|
64
|
+
settingId="text"
|
|
65
|
+
value={`"${text}"`}
|
|
66
|
+
/>
|
|
67
|
+
<div className="nt-tauthor">
|
|
68
|
+
<span className="nt-tavatar">{author[0]}</span>
|
|
69
|
+
<div>
|
|
70
|
+
<EditableText
|
|
71
|
+
as="p"
|
|
72
|
+
className="nt-tname"
|
|
73
|
+
sectionId={id}
|
|
74
|
+
blockId={bid}
|
|
75
|
+
settingId="author"
|
|
76
|
+
value={author}
|
|
77
|
+
/>
|
|
78
|
+
{city ? (
|
|
79
|
+
<EditableText
|
|
80
|
+
as="p"
|
|
81
|
+
className="nt-tcity"
|
|
82
|
+
sectionId={id}
|
|
83
|
+
blockId={bid}
|
|
84
|
+
settingId="city"
|
|
85
|
+
value={city}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const Star = ({ filled }: { filled: boolean }) => (
|
|
100
|
+
<svg
|
|
101
|
+
width="14"
|
|
102
|
+
height="14"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill={filled ? "currentColor" : "none"}
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
strokeWidth="2"
|
|
107
|
+
className={`nt-star${filled ? " is-filled" : ""}`}
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
>
|
|
110
|
+
<path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
111
|
+
</svg>
|
|
112
|
+
);
|