Scripture Index redesign: search, testament/source filters, book chips, collapsible books, on-demand verse text; v2.30.0
Publish Container / build (push) Successful in 15m16s
Publish Container / build (push) Successful in 15m16s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f52437a07
commit
03f46ae683
4 files changed
+250
-90
No files matched your search
+76
-22
@@ -12613,28 +12613,20 @@ a.study-track-card:hover {
|
||||
.kofi-btn:hover { background: var(--brand-gold, #c9a84c); color: var(--brand-black, #0a0a08); }
|
||||
|
||||
/* ── Scripture index page ────────────────────────────────────────────────── */
|
||||
.scripture-index-main {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 5rem;
|
||||
}
|
||||
.scripture-index-note { color: var(--brand-muted, #b0a48c); margin: 2rem 0; }
|
||||
.scripture-study-group { margin-bottom: 4rem; }
|
||||
.scripture-study-title { font-family: var(--brand-font-heading, 'Cormorant Garamond', serif); font-size: 1.6rem; font-weight: 600; color: var(--brand-gold, #c9a84c); margin: 0 0 0; padding-bottom: 0.75rem; border-bottom: 2px solid var(--brand-gold, #c9a84c); }
|
||||
.scripture-entry-table { display: flex; flex-direction: column; }
|
||||
.scripture-entry-row { display: grid; grid-template-columns: 130px 1fr; gap: 0 1.5rem; align-items: start; padding: 1rem 0.75rem; border-bottom: 1px solid var(--brand-border, #2a2518); border-radius: 4px; }
|
||||
.scripture-entry-ref { font-family: var(--brand-font-body, 'EB Garamond', serif); font-size: 1rem; font-weight: 700; color: var(--brand-gold, #c9a84c); white-space: nowrap; padding-top: 0.1rem; }
|
||||
.scripture-entry-body { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
|
||||
.scripture-entry-verse { font-family: var(--brand-font-body, 'EB Garamond', serif); font-size: 0.925rem; line-height: 1.55; color: var(--brand-warm-white, #f0ead8); font-style: italic; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.scripture-entry-sources { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.scripture-source-chip { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--brand-warm-white, #f0ead8); text-decoration: none; padding: 0.3rem 0.6rem; border: 1px solid var(--brand-border, #2a2518); border-radius: 999px; transition: background 0.15s, border-color 0.15s; }
|
||||
.scripture-source-chip:hover { background: rgba(201,168,76,0.1); border-color: var(--brand-gold, #c9a84c); }
|
||||
.scripture-source-kind { font-size: 0.65rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--brand-gold, #c9a84c); }
|
||||
@media (max-width: 600px) {
|
||||
.scripture-entry-row { grid-template-columns: 1fr; }
|
||||
.scripture-entry-ref { grid-column: 1; font-size: 0.9rem; }
|
||||
.scripture-entry-body { grid-column: 1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ── Admin status messages ───────────────────────────────────────────────── */
|
||||
.admin-save-ok { color: #6ebd7c; font-size: 0.85rem; margin: 0; }
|
||||
@@ -13732,3 +13724,65 @@ a.study-track-card:hover {
|
||||
.sl-actions .btn-secondary { flex: 1 1 100%; justify-content: center; }
|
||||
}
|
||||
|
||||
/* ── Scripture Index ───────────────────────────────────────────────────── */
|
||||
.si-main { padding: 2rem 0 3.5rem; }
|
||||
.si-note { color: var(--brand-muted); padding: 1.5rem 0; }
|
||||
.si-toolbar { display: flex; flex-direction: column; gap: 0.85rem; margin-bottom: 1rem; }
|
||||
.si-search {
|
||||
width: 100%; font: inherit; font-size: 1rem; color: var(--brand-warm-white); padding: 0.7rem 1.1rem;
|
||||
background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 999px;
|
||||
}
|
||||
.si-search:focus { outline: 2px solid var(--brand-gold); outline-offset: 1px; }
|
||||
.si-filters { display: flex; flex-wrap: wrap; gap: 0.6rem 1.5rem; }
|
||||
.si-pills, .si-books { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||
.si-books { margin-bottom: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--brand-border); }
|
||||
.si-pill {
|
||||
font: inherit; font-size: 0.85rem; cursor: pointer; padding: 0.3rem 0.85rem; border-radius: 999px;
|
||||
color: var(--brand-muted); background: transparent; border: 1px solid var(--brand-border);
|
||||
}
|
||||
.si-pill span { opacity: 0.6; margin-left: 0.2rem; }
|
||||
.si-pill:hover { border-color: var(--brand-gold-dark); color: var(--brand-warm-white); }
|
||||
.si-pill--on, .si-pill--on:hover { background: var(--brand-gold); border-color: var(--brand-gold); color: var(--brand-black); }
|
||||
|
||||
.si-meta { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; font-size: 0.88rem; color: var(--brand-muted); }
|
||||
.si-meta-actions { display: flex; gap: 1rem; }
|
||||
.si-link { font: inherit; font-size: 0.85rem; background: none; border: 0; padding: 0; cursor: pointer; color: var(--brand-gold); text-decoration: underline; }
|
||||
|
||||
.si-groups { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.si-group { background: var(--brand-black-3); border: 1px solid var(--brand-border); border-radius: 12px; overflow: hidden; }
|
||||
.si-group-head {
|
||||
display: flex; align-items: baseline; gap: 0.75rem; width: 100%; padding: 0.9rem 1.1rem; background: transparent; border: 0;
|
||||
cursor: pointer; text-align: left; color: var(--brand-warm-white); font: inherit;
|
||||
}
|
||||
.si-group-head:hover { background: rgba(201, 168, 76, 0.06); }
|
||||
.si-group-name { font-family: var(--brand-font-heading); font-size: 1.5rem; font-weight: 600; line-height: 1.1; }
|
||||
.si-group-count { font-size: 0.82rem; color: var(--brand-muted); }
|
||||
.si-chev { margin-left: auto; color: var(--brand-gold); font-size: 1.4rem; line-height: 1; }
|
||||
|
||||
.si-list { list-style: none; margin: 0; padding: 0 1.1rem 0.5rem; }
|
||||
.si-row { padding: 0.7rem 0; border-top: 1px solid var(--brand-border); }
|
||||
.si-row-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem 0.9rem; }
|
||||
.si-ref { font-weight: 700; color: var(--brand-gold-light); white-space: nowrap; min-width: 8.5rem; }
|
||||
.si-sources { display: flex; flex-wrap: wrap; gap: 0.4rem; flex: 1; min-width: 0; }
|
||||
.si-chip {
|
||||
display: inline-flex; align-items: baseline; gap: 0.4rem; max-width: 100%; padding: 0.15rem 0.65rem; border-radius: 999px;
|
||||
font-size: 0.85rem; text-decoration: none; color: var(--brand-warm-white); border: 1px solid var(--brand-border);
|
||||
}
|
||||
.si-chip:hover { border-color: var(--brand-gold-dark); }
|
||||
.si-chip-kind { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--brand-gold); flex-shrink: 0; }
|
||||
.si-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.si-verse-btn { margin-left: auto; font: inherit; font-size: 0.8rem; background: none; border: 0; padding: 0; cursor: pointer; color: var(--brand-muted); text-decoration: underline; }
|
||||
.si-verse-btn:hover { color: var(--brand-gold); }
|
||||
.si-verse { margin: 0.5rem 0 0; padding-left: 0.9rem; border-left: 2px solid var(--brand-gold-dark); font-size: 0.95rem; line-height: 1.65; font-style: italic; color: var(--brand-muted); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.si-main { padding: 1.25rem 0 2.5rem; }
|
||||
.si-pills, .si-books { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 0.2rem; }
|
||||
.si-pill { flex-shrink: 0; }
|
||||
.si-filters { flex-direction: column; gap: 0.6rem; }
|
||||
.si-group-name { font-size: 1.3rem; }
|
||||
.si-list { padding: 0 0.85rem 0.4rem; }
|
||||
.si-ref { min-width: 0; width: 100%; }
|
||||
.si-verse-btn { margin-left: 0; }
|
||||
}
|
||||
|
||||
+171
-65
@@ -2350,7 +2350,14 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
|
||||
)
|
||||
const [entries, setEntries] = useState<ScriptureIndexEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [query, setQuery] = useState('')
|
||||
const [testament, setTestament] = useState<'all' | 'ot' | 'nt'>('all')
|
||||
const [sourceFilter, setSourceFilter] = useState<'all' | 'episode' | 'study'>('all')
|
||||
const [bookFilter, setBookFilter] = useState<string | null>(null)
|
||||
const [toggled, setToggled] = useState<Record<string, boolean>>({})
|
||||
const [verseTexts, setVerseTexts] = useState<Record<string, string>>({})
|
||||
const [shownVerses, setShownVerses] = useState<Set<string>>(new Set())
|
||||
const chapterCache = useRef<Map<string, Promise<HellaoContent>>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/public/scripture-index')
|
||||
@@ -2419,88 +2426,187 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
|
||||
return { bookGroups: groups, otherRows: other }
|
||||
}, [entries])
|
||||
|
||||
useEffect(() => {
|
||||
if (bookGroups.length === 0) return
|
||||
// Group rows by chapter to minimize API calls
|
||||
const chapterMap = new Map<string, Array<{ key: string; verseStart: number; verseEnd: number }>>()
|
||||
for (const group of bookGroups) {
|
||||
for (const row of group.rows) {
|
||||
const ck = `${group.bookId}/${row.chapter}`
|
||||
if (!chapterMap.has(ck)) chapterMap.set(ck, [])
|
||||
// Chapter-spanning refs (e.g. "5:15-6:2") preview only the starting chapter's
|
||||
// verses, since extractVerseRange operates on a single fetched chapter.
|
||||
const verseEnd = row.endChapter !== row.chapter ? Number.MAX_SAFE_INTEGER : row.verseEnd
|
||||
chapterMap.get(ck)!.push({ key: row.key, verseStart: row.verseStart, verseEnd })
|
||||
}
|
||||
}
|
||||
for (const [ck, rows] of chapterMap) {
|
||||
const [bookId, chapter] = ck.split('/')
|
||||
fetch(`https://bible.helloao.org/api/BSB/${bookId}/${chapter}.json`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject())
|
||||
.then((data: { chapter: { content: HellaoContent } }) => {
|
||||
const chContent = data?.chapter?.content ?? []
|
||||
const updates: Record<string, string> = {}
|
||||
for (const r of rows) {
|
||||
const text = extractVerseRange(chContent, r.verseStart, r.verseEnd)
|
||||
if (text) updates[r.key] = text
|
||||
}
|
||||
setVerseTexts(prev => ({ ...prev, ...updates }))
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}, [bookGroups])
|
||||
// Apply the search box and filters
|
||||
const needle = query.trim().toLowerCase()
|
||||
const rowMatches = (row: ScriptureRow) => {
|
||||
if (sourceFilter !== 'all' && !row.sources.some(src => src.type === sourceFilter)) return false
|
||||
if (!needle) return true
|
||||
return row.displayRef.toLowerCase().includes(needle) || row.sources.some(src => src.label.toLowerCase().includes(needle))
|
||||
}
|
||||
const isOldTestament = (bookId: string) => SI_BOOK_ORDER.indexOf(bookId) < 39
|
||||
const visibleGroups = bookGroups
|
||||
.filter(g => testament === 'all' || (testament === 'ot') === isOldTestament(g.bookId))
|
||||
.filter(g => !bookFilter || g.bookId === bookFilter)
|
||||
.map(g => ({ ...g, rows: g.rows.filter(rowMatches) }))
|
||||
.filter(g => g.rows.length > 0 || (needle && g.bookName.toLowerCase().includes(needle)))
|
||||
// A search by book name ("titus") should show the whole book
|
||||
.map(g => (needle && g.rows.length === 0
|
||||
? { ...g, rows: bookGroups.find(x => x.bookId === g.bookId)!.rows.filter(r => sourceFilter === 'all' || r.sources.some(src => src.type === sourceFilter)) }
|
||||
: g))
|
||||
const visibleOther = testament === 'all' && !bookFilter ? otherRows.filter(rowMatches) : []
|
||||
const totalRows = visibleGroups.reduce((n, g) => n + g.rows.length, 0) + visibleOther.length
|
||||
const filtering = Boolean(needle) || testament !== 'all' || sourceFilter !== 'all' || Boolean(bookFilter)
|
||||
|
||||
const hasContent = bookGroups.length > 0 || otherRows.length > 0
|
||||
// With only a few books everything starts open; a long index starts collapsed. Searching opens matches.
|
||||
const isOpen = (id: string) => toggled[id] ?? (filtering || bookGroups.length <= 4)
|
||||
const setAll = (open: boolean) => {
|
||||
const next: Record<string, boolean> = { other: open }
|
||||
for (const g of bookGroups) next[g.bookId] = open
|
||||
setToggled(next)
|
||||
}
|
||||
|
||||
const fetchChapter = (bookId: string, chapter: number) => {
|
||||
const ck = `${bookId}/${chapter}`
|
||||
let promise = chapterCache.current.get(ck)
|
||||
if (!promise) {
|
||||
promise = fetch(`https://bible.helloao.org/api/BSB/${bookId}/${chapter}.json`)
|
||||
.then(r => (r.ok ? r.json() : Promise.reject(new Error('chapter failed'))))
|
||||
.then((data: { chapter: { content: HellaoContent } }) => data?.chapter?.content ?? [])
|
||||
chapterCache.current.set(ck, promise)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
function toggleVerse(row: ScriptureRow) {
|
||||
const open = shownVerses.has(row.key)
|
||||
setShownVerses(prev => {
|
||||
const next = new Set(prev)
|
||||
if (open) next.delete(row.key)
|
||||
else next.add(row.key)
|
||||
return next
|
||||
})
|
||||
if (open || verseTexts[row.key] !== undefined || !row.bookId) return
|
||||
// Chapter-spanning refs (e.g. "5:15-6:2") preview only the starting chapter's verses
|
||||
const verseEnd = row.endChapter !== row.chapter ? Number.MAX_SAFE_INTEGER : row.verseEnd
|
||||
fetchChapter(row.bookId, row.chapter)
|
||||
.then(chContent => setVerseTexts(prev => ({ ...prev, [row.key]: extractVerseRange(chContent, row.verseStart, verseEnd) || '(Verse text unavailable.)' })))
|
||||
.catch(() => setVerseTexts(prev => ({ ...prev, [row.key]: '(Verse text unavailable right now.)' })))
|
||||
}
|
||||
|
||||
function renderRow(row: ScriptureRow) {
|
||||
const showing = shownVerses.has(row.key)
|
||||
return (
|
||||
<div key={row.key} className="scripture-entry-row">
|
||||
<span className="scripture-entry-ref">{row.displayRef}</span>
|
||||
<span className="scripture-entry-body">
|
||||
{verseTexts[row.key] && <span className="scripture-entry-verse">"{verseTexts[row.key]}"</span>}
|
||||
<span className="scripture-entry-sources">
|
||||
{row.sources.map((s, i) => (
|
||||
<Link key={i} to={s.url} className={`scripture-source-chip scripture-source-chip--${s.type}`}>
|
||||
<span className="scripture-source-kind">{s.type === 'episode' ? 'Episode' : 'Study'}</span>
|
||||
{s.label}
|
||||
<li key={row.key} className="si-row">
|
||||
<div className="si-row-top">
|
||||
<span className="si-ref">{row.displayRef}</span>
|
||||
<span className="si-sources">
|
||||
{row.sources.map((src, i) => (
|
||||
<Link key={i} to={src.url} className={`si-chip si-chip--${src.type}`}>
|
||||
<span className="si-chip-kind">{src.type === 'episode' ? 'Episode' : 'Study'}</span>
|
||||
<span className="si-chip-label">{src.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{row.bookId && (
|
||||
<button type="button" className="si-verse-btn" aria-expanded={showing} onClick={() => toggleVerse(row)}>
|
||||
{showing ? 'Hide verse' : 'Show verse'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showing && (
|
||||
<p className="si-verse">{verseTexts[row.key] !== undefined ? `“${verseTexts[row.key]}”` : 'Loading…'}</p>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const hasContent = bookGroups.length > 0 || otherRows.length > 0
|
||||
const pill = (active: boolean) => `si-pill${active ? ' si-pill--on' : ''}`
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader content={content} />
|
||||
<SiteSearchBar content={content} />
|
||||
<PageBanner eyebrow="Reference" title="Scripture Index" desc="Every passage covered across our studies and episodes, organized by Scripture reference. Find the lesson or episode for any verse." />
|
||||
<main className="scripture-index-main page-width">
|
||||
{loading ? (
|
||||
<p className="scripture-index-note">Loading…</p>
|
||||
) : !hasContent ? (
|
||||
<p className="scripture-index-note">No Scripture references are published yet.</p>
|
||||
) : (
|
||||
<>
|
||||
{bookGroups.map(group => (
|
||||
<section key={group.bookId} className="scripture-study-group">
|
||||
<h2 className="scripture-study-title">{group.bookName}</h2>
|
||||
<div className="scripture-entry-table">
|
||||
{group.rows.map(renderRow)}
|
||||
<main className="si-main">
|
||||
<div className="section-inner">
|
||||
{loading ? (
|
||||
<p className="si-note">Loading…</p>
|
||||
) : !hasContent ? (
|
||||
<p className="si-note">No Scripture references are published yet.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="si-toolbar">
|
||||
<input
|
||||
type="search"
|
||||
className="si-search"
|
||||
placeholder="Search a passage or book (e.g. Titus 2)…"
|
||||
aria-label="Search the Scripture index"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
/>
|
||||
<div className="si-filters">
|
||||
<div className="si-pills" role="group" aria-label="Testament">
|
||||
<button type="button" className={pill(testament === 'all')} onClick={() => { setTestament('all'); setBookFilter(null) }}>All</button>
|
||||
<button type="button" className={pill(testament === 'ot')} onClick={() => { setTestament('ot'); setBookFilter(null) }}>Old Testament</button>
|
||||
<button type="button" className={pill(testament === 'nt')} onClick={() => { setTestament('nt'); setBookFilter(null) }}>New Testament</button>
|
||||
</div>
|
||||
<div className="si-pills" role="group" aria-label="Source">
|
||||
<button type="button" className={pill(sourceFilter === 'all')} onClick={() => setSourceFilter('all')}>Everything</button>
|
||||
<button type="button" className={pill(sourceFilter === 'episode')} onClick={() => setSourceFilter('episode')}>Episodes</button>
|
||||
<button type="button" className={pill(sourceFilter === 'study')} onClick={() => setSourceFilter('study')}>Studies</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
{otherRows.length > 0 && (
|
||||
<section className="scripture-study-group">
|
||||
<h2 className="scripture-study-title">Other References</h2>
|
||||
<div className="scripture-entry-table">
|
||||
{otherRows.map(renderRow)}
|
||||
</div>
|
||||
|
||||
{bookGroups.length > 1 && (
|
||||
<div className="si-books" role="group" aria-label="Books covered">
|
||||
{bookGroups.map(g => (
|
||||
<button
|
||||
key={g.bookId}
|
||||
type="button"
|
||||
className={pill(bookFilter === g.bookId)}
|
||||
onClick={() => setBookFilter(bookFilter === g.bookId ? null : g.bookId)}
|
||||
>
|
||||
{g.bookName} <span>{g.rows.length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
<div className="si-meta">
|
||||
<span>{totalRows} passage{totalRows === 1 ? '' : 's'}{filtering ? ' match' : ''}</span>
|
||||
<span className="si-meta-actions">
|
||||
{filtering && (
|
||||
<button type="button" className="si-link" onClick={() => { setQuery(''); setTestament('all'); setSourceFilter('all'); setBookFilter(null) }}>Clear filters</button>
|
||||
)}
|
||||
<button type="button" className="si-link" onClick={() => setAll(true)}>Expand all</button>
|
||||
<button type="button" className="si-link" onClick={() => setAll(false)}>Collapse all</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{totalRows === 0 ? (
|
||||
<p className="si-note">Nothing matches that. Try a book name like “Titus”, or clear the filters.</p>
|
||||
) : (
|
||||
<div className="si-groups">
|
||||
{visibleGroups.map(group => {
|
||||
const open = isOpen(group.bookId)
|
||||
return (
|
||||
<section key={group.bookId} className="si-group">
|
||||
<button type="button" className="si-group-head" aria-expanded={open} onClick={() => setToggled(t => ({ ...t, [group.bookId]: !open }))}>
|
||||
<span className="si-group-name">{group.bookName}</span>
|
||||
<span className="si-group-count">{group.rows.length} passage{group.rows.length === 1 ? '' : 's'}</span>
|
||||
<span className="si-chev" aria-hidden="true">{open ? '−' : '+'}</span>
|
||||
</button>
|
||||
{open && <ul className="si-list">{group.rows.map(renderRow)}</ul>}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
{visibleOther.length > 0 && (
|
||||
<section className="si-group">
|
||||
<button type="button" className="si-group-head" aria-expanded={isOpen('other')} onClick={() => setToggled(t => ({ ...t, other: !isOpen('other') }))}>
|
||||
<span className="si-group-name">Other references</span>
|
||||
<span className="si-group-count">{visibleOther.length} passage{visibleOther.length === 1 ? '' : 's'}</span>
|
||||
<span className="si-chev" aria-hidden="true">{isOpen('other') ? '−' : '+'}</span>
|
||||
</button>
|
||||
{isOpen('other') && <ul className="si-list">{visibleOther.map(renderRow)}</ul>}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
|
||||
Reference in new issue
Block a user