mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(admin): native feed chips + dashboard pill
Add NativeFeeds/NativeFeedChip components to admin/ui.tsx and a NativeFeedPill rendered in both list and table dashboard views when feed.hasNativeFeed is set. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1538,6 +1538,38 @@ describe("Admin Routes", () => {
|
|||||||
expect(await editPage.text()).toContain("checked");
|
expect(await editPage.text()).toContain("checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dashboard shows pill-native for feeds with hasNativeFeed", async () => {
|
||||||
|
const authCookie = await loginAndGetCookie();
|
||||||
|
const repo = FeedRepository.from(mockEnv as unknown as Env);
|
||||||
|
const feedId = FeedId.generate();
|
||||||
|
const mailboxId = MailboxId.unchecked("native.pill.08");
|
||||||
|
const feed = Feed.create(
|
||||||
|
feedId,
|
||||||
|
{
|
||||||
|
title: "N",
|
||||||
|
language: "en",
|
||||||
|
allowedSenders: [],
|
||||||
|
blockedSenders: [],
|
||||||
|
},
|
||||||
|
{ mailboxId },
|
||||||
|
);
|
||||||
|
feed.ingest(
|
||||||
|
{ key: "k1", subject: "s", receivedAt: 1, size: 10 },
|
||||||
|
{
|
||||||
|
maxBytes: 1e9,
|
||||||
|
nativeFeeds: {
|
||||||
|
senderKey: "a@x.com",
|
||||||
|
feeds: [{ url: "https://x.com/rss", type: "rss" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await repo.save(feed);
|
||||||
|
|
||||||
|
const res = await request("/admin", { headers: { Cookie: authCookie } });
|
||||||
|
const body = await res.text();
|
||||||
|
expect(body).toContain("pill-native");
|
||||||
|
});
|
||||||
|
|
||||||
it("clears the toggle when the checkbox is omitted (unchecked)", async () => {
|
it("clears the toggle when the checkbox is omitted (unchecked)", async () => {
|
||||||
const authCookie = await loginAndGetCookie();
|
const authCookie = await loginAndGetCookie();
|
||||||
const repo = FeedRepository.from(mockEnv as unknown as Env);
|
const repo = FeedRepository.from(mockEnv as unknown as Env);
|
||||||
|
|||||||
@@ -229,6 +229,12 @@ const ConfirmationPill = ({ feedId }: { feedId: string }) => (
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const NativeFeedPill = ({ feedId }: { feedId: string }) => (
|
||||||
|
<a class="pill pill-native" href={`/admin/feeds/${feedId}/emails`}>
|
||||||
|
Native feed available
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
// Admin dashboard route
|
// Admin dashboard route
|
||||||
app.get("/", async (c) => {
|
app.get("/", async (c) => {
|
||||||
// Type assertion for environment variables
|
// Type assertion for environment variables
|
||||||
@@ -639,6 +645,9 @@ app.get("/", async (c) => {
|
|||||||
{feed.pendingConfirmation && (
|
{feed.pendingConfirmation && (
|
||||||
<ConfirmationPill feedId={feed.id} />
|
<ConfirmationPill feedId={feed.id} />
|
||||||
)}
|
)}
|
||||||
|
{feed.hasNativeFeed && (
|
||||||
|
<NativeFeedPill feedId={feed.id} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -763,6 +772,9 @@ app.get("/", async (c) => {
|
|||||||
{feed.pendingConfirmation && (
|
{feed.pendingConfirmation && (
|
||||||
<ConfirmationPill feedId={feed.id} />
|
<ConfirmationPill feedId={feed.id} />
|
||||||
)}
|
)}
|
||||||
|
{feed.hasNativeFeed && (
|
||||||
|
<NativeFeedPill feedId={feed.id} />
|
||||||
|
)}
|
||||||
{feed.description && (
|
{feed.description && (
|
||||||
<p class="feed-description">
|
<p class="feed-description">
|
||||||
<span title={descHover}>{descDisplay}</span>
|
<span title={descHover}>{descDisplay}</span>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { interactiveScripts } from "../../scripts/index";
|
|||||||
import { APP_VERSION } from "../../config/version";
|
import { APP_VERSION } from "../../config/version";
|
||||||
import { FAVICON_PATH } from "../favicon";
|
import { FAVICON_PATH } from "../favicon";
|
||||||
import { Env } from "../../types";
|
import { Env } from "../../types";
|
||||||
|
import type { NativeFeed } from "../../types";
|
||||||
import {
|
import {
|
||||||
feedFormatUrl,
|
feedFormatUrl,
|
||||||
feedValidatorUrl,
|
feedValidatorUrl,
|
||||||
@@ -254,6 +255,62 @@ export const FeedFormats = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Native feed chips ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const NATIVE_LABELS: Record<NativeFeed["type"], string> = {
|
||||||
|
rss: "RSS",
|
||||||
|
atom: "Atom",
|
||||||
|
json: "JSON",
|
||||||
|
};
|
||||||
|
|
||||||
|
const NativeFeedChip = ({ feed }: { feed: NativeFeed }) => {
|
||||||
|
const label = NATIVE_LABELS[feed.type];
|
||||||
|
return (
|
||||||
|
<div class="format-chip" data-format={feed.type}>
|
||||||
|
<span class="format-chip-label">{label}</span>
|
||||||
|
<span class="format-chip-actions">
|
||||||
|
<span class="copyable copyable-chip">
|
||||||
|
<span
|
||||||
|
class="copyable-content"
|
||||||
|
title={`Copy native ${label} feed URL`}
|
||||||
|
aria-label={`Copy native ${label} feed URL`}
|
||||||
|
>
|
||||||
|
<span class="copyable-value" data-copy={feed.url} hidden></span>
|
||||||
|
<span class="copy-icon-container">
|
||||||
|
<CopyIcon />
|
||||||
|
<CheckIcon />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
class="chip-action"
|
||||||
|
href={feed.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={`Open native ${label} feed in a new tab`}
|
||||||
|
aria-label={`Open native ${label} feed in a new tab`}
|
||||||
|
>
|
||||||
|
<OpenIcon />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NativeFeeds = ({ feeds }: { feeds: NativeFeed[] }) => {
|
||||||
|
if (feeds.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div class="feed-formats native-feeds">
|
||||||
|
<span class="feed-formats-label">Native feeds</span>
|
||||||
|
<div class="feed-formats-chips">
|
||||||
|
{feeds.map((feed) => (
|
||||||
|
<NativeFeedChip feed={feed} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ── Expiry pill ───────────────────────────────────────────────────────────────
|
// ── Expiry pill ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatExpiry(expiresAt: number): { label: string; expired: boolean } {
|
function formatExpiry(expiresAt: number): { label: string; expired: boolean } {
|
||||||
|
|||||||
Reference in New Issue
Block a user