summaryrefslogtreecommitdiff
path: root/apps/web/app/(marketing)/page.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/(marketing)/page.tsx')
-rw-r--r--apps/web/app/(marketing)/page.tsx250
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 &mdash; 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>
+ )
+}