From c9ab3839e491c93ea88315c6ab41557fcd891f0c Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Fri, 22 May 2026 13:17:38 +0200 Subject: [PATCH] refactor(admin): migrate emails.ts to emails.tsx with JSX rendering Convert feed emails list and single email view GET routes from hono/html tagged template literals to typed JSX. Extracts reusable CopyField and SVG icon components. Inline page scripts are preserved verbatim via dangerouslySetInnerHTML. Raw HTML display in single email view uses dangerouslySetInnerHTML to avoid double-escaping pre-escaped content. Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin/{emails.ts => emails.tsx} | 926 ++++++++------------- 1 file changed, 351 insertions(+), 575 deletions(-) rename src/routes/admin/{emails.ts => emails.tsx} (56%) diff --git a/src/routes/admin/emails.ts b/src/routes/admin/emails.tsx similarity index 56% rename from src/routes/admin/emails.ts rename to src/routes/admin/emails.tsx index 1d06084..6b81a58 100644 --- a/src/routes/admin/emails.ts +++ b/src/routes/admin/emails.tsx @@ -1,5 +1,4 @@ import { Hono } from "hono"; -import { html, raw } from "hono/html"; import { Env, FeedConfig, @@ -8,13 +7,69 @@ import { EmailMetadata, } from "../../types"; import { logger } from "../../lib/logger"; -import { layout, clampText } from "./ui"; +import { Layout, clampText } from "./ui"; import { deleteKeysWithConcurrency } from "./helpers"; type AppEnv = { Bindings: Env }; export const emailsRouter = new Hono(); +// ── Shared SVG icons ────────────────────────────────────────────────────────── + +const CopyIcon = () => ( + + + + +); + +const CheckIcon = () => ( + + + +); + +type CopyFieldProps = { + label: string; + value: string; + display?: string; +}; + +const CopyField = ({ label, value, display }: CopyFieldProps) => ( +
+ {label} +
+ + {display ?? value} + +
+ + +
+
+
+); + // ── View all emails for a feed ──────────────────────────────────────────────── emailsRouter.get("/feeds/:feedId/emails", async (c) => { @@ -35,327 +90,11 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => { return c.text("Feed not found", 404); } - return c.html( - layout( - `${feedConfig.title} - Emails`, - html` -
-
-
-

${feedConfig.title} - Emails

-
- -
+ const emailAddress = `${feedId}@${env.DOMAIN}`; + const rssUrl = `https://${env.DOMAIN}/rss/${feedId}`; -
-

Feed Details

-
-
- Email Address: -
- ${feedId}@${env.DOMAIN} -
- - - - - - - -
-
-
-
- RSS Feed: -
- https://${env.DOMAIN}/rss/${feedId} -
- - - - - - - -
-
-
-
-
- -

- Emails (${feedMetadata.emails.length}) -

- - ${message === "bulkDeleted" - ? html`
-

Deleted ${Number.isFinite(count) ? count : 0} email(s).

-
` - : ""} - ${message === "bulkDeleteNoop" - ? html`

No emails were selected.

` - : ""} - ${feedMetadata.emails.length > 0 - ? html` -
-
-
- - Showing ${feedMetadata.emails.length} - 0 selected - - - -
-
- -
- - - - - - - - - - - - - - - - - ${feedMetadata.emails.map((email: EmailMetadata) => { - const subjectDisplay = clampText(email.subject, 180); - const subjectHover = clampText(email.subject, 1000); - const sortSubject = subjectHover.toLowerCase(); - const sortReceivedAt = String(email.receivedAt); - const searchHaystack = clampText( - email.subject, - 320, - ).toLowerCase(); - return html` - - - - - - - `; - })} - -
- - - -
-
- -
-
- Actions -
-
-
-
- ` - : html`
-

- No emails received yet. Subscribe to newsletters using the - email address above. -

-
`} -
- - - `, - ), + `; + + return c.html( + +
+
+
+

{feedConfig.title} - Emails

+
+ +
+ +
+

Feed Details

+
+ + +
+
+ +

+ Emails ( + {feedMetadata.emails.length}) +

+ + {message === "bulkDeleted" && ( +
+

Deleted {Number.isFinite(count) ? count : 0} email(s).

+
+ )} + {message === "bulkDeleteNoop" && ( +
+

No emails were selected.

+
+ )} + {feedMetadata.emails.length > 0 ? ( +
+
+
+ + + Showing {feedMetadata.emails.length} + + + 0 selected + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + {feedMetadata.emails.map((email: EmailMetadata) => { + const subjectDisplay = clampText(email.subject, 180); + const subjectHover = clampText(email.subject, 1000); + const sortSubject = subjectHover.toLowerCase(); + const sortReceivedAt = String(email.receivedAt); + const searchHaystack = clampText( + email.subject, + 320, + ).toLowerCase(); + return ( + + + + + + + ); + })} + +
+ + + +
+
+ +
+
+ Actions +
+
+
+
+ ) : ( +
+

+ No emails received yet. Subscribe to newsletters using the email + address above. +

+
+ )} +
+ + - `, - ), + `; + + return c.html( + +
+
+
+

Email Content

+
+ +
+ +
+ + +
+ + +
+ + +
+
+ +