Add per-download Amazon button settings for current and archived downloads
This commit is contained in:
@@ -172,6 +172,8 @@ function sanitizeCustomLinks(value) {
|
|||||||
url: sanitizeUrl(item.url),
|
url: sanitizeUrl(item.url),
|
||||||
imageUrl: sanitizeUrl(item.imageUrl),
|
imageUrl: sanitizeUrl(item.imageUrl),
|
||||||
description: typeof item.description === 'string' ? item.description.trim().slice(0, 4000) : '',
|
description: typeof item.description === 'string' ? item.description.trim().slice(0, 4000) : '',
|
||||||
|
amazonUrl: sanitizeUrl(item.amazonUrl),
|
||||||
|
amazonLabel: typeof item.amazonLabel === 'string' ? item.amazonLabel.trim().slice(0, 120) : '',
|
||||||
placement,
|
placement,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -199,6 +201,8 @@ function sanitizeArchivedSeriesResourceLinks(value) {
|
|||||||
label: typeof item.label === 'string' ? item.label.trim().slice(0, 120) : '',
|
label: typeof item.label === 'string' ? item.label.trim().slice(0, 120) : '',
|
||||||
description: typeof item.description === 'string' ? item.description.trim().slice(0, 4000) : '',
|
description: typeof item.description === 'string' ? item.description.trim().slice(0, 4000) : '',
|
||||||
url: sanitizeUrl(item.url),
|
url: sanitizeUrl(item.url),
|
||||||
|
amazonUrl: sanitizeUrl(item.amazonUrl),
|
||||||
|
amazonLabel: typeof item.amazonLabel === 'string' ? item.amazonLabel.trim().slice(0, 120) : '',
|
||||||
}))
|
}))
|
||||||
.filter(item => item.label && item.url)
|
.filter(item => item.label && item.url)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-1
@@ -993,7 +993,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
...series,
|
...series,
|
||||||
resourceLinks: [
|
resourceLinks: [
|
||||||
...(series.resourceLinks ?? []),
|
...(series.resourceLinks ?? []),
|
||||||
{ id: `${seriesId}-${Date.now().toString(36)}`, label: '', description: '', url: '' },
|
{ id: `${seriesId}-${Date.now().toString(36)}`, label: '', description: '', url: '', amazonUrl: '', amazonLabel: '' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: series),
|
: series),
|
||||||
@@ -1048,6 +1048,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
label: source.label,
|
label: source.label,
|
||||||
description: source.description ?? '',
|
description: source.description ?? '',
|
||||||
url: source.url,
|
url: source.url,
|
||||||
|
amazonUrl: source.amazonUrl ?? '',
|
||||||
|
amazonLabel: source.amazonLabel ?? '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -1075,6 +1077,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
label: link.label,
|
label: link.label,
|
||||||
description: link.description ?? '',
|
description: link.description ?? '',
|
||||||
url: link.url,
|
url: link.url,
|
||||||
|
amazonUrl: link.amazonUrl ?? '',
|
||||||
|
amazonLabel: link.amazonLabel ?? '',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (toAdd.length === 0) return series
|
if (toAdd.length === 0) return series
|
||||||
@@ -1646,6 +1650,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
<label htmlFor={`archive-link-url-${link.id}`}>URL</label>
|
<label htmlFor={`archive-link-url-${link.id}`}>URL</label>
|
||||||
<input id={`archive-link-url-${link.id}`} type="url" value={link.url} placeholder="https://..." onChange={e => updateArchivedSeriesLink(series.id, link.id, 'url', e.target.value)} />
|
<input id={`archive-link-url-${link.id}`} type="url" value={link.url} placeholder="https://..." onChange={e => updateArchivedSeriesLink(series.id, link.id, 'url', e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`archive-link-amazon-url-${link.id}`}>Amazon URL (optional)</label>
|
||||||
|
<input id={`archive-link-amazon-url-${link.id}`} type="url" value={link.amazonUrl ?? ''} placeholder="https://amazon.com/..." onChange={e => updateArchivedSeriesLink(series.id, link.id, 'amazonUrl', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`archive-link-amazon-label-${link.id}`}>Amazon Button Label</label>
|
||||||
|
<input id={`archive-link-amazon-label-${link.id}`} type="text" value={link.amazonLabel ?? ''} placeholder="Get it on Amazon" onChange={e => updateArchivedSeriesLink(series.id, link.id, 'amazonLabel', e.target.value)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn-admin-remove" onClick={() => removeArchivedSeriesLink(series.id, link.id)}>Remove</button>
|
<button type="button" className="btn-admin-remove" onClick={() => removeArchivedSeriesLink(series.id, link.id)}>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1724,6 +1736,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
<label htmlFor="resources-study-guide-url">Printed Copy URL (Amazon button)</label>
|
<label htmlFor="resources-study-guide-url">Printed Copy URL (Amazon button)</label>
|
||||||
<input id="resources-study-guide-url" type="url" value={form.studyGuideUrl} placeholder="https://..." onChange={e => handleChange('studyGuideUrl', e.target.value)} />
|
<input id="resources-study-guide-url" type="url" value={form.studyGuideUrl} placeholder="https://..." onChange={e => handleChange('studyGuideUrl', e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor="resources-study-guide-amazon-label">Amazon Button Label</label>
|
||||||
|
<input id="resources-study-guide-amazon-label" type="text" value={form.studyGuideAmazonButtonLabel} placeholder="Get it on Amazon" onChange={e => handleChange('studyGuideAmazonButtonLabel', e.target.value)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1756,6 +1772,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
<label htmlFor={`resource-description-${link.id}`}>Description</label>
|
<label htmlFor={`resource-description-${link.id}`}>Description</label>
|
||||||
<textarea id={`resource-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Description shown on the download page" onChange={e => updateLink(link.id, 'description', e.target.value)} />
|
<textarea id={`resource-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Description shown on the download page" onChange={e => updateLink(link.id, 'description', e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`resource-amazon-url-${link.id}`}>Amazon URL (optional)</label>
|
||||||
|
<input id={`resource-amazon-url-${link.id}`} type="url" value={link.amazonUrl ?? ''} placeholder="https://amazon.com/..." onChange={e => updateLink(link.id, 'amazonUrl', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`resource-amazon-label-${link.id}`}>Amazon Button Label</label>
|
||||||
|
<input id={`resource-amazon-label-${link.id}`} type="text" value={link.amazonLabel ?? ''} placeholder="Get it on Amazon" onChange={e => updateLink(link.id, 'amazonLabel', e.target.value)} />
|
||||||
|
</div>
|
||||||
<div className="admin-field">
|
<div className="admin-field">
|
||||||
<label htmlFor={`resource-image-${link.id}`}>Image URL</label>
|
<label htmlFor={`resource-image-${link.id}`}>Image URL</label>
|
||||||
<input id={`resource-image-${link.id}`} type="text" value={link.imageUrl ?? ''} placeholder="/uploads/example.png" onChange={e => updateLink(link.id, 'imageUrl', e.target.value)} />
|
<input id={`resource-image-${link.id}`} type="text" value={link.imageUrl ?? ''} placeholder="/uploads/example.png" onChange={e => updateLink(link.id, 'imageUrl', e.target.value)} />
|
||||||
@@ -1820,6 +1844,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
<label htmlFor={`resources-archive-link-description-${link.id}`}>Description</label>
|
<label htmlFor={`resources-archive-link-description-${link.id}`}>Description</label>
|
||||||
<textarea id={`resources-archive-link-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Description shown on the download page" onChange={e => updateArchivedSeriesLink(series.id, link.id, 'description', e.target.value)} />
|
<textarea id={`resources-archive-link-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Description shown on the download page" onChange={e => updateArchivedSeriesLink(series.id, link.id, 'description', e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`resources-archive-link-amazon-url-${link.id}`}>Amazon URL (optional)</label>
|
||||||
|
<input id={`resources-archive-link-amazon-url-${link.id}`} type="url" value={link.amazonUrl ?? ''} placeholder="https://amazon.com/..." onChange={e => updateArchivedSeriesLink(series.id, link.id, 'amazonUrl', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="admin-field">
|
||||||
|
<label htmlFor={`resources-archive-link-amazon-label-${link.id}`}>Amazon Button Label</label>
|
||||||
|
<input id={`resources-archive-link-amazon-label-${link.id}`} type="text" value={link.amazonLabel ?? ''} placeholder="Get it on Amazon" onChange={e => updateArchivedSeriesLink(series.id, link.id, 'amazonLabel', e.target.value)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn-admin-remove" onClick={() => removeArchivedSeriesLink(series.id, link.id)}>Remove</button>
|
<button type="button" className="btn-admin-remove" onClick={() => removeArchivedSeriesLink(series.id, link.id)}>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+12
-1
@@ -281,6 +281,8 @@ interface DownloadPageResource {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
buttonText: string
|
buttonText: string
|
||||||
resourceId: string
|
resourceId: string
|
||||||
|
amazonUrl?: string
|
||||||
|
amazonLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDownloadPageResource(content: SiteContent, pageId: string | undefined): DownloadPageResource | null {
|
function resolveDownloadPageResource(content: SiteContent, pageId: string | undefined): DownloadPageResource | null {
|
||||||
@@ -299,6 +301,8 @@ function resolveDownloadPageResource(content: SiteContent, pageId: string | unde
|
|||||||
tags: resource.tags ?? [],
|
tags: resource.tags ?? [],
|
||||||
buttonText: `Download ${resource.label}`,
|
buttonText: `Download ${resource.label}`,
|
||||||
resourceId: buildCustomResourceDownloadId(resource.id),
|
resourceId: buildCustomResourceDownloadId(resource.id),
|
||||||
|
amazonUrl: resource.amazonUrl,
|
||||||
|
amazonLabel: resource.amazonLabel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +320,8 @@ function resolveDownloadPageResource(content: SiteContent, pageId: string | unde
|
|||||||
tags: [],
|
tags: [],
|
||||||
buttonText: `Download ${link.label || series.title || 'Resource'}`,
|
buttonText: `Download ${link.label || series.title || 'Resource'}`,
|
||||||
resourceId: buildArchivedResourceDownloadId(series.id, link.id),
|
resourceId: buildArchivedResourceDownloadId(series.id, link.id),
|
||||||
|
amazonUrl: link.amazonUrl,
|
||||||
|
amazonLabel: link.amazonLabel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +593,7 @@ function StudyGuideSection({ content }: { content: SiteContent }) {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
Get Printed Copy on Amazon
|
{content.studyGuideAmazonButtonLabel || 'Get it on Amazon'}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -819,6 +825,11 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="download-detail-actions">
|
<div className="download-detail-actions">
|
||||||
|
{resource.amazonUrl && (
|
||||||
|
<a href={resource.amazonUrl} target="_blank" rel="noreferrer" className="btn-secondary">
|
||||||
|
{resource.amazonLabel || 'Get it on Amazon'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<Link to="/resources" className="btn-secondary">Back to Downloads</Link>
|
<Link to="/resources" className="btn-secondary">Back to Downloads</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export interface CustomLink {
|
|||||||
placement: 'platforms' | 'footer' | 'resources'
|
placement: 'platforms' | 'footer' | 'resources'
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
amazonUrl?: string
|
||||||
|
amazonLabel?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ export interface ArchivedSeriesResourceLink {
|
|||||||
label: string
|
label: string
|
||||||
description?: string
|
description?: string
|
||||||
url: string
|
url: string
|
||||||
|
amazonUrl?: string
|
||||||
|
amazonLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArchivedSeriesNote {
|
export interface ArchivedSeriesNote {
|
||||||
@@ -140,6 +144,7 @@ export interface SiteContent {
|
|||||||
studyGuideDescription: string
|
studyGuideDescription: string
|
||||||
studyGuideDownloadUrl: string
|
studyGuideDownloadUrl: string
|
||||||
studyGuideUrl: string
|
studyGuideUrl: string
|
||||||
|
studyGuideAmazonButtonLabel: string
|
||||||
// ── Share ──
|
// ── Share ──
|
||||||
shareHeading: string
|
shareHeading: string
|
||||||
shareP: string
|
shareP: string
|
||||||
@@ -250,6 +255,7 @@ export const DEFAULTS: SiteContent = {
|
|||||||
'Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.',
|
'Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.',
|
||||||
studyGuideDownloadUrl: '',
|
studyGuideDownloadUrl: '',
|
||||||
studyGuideUrl: 'https://a.co/d/01sG2tOJ',
|
studyGuideUrl: 'https://a.co/d/01sG2tOJ',
|
||||||
|
studyGuideAmazonButtonLabel: 'Get it on Amazon',
|
||||||
shareHeading: 'Help one more person hear the Word this week.',
|
shareHeading: 'Help one more person hear the Word this week.',
|
||||||
shareP: 'Scan the QR code or text the show link to a friend who needs encouragement today.',
|
shareP: 'Scan the QR code or text the show link to a friend who needs encouragement today.',
|
||||||
whereToNextEyebrow: 'Where to Next',
|
whereToNextEyebrow: 'Where to Next',
|
||||||
|
|||||||
Reference in New Issue
Block a user