Files
Magent/frontend/app/email-recaps/page.tsx
T
Assclaw 1979e02cde
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
Add opt-in monthly email recaps with scheduling and delivery history
2026-09-09 22:39:22 +12:00

67 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
}