diff --git a/app/page.tsx b/app/page.tsx index a44ef7b..baa4424 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ import { headers } from "next/headers"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { ProductDescription } from "@/components/partials/product-description"; import { Button } from "@/components/ui/button"; import { auth } from "@/lib/auth"; @@ -14,15 +15,18 @@ export default async function Home() { } return ( -
-
-

📡 rundown

- -
-
+
+

📡

+

rundown

+ +

+ AI-powered RSS reader that helps you read less and learn more +

+ +
); } diff --git a/components/partials/app-sidebar.tsx b/components/partials/app-sidebar.tsx index 1598805..f9c53ae 100644 --- a/components/partials/app-sidebar.tsx +++ b/components/partials/app-sidebar.tsx @@ -1,4 +1,4 @@ -import { Home, Rss, Settings2 } from "lucide-react"; +import { Heart, Home, Rss, Settings2 } from "lucide-react"; import Link from "next/link"; import { Sidebar, @@ -10,8 +10,10 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar"; import { getUserId } from "@/lib/auth"; +import { cn } from "@/lib/utils"; import { ListUserFeed } from "@/server/queries/list-user-feed"; import { Button } from "../ui/button"; +import { Separator } from "../ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { SidebarFoldButton } from "./sidebar-fold-button"; import { SignOutButton } from "./sign-out-button"; @@ -19,14 +21,34 @@ import { SignOutButton } from "./sign-out-button"; export async function AppSidebar() { return ( - - - + + {process.env.NEXT_PUBLIC_SPONSOR && ( + <> + + + + )} +
+ + +
@@ -63,10 +85,6 @@ async function FeedItems() { const feeds = await ListUserFeed({ userId }); - if (feeds.length === 0) { - return
No feeds available
; - } - return ( {feeds.map((feed) => ( @@ -76,7 +94,12 @@ async function FeedItems() { ))} -
+
+ Add RSS URL
diff --git a/components/partials/auth-form.tsx b/components/partials/auth-form.tsx index 4b091a3..c0b43e0 100644 --- a/components/partials/auth-form.tsx +++ b/components/partials/auth-form.tsx @@ -37,7 +37,7 @@ export function AuthForm() { setIsLoading(true); }, onSuccess: () => { - router.push("/"); + router.push("/settings/summarize"); }, onError: (ctx) => { toast.error(ctx.error.message); diff --git a/components/partials/product-description.tsx b/components/partials/product-description.tsx new file mode 100644 index 0000000..630e2f1 --- /dev/null +++ b/components/partials/product-description.tsx @@ -0,0 +1,51 @@ +import { RenderedMarkdown } from "../shared/rendered-markdown"; + +const DESCRIPTION = ` +rundown crawls your subscribed RSS feeds every 15 minutes, detects new or updated articles, and generates AI-powered summaries. +You can customize the summary language and length, and receive notifications via Discord Webhook. + +[GitHub](https://github.com/howyi/rundown) + +--- + +## Key Features + +### RSS Feed Registration & Management + +* Register and manage multiple RSS feeds in one place. +* Simple UI for adding, editing, and deleting feeds. + +![RSS Feed Registration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fd4crqjtcgnflzv6o8o.png) + +### Update Detection + AI Summarization + +* Checks feeds every 15 minutes for new articles. +* Summarizes using **gpt-5-nano** with multi-language and adjustable length options. + +![AI Summarization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ogghc2n78ife7k0pdon.gif) + +### Timeline View + +* Browse summarized articles in chronological order. +* Easily access past articles. + +![Timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ra9g6ldpiq3dd7d6wtbq.png) + +### Discord Webhook Notifications + +* Get instant updates in your Discord channels. +* Ideal for teams and communities. + +![Discord Webhook Notifications](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1aae1g7ytkh5556mtoz9.png) + +### MCP Integration + +* Access feed and article data programmatically via the MCP server. +* Connect to \`rundown.sbox.studio/mcp\` using an API key generated from the settings page. + +![MCP Integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17450f6wwhtndbzfjs6s.png) +`; + +export function ProductDescription() { + return {DESCRIPTION}; +} diff --git a/components/partials/summertize-setting-form.tsx b/components/partials/summertize-setting-form.tsx index b1ca429..7d3a46a 100644 --- a/components/partials/summertize-setting-form.tsx +++ b/components/partials/summertize-setting-form.tsx @@ -10,7 +10,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { SummaryLengthOptions } from "@/lib/const"; -import type { Article } from "@/lib/types"; +import type { ArticleWithFeed } from "@/lib/types"; import { PreviewSummarizeAction, SaveSummarySettingAction, @@ -35,7 +35,7 @@ export function SummarizeSettingForm({ language: string; length: string; customInstructions: string; - articles: Article[]; + articles: ArticleWithFeed[]; }) { const [saved, setSaved] = useState({ language: initialLanguage, @@ -156,40 +156,36 @@ export function SummarizeSettingForm({ - + Select Preview Article - - Select an article from{" "} - - the week in react - {" "} - RSS - + Select an example article +
{articles.map((article) => (
- - {article.title} -
+

+ {article.feed.title} +

+
+ +

+ {article.title} +

+
- {/* */}
-
+
Preview Article:{" "} + + + + + GitHub + + + Source + + + diff --git a/server/queries/list-example-article.ts b/server/queries/list-example-article.ts index e135d2d..87f3056 100644 --- a/server/queries/list-example-article.ts +++ b/server/queries/list-example-article.ts @@ -2,49 +2,59 @@ import { db } from "@/database"; import type { ArticleWithFeed } from "@/lib/types"; import { AddFeed } from "../mutations/add-feed"; -const PREVIEW_RSS = "https://thisweekinreact.com/newsletter/rss.xml"; +const PREVIEW_RSS = [ + "https://thisweekinreact.com/newsletter/rss.xml", + "https://rss.nytimes.com/services/xml/rss/nyt/World.xml", + "https://www.reddit.com/r/nextjs.rss", +]; export async function ListExampleArticle(): Promise { - let feedRecord = await db.query.feed.findFirst({ - where: (feed, { eq }) => eq(feed.rssUrl, PREVIEW_RSS), - with: { - articles: { - orderBy: (article, { desc }) => desc(article.publishedAt), - limit: 10, - }, - }, - }); - if (!feedRecord) { - await AddFeed({ - url: PREVIEW_RSS, - }); - feedRecord = await db.query.feed.findFirst({ - where: (feed, { eq }) => eq(feed.rssUrl, PREVIEW_RSS), + const respones: ArticleWithFeed[] = []; + for (const rssUrl of PREVIEW_RSS) { + let feedRecord = await db.query.feed.findFirst({ + where: (feed, { eq }) => eq(feed.rssUrl, rssUrl), with: { articles: { orderBy: (article, { desc }) => desc(article.publishedAt), - limit: 10, + limit: 2, }, }, }); - } + if (!feedRecord) { + await AddFeed({ + url: rssUrl, + }); + feedRecord = await db.query.feed.findFirst({ + where: (feed, { eq }) => eq(feed.rssUrl, rssUrl), + with: { + articles: { + orderBy: (article, { desc }) => desc(article.publishedAt), + limit: 10, + }, + }, + }); + } - if (!feedRecord || feedRecord.articles.length === 0) { - return []; - } + if (!feedRecord || feedRecord.articles.length === 0) { + return []; + } - return feedRecord.articles.map((article) => ({ - id: article.id, - title: article.title || "", - url: article.url || "", - summary: "", - publishedAt: article.publishedAt, - feed: { - id: feedRecord.id, - title: feedRecord.title || "", - url: feedRecord.url || "", - rssUrl: feedRecord.rssUrl || "", - description: feedRecord.description || "", - }, - })); + respones.push( + ...feedRecord.articles.map((article) => ({ + id: article.id, + title: article.title || "", + url: article.url || "", + summary: "", + publishedAt: article.publishedAt, + feed: { + id: feedRecord.id, + title: feedRecord.title || "", + url: feedRecord.url || "", + rssUrl: feedRecord.rssUrl || "", + description: feedRecord.description || "", + }, + })), + ); + } + return respones; }