273 lines
7 KiB
Text
273 lines
7 KiB
Text
---
|
|
import getAllFeedItems from "../services/feeds";
|
|
const start = performance.now();
|
|
function isRecent(date: number): boolean {
|
|
const now = Date.now();
|
|
const sixHours = 6 * 60 * 60 * 1000;
|
|
return now - date < sixHours;
|
|
}
|
|
const STATIC_CATEGORIES = {
|
|
All: "All",
|
|
Recent: "Recent",
|
|
};
|
|
|
|
const yazzyUrl = import.meta.env.YAZZY_URL;
|
|
const feedItems = await getAllFeedItems();
|
|
const parseTime = performance.now() - start;
|
|
const updateTime = new Date().toLocaleString();
|
|
|
|
const categories = [
|
|
STATIC_CATEGORIES.All,
|
|
STATIC_CATEGORIES.Recent,
|
|
...Array.from(
|
|
new Set(feedItems.contents.map((item) => item.category)),
|
|
).sort(),
|
|
];
|
|
const categoryCounts = categories.reduce((acc, category) => {
|
|
if (category === STATIC_CATEGORIES.All) {
|
|
acc[category] = feedItems.contents.length;
|
|
return acc;
|
|
}
|
|
if (category === STATIC_CATEGORIES.Recent) {
|
|
acc[category] = feedItems.contents.filter((item) =>
|
|
isRecent(item.pubIsoDate),
|
|
).length;
|
|
return acc;
|
|
}
|
|
acc[category] = feedItems.contents.filter(
|
|
(item) => item.category === category,
|
|
).length;
|
|
return acc;
|
|
}, {});
|
|
|
|
const accentColors = {
|
|
slate: {
|
|
bg: "bg-slate-200 dark:bg-slate-600",
|
|
bl: "border-l-slate-200 dark:border-l-slate-600",
|
|
},
|
|
stone: {
|
|
bg: "bg-stone-200 dark:bg-stone-600",
|
|
bl: "border-l-stone-200 dark:border-l-stone-600",
|
|
},
|
|
red: {
|
|
bg: "bg-red-200 dark:bg-red-600",
|
|
bl: "border-l-red-200 dark:border-l-red-600",
|
|
},
|
|
amber: {
|
|
bg: "bg-amber-200 dark:bg-amber-600",
|
|
bl: "border-l-amber-200 dark:border-l-amber-600",
|
|
},
|
|
lime: {
|
|
bg: "bg-lime-200 dark:bg-lime-600",
|
|
bl: "border-l-lime-200 dark:border-l-lime-600",
|
|
},
|
|
emerald: {
|
|
bg: "bg-emerald-200 dark:bg-emerald-600",
|
|
bl: "border-l-emerald-200 dark:border-l-emerald-600",
|
|
},
|
|
cyan: {
|
|
bg: "bg-cyan-200 dark:bg-cyan-600",
|
|
bl: "border-l-cyan-200 dark:border-l-cyan-600",
|
|
},
|
|
sky: {
|
|
bg: "bg-sky-200 dark:bg-sky-600",
|
|
bl: "border-l-sky-200 dark:border-l-sky-600",
|
|
},
|
|
indigo: {
|
|
bg: "bg-indigo-200 dark:bg-indigo-600",
|
|
bl: "border-l-indigo-200 dark:border-l-indigo-600",
|
|
},
|
|
fuchsia: {
|
|
bg: "bg-fuchsia-200 dark:bg-fuchsia-600",
|
|
bl: "border-l-fuchsia-200 dark:border-l-fuchsia-600",
|
|
},
|
|
pink: {
|
|
bg: "bg-pink-200 dark:bg-pink-600",
|
|
bl: "border-l-pink-200 dark:border-l-pink-600",
|
|
},
|
|
rose: {
|
|
bg: "bg-rose-200 dark:bg-rose-600",
|
|
bl: "border-l-rose-200 dark:border-l-rose-600",
|
|
},
|
|
};
|
|
|
|
function cssAttribute(str: string): string {
|
|
return str.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
}
|
|
|
|
function pickRandomAccentColor() {
|
|
const accentColorsKeys = Object.keys(accentColors);
|
|
const index = Math.floor(Math.random() * accentColorsKeys.length);
|
|
return accentColors[accentColorsKeys[index]];
|
|
}
|
|
|
|
const categoryColors = categories.reduce((acc, category) => {
|
|
acc[category] = pickRandomAccentColor();
|
|
return acc;
|
|
}, {});
|
|
|
|
const categoriesSelectorCss = categories
|
|
.filter((c) => c !== STATIC_CATEGORIES.All)
|
|
.map(
|
|
(c) => `
|
|
#category-picker:has(#${cssAttribute(c)}:checked) ~ main ul {
|
|
> .${cssAttribute(c)}-item {
|
|
display: block;
|
|
}
|
|
> *:not(.${cssAttribute(c)}-item) {
|
|
display: none;
|
|
}
|
|
}`,
|
|
)
|
|
.join("\n");
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en" class="h-full overflow-auto">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
|
<title>Carter's RSS Feeds</title>
|
|
<link
|
|
rel="icon"
|
|
href="/rss-reader/news-emoji.svg"
|
|
sizes="any"
|
|
type="image/svg+xml"
|
|
/>
|
|
<link rel="manifest" href="/rss-reader/manifest.json" />
|
|
<style set:html={categoriesSelectorCss}></style>
|
|
</head>
|
|
<body
|
|
class="font-system text-base md:grid grid-cols-12 auto-rows-min p-4 bg-orange-50 dark:bg-slate-900 dark:text-slate-300 text-stone-700 font-semibold"
|
|
>
|
|
<header
|
|
class="col-span-12 row-span-1 h-fit mb-4 flex flex-row gap-8 items-center overflow-auto"
|
|
>
|
|
<h1 class="">Carter's RSS Feeds</h1>
|
|
<hr class="flex-grow h-px border-0 border-b-2 border-b-stone-500" />
|
|
<ul class="flex flex-row gap-2">
|
|
{
|
|
feedItems.errors.length > 0 ? (
|
|
<li>{feedItems.errors.length} error(s)</li>
|
|
) : null
|
|
}
|
|
<li>
|
|
Took {(parseTime / 1000).toFixed(0)}s
|
|
</li>
|
|
<li>
|
|
Last updated at {updateTime}
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="https://github.com/carterworks/rss-reader"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="underline cursor-pointer">Github</a
|
|
>
|
|
</li>
|
|
</ul>
|
|
</header>
|
|
<nav
|
|
id="category-picker"
|
|
class="flex flex-row flex-wrap mb-2 md:flex-col gap-2 md:gap-1 col-span-2 md:sticky md:top-4 row-span-1 md:mr-4"
|
|
>
|
|
{
|
|
categories.map((category) => (
|
|
<div>
|
|
<input
|
|
type="radio"
|
|
id={cssAttribute(category)}
|
|
name="category"
|
|
class="hidden peer"
|
|
checked={category === STATIC_CATEGORIES.Recent}
|
|
/>
|
|
<label
|
|
class="cursor-pointer transition-all opacity-25 peer-checked:opacity-100 hover:opacity-100 flex flex-row gap-2 items-center"
|
|
for={cssAttribute(category)}
|
|
>
|
|
<div
|
|
class:list={[
|
|
"w-4 h-4 rounded-full",
|
|
categoryColors[category].bg,
|
|
]}
|
|
/>
|
|
<span class="peer-checked:underline">{category}</span>
|
|
<span class="ml-auto">{categoryCounts[category]}</span>
|
|
</label>
|
|
</div>
|
|
))
|
|
}
|
|
</nav>
|
|
<main class="col-span-10 row-span-11">
|
|
<ul class="flex flex-col gap-2">
|
|
{
|
|
feedItems.contents.map((item) => (
|
|
<li
|
|
class:list={[
|
|
`${cssAttribute(item.category)}-item`,
|
|
{
|
|
[`${cssAttribute(STATIC_CATEGORIES.Recent)}-item`]: isRecent(
|
|
item.pubIsoDate,
|
|
),
|
|
},
|
|
"w-full",
|
|
"border-l-8",
|
|
categoryColors[item.category].bl,
|
|
"hover:bg-orange-200 dark:hover:bg-slate-600",
|
|
"transition-all",
|
|
"active:bg-orange-300 dark:active:bg-slate-500",
|
|
"bg-orange-100 dark:bg-slate-700",
|
|
"py-1 px-2",
|
|
"rounded-xl",
|
|
]}
|
|
>
|
|
<a
|
|
href={item.link}
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
set:html={item.title ?? item.link}
|
|
class="underline block break-words"
|
|
/>
|
|
<div class="no-underline text-sm font-normal">
|
|
<span>{new Date(item.pubIsoDate).toDateString()}</span>
|
|
<span>|</span>
|
|
<span>{item.feedName}</span>
|
|
<span>|</span>
|
|
<span>{item.category}</span>
|
|
{Boolean(yazzyUrl) ? (
|
|
<>
|
|
<span>|</span>
|
|
<a
|
|
href={`${yazzyUrl}/${item.link}`}
|
|
class="underline"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
yazzy
|
|
</a>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
</main>
|
|
<footer
|
|
class="mt-4 md:mr-4 md:mt-0 text-sm row-end-13 row-span-1 col-span-2 overflow-auto max-h-20 opacity-25 hover:opacity-100 transition-opacity"
|
|
>
|
|
{
|
|
feedItems.errors.length === 0 ? (
|
|
<>No errors</>
|
|
) : (
|
|
<ul>
|
|
{feedItems.errors.map((error) => (
|
|
<li class="break-words mb-2">{error}</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|
|
</footer>
|
|
</body>
|
|
</html>
|