Files
Magent/frontend/app/newsletter-subscription/page.tsx
T
Assclaw f852e7c941
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped
chore: standardize security and quality foundations
2026-09-17 20:03:47 +12:00

158 lines
5.7 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, 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"
? "Youre 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. 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 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>
);
}