Fix homepage external link placement and publish sanitization

This commit is contained in:
nmemmert
2026-06-01 13:31:26 -04:00
parent 1d536827e3
commit 33ac2fb5d0
8 changed files with 311 additions and 66 deletions
+115 -3
View File
@@ -1613,6 +1613,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
function getCustomLinkPlacementLabel(placement: CustomLink['placement']) {
if (placement === 'platforms') return 'Homepage listen/platform links'
if (placement === 'footer') return 'Footer navigation links'
if (placement === 'otherSites') return 'Other site links in footer dropdown'
if (placement === 'externalSites') return 'Homepage external site links'
return 'Downloads page (More Resources)'
}
@@ -1755,6 +1757,26 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
}))
}
function addOtherSiteLink() {
setForm(f => ({
...f,
customLinks: [
...(f.customLinks ?? []),
{ id: Date.now().toString(36), label: '', url: '', imageUrl: '', placement: 'otherSites' as const },
],
}))
}
function addExternalSiteLink() {
setForm(f => ({
...f,
customLinks: [
...(f.customLinks ?? []),
{ id: Date.now().toString(36), label: '', url: '', imageUrl: '', description: '', placement: 'externalSites' as const },
],
}))
}
function addResource() {
setForm(f => ({
...f,
@@ -3781,14 +3803,61 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<p>Add links to show in platform buttons, footer navigation, or the More Resources download library.</p>
</div>
<div className="admin-panel-subhead">
<h3>Homepage External Links</h3>
<p>Links shown on the homepage, separate from the footer.</p>
</div>
{(form.customLinks ?? []).filter(link => link.placement === 'externalSites').length === 0 && (
<p className="admin-stats-note">No homepage external links yet.</p>
)}
{(form.customLinks ?? []).filter(link => link.placement === 'externalSites').map(link => (
<div key={link.id} className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`external-site-link-label-${link.id}`}>Label</label>
<input id={`external-site-link-label-${link.id}`} type="text" value={link.label} placeholder="e.g. Bible study partner" onChange={e => updateLink(link.id, 'label', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`external-site-link-url-${link.id}`}>URL</label>
<input id={`external-site-link-url-${link.id}`} type="url" value={link.url} placeholder="https://..." onChange={e => updateLink(link.id, 'url', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`external-site-link-description-${link.id}`}>Description</label>
<textarea id={`external-site-link-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Short description shown on the homepage" onChange={e => updateLink(link.id, 'description', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`external-site-link-image-${link.id}`}>Image URL</label>
<input id={`external-site-link-image-${link.id}`} type="text" value={link.imageUrl ?? ''} placeholder="/uploads/example.png" onChange={e => updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `external-site-link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'External site link'} image preview`)}
</div>
<div className="admin-field">
<label htmlFor={`external-site-link-tags-${link.id}`}>Tags</label>
<input id={`external-site-link-tags-${link.id}`} type="text" value={(link.tags ?? []).join(', ')} placeholder="external, homepage, ministry" onChange={e => updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
</div>
<div className="admin-field">
<label>Quick Action</label>
<button type="button" className="btn-admin-apply" onClick={() => updateLink(link.id, 'placement', 'footer')}>Move to Footer Nav</button>
</div>
<div className="admin-field">
<label>Shows On</label>
<p className="admin-stats-note">{getCustomLinkPlacementLabel(link.placement)}</p>
<a href={getCustomLinkPreviewPath(link.placement)} target="_blank" rel="noopener noreferrer" className="btn-admin-secondary">Preview Destination Page</a>
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeLink(link.id)}>Remove</button>
</div>
))}
<button type="button" className="btn-admin-add" onClick={addExternalSiteLink}>+ Add Homepage External Link</button>
<div className="admin-panel-subhead">
<h3>Platform + Footer Links</h3>
<p>Links that appear on the listen buttons row or footer nav.</p>
</div>
{(form.customLinks ?? []).filter(link => link.placement !== 'resources').length === 0 && (
{(form.customLinks ?? []).filter(link => link.placement !== 'resources' && link.placement !== 'otherSites' && link.placement !== 'externalSites').length === 0 && (
<p className="admin-stats-note">No custom links yet.</p>
)}
{(form.customLinks ?? []).filter(link => link.placement !== 'resources').map(link => (
{(form.customLinks ?? []).filter(link => link.placement !== 'resources' && link.placement !== 'otherSites' && link.placement !== 'externalSites').map(link => (
<div key={link.id} className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
@@ -3830,7 +3899,50 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<button type="button" className="btn-admin-remove" onClick={() => removeLink(link.id)}>Remove</button>
</div>
))}
<button type="button" className="btn-admin-add" onClick={addLink}>+ Add Link</button>
<button type="button" className="btn-admin-add" onClick={addLink}>+ Add Platform/Footer Link</button>
<div className="admin-panel-subhead">
<h3>Other Site Links</h3>
<p>External links you want to surface in the footer dropdown.</p>
</div>
{(form.customLinks ?? []).filter(link => link.placement === 'otherSites').length === 0 && (
<p className="admin-stats-note">No other site links yet.</p>
)}
{(form.customLinks ?? []).filter(link => link.placement === 'otherSites').map(link => (
<div key={link.id} className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`other-site-link-label-${link.id}`}>Label</label>
<input id={`other-site-link-label-${link.id}`} type="text" value={link.label} placeholder="e.g. Recommended ministry" onChange={e => updateLink(link.id, 'label', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`other-site-link-url-${link.id}`}>URL</label>
<input id={`other-site-link-url-${link.id}`} type="url" value={link.url} placeholder="https://..." onChange={e => updateLink(link.id, 'url', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`other-site-link-image-${link.id}`}>Image URL</label>
<input id={`other-site-link-image-${link.id}`} type="text" value={link.imageUrl ?? ''} placeholder="/uploads/example.png" onChange={e => updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `other-site-link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'Other site link'} image preview`)}
</div>
<div className="admin-field">
<label htmlFor={`other-site-link-tags-${link.id}`}>Tags</label>
<input id={`other-site-link-tags-${link.id}`} type="text" value={(link.tags ?? []).join(', ')} placeholder="external, ministry, study" onChange={e => updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
</div>
<div className="admin-field">
<label>Quick Action</label>
<button type="button" className="btn-admin-apply" onClick={() => updateLink(link.id, 'placement', 'footer')}>Move to Footer Nav</button>
</div>
<div className="admin-field">
<label>Shows On</label>
<p className="admin-stats-note">{getCustomLinkPlacementLabel(link.placement)}</p>
<a href={getCustomLinkPreviewPath(link.placement)} target="_blank" rel="noopener noreferrer" className="btn-admin-secondary">Preview Destination Page</a>
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeLink(link.id)}>Remove</button>
</div>
))}
<button type="button" className="btn-admin-add" onClick={addOtherSiteLink}>+ Add Other Site Link</button>
<div className="admin-panel-subhead">
<h3>More Resources Links</h3>
+32 -2
View File
@@ -494,7 +494,7 @@ function SiteFooter({ content }: { content: SiteContent }) {
const creatorUrl = content.platformCreatorUrl || 'https://creators.spotify.com/pod/profile/nmemmert/'
const facebookUrl = content.platformFacebookUrl || 'https://facebook.com/versebyversewithnate'
const extraFooterLinks = (content.customLinks ?? []).filter(
l => l.placement === 'footer' || l.placement === 'platforms',
l => l.placement === 'footer' || l.placement === 'platforms' || l.placement === 'otherSites',
)
return (
@@ -524,7 +524,7 @@ function SiteFooter({ content }: { content: SiteContent }) {
<>
<span aria-hidden="true">·</span>
<details className="footer-more-links">
<summary>More Platforms</summary>
<summary>More Links</summary>
<div className="footer-more-links-menu">
{extraFooterLinks.map(link => (
<a key={link.id} href={link.url} target="_blank" rel="noreferrer">{link.label}</a>
@@ -950,6 +950,34 @@ function CustomBlocksSection({ content, page }: { content: SiteContent; page: st
)
}
function ExternalSitesSection({ content }: { content: SiteContent }) {
const links = (content.customLinks ?? []).filter(link => link.placement === 'externalSites')
if (links.length === 0) return null
return (
<section className="section-resources" aria-label="External sites">
<div className="section-inner">
<div className="download-library-head">
<p className="eyebrow">Homepage Links</p>
<h2 className="section-heading">External sites and ministries worth checking out.</h2>
<p className="download-library-copy">These links are shown on the homepage, separate from the footer menu.</p>
</div>
<div className="resources-list">
{links.map(link => (
<a key={link.id} href={link.url} target="_blank" rel="noreferrer" className="resource-link">
<div className="resource-download-meta">
<span className="resource-link-label">{link.label}</span>
{link.description && <p className="resource-link-description">{link.description}</p>}
<span className="resource-link-action">Open external site </span>
</div>
</a>
))}
</div>
</div>
</section>
)
}
function LandingPage({ content }: { content: SiteContent }) {
const featuredStudy = getHomepageFeaturedStudy(content)
@@ -1068,6 +1096,8 @@ function LandingPage({ content }: { content: SiteContent }) {
</div>
</section>
<ExternalSitesSection content={content} />
<section className="section-home-jump" aria-label="Homepage navigation">
<div className="section-inner">
<div className="home-jump-head">
+1 -1
View File
@@ -25,7 +25,7 @@ export interface CustomLink {
id: string
label: string
url: string
placement: 'platforms' | 'footer' | 'resources'
placement: 'platforms' | 'footer' | 'resources' | 'otherSites' | 'externalSites'
imageUrl?: string
description?: string
amazonUrl?: string