feat(release): publish minimal self-contained Magent source

This commit is contained in:
Magent release tooling
2026-09-19 16:58:12 +12:00
commit 5fa5d45535
272 changed files with 79305 additions and 0 deletions
+924
View File
@@ -0,0 +1,924 @@
"use client";
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import AdminShell from "../../ui/AdminShell";
import { authFetch, getApiBase } from "../../lib/auth";
import "../../email-recaps/recaps.css";
import "./newsletters.css";
type Settings = {
enabled: boolean;
weekday: number;
hour: number;
limit_titles: number;
public_url: string;
intro: string;
revision: number;
next_send_at?: number | null;
last_error?: string;
};
type Title = {
id: string;
title: string;
type: "movie" | "series";
year: number | null;
has_artwork: boolean;
items: { id: string; season: number | null; number: number | null }[];
selected: boolean;
featured: boolean;
};
type Edition = {
id: string;
subject: string;
intro: string;
revision: number;
state: string;
origin: string;
send_at: number | null;
created_at: number;
content: { titles: Title[]; total_titles: number; period_start: string; period_end: string };
};
type Summary = Omit<Edition, "content"> & { titles: number; period_start: string; period_end: string };
type Delivery = {
id: string;
subject: string;
kind: string;
email: string;
username: string | null;
state: string;
attempts: number;
updated_at: number;
next_attempt_at: number;
detail: string;
};
type Overview = {
settings: Settings;
ready: boolean;
detail: string;
editions: Summary[];
deliveries: Delivery[];
total: number;
subscribers: number;
};
type Preview = { id: string; revision: number; subject: string; body_html: string; body_text: string };
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const dateLabel = (value?: number | null) =>
value
? `${new Date(value * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC`
: "Not scheduled";
const labels: Record<string, string> = {
draft: "Draft",
scheduled: "Scheduled",
queued: "Queued",
complete: "Finished",
skipped: "Skipped",
preparing: "Preparing email",
sending: "Sending",
sent: "Accepted by mail server",
retry: "Retry scheduled",
failed: "Failed",
unknown: "Needs review",
cancelled: "Cancelled",
};
const editableFields = (edition: Edition) => ({
subject: edition.subject,
intro: edition.intro,
titles: edition.content.titles.map(({ id, selected, featured }) => ({ id, selected, featured })),
});
const scheduleFields = (settings: Settings) => ({
enabled: settings.enabled,
weekday: settings.weekday,
hour: settings.hour,
limit_titles: settings.limit_titles,
intro: settings.intro,
revision: settings.revision,
});
function Poster({ title }: { title: Title }) {
const [failed, setFailed] = useState(false);
return (
<div className="newsletter-poster">
<span aria-hidden="true">{title.type === "series" ? "TV" : "MOVIE"}</span>
{title.has_artwork && !failed && (
<img
src={`${getApiBase()}/admin/newsletters/artwork/${title.id}`}
alt=""
loading="lazy"
onError={() => setFailed(true)}
/>
)}
</div>
);
}
export default function NewslettersAdminPage() {
const router = useRouter();
const [data, setData] = useState<Overview | null>(null);
const [settings, setSettings] = useState<Settings | null>(null);
const [tab, setTab] = useState<"editions" | "schedule" | "history">("editions");
const [edition, setEdition] = useState<Edition | null>(null);
const [saved, setSaved] = useState<Edition | null>(null);
const [preview, setPreview] = useState<Preview | null>(null);
const [mode, setMode] = useState<"html" | "text">("html");
const [days, setDays] = useState(7);
const [sendAt, setSendAt] = useState("");
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [busy, setBusy] = useState("");
const [offset, setOffset] = useState(0);
const [refresh, setRefresh] = useState(0);
const testRequest = useRef<{ key: string; id: string } | null>(null);
const initialized = useRef(false);
const actionController = useRef<AbortController | null>(null);
const parse = useCallback(
async (response: Response) => {
if (response.status === 401) {
router.replace("/login?next=%2Fadmin%2Fnewsletters");
throw new Error("Sign in to continue.");
}
if (response.status === 403) {
router.replace("/");
throw new Error("Administrator access is required.");
}
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(
typeof result.detail === "string" ? result.detail : "Could not complete this action. Please try again.",
);
return result;
},
[router],
);
useEffect(() => {
void refresh;
const abort = new AbortController();
void authFetch(`${getApiBase()}/admin/newsletters?offset=${offset}`, { signal: abort.signal })
.then(parse)
.then((result: Overview) => {
if (abort.signal.aborted) return;
setData(result);
if (!initialized.current) {
setSettings(result.settings);
initialized.current = true;
}
})
.catch((err: Error) => {
if (!abort.signal.aborted) setError(err.message);
});
return () => abort.abort();
}, [offset, refresh, parse]);
useEffect(() => {
if (
!data?.editions.some((row) => ["scheduled", "queued"].includes(row.state)) &&
!data?.deliveries.some((row) => ["queued", "preparing", "sending", "retry"].includes(row.state))
)
return;
const timer = window.setInterval(() => setRefresh((value) => value + 1), 10000);
return () => window.clearInterval(timer);
}, [data]);
useEffect(() => () => actionController.current?.abort(), []);
const dirty =
!!edition && !!saved && JSON.stringify(editableFields(edition)) !== JSON.stringify(editableFields(saved));
const settingsDirty =
!!settings && !!data && JSON.stringify(scheduleFields(settings)) !== JSON.stringify(scheduleFields(data.settings));
const selected = edition?.content.titles.filter((title) => title.selected) || [];
const featured = selected.filter((title) => title.featured).length;
const isDraft = edition?.state === "draft";
const validPreview =
!!edition && !!preview && preview.id === edition.id && preview.revision === edition.revision && !dirty;
const hasContent = selected.length > 0 || !!edition?.intro.trim();
const remember = (row: Edition) => {
setEdition(row);
setSaved(row);
setPreview(null);
setSendAt("");
testRequest.current = null;
};
const action = async <T,>(
name: string,
path: string,
method: string,
payload: unknown,
done: (result: T) => void,
) => {
if (busy) return;
const abort = new AbortController();
actionController.current = abort;
setBusy(name);
setError("");
setNotice("");
try {
const result = await parse(
await authFetch(`${getApiBase()}/admin/newsletters${path}`, {
method,
signal: abort.signal,
headers: { "Content-Type": "application/json" },
...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
}),
);
if (!abort.signal.aborted) {
done(result as T);
setRefresh((value) => value + 1);
}
} catch (err) {
if (!abort.signal.aborted) setError(err instanceof Error ? err.message : "Could not complete this action.");
} finally {
if (!abort.signal.aborted) setBusy("");
}
};
const changeTitle = (id: string, field: "selected" | "featured", value: boolean) => {
if (!edition) return;
setEdition({
...edition,
content: {
...edition.content,
titles: edition.content.titles.map((title) =>
title.id !== id
? title
: { ...title, [field]: value, ...(field === "selected" && !value ? { featured: false } : {}) },
),
},
});
setPreview(null);
};
const saveDraft = (event: FormEvent) => {
event.preventDefault();
if (edition)
void action(
"save",
`/editions/${edition.id}`,
"PUT",
{ revision: edition.revision, ...editableFields(edition) },
(row: Edition) => {
remember(row);
setNotice("Draft saved. Preview this version before sending.");
},
);
};
const publish = (scheduled: boolean) => {
if (!edition || !validPreview || !hasContent) return;
void action(
"publish",
`/editions/${edition.id}/publish`,
"POST",
{ revision: edition.revision, send_at: scheduled ? `${sendAt}:00Z` : null },
(row: Edition) => {
remember(row);
setNotice(`Edition scheduled for ${dateLabel(row.send_at)}. The saved content is now fixed.`);
},
);
};
const sendTest = () => {
if (!edition || !validPreview) return;
const key = `${edition.id}:${edition.revision}`;
if (testRequest.current?.key !== key) testRequest.current = { key, id: crypto.randomUUID() };
void action(
"test",
`/editions/${edition.id}/test`,
"POST",
{ revision: edition.revision, request_id: testRequest.current.id },
(result: { message: string }) => {
setNotice(result.message);
testRequest.current = null;
setOffset(0);
},
);
};
return (
<AdminShell
title="Newsletters"
subtitle="New arrivals, fresh episodes and a little inspiration for the next watch."
actions={
<a className="ghost-button" href="/admin/notifications">
Email settings
</a>
}
>
<div className="recap-admin newsletter-admin">
{error && (
<p className="error-banner" role="alert">
{error}
</p>
)}
{notice && (
<p className="status-banner" role="status">
{notice}
</p>
)}
{!data && !error && <p role="status">Loading newsletters</p>}
{!data && error && (
<button
type="button"
className="ghost-button"
onClick={() => {
setError("");
setRefresh((value) => value + 1);
}}
>
Try again
</button>
)}
{data && settings && (
<>
<div className="recap-overview-strip">
<div>
<span className={`recap-pill ${data.settings.enabled ? "is-enabled" : ""}`}>
{data.settings.enabled ? "Weekly sending is on" : "Weekly sending is paused"}
</span>
<p>
{data.settings.enabled
? `Next edition ${dateLabel(data.settings.next_send_at)}`
: "Create a one-off edition or set a weekly rhythm."}
</p>
</div>
<div className="recap-subscriber-count">
<strong>{data.subscribers}</strong>
<span>confirmed {data.subscribers === 1 ? "subscriber" : "subscribers"}</span>
</div>
</div>
<nav className="newsletter-tabs" aria-label="Newsletter sections">
{(["editions", "schedule", "history"] as const).map((value) => (
<button type="button" key={value} aria-pressed={tab === value} onClick={() => setTab(value)}>
{{ editions: "Editions", schedule: "Weekly schedule", history: "Delivery history" }[value]}
</button>
))}
</nav>
{!data.ready && (
<p className="recap-setup-note">
{data.detail}{" "}
<a className="recap-text-link" href="/admin/notifications">
Email settings
</a>{" "}
·{" "}
<a className="recap-text-link" href="/admin/jellyfin">
Jellyfin settings
</a>
</p>
)}
{tab === "editions" && (
<>
<section className="admin-panel recap-panel">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">A fresh edition</span>
<h2>Whats new in your library</h2>
<p>Collect arrivals from Jellyfin, choose your picks and add a note to your community.</p>
</div>
<div className="newsletter-create">
<label className="recap-month-label" htmlFor="arrival-period">
Arrival period
<select
id="arrival-period"
value={days}
disabled={!!busy || dirty}
onChange={(event) => setDays(Number(event.target.value))}
>
{[7, 14, 30].map((value) => (
<option key={value} value={value}>
Last {value} days
</option>
))}
</select>
</label>
<button
type="button"
className="account-primary"
disabled={!!busy || dirty}
onClick={() =>
void action("create", "/drafts", "POST", { days }, (row: Edition) => {
remember(row);
setNotice("Arrivals collected. Choose the titles you want to include.");
})
}
>
{busy === "create" ? "Collecting arrivals…" : "Create draft"}
</button>
</div>
</div>
{data.editions.length ? (
<div className="newsletter-editions">
{data.editions.map((row) => (
<button
type="button"
className={`newsletter-edition ${edition?.id === row.id ? "is-active" : ""}`}
key={row.id}
disabled={!!busy || dirty}
onClick={() => void action("open", `/editions/${row.id}`, "GET", undefined, remember)}
>
<span>
<strong>{row.subject}</strong>
<small>
{row.titles} {row.titles === 1 ? "title" : "titles"} ·{" "}
{row.origin === "weekly" ? "Weekly edition" : "Custom edition"} ·{" "}
{dateLabel(row.send_at || row.created_at)}
</small>
</span>
<span className="recap-pill">{labels[row.state] || row.state}</span>
</button>
))}
</div>
) : (
<div className="recap-empty">
<span aria-hidden="true"></span>
<h3>Something good to watch</h3>
<p>
Your first edition starts with the latest additions to your library. Collect a draft to begin.
</p>
</div>
)}
{dirty && (
<p className="recap-muted">Save or discard the current changes before opening another edition.</p>
)}
</section>
{edition && (
<section className="admin-panel recap-panel newsletter-editor">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">{isDraft ? "Make it yours" : "Saved edition"}</span>
<h2>{isDraft ? "Edit your newsletter" : edition.subject}</h2>
<p>
Arrivals from {edition.content.period_start.slice(0, 10)} to{" "}
{edition.content.period_end.slice(0, 10)} (UTC). TV additions are grouped by show.
</p>
</div>
<span className="recap-pill">{labels[edition.state] || edition.state}</span>
</div>
<form className="recap-schedule-form" onSubmit={saveDraft}>
<label htmlFor="newsletter-subject">
Email subject
<input
id="newsletter-subject"
maxLength={150}
required
value={edition.subject}
disabled={!!busy || !isDraft}
onChange={(event) => {
setEdition({ ...edition, subject: event.target.value });
setPreview(null);
}}
/>
</label>
<label htmlFor="newsletter-intro">
Announcement <span className="newsletter-optional">Optional</span>
<textarea
id="newsletter-intro"
rows={4}
maxLength={2000}
placeholder="A welcome, a weekend recommendation, or a quick update…"
disabled={!!busy || !isDraft}
value={edition.intro}
onChange={(event) => {
setEdition({ ...edition, intro: event.target.value });
setPreview(null);
}}
/>
<small>Plain text, shared with every subscriber receiving this edition.</small>
</label>
<div className="newsletter-selection-heading">
<h3>Choose the lineup</h3>
<span>
{selected.length}/24 included · {featured}/3 featured
</span>
</div>
{edition.content.total_titles > edition.content.titles.length && (
<p className="recap-muted">
Showing the {edition.content.titles.length} newest titles of {edition.content.total_titles}{" "}
found in this period.
</p>
)}
{edition.content.titles.length ? (
<div className="newsletter-titles">
{edition.content.titles.map((title) => (
<article
className={`newsletter-title ${title.selected ? "is-selected" : ""}`}
key={title.id}
>
<Poster title={title} />
<div className="newsletter-title-copy">
<h4>{title.title}</h4>
<p>
{title.type === "movie"
? `Movie${title.year ? ` · ${title.year}` : ""}`
: `${title.items.length} new ${title.items.length === 1 ? "episode" : "episodes"}`}
</p>
<label className="recap-checkbox">
<input
type="checkbox"
aria-label={`Include ${title.title}`}
checked={title.selected}
disabled={!!busy || !isDraft || (!title.selected && selected.length >= 24)}
onChange={(event) => changeTitle(title.id, "selected", event.target.checked)}
/>
<span>Include</span>
</label>
<label className="recap-checkbox">
<input
type="checkbox"
aria-label={`Feature ${title.title}`}
checked={title.featured}
disabled={
!!busy || !isDraft || !title.selected || (!title.featured && featured >= 3)
}
onChange={(event) => changeTitle(title.id, "featured", event.target.checked)}
/>
<span>Featured pick</span>
</label>
</div>
</article>
))}
</div>
) : (
<p>No new titles were found in this period. You can still create an announcement edition.</p>
)}
<div className="recap-actions">
{isDraft && (
<button type="submit" className="account-primary" disabled={!!busy || !dirty}>
{busy === "save" ? "Saving…" : "Save draft"}
</button>
)}
<button
type="button"
className="account-secondary"
disabled={!!busy}
onClick={() => void action("reload", `/editions/${edition.id}`, "GET", undefined, remember)}
>
{dirty ? "Discard changes" : "Reload edition"}
</button>
<button
type="button"
className="account-secondary"
disabled={!!busy || dirty || settingsDirty || edition.state === "cancelled"}
onClick={() =>
void action(
"preview",
`/editions/${edition.id}/preview`,
"POST",
{ revision: edition.revision },
(result: Preview) => {
setPreview(result);
setMode("html");
},
)
}
>
{busy === "preview" ? "Preparing preview…" : "Preview edition"}
</button>
</div>
</form>
<p className="recap-muted">
Each recipients email includes only titles available to their linked Jellyfin account. Posters
are included in the email.
</p>
{dirty && <p className="recap-muted">Save the draft to preview and send this version.</p>}
{settingsDirty && (
<p className="recap-muted">Save or reload your weekly settings before previewing or sending.</p>
)}
{validPreview && preview && (
<div className="recap-preview">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">Email preview</span>
<h3>{preview.subject}</h3>
<p>This shows the full selection. Your test uses your own library access.</p>
</div>
<div className="recap-mode-buttons">
<button type="button" aria-pressed={mode === "html"} onClick={() => setMode("html")}>
Email design
</button>
<button type="button" aria-pressed={mode === "text"} onClick={() => setMode("text")}>
Plain text
</button>
</div>
</div>
{mode === "html" ? (
<iframe
title="Newsletter email preview"
sandbox=""
referrerPolicy="no-referrer"
srcDoc={preview.body_html}
/>
) : (
<pre className="recap-plain-preview">{preview.body_text}</pre>
)}
</div>
)}
{edition.state !== "cancelled" && (
<div className="newsletter-send">
<h3>{isDraft ? "Ready for the inbox?" : "Delivery controls"}</h3>
<p>
A test goes to your own confirmed newsletter email.{" "}
<a href="/profile#newsletters">Manage your subscription </a>
</p>
<div className="recap-actions">
<button
type="button"
className="account-secondary"
disabled={!!busy || !validPreview || !hasContent || !data.ready || settingsDirty}
onClick={sendTest}
>
{busy === "test" ? "Queuing test…" : "Send newsletter test to me"}
</button>
</div>
{isDraft ? (
<>
<label className="recap-month-label" htmlFor="newsletter-send-time">
Schedule for (UTC)
<input
id="newsletter-send-time"
type="datetime-local"
value={sendAt}
disabled={!!busy}
onChange={(event) => setSendAt(event.target.value)}
/>
</label>
<div className="recap-actions">
<button
type="button"
className="account-primary"
disabled={
!!busy || !validPreview || !hasContent || !data.ready || settingsDirty || !sendAt
}
onClick={() => publish(true)}
>
Schedule edition
</button>
<button
type="button"
className="account-secondary"
disabled={
!!busy ||
!validPreview ||
!hasContent ||
!data.ready ||
settingsDirty ||
!data.subscribers
}
onClick={() => publish(false)}
>
Send now to {data.subscribers} {data.subscribers === 1 ? "subscriber" : "subscribers"}
</button>
</div>
<p className="recap-muted">
Preview the saved edition before sending. Scheduling fixes the content for this edition.
Users must be subscribed by its send time.
</p>
</>
) : (
<p>{dateLabel(edition.send_at)} · Check Delivery history for individual results.</p>
)}
{["draft", "scheduled", "queued"].includes(edition.state) && (
<button
type="button"
className="ghost-button newsletter-cancel"
disabled={!!busy || dirty}
onClick={() =>
void action("cancel", `/editions/${edition.id}/cancel`, "POST", {}, (row: Edition) => {
remember(row);
setNotice("Edition cancelled. Pending emails have been stopped.");
})
}
>
Cancel edition
</button>
)}
</div>
)}
</section>
)}
</>
)}
{tab === "schedule" && (
<section className="admin-panel recap-panel">
<span className="recap-eyebrow">Set the rhythm</span>
<h2>A weekly discovery</h2>
<p>
Automatically collect the previous seven days of arrivals and send an edition to confirmed
subscribers. Weeks without new arrivals are skipped.
</p>
<form
className="recap-schedule-form"
onSubmit={(event) => {
event.preventDefault();
void action("settings", "", "PUT", scheduleFields(settings), (result: Settings) => {
setSettings(result);
setData({ ...data, settings: result });
setPreview(null);
setNotice(
result.enabled
? `Weekly settings saved. Next edition ${dateLabel(result.next_send_at)}.`
: "Settings saved. Automatic weekly editions are paused.",
);
});
}}
>
<label htmlFor="newsletter-public-url">
Public Magent address
<input id="newsletter-public-url" type="url" value={settings.public_url} readOnly />
<small>
Inherited from <Link href="/admin/general">Hosting &amp; proxy</Link>. Watch links use the public
playback URL in <Link href="/admin/jellyfin">Jellyfin settings</Link>.
</small>
</label>
<div className="recap-schedule-fields">
<label htmlFor="newsletter-weekday">
Send day
<select
id="newsletter-weekday"
value={settings.weekday}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, weekday: Number(event.target.value) })}
>
{daysOfWeek.map((day, index) => (
<option value={index} key={day}>
{day}
</option>
))}
</select>
</label>
<label htmlFor="newsletter-hour">
Send time (UTC)
<select
id="newsletter-hour"
value={settings.hour}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, hour: Number(event.target.value) })}
>
{Array.from({ length: 24 }, (_, hour) => (
<option key={hour} value={hour}>
{String(hour).padStart(2, "0")}:00 UTC
</option>
))}
</select>
</label>
</div>
<label htmlFor="newsletter-limit">
Titles per weekly edition
<select
id="newsletter-limit"
value={settings.limit_titles}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, limit_titles: Number(event.target.value) })}
>
{Array.from({ length: 24 }, (_, index) => (
<option key={index + 1} value={index + 1}>
{index + 1}
</option>
))}
</select>
<small>Newest titles first. Multiple episodes count as one show.</small>
</label>
<label htmlFor="newsletter-default-intro">
Default announcement
<textarea
id="newsletter-default-intro"
rows={4}
maxLength={2000}
value={settings.intro}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, intro: event.target.value })}
/>
<small>Appears in future weekly editions and newly created drafts.</small>
</label>
<label className="recap-checkbox">
<input
type="checkbox"
checked={settings.enabled}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, enabled: event.target.checked })}
/>
<span>Enable automatic weekly newsletters</span>
</label>
<p className="recap-muted">
The schedule starts at the next future send time, in UTC. Pausing stops pending automatic editions.
Custom editions keep their individual schedules.
</p>
<div className="recap-actions">
<button type="submit" className="account-primary" disabled={!!busy || !settingsDirty}>
{busy === "settings" ? "Saving…" : "Save weekly settings"}
</button>
<button
type="button"
className="account-secondary"
disabled={!!busy}
onClick={() =>
void action("settings-reload", "", "GET", undefined, (result: Overview) => {
setData(result);
setSettings(result.settings);
setPreview(null);
})
}
>
Reload settings
</button>
</div>
</form>
{data.settings.last_error && <p className="recap-setup-note">{data.settings.last_error}</p>}
</section>
)}
{tab === "history" && (
<section className="admin-panel recap-panel">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">From queue to inbox</span>
<h2>Delivery history</h2>
<p>Server acceptance is recorded here. Inbox placement depends on your mail provider.</p>
</div>
<button
type="button"
className="ghost-button"
disabled={!!busy}
onClick={() => setRefresh((value) => value + 1)}
>
Refresh history
</button>
</div>
{data.deliveries.length ? (
<>
<div className="recap-history-scroll">
<table className="recap-history">
<thead>
<tr>
<th scope="col">Recipient</th>
<th scope="col">Edition</th>
<th scope="col">Delivery</th>
<th scope="col">Updated</th>
</tr>
</thead>
<tbody>
{data.deliveries.map((row) => (
<tr key={row.id}>
<td>
<strong>{row.username || "Removed account"}</strong>
<small>{row.email}</small>
</td>
<td>
{row.subject}
<small>{row.kind === "test" ? "Test email" : "Newsletter"}</small>
</td>
<td>
<span
className={`recap-pill ${row.state === "sent" ? "is-enabled" : ["failed", "unknown"].includes(row.state) ? "is-attention" : ""}`}
>
{labels[row.state] || row.state}
</span>
<small>
{row.attempts} {row.attempts === 1 ? "attempt" : "attempts"} ·{" "}
{row.detail || "Waiting for the next worker check."}
</small>
{row.state === "retry" && <small>Next attempt {dateLabel(row.next_attempt_at)}</small>}
{row.state === "unknown" && (
<small>Automatic retries are stopped to avoid a duplicate email.</small>
)}
</td>
<td>{dateLabel(row.updated_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="recap-pagination">
<span>
{offset + 1}{Math.min(offset + 50, data.total)} of {data.total}
</span>
<div className="recap-actions">
<button
type="button"
className="ghost-button"
disabled={!offset}
onClick={() => setOffset(Math.max(0, offset - 50))}
>
Previous
</button>
<button
type="button"
className="ghost-button"
disabled={offset + 50 >= data.total}
onClick={() => setOffset(offset + 50)}
>
Next
</button>
</div>
</div>
</>
) : (
<div className="recap-empty">
<span aria-hidden="true"></span>
<h3>Your first edition starts here</h3>
<p>Preview a draft and send yourself a test. Delivery results will appear here.</p>
</div>
)}
</section>
)}
</>
)}
</div>
</AdminShell>
);
}