From e57572dcf6b5616ec0a63abb95ce509d7f330307 Mon Sep 17 00:00:00 2001 From: v0 Date: Sat, 25 Apr 2026 10:01:31 +0000 Subject: [PATCH 1/2] redesign Biviant web app with new visual hierarchy and design system Header, Landing, Feed, Event Detail, Dashboard, Auth, Bookmarks, and other components updated Co-authored-by: Flavius Cojocaru <102308258+flvvius@users.noreply.github.com> --- apps/web/src/components/bias-indicator.tsx | 50 ++- apps/web/src/components/bookmark-button.tsx | 20 +- .../web/src/components/feed/articles-list.tsx | 71 +-- apps/web/src/components/feed/event-card.tsx | 180 ++++---- apps/web/src/components/header.tsx | 131 ++++-- apps/web/src/components/sign-in-form.tsx | 256 ++++++----- apps/web/src/components/sign-up-form.tsx | 300 +++++++------ apps/web/src/components/user-menu.tsx | 51 ++- apps/web/src/routes/__root.tsx | 14 +- apps/web/src/routes/bookmarks.tsx | 99 ++-- apps/web/src/routes/dashboard.tsx | 194 +++++++- apps/web/src/routes/event.$slug.tsx | 283 +++++++----- apps/web/src/routes/feed.tsx | 178 +++++--- apps/web/src/routes/index.tsx | 422 ++++++++++++------ apps/web/src/routes/unsubscribe.tsx | 106 +++-- 15 files changed, 1535 insertions(+), 820 deletions(-) diff --git a/apps/web/src/components/bias-indicator.tsx b/apps/web/src/components/bias-indicator.tsx index 6e5ba00..7ada191 100644 --- a/apps/web/src/components/bias-indicator.tsx +++ b/apps/web/src/components/bias-indicator.tsx @@ -15,7 +15,6 @@ function validateThresholds(raw: unknown): number[] { ) { return DEFAULT_THRESHOLDS; } - // Ensure ascending order for the first 4 values const sorted = (raw.slice(0, 4) as number[]).sort((a, b) => a - b); return sorted; } @@ -48,29 +47,60 @@ const BiasIndicator = ({ return "Right"; }; + // Determine text color + const getTextColor = (biasValue: number) => { + if (biasValue < thresholds[0]) return "text-bias-left"; + if (biasValue < thresholds[1]) return "text-bias-left-muted"; + if (biasValue <= thresholds[2]) return "text-bias-center"; + if (biasValue <= thresholds[3]) return "text-bias-right-muted"; + return "text-bias-right"; + }; + const sizeClasses = { - sm: "h-1.5 w-16", - md: "h-2 w-24", - lg: "h-3 w-32", + sm: "h-1.5 w-20", + md: "h-2 w-28", + lg: "h-2.5 w-36", }; const dotSizeClasses = { - sm: "h-2.5 w-2.5", - md: "h-3 w-3", - lg: "h-4 w-4", + sm: "size-3", + md: "size-4", + lg: "size-5", + }; + + const textSizeClasses = { + sm: "text-xs", + md: "text-xs", + lg: "text-sm", }; return (
+ {/* Gradient track for visual interest */} +
+
+
+ + {/* Indicator dot */}
- + {getLabel(bias)}
diff --git a/apps/web/src/components/bookmark-button.tsx b/apps/web/src/components/bookmark-button.tsx index 5681455..c21723e 100644 --- a/apps/web/src/components/bookmark-button.tsx +++ b/apps/web/src/components/bookmark-button.tsx @@ -84,19 +84,35 @@ export default function BookmarkButton({ ); const bookmarked = isBookmarked === true; + const iconSize = size === "sm" ? "size-4" : "size-5"; + const buttonSize = size === "sm" ? "size-8" : "size-9"; return ( ); } diff --git a/apps/web/src/components/feed/articles-list.tsx b/apps/web/src/components/feed/articles-list.tsx index 2a93f9d..4e45fc3 100644 --- a/apps/web/src/components/feed/articles-list.tsx +++ b/apps/web/src/components/feed/articles-list.tsx @@ -3,6 +3,7 @@ import { useQuery } from "convex/react"; import { api } from "@news-app/backend/convex/_generated/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import BiasIndicator from "@/components/bias-indicator"; +import { ExternalLink, Newspaper } from "lucide-react"; type Article = { _id: Id<"articles">; @@ -23,7 +24,6 @@ type ArticlesListProps = { }; const ArticlesList = ({ articles }: ArticlesListProps) => { - // Single subscription for the whole list — passed down to each BiasIndicator const thresholdsConfig = useQuery(api.config.get, { key: "bias_thresholds", }); @@ -31,20 +31,35 @@ const ArticlesList = ({ articles }: ArticlesListProps) => { (thresholdsConfig?.value as number[] | undefined) ?? undefined; return ( - - - Articles ({articles.length}) + + +
+
+ +
+
+ + Articles{" "} + + ({articles.length}) + + +

+ Coverage from multiple sources +

+
+
- -
+ +
{articles.map((article) => ( -
{/* Source logo */} {article.source?.logoUrl && ( -
+
{article.source.name} {
{/* Article title */} -

+

{article.title}

- {/* Source name and bias */} -
+ {/* Source name, bias, and date */} +
{article.source && ( <> - + {article.source.name} { )} - {new Date(article.publishedAt).toLocaleDateString()} + {new Date(article.publishedAt).toLocaleDateString( + undefined, + { + month: "short", + day: "numeric", + year: "numeric", + } + )}
{/* Article summary */} {article.summary && ( -

+

{article.summary}

)} @@ -90,26 +112,13 @@ const ArticlesList = ({ articles }: ArticlesListProps) => { href={article.canonicalUrl} target="_blank" rel="noopener noreferrer" - className="text-sm text-primary hover:underline inline-flex items-center gap-1" + className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline group" > Read original - +
-
+ ))}
diff --git a/apps/web/src/components/feed/event-card.tsx b/apps/web/src/components/feed/event-card.tsx index 4eda0b3..e0ba374 100644 --- a/apps/web/src/components/feed/event-card.tsx +++ b/apps/web/src/components/feed/event-card.tsx @@ -1,8 +1,8 @@ import type { Id } from "@news-app/backend/convex/_generated/dataModel"; import { Link } from "@tanstack/react-router"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import BookmarkButton from "@/components/bookmark-button"; +import { ArrowUpRight, Newspaper } from "lucide-react"; type EventCardProps = { event: { @@ -34,92 +34,114 @@ const EventCard = ({ }: EventCardProps) => { const topics = event.topicIds.map((id) => topicNamesById[id]).filter(Boolean); const primaryTopic = topics[0] ?? "General"; + const remainingSources = event.sources + ? Math.max(0, event.sources.length - maxSources) + : 0; return ( - - -
- {event.imageUrl ? ( -
- {event.title} -
- ) : ( -
- - {primaryTopic} - -
- )} -
- -
- {topics.length > 0 ? ( - topics.map((topic) => ( - - )) + + +
+ {/* Image Section */} +
+ {event.imageUrl ? ( + <> + {event.title} +
+ ) : ( - +
+ + + {primaryTopic} + +
)} -
- {event.title} - - -

- {event.perspectiveSummaries.center} -

- -
-
- {event.sources && event.sources.length > 0 && ( -
- {event.sources.slice(0, maxSources).map((source) => ( -
- {source.name} -
- ))} -
- )} -
-
- {event.articleCount !== undefined && ( - - {event.articleCount}{" "} - {event.articleCount === 1 ? "article" : "articles"} + {/* Topic badges - positioned on image */} +
+ {topics.length > 0 ? ( + topics.slice(0, 2).map((topic) => ( + + {topic} + + )) + ) : ( + + General )} -
- + + {/* Content Section */} + +
+ {/* Title */} +

+ {event.title} +

+ + {/* Summary */} +

+ {event.perspectiveSummaries.center} +

+
+ + {/* Footer */} +
+ {/* Sources */} +
+ {event.sources && event.sources.length > 0 && ( +
+
+ {event.sources.slice(0, maxSources).map((source) => ( +
+ {source.name} +
+ ))} +
+ {remainingSources > 0 && ( + + +{remainingSources} + + )} +
+ )} +
+ + {/* Actions */} +
+ {event.articleCount !== undefined && ( + + {event.articleCount}{" "} + {event.articleCount === 1 ? "article" : "articles"} + + )} + +
+ +
+
+
+
+
); diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 01e6325..b044351 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { Link } from "@tanstack/react-router"; -import { Menu } from "lucide-react"; +import { Link, useRouterState } from "@tanstack/react-router"; +import { Menu, Newspaper, Bookmark, LayoutDashboard, Home } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Sheet, @@ -10,65 +10,102 @@ import { } from "@/components/ui/sheet"; const links = [ - { to: "/", label: "Home" }, - { to: "/feed", label: "Feed" }, - { to: "/bookmarks", label: "Bookmarks" }, - { to: "/dashboard", label: "Dashboard" }, + { to: "/", label: "Home", icon: Home }, + { to: "/feed", label: "Feed", icon: Newspaper }, + { to: "/bookmarks", label: "Bookmarks", icon: Bookmark }, + { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, ] as const; export default function Header() { const [open, setOpen] = useState(false); + const routerState = useRouterState(); + const currentPath = routerState.location.pathname; return ( -
-
- - Biviant - +
+
+
+ {/* Logo */} + +
+ B +
+
+ Biviant + - {/* Desktop nav */} - - - {/* Mobile burger */} - - - - - - Menu - + + {/* Mobile burger */} + + + + + +
+
+ +
+ B +
+ Biviant +
+
+ +
+
+
+
-
); } diff --git a/apps/web/src/components/sign-in-form.tsx b/apps/web/src/components/sign-in-form.tsx index a215218..8c79452 100644 --- a/apps/web/src/components/sign-in-form.tsx +++ b/apps/web/src/components/sign-in-form.tsx @@ -6,6 +6,8 @@ import z from "zod"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Loader2, Mail } from "lucide-react"; export default function SignInForm({ onSwitchToSignUp, @@ -35,7 +37,7 @@ export default function SignInForm({ onError: (error) => { toast.error(error.error.message || error.error.statusText); }, - }, + } ); }, validators: { @@ -47,128 +49,152 @@ export default function SignInForm({ }); return ( -
-

Welcome Back

- -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-4" - > -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error, i) => ( -

- {error?.message} -

- ))} -
- )} -
+ + +
+
+ Welcome Back +

+ Sign in to your Biviant account +

+
-
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error, i) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + className="h-11" + /> + {field.state.meta.errors.map((error, i) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
- - {(state) => ( - - )} - - +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + className="h-11" + /> + {field.state.meta.errors.map((error, i) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
-
-
- -
-
- - Or continue with - -
-
+ + {(state) => ( + + )} + + - +
+
+ +
+
+ + Or continue with + +
+
-
-
-
+ +
+

+ Need an account?{" "} + +

+
+ + ); } diff --git a/apps/web/src/components/sign-up-form.tsx b/apps/web/src/components/sign-up-form.tsx index 6a9bda8..f7530a7 100644 --- a/apps/web/src/components/sign-up-form.tsx +++ b/apps/web/src/components/sign-up-form.tsx @@ -6,6 +6,8 @@ import z from "zod"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Loader2, UserPlus } from "lucide-react"; export default function SignUpForm({ onSwitchToSignIn, @@ -37,7 +39,7 @@ export default function SignUpForm({ onError: (error) => { toast.error(error.error.message || error.error.statusText); }, - }, + } ); }, validators: { @@ -50,150 +52,176 @@ export default function SignUpForm({ }); return ( -
-

Create Account

- -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-4" - > -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error, i) => ( -

- {error?.message} -

- ))} -
- )} -
+ + +
+
+ Create Account +

+ Join Biviant and see the whole story +

+
-
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error, i) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + className="h-11" + /> + {field.state.meta.errors.map((error, i) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
-
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error, i) => ( -

- {error?.message} -

- ))} -
- )} -
-
+
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + className="h-11" + /> + {field.state.meta.errors.map((error, i) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
- - {(state) => ( - - )} - - +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + className="h-11" + /> + {field.state.meta.errors.map((error, i) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
-
-
- -
-
- - Or continue with - -
-
+ + {(state) => ( + + )} + + - +
+
+ +
+
+ + Or continue with + +
+
-
-
-
+ +
+

+ Already have an account?{" "} + +

+
+ + ); } diff --git a/apps/web/src/components/user-menu.tsx b/apps/web/src/components/user-menu.tsx index ae812d3..f4e639f 100644 --- a/apps/web/src/components/user-menu.tsx +++ b/apps/web/src/components/user-menu.tsx @@ -10,6 +10,7 @@ import { authClient } from "@/lib/auth-client"; import { Button } from "./ui/button"; import { useQuery } from "convex/react"; import { api } from "@news-app/backend/convex/_generated/api"; +import { ChevronDown, LogOut, User } from "lucide-react"; export default function UserMenu() { const user = useQuery(api.user.getCurrentUser); @@ -24,25 +25,49 @@ export default function UserMenu() { }); }; + const userName = user?.profile?.name || user?.email || "Account"; + const userInitial = userName.charAt(0).toUpperCase(); + return ( - - - My Account + + +
+

{userName}

+ {user?.email && ( +

+ {user.email} +

+ )} +
+
+ + + + Profile (Coming soon) + - {user?.email} - - + + + Sign Out
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 20f3a89..f900d4c 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -86,17 +86,15 @@ function RootDocument() { authClient={authClient} initialToken={context.token} > - + - -
-
-
- -
-
+ +
+
+ +
{import.meta.env.DEV && ( diff --git a/apps/web/src/routes/bookmarks.tsx b/apps/web/src/routes/bookmarks.tsx index 666f294..b983d34 100644 --- a/apps/web/src/routes/bookmarks.tsx +++ b/apps/web/src/routes/bookmarks.tsx @@ -10,7 +10,7 @@ import { import EventCard from "@/components/feed/event-card"; import { Button } from "@/components/ui/button"; import { SITE } from "@/lib/seo"; -import { Bookmark } from "lucide-react"; +import { Bookmark, Loader2, LogIn, Newspaper } from "lucide-react"; /** Validate and clamp a config value to a non-negative integer. */ function safePositiveInt(raw: unknown, fallback: number): number { @@ -35,23 +35,38 @@ function BookmarksPage() { -
- -

- Sign in to see your bookmarks -

-

- Bookmark events to read later from any device. -

- - - +
+
+
+
+ +
+
+ +
+
+
+

+ Sign in to see your bookmarks +

+

+ Bookmark events to read later from any device. Your bookmarks + sync across all your sessions. +

+
+ + + +
-
-
- Loading… +
+
+ +

Loading...

@@ -77,34 +92,56 @@ function BookmarksContent() { if (bookmarks === undefined) { return ( -
-
- Loading bookmarks… +
+
+ +

+ Loading bookmarks... +

); } return ( -
-
-
-

Bookmarks

-

- Events you saved for later. -

-
+
+ {/* Page Header */} +
+
+
+
+ +
+
+

Bookmarks

+

Events you saved for later

+
+
+
+
+ {/* Content */} +
{bookmarks.length === 0 ? ( -
- -

No bookmarks yet

+
+
+ +
+
+

No bookmarks yet

+

+ When you find an interesting story, bookmark it to read later. + Your bookmarks will appear here. +

+
- +
) : ( -
+
{bookmarks.map((event) => ( ({ @@ -30,15 +40,22 @@ function RouteComponent() { - {showSignIn ? ( - setShowSignIn(false)} /> - ) : ( - setShowSignIn(true)} /> - )} +
+
+ {showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + )} +
+
-
-
Loading...
+
+
+ +

Loading...

+
@@ -51,24 +68,159 @@ function AuthenticatedDashboard() { if (currentUser === undefined) { return ( -
-
Loading...
+
+
+ +

Loading...

+
); } + const userName = currentUser?.profile?.name || currentUser?.email || "User"; + const userEmail = currentUser?.email; + return ( -
-

Dashboard

- {currentUser && ( -

- Welcome, {currentUser.profile?.name || currentUser.email}! -

- )} -

- privateData: {privateData?.message} -

- +
+ {/* Page Header */} +
+
+
+
+
+ {userName.charAt(0).toUpperCase()} +
+
+

+ Welcome back, {userName.split(" ")[0]} +

+

{userEmail}

+
+
+ +
+
+
+ + {/* Dashboard Content */} +
+
+ {/* Quick Stats */} + + + + Reading Streak + + + + +
0 days
+

+ Start reading to build your streak +

+
+
+ + + + + Bookmarks + + + + +
0
+

+ Saved for later +

+
+
+ + + + + Articles Read + + + + +
0
+

This week

+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + +
+ +
+
+

Browse Feed

+

+ See today's stories +

+
+
+
+ + + + + +
+ +
+
+

Bookmarks

+

+ Saved stories +

+
+
+
+ + + + +
+ +
+
+

Profile

+

Coming soon

+
+
+
+ + + +
+ +
+
+

Settings

+

Coming soon

+
+
+
+
+
+ + {/* Debug info - only in development */} + {privateData?.message && ( +
+

+ Debug: {privateData.message} +

+
+ )} +
); } diff --git a/apps/web/src/routes/event.$slug.tsx b/apps/web/src/routes/event.$slug.tsx index af46e16..4e9e7f8 100644 --- a/apps/web/src/routes/event.$slug.tsx +++ b/apps/web/src/routes/event.$slug.tsx @@ -7,11 +7,16 @@ import { Button } from "@/components/ui/button"; import ArticlesList from "@/components/feed/articles-list"; import BookmarkButton from "@/components/bookmark-button"; import { SITE } from "@/lib/seo"; +import { + ArrowLeft, + Globe, + Loader2, + MessageSquare, + Newspaper, +} from "lucide-react"; export const Route = createFileRoute("/event/$slug")({ loader: async ({ context, params }) => { - // Fetch event data server-side so head() can set dynamic meta tags for SEO. - // serverHttpClient is only available during SSR — returns null on client nav. const httpClient = context.convexQueryClient.serverHttpClient; if (!httpClient) return null; try { @@ -21,7 +26,7 @@ export const Route = createFileRoute("/event/$slug")({ } catch (error) { console.error( `[SSR] Failed to load event (slug: ${params.slug}):`, - error, + error ); return null; } @@ -64,22 +69,32 @@ function EventDetailPage() { if (eventData === undefined) { return ( -
-
Loading...
+
+
+ +

Loading event...

+
); } if (eventData === null) { return ( -
-
-

Event not found

-

- The event you're looking for doesn't exist. +

+
+
+ +
+

Event not found

+

+ The event you're looking for doesn't exist or has been + removed.

- +
@@ -91,104 +106,172 @@ function EventDetailPage() { event.perspectiveSummaries.left || event.perspectiveSummaries.right; return ( -
-
- {/* Back button */} - - ← Back to feed - - - {/* Event header */} -
-
-

{event.title}

- +
+ {/* Hero Section */} +
+ {/* Background image */} + {event.imageUrl && ( +
+ +
- {event.imageUrl && ( -
- {event.title} + )} + + {/* Content overlay */} +
+ {/* Back button */} + + + Back to feed + + + {/* Event header */} +
+
+

+ {event.title} +

+
- )} +
+
- {/* Perspective summaries */} - {hasPerspectives ? ( - - - Multiple Perspectives - - - - - {event.perspectiveSummaries.left && ( - Left - )} - Center - {event.perspectiveSummaries.right && ( - Right - )} - - - {event.perspectiveSummaries.left && ( - -

- {event.perspectiveSummaries.left} + {/* Main Content */} +

+
+ {/* Perspective summaries */} + {hasPerspectives ? ( + + +
+
+ +
+
+ Multiple Perspectives +

+ See how different sources cover this story

- - )} +
+
+
+ + +
+ + {event.perspectiveSummaries.left && ( + + + + Left + + + )} + + + + Center + + + {event.perspectiveSummaries.right && ( + + + + Right + + + )} + +
- -

- {event.perspectiveSummaries.center} -

-
+
+ {event.perspectiveSummaries.left && ( + +

+ {event.perspectiveSummaries.left} +

+
+ )} - {event.perspectiveSummaries.right && ( - -

- {event.perspectiveSummaries.right} -

-
- )} - - - - ) : ( - - - Summary - - -

- {event.perspectiveSummaries.center} -

-
-
- )} + +

+ {event.perspectiveSummaries.center} +

+
- {/* Global Impact */} - {event.globalImpact && ( - - - What This Means - - -

- {event.globalImpact} -

-
-
- )} + {event.perspectiveSummaries.right && ( + +

+ {event.perspectiveSummaries.right} +

+
+ )} +
+
+
+
+ ) : ( + + +
+
+ +
+ Summary +
+
+ +

+ {event.perspectiveSummaries.center} +

+
+
+ )} - {/* Articles */} - + {/* Global Impact */} + {event.globalImpact && ( + + +
+
+ +
+
+ What This Means +

+ How this story affects you +

+
+
+
+ +

+ {event.globalImpact} +

+
+
+ )} + + {/* Articles */} + +
); diff --git a/apps/web/src/routes/feed.tsx b/apps/web/src/routes/feed.tsx index 88ffd20..2dc713c 100644 --- a/apps/web/src/routes/feed.tsx +++ b/apps/web/src/routes/feed.tsx @@ -6,6 +6,7 @@ import { usePaginatedQuery, useQuery } from "convex/react"; import EventCard from "@/components/feed/event-card"; import { Button } from "@/components/ui/button"; import { SITE } from "@/lib/seo"; +import { Loader2, Newspaper, SlidersHorizontal } from "lucide-react"; export const Route = createFileRoute("/feed")({ head: () => ({ @@ -48,7 +49,7 @@ function FeedComponent() { : 5; const [selectedTopic, setSelectedTopic] = useState | "all">( - "all", + "all" ); const { @@ -58,7 +59,7 @@ function FeedComponent() { } = usePaginatedQuery( api.events.getPublishedEvents, selectedTopic === "all" ? {} : { topicId: selectedTopic }, - { initialNumItems: pageSize }, + { initialNumItems: pageSize } ); const topicNamesById = useMemo(() => { @@ -70,74 +71,135 @@ function FeedComponent() { }, [topics]); return ( -
-
-
-

Today's Events

-

- Track the same story across perspectives. -

-
+
+ {/* Page Header */} +
+
+
+
+
+

+ Today's Events +

+

+ Track the same story across perspectives +

+
+
+ + Filter by topic +
+
-
- - {topics?.map((topic) => ( - - ))} + {/* Topic filters */} +
+ + {topics?.map((topic) => ( + + ))} +
+
+
-
+ {/* Feed Content */} +
+
+ {/* Loading state */} {status === "LoadingFirstPage" && ( -
Loading…
+
+ +

+ Loading events... +

+
)} - {events?.map((event) => ( - - ))} + {/* Events grid */} + {events && events.length > 0 && ( +
+ {events.map((event) => ( + + ))} +
+ )} + {/* Empty state */} {status !== "LoadingFirstPage" && (!events || events.length === 0) && ( -
- No events found. +
+
+ +
+
+

No events found

+

+ {selectedTopic !== "all" + ? "Try selecting a different topic or check back later." + : "Check back later for new stories."} +

+
+ {selectedTopic !== "all" && ( + + )}
)} -
- {status === "CanLoadMore" && ( -
- -
- )} + {/* Load more */} + {status === "CanLoadMore" && ( +
+ +
+ )} + + {/* Loading more indicator */} + {status === "LoadingMore" && ( +
+
+ + Loading more... +
+
+ )} +
); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 8a2a990..d195ac2 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -9,13 +9,25 @@ import EventCard from "@/components/feed/event-card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; +import { + ArrowRight, + Eye, + Shield, + Target, + Zap, + TrendingUp, + Users, + Sparkles, +} from "lucide-react"; function WaitlistForm({ className, buttonText = "Get Early Access", + variant = "default", }: { className?: string; buttonText?: string; + variant?: "default" | "hero"; }) { const [email, setEmail] = useState(""); const [name, setName] = useState(""); @@ -35,11 +47,11 @@ function WaitlistForm({ onSuccess: (result) => { if (result.alreadyExists) { setMessage( - `You're already on the waitlist at position #${result.position}!`, + `You're already on the waitlist at position #${result.position}!` ); } else { setMessage( - `You're in! You're #${result.position} on the waitlist. Check your email for details.`, + `You're in! You're #${result.position} on the waitlist. Check your email for details.` ); setEmail(""); setName(""); @@ -87,6 +99,61 @@ function WaitlistForm({ ? "success" : "idle"; + if (variant === "hero") { + return ( +
+
+
+ setEmail(e.target.value)} + required + className="flex-1 h-12 px-4 text-base bg-card border-border" + disabled={isPending} + /> + +
+ setName(e.target.value)} + className="h-12 px-4 text-base bg-card border-border" + disabled={isPending} + /> +
+ {message && ( +

+ {message} +

+ )} +
+ ); + } + return (
@@ -157,7 +224,6 @@ export const Route = createFileRoute("/")({ }); function LandingPage() { - // Get a few events for preview const previewCountConfig = useQuery(api.config.get, { key: "landing_preview_count", }); @@ -180,7 +246,7 @@ function LandingPage() { api.events.getPublishedEvents, previewCountConfig !== undefined ? { paginationOpts: { numItems: previewCount, cursor: null } } - : "skip", + : "skip" ); const topics = useQuery(api.topics.getTopics); @@ -194,7 +260,7 @@ function LandingPage() { return (
- {/* Structured data — visible to search engines */} + {/* Structured data */}