diff --git a/src/components/sections/RssFeed.svelte b/src/components/sections/RssFeed.svelte index ec83f80..f224f7d 100644 --- a/src/components/sections/RssFeed.svelte +++ b/src/components/sections/RssFeed.svelte @@ -14,17 +14,28 @@ let items = $state([]); let loading = $state(true); let error = $state(''); + const dateFormatter = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + const formatDate = (pubDate: string) => { + if (!pubDate) { + return 'Date unavailable'; + } + + const parsedDate = new Date(pubDate); + if (Number.isNaN(parsedDate.getTime())) { + return 'Date unavailable'; + } + + return dateFormatter.format(parsedDate); + }; const formattedItems = $derived( items.map((item) => ({ ...item, - formattedDate: item.pubDate - ? new Date(item.pubDate).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }) - : 'Date unavailable', + formattedDate: formatDate(item.pubDate), })) ); @@ -33,6 +44,7 @@ maxItems; let cancelled = false; + const abortController = new AbortController(); loading = true; error = ''; items = []; @@ -45,7 +57,7 @@ params.set('max', String(maxItems)); try { - const response = await fetch(`/api/rss?${params.toString()}`); + const response = await fetch(`/api/rss?${params.toString()}`, { signal: abortController.signal }); const payload = (await response.json()) as { items?: FeedItem[]; error?: string }; if (!response.ok) { @@ -56,6 +68,9 @@ items = payload.items ?? []; } } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + return; + } console.error('[RssFeed] Failed to load feed:', err); if (!cancelled) { error = err instanceof Error ? err.message : 'Failed to load feed.'; @@ -71,6 +86,7 @@ return () => { cancelled = true; + abortController.abort(); }; }); @@ -227,6 +243,14 @@ color: var(--color-cyan, #00d4ff); } + .feed-title-link:focus-visible, + .feed-read-more:focus-visible, + .view-all-link:focus-visible { + outline: 2px solid var(--color-cyan, #00d4ff); + outline-offset: 2px; + border-radius: 2px; + } + /* ── Description ────────────────────────────────────── */ .feed-desc { font-size: 0.875rem; diff --git a/src/components/sections/RssFeed.test.ts b/src/components/sections/RssFeed.test.ts index 07a4046..61623c3 100644 --- a/src/components/sections/RssFeed.test.ts +++ b/src/components/sections/RssFeed.test.ts @@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url'; const rssFeedPath = fileURLToPath(new URL('./RssFeed.svelte', import.meta.url)); const rssFeedSource = readFileSync(rssFeedPath, 'utf8'); +const feedCardBlockMatch = rssFeedSource.match(/\.feed-card \{[\s\S]*?\}/); +const feedCardBlock = feedCardBlockMatch?.[0] ?? ''; describe('RssFeed section', () => { it('uses BootLabel with "WHAT I THINK" label', () => { @@ -28,6 +30,7 @@ describe('RssFeed section', () => { }); it('keeps only local layout and transform styles on feed-card so shared Phosphor utilities own the border glow', () => { + expect(feedCardBlockMatch).not.toBeNull(); expect(rssFeedSource).toContain('.feed-card {'); expect(rssFeedSource).toContain('position: relative;'); expect(rssFeedSource).toContain('background: var(--color-card, #111827);'); @@ -35,9 +38,9 @@ describe('RssFeed section', () => { expect(rssFeedSource).toContain('transition: transform 0.2s ease;'); expect(rssFeedSource).toContain('.feed-card:hover {'); expect(rssFeedSource).toContain('transform: translateY(-3px);'); - expect(rssFeedSource).not.toContain('border:'); - expect(rssFeedSource).not.toContain('box-shadow:'); - expect(rssFeedSource).not.toContain('border-color:'); + expect(feedCardBlock).not.toContain('border:'); + expect(feedCardBlock).not.toContain('box-shadow:'); + expect(feedCardBlock).not.toContain('border-color:'); }); it('uses Svelte fade transition on feed item reveal', () => { @@ -47,7 +50,9 @@ describe('RssFeed section', () => { it('derives formatted date strings from raw pubDate', () => { expect(rssFeedSource).toContain('formattedDate'); - expect(rssFeedSource).toContain('toLocaleDateString'); + expect(rssFeedSource).toContain('new Intl.DateTimeFormat'); + expect(rssFeedSource).toContain('Number.isNaN(parsedDate.getTime())'); + expect(rssFeedSource).toContain('dateFormatter.format(parsedDate)'); expect(rssFeedSource).toContain("'Date unavailable'"); }); diff --git a/src/pages/index.astro b/src/pages/index.astro index 3c09c8d..b3e2825 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -31,7 +31,7 @@ const { runtime } = Astro.locals; - +