feat: add rss widget (#760)

Co-authored-by: SeDemal <demal.sebastien@bluewin.ch>
This commit is contained in:
Manuel
2024-07-27 18:11:29 +02:00
committed by GitHub
parent 4380aa9b3e
commit 15d9327d46
23 changed files with 528 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
.backgroundImage {
position: absolute;
width: 100%;
height: 100%;
filter: blur(5px);
transform: scaleX(-1.05) scaleZ(-1.05);
opacity: 0.25;
top: 0;
left: 0;
object-fit: cover;
}

View File

@@ -0,0 +1,74 @@
import React from "react";
import { Card, Flex, Group, Image, ScrollArea, Stack, Text } from "@mantine/core";
import { IconClock } from "@tabler/icons-react";
import dayjs from "dayjs";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss";
export default function RssFeed({ serverData, options }: WidgetComponentProps<"rssFeed">) {
if (serverData?.initialData === undefined) {
return null;
}
const entries = serverData.initialData
.filter((feedGroup) => feedGroup.feed.entries !== undefined)
.flatMap((feedGroup) => feedGroup.feed.entries)
.filter((entry) => entry !== undefined)
.sort((entryA, entryB) => {
if (!entryA.published || !entryB.published) {
return -1;
}
return new Date(entryB.published).getTime() - new Date(entryA.published).getTime();
})
.slice(0, options.maximumAmountPosts as number);
return (
<ScrollArea className="scroll-area-w100" w="100%" p="4cqmin">
<Stack w={"100%"} gap="4cqmin">
{entries.map((feedEntry) => (
<Card
key={feedEntry.id}
withBorder
component={"a"}
href={feedEntry.link}
radius="2.5cqmin"
target="_blank"
w="100%"
p="2.5cqmin"
>
{feedEntry.enclosure && (
<Image className={classes.backgroundImage} src={feedEntry.enclosure} alt="backdrop" />
)}
<Flex gap="2.5cqmin" direction="column" w="100%">
<Text fz="4cqmin" lh="5cqmin" lineClamp={2}>
{feedEntry.title}
</Text>
{feedEntry.description && (
<Text
className={feedEntry.description}
c="dimmed"
size="3.5cqmin"
lineClamp={options.textLinesClamp as number}
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
/>
)}
{feedEntry.published && <InfoDisplay date={dayjs(feedEntry.published).fromNow()} />}
</Flex>
</Card>
))}
</Stack>
</ScrollArea>
);
}
const InfoDisplay = ({ date }: { date: string }) => (
<Group gap="2.5cqmin">
<IconClock size="2.5cqmin" />
<Text size="2.5cqmin" c="dimmed" pt="1cqmin">
{date}
</Text>
</Group>
);

View File

@@ -0,0 +1,32 @@
import { IconRss } from "@tabler/icons-react";
import { z } from "@homarr/validation";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
/**
* Feed must conform to one of the following standards:
* - https://www.rssboard.org/rss-specification (https://web.resource.org/rss/1.0/spec)
* - https://datatracker.ietf.org/doc/html/rfc5023
* - https://www.jsonfeed.org/version/1.1/
*/
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("rssFeed", {
icon: IconRss,
options: optionsBuilder.from((factory) => ({
feedUrls: factory.multiText({
defaultValue: [],
validate: z.string().url(),
}),
textLinesClamp: factory.number({
defaultValue: 5,
validate: z.number().min(1).max(50),
}),
maximumAmountPosts: factory.number({
defaultValue: 100,
validate: z.number().min(1).max(9999),
}),
})),
})
.withServerData(() => import("./serverData"))
.withDynamicImport(() => import("./component"));

View File

@@ -0,0 +1,21 @@
"use server";
import { api } from "@homarr/api/server";
import type { WidgetProps } from "../definition";
export default async function getServerDataAsync({ itemId }: WidgetProps<"rssFeed">) {
if (!itemId) {
return {
initialData: undefined,
lastUpdatedAt: null,
};
}
const data = await api.widget.rssFeed.getFeeds({
itemId,
});
return {
initialData: data?.data,
lastUpdatedAt: data?.timestamp,
};
}