198 lines
7.5 KiB
TypeScript
198 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
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;
|
|
weekday: 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 NewsletterPreference() {
|
|
const router = useRouter();
|
|
const [data, setData] = useState<Preference | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [notice, setNotice] = useState("");
|
|
const [revision, setRevision] = useState(0);
|
|
const [now, setNow] = useState(Date.now());
|
|
|
|
useEffect(() => {
|
|
void revision;
|
|
const abort = new AbortController();
|
|
setError("");
|
|
void authFetch(`${getApiBase()}/profile/newsletters`, { signal: abort.signal })
|
|
.then(async (response) => {
|
|
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);
|
|
})
|
|
.catch((err: Error) => {
|
|
if (!abort.signal.aborted) setError(err.message);
|
|
});
|
|
return () => abort.abort();
|
|
}, [revision, router]);
|
|
|
|
useEffect(() => {
|
|
if (!data?.resend_after || data.state === "enabled" || data.resend_after * 1000 <= Date.now()) return;
|
|
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
|
return () => window.clearInterval(timer);
|
|
}, [data?.resend_after, data?.state]);
|
|
|
|
const save = async (enabled: boolean) => {
|
|
if (busy) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setNotice("");
|
|
try {
|
|
const response = await authFetch(`${getApiBase()}/profile/newsletters`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
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 ? "Newsletters are on." : "Newsletters 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/newsletters`).catch(() => null);
|
|
if (response?.ok) {
|
|
setData(await response.json());
|
|
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="newsletters" aria-labelledby="newsletter-preference-title">
|
|
<div className="recap-section-heading">
|
|
<div>
|
|
<span className="recap-eyebrow">Your next watch</span>
|
|
<h2 id="newsletter-preference-title">New in your library.</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>
|
|
{!data && !error && <p role="status">Loading your email preference…</p>}
|
|
{data && (
|
|
<>
|
|
{data.state === "enabled" ? (
|
|
<p className="recap-delivery-address">
|
|
Newsletters will go to <strong>{data.email}</strong>.{" "}
|
|
{data.schedule_enabled && data.next_send_at
|
|
? `Next scheduled send: ${scheduled(data.next_send_at)}.`
|
|
: "Weekly sending is paused. You may still receive editions scheduled by your administrator."}
|
|
</p>
|
|
) : (
|
|
<p>
|
|
{data.state === "pending"
|
|
? `Check ${data.email} for a confirmation link. It expires after 24 hours. Your newsletter subscription starts after you confirm.`
|
|
: data.state === "expired"
|
|
? "Request a new confirmation link to turn on newsletters."
|
|
: "Subscribe with your profile email. If you already confirmed it for monthly recaps, you can turn this on straight away. Otherwise, we?ll send a confirmation link."}
|
|
</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, ready for the next edition your administrator sends.</p>
|
|
)}
|
|
<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 new arrivals"
|
|
: "Resend newsletter confirmation"}
|
|
</button>
|
|
)}
|
|
{data.state !== "off" && (
|
|
<button type="button" className="account-secondary" disabled={busy} onClick={() => void save(false)}>
|
|
{busy
|
|
? "Updating…"
|
|
: data.state === "enabled"
|
|
? "Turn off newsletters"
|
|
: "Cancel newsletter subscription"}
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="account-secondary"
|
|
disabled={busy}
|
|
onClick={() => {
|
|
setNotice("");
|
|
setRevision((value) => value + 1);
|
|
}}
|
|
>
|
|
Refresh newsletter 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>
|
|
)}
|
|
</>
|
|
)}
|
|
{error && (
|
|
<p className="account-notice is-error" role="alert">
|
|
{error}
|
|
{!data && (
|
|
<button type="button" className="account-secondary" onClick={() => setRevision((value) => value + 1)}>
|
|
Try again
|
|
</button>
|
|
)}
|
|
</p>
|
|
)}
|
|
{notice && (
|
|
<p className="account-notice is-status" role="status">
|
|
{notice}
|
|
</p>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|