Let users email personal reports on demand
Magent CI/CD / verify (push) Successful in 11m6s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m40s

This commit is contained in:
2026-09-10 16:15:39 +12:00
parent 6e473fd0a7
commit b286ca3c42
13 changed files with 261 additions and 36 deletions
+19 -13
View File
@@ -5,12 +5,13 @@ import { useRouter } from 'next/navigation'
import { authFetch, getApiBase } from '../lib/auth'
import '../email-recaps/recaps.css'
type Preference = { state: 'off' | 'pending' | 'expired' | 'enabled'; email: string | null; can_subscribe: boolean; detail: string; schedule_enabled: boolean; next_send_at: number | null; day: number; hour: number; resend_after: number | null }
type Preference = { automatic_monthly: boolean; state: 'off' | 'pending' | 'expired' | 'enabled'; email: string | null; can_subscribe: boolean; detail: string; schedule_enabled: boolean; next_send_at: number | null; day: number; hour: number; resend_after: number | null }
const scheduled = (value: number) => `${new Date(value * 1000).toLocaleString(undefined, { dateStyle: 'long', timeStyle: 'short', timeZone: 'UTC' })} UTC`
export default function MonthlyRecapPreference() {
const router = useRouter()
const [data, setData] = useState<Preference | null>(null)
const [automatic, setAutomatic] = useState(false)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
@@ -24,7 +25,7 @@ export default function MonthlyRecapPreference() {
if (response.status === 401) { router.replace('/login?next=%2Fprofile'); return }
if (!response.ok) throw new Error('Could not load your email preference. Please try again.')
const result = await response.json() as Preference
if (!abort.signal.aborted) setData(result)
if (!abort.signal.aborted) { setData(result); setAutomatic(result.automatic_monthly) }
}).catch((err: Error) => { if (!abort.signal.aborted) setError(err.message) })
return () => abort.abort()
}, [revision, router])
@@ -35,38 +36,43 @@ export default function MonthlyRecapPreference() {
return () => window.clearInterval(timer)
}, [data?.resend_after, data?.state])
const save = async (enabled: boolean) => {
const save = async (enabled: boolean, monthly = automatic) => {
if (busy) return
setBusy(true); setError(''); setNotice('')
try {
const response = await authFetch(`${getApiBase()}/profile/email-recaps`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }),
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, automatic_monthly: monthly }),
})
if (response.status === 401) { router.replace('/login?next=%2Fprofile'); return }
const result = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Could not update your email preference.')
setData(result); setNow(Date.now())
setNotice(result.message || (enabled ? 'Monthly recaps are on.' : 'Monthly recaps are off.'))
setData(result); setAutomatic(result.automatic_monthly); setNow(Date.now())
setNotice(result.message || (enabled ? 'Personal report emails are enabled.' : 'Personal report emails are off.'))
} catch (err) {
setError(err instanceof Error ? err.message : 'Could not update your email preference.')
// A confirmation may be pending even if SMTP could not confirm delivery.
const response = await authFetch(`${getApiBase()}/profile/email-recaps`).catch(() => null)
if (response?.ok) { setData(await response.json()); setNow(Date.now()) }
if (response?.ok) { const fresh = await response.json(); setData(fresh); setAutomatic(fresh.automatic_monthly); setNow(Date.now()) }
} finally { setBusy(false) }
}
const cooldown = data?.resend_after ? Math.max(0, Math.ceil(data.resend_after - now / 1000)) : 0
return <section className="recap-preference" id="monthly-recaps" aria-labelledby="recap-preference-title">
<div className="recap-section-heading"><div><span className="recap-eyebrow">A little look back</span><h2 id="recap-preference-title">Your month, delivered.</h2></div>{data && <span className={`recap-pill ${data.state === 'enabled' ? 'is-enabled' : ''}`}>{({ off: 'Off', pending: 'Check your inbox', expired: 'Confirmation expired', enabled: 'Subscribed' })[data.state]}</span>}</div>
<p>Your minutes, movies, episodes, longest run and requests, in one personal monthly email. <a href="/insights/reports">Explore your latest report </a></p>
<div className="recap-section-heading"><div><span className="recap-eyebrow">A little look back</span><h2 id="recap-preference-title">Your reports, your choice.</h2></div>{data && <span className={`recap-pill ${data.state === 'enabled' ? 'is-enabled' : ''}`}>{({ off: 'Off', pending: 'Check your inbox', expired: 'Confirmation expired', enabled: 'Email confirmed' })[data.state]}</span>}</div>
<p>Email yourself your viewing report whenever you want. Choose a previous month or the current month so far, and decide whether you also want automatic monthly emails. <a href="/insights/reports">Explore your latest report </a></p>
{!data && !error && <p role="status">Loading your email preference</p>}
{data && <>
{data.state === 'enabled' ? <p className="recap-delivery-address">Recaps will go to <strong>{data.email}</strong>. {data.schedule_enabled && data.next_send_at ? `Next scheduled send: ${scheduled(data.next_send_at)}.` : 'The administrator has paused scheduled delivery.'}</p> : <p>{data.state === 'pending' ? `Check ${data.email} for a confirmation link. It expires after 24 hours. No viewing history is emailed until you confirm.` : data.state === 'expired' ? 'Request a new confirmation link to turn on your recaps.' : 'Confirm your profile email to turn this on. You can unsubscribe from any recap or here in Profile.'}</p>}
{data.state === 'enabled' ? <p className="recap-delivery-address">Recaps will go to <strong>{data.email}</strong>. {!data.automatic_monthly ? 'On demand only: choose a month in Reports and email it whenever you want.' : data.schedule_enabled && data.next_send_at ? `Next scheduled send: ${scheduled(data.next_send_at)}.` : 'The administrator has paused scheduled delivery.'}</p> : <p>{data.state === 'pending' ? `Check ${data.email} for a confirmation link. It expires after 24 hours. No viewing history is emailed until you confirm.` : data.state === 'expired' ? 'Request a new confirmation link to turn on your recaps.' : 'Confirm your profile email to turn this on. You can unsubscribe from any recap or here in Profile.'}</p>}
{!data.can_subscribe && data.state !== 'enabled' && <p className="recap-muted">{data.detail}</p>}
{data.state !== 'enabled' && data.can_subscribe && !data.schedule_enabled && <p className="recap-muted">You can subscribe now. Monthly sends will begin when your administrator starts the schedule.</p>}
{data.state !== 'enabled' && data.can_subscribe && automatic && !data.schedule_enabled && <p className="recap-muted">You can subscribe now. Monthly sends will begin when your administrator starts the schedule.</p>}
<label className="recap-delivery-choice">Delivery preference<select value={automatic ? 'monthly' : 'manual'} disabled={busy || data.state === 'pending'} onChange={(event) => {
const monthly = event.target.value === 'monthly'
setAutomatic(monthly)
if (data.state === 'enabled') void save(true, monthly)
}}><option value="manual">On demand only</option><option value="monthly">On demand + automatic monthly emails</option></select></label>
<div className="recap-actions">
{data.state !== 'enabled' && <button type="button" className="account-primary" disabled={busy || !data.can_subscribe || cooldown > 0} onClick={() => void save(true)}>{busy ? 'Sending confirmation…' : data.state === 'off' ? 'Email me my monthly recap' : 'Send a new confirmation'}</button>}
{data.state !== 'off' && <button type="button" className="account-secondary" disabled={busy} onClick={() => void save(false)}>{busy ? 'Updating…' : data.state === 'enabled' ? 'Turn off recaps' : 'Cancel subscription'}</button>}
{data.state !== 'enabled' && <button type="button" className="account-primary" disabled={busy || !data.can_subscribe || cooldown > 0} onClick={() => void save(true)}>{busy ? 'Sending confirmation…' : data.state === 'off' ? 'Confirm my email for reports' : 'Send a new confirmation'}</button>}
{data.state !== 'off' && <button type="button" className="account-secondary" disabled={busy} onClick={() => void save(false)}>{busy ? 'Updating…' : data.state === 'enabled' ? 'Turn off report emails' : 'Cancel subscription'}</button>}
<button type="button" className="account-secondary" disabled={busy} onClick={() => { setNotice(''); setRevision((value) => value + 1) }}>Refresh preference</button>
</div>
{cooldown > 0 && data.state !== 'enabled' && <p className="recap-muted">Another confirmation can be requested in {Math.ceil(cooldown / 60)} {Math.ceil(cooldown / 60) === 1 ? 'minute' : 'minutes'}.</p>}