Add Grizzlyflix newsletters with curated editions and weekly delivery
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getApiBase } from '../lib/auth'
|
||||
import BrandingLogo from '../ui/BrandingLogo'
|
||||
import '../email-recaps/recaps.css'
|
||||
|
||||
type LinkAction = { action: 'confirm' | 'unsubscribe'; token: string }
|
||||
|
||||
export default function NewsletterLinkPage() {
|
||||
const [link, setLink] = useState<LinkAction | null>(null)
|
||||
const [state, setState] = useState('loading')
|
||||
const [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const currentLink = useRef<LinkAction | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let controller: AbortController | null = null
|
||||
const checkLink = () => {
|
||||
controller?.abort()
|
||||
const abort = new AbortController()
|
||||
controller = abort
|
||||
setError(''); setState('loading'); setLink(null); setBusy(false); currentLink.current = 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 newsletters.'); setState('error'); return
|
||||
}
|
||||
const payload = { action, token } as LinkAction
|
||||
currentLink.current = payload
|
||||
setLink(payload)
|
||||
void fetch(`${getApiBase()}/newsletter-subscription/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 () => { currentLink.current = null; controller?.abort(); window.removeEventListener('hashchange', checkLink) }
|
||||
}, [])
|
||||
|
||||
const apply = async () => {
|
||||
if (!link || busy) return
|
||||
const payload = link
|
||||
setBusy(true); setError('')
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/newsletter-subscription/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'omit' })
|
||||
const result = await response.json().catch(() => ({}))
|
||||
if (currentLink.current !== payload) return
|
||||
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, '', '/newsletter-subscription')
|
||||
} catch (err) { if (currentLink.current === payload) setError(err instanceof Error ? err.message : 'Could not update your preference.') }
|
||||
finally { if (currentLink.current === payload) 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">Grizzlyflix newsletters</span>
|
||||
<h1>{state === 'enabled' ? 'You’re on the list.' : state === 'off' ? 'Newsletters are turned off.' : state === 'loading' ? 'Checking your email link' : state === 'error' ? 'This link needs another look' : link?.action === 'unsubscribe' ? 'Unsubscribe from newsletters?' : 'Your next watch starts here.'}</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 new-arrival newsletters. Your personal monthly recaps are managed separately.' : state === 'ready' ? 'Confirm to receive new movies, TV updates and featured picks, with posters and links to watch.' : ''}</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 newsletters' : 'Confirm newsletter subscription'}</button>}
|
||||
{(done || state === 'error') && <a className="recap-text-link" href="/profile#newsletters">Manage email preferences ↗</a>}
|
||||
{state === 'loading' && <p role="status">One moment…</p>}
|
||||
</section></main>
|
||||
}
|
||||
Reference in New Issue
Block a user