Add opt-in monthly email recaps with scheduling and delivery history
Magent CI/CD / verify (push) Canceled after 3m55s
Magent CI/CD / deploy-prod (push) Canceled after 0s
Magent CI/CD / deploy-beta (push) Canceled after 0s

This commit is contained in:
2026-09-09 22:39:22 +12:00
parent 333a799e21
commit 1979e02cde
18 changed files with 1832 additions and 7 deletions
+66
View File
@@ -0,0 +1,66 @@
'use client'
import { useEffect, useState } from 'react'
import { getApiBase } from '../lib/auth'
import BrandingLogo from '../ui/BrandingLogo'
import './recaps.css'
type LinkAction = { action: 'confirm' | 'unsubscribe'; token: string }
export default function EmailRecapLinkPage() {
const [link, setLink] = useState<LinkAction | null>(null)
const [state, setState] = useState('loading')
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
useEffect(() => {
let controller: AbortController | null = null
const checkLink = () => {
controller?.abort()
const abort = new AbortController()
controller = abort
setError(''); setState('loading'); setLink(null)
// Fragments stay out of web-server access logs and referrers. Opening the link only checks it.
const params = new URLSearchParams(window.location.hash.slice(1))
const action = params.get('action')
const token = params.get('token') || ''
if ((action !== 'confirm' && action !== 'unsubscribe') || !/^[A-Za-z0-9_-]{40,100}$/.test(token)) {
setError('This email link is incomplete. Open Profile to manage your monthly recaps.'); setState('error'); return
}
const payload = { action, token } as LinkAction
setLink(payload)
void fetch(`${getApiBase()}/email-recaps/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: abort.signal, credentials: 'omit' }).then(async (response) => {
const result = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Could not check this email link. Please open it again.')
if (!abort.signal.aborted) setState(result.state)
}).catch((err: Error) => { if (!abort.signal.aborted) { setError(err.message); setState('error') } })
}
checkLink()
window.addEventListener('hashchange', checkLink)
return () => { controller?.abort(); window.removeEventListener('hashchange', checkLink) }
}, [])
const apply = async () => {
if (!link || busy) return
setBusy(true); setError('')
try {
const response = await fetch(`${getApiBase()}/email-recaps/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(link), credentials: 'omit' })
const result = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Could not update your preference. Please try again.')
setState(result.state)
window.history.replaceState(null, '', '/email-recaps')
} catch (err) { setError(err instanceof Error ? err.message : 'Could not update your preference.') }
finally { setBusy(false) }
}
const done = state === 'enabled' || state === 'off'
return <main className="recap-link-page"><a className="recap-brand" href="/login"><BrandingLogo className="brand-logo" /><span>Magent</span></a><section className="account-panel">
<span className="recap-eyebrow">Personal monthly recaps</span>
<h1>{state === 'enabled' ? 'Youre on the list.' : state === 'off' ? 'Recaps are turned off.' : state === 'loading' ? 'Checking your email link' : state === 'error' ? 'This link needs another look' : link?.action === 'unsubscribe' ? 'Unsubscribe from recaps?' : 'Your month, delivered.'}</h1>
<p>{state === 'enabled' ? 'Your email is confirmed. Youll receive your personal viewing recap when the monthly schedule runs.' : state === 'off' ? 'You wont receive further monthly recaps. You can turn them back on in Profile.' : state === 'ready' && link?.action === 'unsubscribe' ? 'This turns off your monthly viewing emails. You can still explore all your reports in Magent.' : state === 'ready' ? 'Confirm to receive your minutes, movies, episodes, longest run and requests each month.' : ''}</p>
{error && <p className="account-notice is-error" role="alert">{error}</p>}
{state === 'ready' && <button type="button" className="account-primary" disabled={busy} onClick={() => void apply()}>{busy ? 'Updating…' : link?.action === 'unsubscribe' ? 'Unsubscribe from recaps' : 'Confirm email recaps'}</button>}
{(done || state === 'error') && <a className="recap-text-link" href="/profile#monthly-recaps">Manage email preferences </a>}
{state === 'loading' && <p role="status">One moment</p>}
</section></main>
}