diff options
Diffstat (limited to 'apps/web/app/(marketing)/page.tsx')
| -rw-r--r-- | apps/web/app/(marketing)/page.tsx | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx new file mode 100644 index 0000000..534f252 --- /dev/null +++ b/apps/web/app/(marketing)/page.tsx @@ -0,0 +1,250 @@ +import Link from "next/link" +import { redirect } from "next/navigation" +import { createSupabaseServerClient } from "@/lib/supabase/server" +import { createSupabaseAdminClient } from "@/lib/supabase/admin" +import { sanitizeEntryContent } from "@/lib/sanitize" +import { InteractiveDemo } from "./_components/interactive-demo" +import { FeatureGrid } from "./_components/feature-grid" +import { PricingTable } from "./_components/pricing-table" +import type { ShowcaseEntry } from "./_components/showcase-types" + +export const revalidate = 300 + +interface ShowcaseEntryRow { + id: string + title: string | null + url: string | null + author: string | null + summary: string | null + content_html: string | null + image_url: string | null + published_at: string | null + enclosure_url: string | null + enclosure_type: string | null + feeds: { + title: string | null + url: string + feed_type: string | null + } +} + +const SHOWCASE_FEED_URLS = [ + "https://techcrunch.com/feed/", + "https://blog.cloudflare.com/rss/", + "https://hacks.mozilla.org/feed/", + "https://feeds.arstechnica.com/arstechnica/index", + "https://www.wired.com/feed/rss", +] + +async function fetchShowcaseEntries(): Promise<ShowcaseEntry[]> { + const adminClient = createSupabaseAdminClient() + + const { data: feedRows } = await adminClient + .from("feeds") + .select("id") + .in("url", SHOWCASE_FEED_URLS) + + const feedIdentifiers = (feedRows ?? []).map((row) => row.id) + + if (feedIdentifiers.length === 0) { + return FALLBACK_ENTRIES + } + + const entriesPerFeed = 5 + const perFeedResults = await Promise.all( + feedIdentifiers.map((feedIdentifier) => + adminClient + .from("entries") + .select( + "id, title, url, author, summary, content_html, image_url, published_at, enclosure_url, enclosure_type, feeds!inner(title, url, feed_type)" + ) + .is("owner_id", null) + .eq("feed_id", feedIdentifier) + .order("published_at", { ascending: false }) + .limit(entriesPerFeed) + ) + ) + + const combinedRows = perFeedResults.flatMap( + ({ data: rows }) => (rows as unknown as ShowcaseEntryRow[]) ?? [] + ) + + if (combinedRows.length === 0) { + return FALLBACK_ENTRIES + } + + combinedRows.sort( + (a, b) => + new Date(b.published_at ?? 0).getTime() - + new Date(a.published_at ?? 0).getTime() + ) + + return combinedRows.map((row) => ({ + entryIdentifier: row.id, + feedTitle: row.feeds.title ?? "untitled feed", + feedUrl: row.feeds.url, + feedType: row.feeds.feed_type, + entryTitle: row.title ?? "untitled", + entryUrl: row.url ?? "", + author: row.author, + summary: row.summary, + contentHtml: row.content_html + ? sanitizeEntryContent(row.content_html) + : null, + imageUrl: row.image_url, + publishedAt: row.published_at ?? new Date().toISOString(), + enclosureUrl: row.enclosure_url, + enclosureType: row.enclosure_type, + })) +} + +const FALLBACK_ENTRIES: ShowcaseEntry[] = [ + { + entryIdentifier: "fallback-1", + feedTitle: "TechCrunch", + feedUrl: "https://techcrunch.com/feed/", + feedType: null, + entryTitle: "The resurgence of rss in 2026", + entryUrl: "https://example.com", + author: "Sarah Chen", + summary: "RSS feeds are making a comeback as users seek alternatives to algorithmic timelines.", + contentHtml: "<p>RSS feeds are making a comeback as users seek alternatives to algorithmic timelines. More people are turning to chronological, ad-free reading experiences.</p>", + imageUrl: null, + publishedAt: new Date(Date.now() - 3600000).toISOString(), + enclosureUrl: null, + enclosureType: null, + }, + { + entryIdentifier: "fallback-2", + feedTitle: "The Cloudflare Blog", + feedUrl: "https://blog.cloudflare.com/rss/", + feedType: null, + entryTitle: "How we built a global edge caching layer", + entryUrl: "https://example.com", + author: "Cloudflare Engineering", + summary: "A deep dive into the architecture behind Cloudflare's edge caching infrastructure.", + contentHtml: "<p>A deep dive into the architecture behind Cloudflare's edge caching infrastructure. Learn how requests are routed, cached, and invalidated across hundreds of data centres worldwide.</p>", + imageUrl: null, + publishedAt: new Date(Date.now() - 7200000).toISOString(), + enclosureUrl: null, + enclosureType: null, + }, + { + entryIdentifier: "fallback-3", + feedTitle: "Mozilla Hacks", + feedUrl: "https://hacks.mozilla.org/feed/", + feedType: null, + entryTitle: "Exploring the future of web components", + entryUrl: "https://example.com", + author: "Mozilla", + summary: "Web components are evolving rapidly. Here's what's coming next for the open web platform.", + contentHtml: "<p>Web components are evolving rapidly. Here's what's coming next for the open web platform. From declarative shadow DOM to scoped custom element registries, the standards are maturing fast.</p>", + imageUrl: null, + publishedAt: new Date(Date.now() - 10800000).toISOString(), + enclosureUrl: null, + enclosureType: null, + }, + { + entryIdentifier: "fallback-4", + feedTitle: "Ars Technica", + feedUrl: "https://feeds.arstechnica.com/arstechnica/index", + feedType: null, + entryTitle: "Building a personal information diet", + entryUrl: "https://example.com", + author: "Jordan Lee", + summary: "How curating your own feeds leads to better focus and less information overload.", + contentHtml: "<p>How curating your own feeds leads to better focus and less information overload. The key is choosing sources deliberately rather than letting algorithms decide.</p>", + imageUrl: null, + publishedAt: new Date(Date.now() - 14400000).toISOString(), + enclosureUrl: null, + enclosureType: null, + }, + { + entryIdentifier: "fallback-5", + feedTitle: "Wired", + feedUrl: "https://www.wired.com/feed/rss", + feedType: null, + entryTitle: "The quiet revolution in personal information tools", + entryUrl: "https://example.com", + author: "Morgan Hayes", + summary: "A new wave of tools is helping people take control of their information diet.", + contentHtml: "<p>A new wave of tools is helping people take control of their information diet. From RSS readers to read-later apps, the focus is shifting back to user choice.</p>", + imageUrl: null, + publishedAt: new Date(Date.now() - 18000000).toISOString(), + enclosureUrl: null, + enclosureType: null, + }, +] + +export default async function LandingPage() { + const supabaseClient = await createSupabaseServerClient() + const { + data: { user }, + } = await supabaseClient.auth.getUser() + + if (user) redirect("/reader") + + const showcaseEntries = await fetchShowcaseEntries() + + return ( + <div className="min-h-screen"> + <header className="flex items-center justify-between border-b border-border px-6 py-3"> + <span className="text-text-primary">asa.news</span> + <Link + href="/sign-in" + className="text-text-secondary transition-colors hover:text-text-primary" + > + sign in + </Link> + </header> + + <section className="mx-auto max-w-4xl px-6 py-16 text-center"> + <h1 className="mb-3 text-xl text-text-primary">asa.news</h1> + <p className="mb-8 text-text-secondary"> + a fast, minimal rss reader for staying informed + </p> + <div className="flex items-center justify-center gap-4"> + <Link + href="/sign-up" + className="border border-border px-4 py-2 text-text-primary transition-colors hover:bg-background-tertiary" + > + sign up + </Link> + <Link + href="/sign-in" + className="px-4 py-2 text-text-secondary transition-colors hover:text-text-primary" + > + sign in + </Link> + </div> + </section> + + <section className="mx-auto max-w-6xl px-6 pb-16"> + <p className="mb-4 text-center text-text-dim"> + live preview — real entries from real feeds + </p> + <InteractiveDemo showcaseEntries={showcaseEntries} /> + </section> + + <section className="mx-auto max-w-4xl px-6 pb-16"> + <h2 className="mb-6 text-center text-text-primary">features</h2> + <FeatureGrid /> + </section> + + <section className="mx-auto max-w-4xl px-6 pb-16"> + <h2 className="mb-6 text-center text-text-primary">pricing</h2> + <PricingTable /> + </section> + + <section className="border-t border-border px-6 py-16 text-center"> + <p className="mb-6 text-text-secondary">start reading today</p> + <Link + href="/sign-up" + className="border border-border px-6 py-2 text-text-primary transition-colors hover:bg-background-tertiary" + > + sign up free + </Link> + </section> + </div> + ) +} |