Ask for name and email before sending visitors to Amazon or Apple Books (switchable in admin → Books); v2.29.0
Publish Container / build (push) Successful in 15m46s
Publish Container / build (push) Successful in 15m46s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c342f14bd
commit
9f52437a07
10 files changed
+277
-11
No files matched your search
@@ -222,6 +222,81 @@ export function register(app) {
|
||||
}
|
||||
})
|
||||
|
||||
// Collects a visitor's details, then hands back the Amazon / Apple Books link for a guide or book.
|
||||
// The destination always comes from the saved site content, never from the request.
|
||||
app.post('/api/store-link', studyDownloadRateLimit, async (req, res) => {
|
||||
try {
|
||||
const { source, store, firstName, lastName, email, subscribe, _honey } = req.body ?? {}
|
||||
if (_honey) { res.json({ ok: true }); return }
|
||||
|
||||
if (typeof source !== 'string' || !source || (store !== 'amazon' && store !== 'apple')) {
|
||||
res.status(400).json({ message: 'That link is not available.' }); return
|
||||
}
|
||||
if (!firstName || typeof firstName !== 'string' || firstName.trim().length < 1 || firstName.trim().length > 100) {
|
||||
res.status(400).json({ message: 'First name is required.' }); return
|
||||
}
|
||||
if (!lastName || typeof lastName !== 'string' || lastName.trim().length < 1 || lastName.trim().length > 100) {
|
||||
res.status(400).json({ message: 'Last name is required.' }); return
|
||||
}
|
||||
if (!email || typeof email !== 'string' || !/^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}$/.test(email.trim())) {
|
||||
res.status(400).json({ message: 'A valid email address is required.' }); return
|
||||
}
|
||||
|
||||
const published = await loadSiteContentFile(DATA_FILE)
|
||||
const content = published?.siteContent ?? {}
|
||||
const pick = (amazon, apple) => (store === 'amazon' ? amazon : apple)
|
||||
let label = ''
|
||||
let url = ''
|
||||
|
||||
if (source === 'page:primary-guide') {
|
||||
label = content.studyGuideTitle
|
||||
url = pick(content.studyGuideUrl, content.studyGuideAppleBooksUrl)
|
||||
} else if (source === 'page:second-guide') {
|
||||
const guide = content.secondSeries?.guide
|
||||
label = guide?.title
|
||||
url = pick(guide?.amazonUrl, guide?.appleBooksUrl)
|
||||
} else if (source.startsWith('page:book-guide--')) {
|
||||
const id = source.slice('page:book-guide--'.length)
|
||||
const guide = (Array.isArray(content.finishedBooks) ? content.finishedBooks : []).find(b => b?.id === id)?.guide
|
||||
label = guide?.title
|
||||
url = pick(guide?.amazonUrl, guide?.appleBooksUrl)
|
||||
} else if (source.startsWith('page:custom--')) {
|
||||
const id = source.slice('page:custom--'.length)
|
||||
const link = (content.customLinks ?? []).find(l => l?.id === id && l.placement === 'resources')
|
||||
label = link?.label
|
||||
url = pick(link?.amazonUrl, link?.appleBooksUrl)
|
||||
} else if (source.startsWith('book:')) {
|
||||
const id = source.slice('book:'.length)
|
||||
const book = (content.customLinks ?? []).find(l => l?.id === id && l.placement === 'books')
|
||||
label = book?.label
|
||||
url = pick(book?.amazonUrl || book?.url, book?.appleBooksUrl)
|
||||
}
|
||||
|
||||
const destination = sanitizeUrl(url)
|
||||
if (!destination) { res.status(404).json({ message: 'That link is not available.' }); return }
|
||||
|
||||
const name = `${firstName.trim()} ${lastName.trim()}`.trim()
|
||||
const trimmedEmail = email.trim()
|
||||
const wantsSubscribe = subscribe !== false
|
||||
const storeName = store === 'amazon' ? 'Amazon' : 'Apple Books'
|
||||
|
||||
addContactSubmission({
|
||||
name,
|
||||
email: trimmedEmail,
|
||||
message: `Requested ${storeName} link: ${label || source}`,
|
||||
messageType: 'general',
|
||||
subscribe: wantsSubscribe,
|
||||
})
|
||||
if (wantsSubscribe) await syncContactToResend(name, trimmedEmail)
|
||||
incrementDownloadCount(`store:${source}:${store}`)
|
||||
|
||||
res.json({ ok: true, url: destination })
|
||||
} catch (err) {
|
||||
console.error('[store-link] request error:', err)
|
||||
res.status(500).json({ message: 'Failed to process your request. Please try again.' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/admin-download-stats', requireAdminAuth, (_req, res) => {
|
||||
res.json({ counts: state.downloadCounts })
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user