Skip to content

Commit 239441e

Browse files
committed
chore: Dashboard feed updates
1 parent 2f81fc0 commit 239441e

File tree

5 files changed

+103
-26
lines changed

5 files changed

+103
-26
lines changed

src/pages/dashboard/components/AddFeedModal.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type AddFeedModalProps = {
1010
export default function AddFeedModal({ opened, onClose, onAdd }: AddFeedModalProps) {
1111
const [feedUrl, setFeedUrl] = useState("");
1212
const [feedLabel, setFeedLabel] = useState("");
13+
const [feedUrlError, setFeedUrlError] = useState<string | null>(null);
1314

1415
function extractLabel(url: string): string | undefined {
1516
try {
@@ -22,14 +23,37 @@ export default function AddFeedModal({ opened, onClose, onAdd }: AddFeedModalPro
2223
}
2324
}
2425

26+
function validateFeedUrl(value: string): string | null {
27+
const trimmed = value.trim();
28+
if (!trimmed) return "Feed URL is required";
29+
try {
30+
const u = new URL(trimmed);
31+
if (u.protocol !== "http:" && u.protocol !== "https:") {
32+
return "Only http(s) URLs are supported";
33+
}
34+
const pathPlusQuery = `${u.pathname}${u.search}`.toLowerCase();
35+
const looksLikeFeed =
36+
/\.(xml|rss|atom)(?:$|[?#])/i.test(pathPlusQuery) || /(feed|rss|atom)/i.test(pathPlusQuery);
37+
if (!looksLikeFeed) {
38+
return "URL does not look like an RSS/Atom XML feed";
39+
}
40+
return null;
41+
} catch (_err) {
42+
return "Enter a valid URL";
43+
}
44+
}
45+
2546
function handleAdd() {
2647
const url = feedUrl.trim();
27-
if (!url) return;
48+
const validationError = validateFeedUrl(url);
49+
setFeedUrlError(validationError);
50+
if (validationError) return;
2851
let label = feedLabel.trim();
2952
if (!label) label = extractLabel(url) || "";
3053
onAdd(url, label || undefined);
3154
setFeedUrl("");
3255
setFeedLabel("");
56+
setFeedUrlError(null);
3357
onClose();
3458
}
3559

@@ -40,7 +64,12 @@ export default function AddFeedModal({ opened, onClose, onAdd }: AddFeedModalPro
4064
label="Feed URL"
4165
placeholder="https://example.com/feed.xml"
4266
value={feedUrl}
43-
onChange={e => setFeedUrl(e.currentTarget.value)}
67+
onChange={e => {
68+
const v = e.currentTarget.value;
69+
setFeedUrl(v);
70+
setFeedUrlError(v ? validateFeedUrl(v) : "Feed URL is required");
71+
}}
72+
error={feedUrlError || undefined}
4473
/>
4574
<TextInput
4675
label="Label (optional)"
@@ -49,7 +78,9 @@ export default function AddFeedModal({ opened, onClose, onAdd }: AddFeedModalPro
4978
onChange={e => setFeedLabel(e.currentTarget.value)}
5079
/>
5180
<Group justify="flex-end">
52-
<Button onClick={handleAdd}>Add</Button>
81+
<Button onClick={handleAdd} disabled={!!feedUrlError || !feedUrl.trim()}>
82+
Add
83+
</Button>
5384
</Group>
5485
</Stack>
5586
</Modal>

src/pages/dashboard/components/FeedItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function SourceIcon({ item }: { item: RssItem }) {
5353
export function FeedItem({ item, variant }: FeedItemProps) {
5454
if (variant === "card") {
5555
return (
56-
<Paper withBorder key={item.link} p="sm">
56+
<Paper withBorder key={item.link} p="sm" className={classes.feedCard}>
5757
<Group align="flex-start" gap={8} wrap="nowrap">
5858
<SourceIcon item={item} />
5959
<Stack gap={4} flex={1}>
@@ -62,7 +62,7 @@ export function FeedItem({ item, variant }: FeedItemProps) {
6262
className={classes.feedTitle}
6363
onClick={() => openExternal(item.link)}
6464
ta="left"
65-
fw={600}
65+
fw={500}
6666
size="sm"
6767
>
6868
{item.title}

src/pages/dashboard/components/RSSFeed.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,7 @@ export default function RSSFeed({
4949
>
5050
<Group justify="space-between" align="center">
5151
<Title order={4}>Developer news</Title>
52-
<Group gap={4}>
53-
<SegmentedControl
54-
size="xs"
55-
value={viewMode}
56-
onChange={v => setViewMode(v as any)}
57-
data={[
58-
{ label: "Card", value: "card" },
59-
{ label: "List", value: "list" },
60-
]}
61-
/>
52+
<Group gap={8}>
6253
<Button
6354
size="xs"
6455
variant="subtle"
@@ -68,9 +59,18 @@ export default function RSSFeed({
6859
>
6960
Refresh feeds
7061
</Button>
71-
<Button size="xs" leftSection={<BsPlus />} variant="light" onClick={onAddRequest}>
62+
<Button size="xs" leftSection={<BsPlus />} variant="subtle" onClick={onAddRequest}>
7263
Add feed
7364
</Button>
65+
<SegmentedControl
66+
size="xs"
67+
value={viewMode}
68+
onChange={v => setViewMode(v as any)}
69+
data={[
70+
{ label: "Card", value: "card" },
71+
{ label: "List", value: "list" },
72+
]}
73+
/>
7474
</Group>
7575
</Group>
7676

@@ -79,8 +79,10 @@ export default function RSSFeed({
7979
<Badge
8080
key={f.id}
8181
size="sm"
82-
opacity={f.enabled ? 1 : 0.8}
83-
variant={f.enabled ? "filled" : "light"}
82+
bg={f.enabled ? "var(--mantine-color-dark-6)" : "var(--mantine-color-dark-7)"}
83+
color={f.enabled ? "var(--mantine-color-gray-3)" : "var(--mantine-color-gray-5)"}
84+
opacity={f.enabled ? 0.8 : 0.5}
85+
variant="outline"
8486
onClick={() => onToggleFeed(f.id)}
8587
rightSection={
8688
<ActionIcon
@@ -100,7 +102,10 @@ export default function RSSFeed({
100102
×
101103
</ActionIcon>
102104
}
103-
style={{ cursor: "pointer" }}
105+
style={{
106+
cursor: "pointer",
107+
border: `1px solid ${f.enabled ? "var(--mantine-color-dark-4)" : "var(--mantine-color-dark-5)"}`,
108+
}}
104109
>
105110
{f.title || f.url}
106111
</Badge>

src/pages/dashboard/hooks/useRssFeeds.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,22 @@ type FeedItem = {
1919
const DEFAULT_FEEDS: Omit<Feed, "id" | "addedAt">[] = [
2020
{ url: "https://engineering.atspotify.com/feed", title: "Spotify", enabled: true },
2121
{ url: "https://medium.com/feed/better-programming", title: "Better Programming", enabled: true },
22-
{ url: "https://blog.cloudflare.com/rss/", title: "Cloudflare Blog", enabled: true },
2322
{ url: "https://overreacted.io/rss.xml", title: "Overreacted", enabled: true },
24-
{ url: "https://rss.beehiiv.com/feeds/ypr2bi0H9m.xml", title: "Hungry Minds", enabled: true },
23+
{ url: "https://rss.beehiiv.com/feeds/ypr2bi0H9m.xml", title: "Hungry Minds", enabled: false },
24+
{ url: "https://css-tricks.com/feed/", title: "CSS Tricks", enabled: true },
25+
{ url: "https://feeds2.feedburner.com/tympanus", title: "Codrops", enabled: true },
26+
{ url: "https://github.blog/feed/", title: "GitHub", enabled: true },
27+
{ url: "https://blog.codinghorror.com/rss/", title: "Coding Horror", enabled: true },
28+
{ url: "https://martinfowler.com/feed.atom", title: "Martin Fowler", enabled: true },
29+
{
30+
url: "https://www.tbray.org/ongoing/ongoing.atom",
31+
title: "ongoing by Tim Bray",
32+
enabled: false,
33+
},
2534
{
2635
url: "https://www.thecrazyprogrammer.com/category/programming/feed",
2736
title: "The Crazy Programmer",
28-
enabled: true,
37+
enabled: false,
2938
},
3039
];
3140

@@ -39,7 +48,12 @@ function toRelativeTime(dateStr: string): string {
3948
const hours = Math.floor(minutes / 60);
4049
if (hours < 24) return `${hours}h ago`;
4150
const days = Math.floor(hours / 24);
42-
return `${days}d ago`;
51+
52+
if (days < 30) return `${days}d ago`;
53+
const months = Math.floor(days / 30);
54+
if (months < 12) return `${months}mo ago`;
55+
const years = Math.floor(months / 12);
56+
return `${years}y ago`;
4357
}
4458

4559
export function useRssFeeds() {
@@ -196,9 +210,20 @@ export function useRssFeeds() {
196210
try {
197211
const enabledFeeds = feeds.filter(f => f.enabled);
198212
const results = await Promise.all(enabledFeeds.map(f => parseRss(f.url, force)));
199-
const flat = results
200-
.flat()
201-
.sort((a, b) => new Date(b.published).getTime() - new Date(a.published).getTime());
213+
// mixmatch same source entries, so that no entry with same source appears twice
214+
const mixed = results.flat().reduce(
215+
(acc, item) => {
216+
const key = `${item.source}-${item.title}-${item.link}`;
217+
if (!acc[key]) {
218+
acc[key] = item;
219+
}
220+
return acc;
221+
},
222+
{} as Record<string, FeedItem>
223+
);
224+
const flat = Object.values(mixed).sort(
225+
(a, b) => new Date(b.published).getTime() - new Date(a.published).getTime()
226+
);
202227
setItems(flat);
203228
} finally {
204229
setLoading(false);

src/pages/dashboard/styles.module.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@
8787
0 6px 12px rgba(0, 0, 0, 0.06);
8888
transform: translateY(-1px);
8989
}
90+
91+
.feedCard {
92+
transition:
93+
background-color 0.15s ease,
94+
border-color 0.15s ease,
95+
box-shadow 0.15s ease,
96+
transform 0.06s ease;
97+
}
98+
99+
.feedCard:hover {
100+
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
101+
box-shadow:
102+
0 1px 0 rgba(0, 0, 0, 0.03),
103+
0 6px 12px rgba(0, 0, 0, 0.06);
104+
transform: translateY(-1px);
105+
}

0 commit comments

Comments
 (0)