Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions src/components/sections/RssFeed.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,28 @@
let items = $state<FeedItem[]>([]);
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),
}))
Comment thread
jaypatrick marked this conversation as resolved.
);

Expand All @@ -33,6 +44,7 @@
maxItems;

let cancelled = false;
const abortController = new AbortController();
loading = true;
error = '';
items = [];
Expand All @@ -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) {
Expand All @@ -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.';
Expand All @@ -71,6 +86,7 @@

return () => {
cancelled = true;
abortController.abort();
};
});
</script>
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 9 additions & 4 deletions src/components/sections/RssFeed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? '';

Comment thread
jaypatrick marked this conversation as resolved.
describe('RssFeed section', () => {
it('uses BootLabel with "WHAT I THINK" label', () => {
Expand All @@ -28,16 +30,17 @@ 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);');
expect(rssFeedSource).toContain('overflow: hidden;');
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', () => {
Expand All @@ -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'");
});

Expand Down
2 changes: 1 addition & 1 deletion src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const { runtime } = Astro.locals;
<!-- Portfolio: case studies + clients -->
<Portfolio client:visible />

<RssFeed client:load feedUrl="https://blog.jaysonknight.com/feed/" maxItems={5} />
<RssFeed client:visible feedUrl="https://blog.jaysonknight.com/feed/" maxItems={5} />

<!-- Contact: form + phone + location -->
<Contact client:visible />
Expand Down
Loading