67 lines
4.4 KiB
TypeScript
67 lines
4.4 KiB
TypeScript
'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 viewing reports</span>
|
||
<h1>{state === 'enabled' ? 'You’re 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. You’ll receive your personal viewing recap when the monthly schedule runs.' : state === 'off' ? 'You won’t receive further monthly recaps. You can turn them back on in Profile.' : state === 'ready' && link?.action === 'unsubscribe' ? 'This turns off all personal viewing report emails. You can still explore all your reports in Magent.' : state === 'ready' ? 'Confirm to email yourself your minutes, movies, episodes, longest run and requests. Manage automatic monthly delivery separately in Profile.' : ''}</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>
|
||
}
|