···11import { Link } from "@tanstack/react-router";
22import {
33- CalendarRange,
33+ ArrowRight,
44 Clock3,
55 Database,
66- Film,
76 ListChecks,
87 LogIn,
98 Search,
109 ShieldCheck,
1111- Tv,
1210} from "lucide-react";
1311import { M3Button } from "@/components/ui/m3-button";
1412import {
···1917 M3CardTitle,
2018} from "@/components/ui/m3-card";
21192222-const featureCards = [
2020+const comparisonItems = [
2321 {
2424- icon: Tv,
2525- title: "Movie, show, season, episode",
2626- description:
2727- "Track at exactly the level you want, from full-series completion down to single episodes.",
2222+ current: "Most apps help you mark something watched.",
2323+ better: "OpnShelf helps you discover what belongs on your shelf next.",
2824 },
2925 {
3030- icon: Clock3,
3131- title: "Full watch history",
3232- description:
3333- "Log rewatches, keep each watch date, and build a complete timeline of your viewing activity.",
2626+ current: "Most apps give you one watchlist and call it enough.",
2727+ better: "OpnShelf lets you build lists that stay useful over time.",
3428 },
3529 {
3636- icon: ListChecks,
3737- title: "Powerful list workflows",
3838- description:
3939- "Use default lists and custom lists to organize favorites, queues, themes, and deep cuts.",
3030+ current: "Most apps own your identity.",
3131+ better: "OpnShelf is built around portable identity and owning your data.",
4032 },
3333+];
3434+3535+const narrativeSections = [
4136 {
4242- icon: Database,
4343- title: "Import your history",
3737+ icon: Search,
3838+ title: "Discover beyond the algorithm",
4439 description:
4545- "Import history from a public Trakt username or CSV to start with real data instead of a blank slate.",
4040+ "Browse movies and shows, follow what catches your eye, and turn discovery into something you will actually come back to.",
4141+ points: [
4242+ "Browse movies and shows in one place",
4343+ "Turn discoveries into saved intent",
4444+ "Keep browsing without signing in",
4545+ ],
4646 },
4747 {
4848- icon: CalendarRange,
4949- title: "Timezone-aware activity",
4848+ icon: ListChecks,
4949+ title: "Build lists that stay useful",
5050 description:
5151- "Keep your watch dates accurate with timezone and 12h/24h preferences built into your profile.",
5151+ "Keep watchlists, favorites, themed collections, and deep-cut queues in a structure that still makes sense months later.",
5252+ points: [
5353+ "Default lists plus custom lists",
5454+ "Queue, favorites, and themed collections",
5555+ "Organize around how you actually decide what to watch",
5656+ ],
5257 },
5358 {
5454- icon: ShieldCheck,
5555- title: "AT Protocol identity",
5959+ icon: Database,
6060+ title: "Own your record",
5661 description:
5757- "Sign in with your Atmosphere account and keep your identity and data model portable across apps.",
6262+ "Bring over the history you already care about, keep every watch date that matters, and avoid starting from zero.",
6363+ points: [
6464+ "Import from a public Trakt username",
6565+ "Import from CSV",
6666+ "Portable identity on the AT Protocol",
6767+ ],
5868 },
5969];
60706171export function LandingHomePage() {
6272 return (
6373 <div
6464- className="min-h-screen"
7474+ className="min-h-screen overflow-hidden"
6575 style={{
6676 backgroundColor: "var(--md-sys-color-background)",
6777 color: "var(--md-sys-color-on-background)",
6878 }}
6979 >
7070- <div
7171- className="border-b"
7272- style={{ borderColor: "var(--md-sys-color-outline-variant)" }}
7373- >
7474- <div className="container mx-auto px-4 py-14 md:py-20 max-w-6xl">
7575- <div className="grid gap-10 lg:grid-cols-[1.15fr_0.85fr] lg:items-center">
7676- <div>
7777- <div className="flex items-center gap-3 mb-6">
7878- <img
7979- src="/icon.png"
8080- alt="OpnShelf"
8181- className="w-14 h-14 rounded-xl"
8282- />
8383- <span
8484- className="md-label-large px-3 py-1 rounded-full"
8585- style={{
8686- backgroundColor: "var(--md-sys-color-secondary-container)",
8787- color: "var(--md-sys-color-on-secondary-container)",
8888- }}
8989- >
9090- Built for serious tracking
9191- </span>
9292- </div>
9393- <h1 className="md-display-medium mb-4">
9494- Track every watch. Organize every obsession.
9595- </h1>
9696- <p
9797- className="md-title-large mb-6"
9898- style={{ color: "var(--md-sys-color-on-surface-variant)" }}
8080+ <section className="relative border-b border-[var(--md-sys-color-outline-variant)]">
8181+ <div
8282+ className="absolute inset-0 opacity-90"
8383+ style={{
8484+ background:
8585+ "radial-gradient(circle at top left, rgba(243, 188, 0, 0.18), transparent 35%), radial-gradient(circle at 85% 20%, rgba(176, 207, 186, 0.12), transparent 30%), linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0))",
8686+ }}
8787+ />
8888+ <div className="container relative mx-auto max-w-6xl px-4 py-16 md:py-24">
8989+ <div className="max-w-5xl">
9090+ <div className="mb-6 flex items-center gap-3">
9191+ <img
9292+ src="/icon.png"
9393+ alt="OpnShelf"
9494+ className="size-14 rounded-2xl shadow-[0_12px_30px_rgba(0,0,0,0.28)]"
9595+ />
9696+ <span
9797+ className="md-label-large rounded-full px-4 py-1.5"
9898+ style={{
9999+ backgroundColor: "var(--md-sys-color-secondary-container)",
100100+ color: "var(--md-sys-color-on-secondary-container)",
101101+ }}
99102 >
100100- OpnShelf gives you movie and show tracking down to season and
101101- episode level, complete watch history, list organization, and a
102102- portable AT Protocol account.
103103- </p>
104104- <div className="flex flex-wrap gap-3">
105105- <M3Button variant="filled" size="lg" asChild>
106106- <Link to="/login">
107107- <LogIn className="w-5 h-5 mr-2" />
108108- Sign in to start tracking
109109- </Link>
110110- </M3Button>
111111- <M3Button variant="outlined" size="lg" asChild>
112112- <Link to="/search" search={{ q: "", type: "all" }}>
113113- <Search className="w-5 h-5 mr-2" />
114114- Browse catalog
115115- </Link>
116116- </M3Button>
117117- </div>
103103+ For people who want a real record of what they watch
104104+ </span>
118105 </div>
119106120120- <M3Card variant="elevated" className="h-fit">
121121- <M3CardHeader>
122122- <M3CardTitle>Why people use OpnShelf</M3CardTitle>
123123- <M3CardDescription>
124124- Built for people who want more than a single watched toggle.
125125- </M3CardDescription>
126126- </M3CardHeader>
127127- <M3CardContent>
128128- <div className="space-y-4">
129129- {[
130130- {
131131- title: "Granular tracking",
132132- description:
133133- "Track movies, shows, seasons, and episodes as separate items.",
134134- },
135135- {
136136- title: "Real watch history",
137137- description:
138138- "Keep every watch date and rewatch, not just a binary status.",
139139- },
140140- {
141141- title: "Lists that stay useful",
142142- description:
143143- "Combine default lists with your own lists for any workflow.",
144144- },
145145- ].map((item, index) => (
146146- <div key={item.title} className="flex items-start gap-3">
147147- <span
148148- className="md-label-large w-7 h-7 rounded-full flex items-center justify-center"
149149- style={{
150150- backgroundColor:
151151- "var(--md-sys-color-primary-container)",
152152- color: "var(--md-sys-color-on-primary-container)",
153153- }}
154154- >
155155- {index + 1}
156156- </span>
157157- <div>
158158- <p className="md-title-small">{item.title}</p>
159159- <p
160160- className="md-body-small"
161161- style={{
162162- color: "var(--md-sys-color-on-surface-variant)",
163163- }}
164164- >
165165- {item.description}
166166- </p>
167167- </div>
168168- </div>
169169- ))}
170170- </div>
171171- </M3CardContent>
172172- </M3Card>
107107+ <h1 className="md-display-medium max-w-4xl text-balance sm:text-[3.4rem] sm:leading-[3.8rem]">
108108+ Discover more. Curate better. Own your watch history.
109109+ </h1>
110110+ <p
111111+ className="md-title-large mt-5 max-w-4xl"
112112+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
113113+ >
114114+ Browse movies and shows, create lists for every mood and
115115+ obsession, and keep your data portable with AT Protocol identity.
116116+ </p>
117117+ <p
118118+ className="md-body-large mt-4 max-w-3xl"
119119+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
120120+ >
121121+ Find your next watch, save it to the right list, and keep a watch
122122+ record you can take with you.
123123+ </p>
124124+125125+ <div className="mt-8 flex flex-wrap gap-3">
126126+ <M3Button
127127+ variant="filled"
128128+ size="lg"
129129+ asChild
130130+ className="rounded-full"
131131+ >
132132+ <Link to="/login">
133133+ <LogIn className="size-5" />
134134+ Sign in and build your shelf
135135+ </Link>
136136+ </M3Button>
137137+ <M3Button
138138+ variant="outlined"
139139+ size="lg"
140140+ asChild
141141+ className="rounded-full"
142142+ >
143143+ <Link to="/search" search={{ q: "", type: "all" }}>
144144+ <Search className="size-5" />
145145+ Browse the catalog
146146+ </Link>
147147+ </M3Button>
148148+ </div>
173149 </div>
174150 </div>
175175- </div>
151151+ </section>
176152177177- <div className="container mx-auto px-4 py-12 max-w-6xl">
178178- <div className="mb-6">
179179- <h2 className="md-headline-small mb-2">Features</h2>
153153+ <section className="border-b border-[var(--md-sys-color-outline-variant)] bg-[color:rgba(255,255,255,0.02)]">
154154+ <div className="container mx-auto max-w-6xl px-4 py-10 md:py-14">
155155+ <div className="mb-8 max-w-2xl">
156156+ <p
157157+ className="md-label-large uppercase tracking-[0.12em]"
158158+ style={{ color: "var(--md-sys-color-primary)" }}
159159+ >
160160+ Why OpnShelf
161161+ </p>
162162+ <h2 className="md-headline-medium mt-2">
163163+ Not just a watched toggle. A place to browse, curate, and keep the
164164+ record.
165165+ </h2>
166166+ </div>
167167+ <div className="grid gap-4 md:grid-cols-3">
168168+ {comparisonItems.map((item) => (
169169+ <div
170170+ key={item.current}
171171+ className="rounded-[24px] border p-6"
172172+ style={{
173173+ borderColor: "var(--md-sys-color-outline-variant)",
174174+ backgroundColor: "rgba(255, 255, 255, 0.025)",
175175+ }}
176176+ >
177177+ <p
178178+ className="md-title-small"
179179+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
180180+ >
181181+ {item.current}
182182+ </p>
183183+ <p className="md-title-large mt-4 text-(--md-sys-color-on-surface)">
184184+ {item.better}
185185+ </p>
186186+ </div>
187187+ ))}
188188+ </div>
189189+ </div>
190190+ </section>
191191+192192+ <section className="container mx-auto max-w-6xl px-4 py-16 md:py-20">
193193+ <div className="mb-10 max-w-2xl">
180194 <p
181181- className="md-body-large"
182182- style={{ color: "var(--md-sys-color-on-surface-variant)" }}
195195+ className="md-label-large uppercase tracking-[0.12em]"
196196+ style={{ color: "var(--md-sys-color-tertiary)" }}
183197 >
184184- Everything you need to track and organize what you watch.
198198+ How It Works
185199 </p>
200200+ <h2 className="md-headline-medium mt-2">
201201+ Three reasons the product feels better the longer you use it.
202202+ </h2>
186203 </div>
187187-188188- <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
189189- {featureCards.map((card) => {
190190- const Icon = card.icon;
204204+ <div className="grid gap-5 lg:grid-cols-3">
205205+ {narrativeSections.map((section) => {
206206+ const Icon = section.icon;
191207192208 return (
193193- <M3Card key={card.title} variant="elevated">
194194- <M3CardHeader>
195195- <M3CardTitle className="flex items-center gap-2">
196196- <Icon className="w-5 h-5" />
197197- {card.title}
209209+ <M3Card
210210+ key={section.title}
211211+ variant="elevated"
212212+ className="rounded-[28px] border border-[var(--md-sys-color-outline-variant)]"
213213+ >
214214+ <M3CardHeader className="px-6 pt-6">
215215+ <div
216216+ className="mb-3 flex size-12 items-center justify-center rounded-2xl"
217217+ style={{
218218+ backgroundColor:
219219+ "var(--md-sys-color-surface-container-high)",
220220+ color: "var(--md-sys-color-primary)",
221221+ }}
222222+ >
223223+ <Icon className="size-5" />
224224+ </div>
225225+ <M3CardTitle className="md-headline-small">
226226+ {section.title}
198227 </M3CardTitle>
228228+ <M3CardDescription className="md-body-large">
229229+ {section.description}
230230+ </M3CardDescription>
199231 </M3CardHeader>
200200- <M3CardContent>
201201- <M3CardDescription>{card.description}</M3CardDescription>
232232+ <M3CardContent className="px-6 pb-6">
233233+ <ul className="space-y-3">
234234+ {section.points.map((point) => (
235235+ <li key={point} className="flex items-start gap-3">
236236+ <span
237237+ className="mt-1 size-2.5 shrink-0 rounded-full"
238238+ style={{
239239+ backgroundColor: "var(--md-sys-color-primary)",
240240+ }}
241241+ />
242242+ <span className="md-body-large text-(--md-sys-color-on-surface)">
243243+ {point}
244244+ </span>
245245+ </li>
246246+ ))}
247247+ </ul>
202248 </M3CardContent>
203249 </M3Card>
204250 );
205251 })}
206252 </div>
253253+ </section>
207254208208- <M3Card variant="elevated" className="mt-8">
209209- <M3CardHeader>
210210- <M3CardTitle className="flex items-center gap-2">
211211- <Film className="w-5 h-5" />
212212- Explore without signing in
213213- </M3CardTitle>
214214- <M3CardDescription>
215215- Explore movies and shows right away, then sign in when you are
216216- ready to track.
217217- </M3CardDescription>
218218- </M3CardHeader>
219219- <M3CardContent className="flex flex-wrap gap-3">
220220- <M3Button variant="filled-tonal" asChild>
221221- <Link to="/search" search={{ q: "", type: "all" }}>
222222- <Search className="w-4 h-4 mr-2" />
223223- Start searching
255255+ <section className="container mx-auto max-w-6xl px-4 pb-16">
256256+ <div
257257+ className="grid gap-6 overflow-hidden rounded-[32px] border px-6 py-8 md:px-10 md:py-10 lg:grid-cols-[1fr_auto] lg:items-center"
258258+ style={{
259259+ borderColor: "rgba(243, 188, 0, 0.34)",
260260+ background:
261261+ "linear-gradient(135deg, rgba(92, 69, 0, 0.7), rgba(33, 31, 38, 0.95) 48%, rgba(50, 75, 59, 0.7))",
262262+ boxShadow: "0 24px 60px rgba(0, 0, 0, 0.28)",
263263+ }}
264264+ >
265265+ <div className="max-w-3xl">
266266+ <p
267267+ className="md-label-large uppercase tracking-[0.12em]"
268268+ style={{ color: "var(--md-sys-color-primary)" }}
269269+ >
270270+ Import First
271271+ </p>
272272+ <h2 className="md-headline-medium mt-3">
273273+ Bring your history with you
274274+ </h2>
275275+ <p
276276+ className="md-body-large mt-4 max-w-2xl"
277277+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
278278+ >
279279+ Starting from zero is the fastest way to abandon a tracker. Import
280280+ from Trakt or CSV, then keep building your shelf with lists,
281281+ discovery, and full watch history.
282282+ </p>
283283+ </div>
284284+285285+ <M3Button variant="filled" size="lg" asChild className="rounded-full">
286286+ <Link to="/login">
287287+ <Database className="size-5" />
288288+ Sign in and import your history
289289+ </Link>
290290+ </M3Button>
291291+ </div>
292292+ </section>
293293+294294+ <section className="container mx-auto max-w-6xl px-4 pb-16">
295295+ <div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr] lg:items-center">
296296+ <div className="max-w-xl">
297297+ <p
298298+ className="md-label-large uppercase tracking-[0.12em]"
299299+ style={{ color: "var(--md-sys-color-tertiary)" }}
300300+ >
301301+ Portable Identity
302302+ </p>
303303+ <h2 className="md-headline-medium mt-3">
304304+ Built on portable identity
305305+ </h2>
306306+ </div>
307307+308308+ <div
309309+ className="rounded-[28px] border p-6 md:p-8"
310310+ style={{
311311+ borderColor: "var(--md-sys-color-outline-variant)",
312312+ backgroundColor: "var(--md-sys-color-surface-container-low)",
313313+ }}
314314+ >
315315+ <div className="flex items-start gap-4">
316316+ <div
317317+ className="flex size-12 shrink-0 items-center justify-center rounded-2xl"
318318+ style={{
319319+ backgroundColor: "var(--md-sys-color-tertiary-container)",
320320+ color: "var(--md-sys-color-on-tertiary-container)",
321321+ }}
322322+ >
323323+ <ShieldCheck className="size-5" />
324324+ </div>
325325+ <p
326326+ className="md-body-large"
327327+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
328328+ >
329329+ OpnShelf uses your AT Protocol account so your identity is not
330330+ trapped inside one media app. That matters if you care about
331331+ long-term ownership of your data and profile.
332332+ </p>
333333+ </div>
334334+ </div>
335335+ </div>
336336+ </section>
337337+338338+ <section className="container mx-auto max-w-6xl px-4 pb-20">
339339+ <div
340340+ className="rounded-[32px] border px-6 py-10 text-center md:px-10"
341341+ style={{
342342+ borderColor: "var(--md-sys-color-outline-variant)",
343343+ backgroundColor: "var(--md-sys-color-surface-container)",
344344+ }}
345345+ >
346346+ <h2 className="md-headline-medium">
347347+ Find your next watch and keep the record
348348+ </h2>
349349+ <p
350350+ className="md-body-large mx-auto mt-4 max-w-2xl"
351351+ style={{ color: "var(--md-sys-color-on-surface-variant)" }}
352352+ >
353353+ Browse first if you want. Sign in when you are ready to build lists,
354354+ track what matters, and keep your data yours.
355355+ </p>
356356+ <div className="mt-8 flex flex-wrap justify-center gap-3">
357357+ <M3Button
358358+ variant="filled"
359359+ size="lg"
360360+ asChild
361361+ className="rounded-full"
362362+ >
363363+ <Link to="/login">
364364+ <LogIn className="size-5" />
365365+ Start discovering
224366 </Link>
225367 </M3Button>
226226- <M3Button variant="filled" asChild>
227227- <Link to="/login">
228228- <LogIn className="w-4 h-4 mr-2" />
229229- Unlock full tracking
368368+ <M3Button
369369+ variant="filled-tonal"
370370+ size="lg"
371371+ asChild
372372+ className="rounded-full"
373373+ >
374374+ <Link to="/search" search={{ q: "", type: "all" }}>
375375+ <Search className="size-5" />
376376+ Browse catalog
377377+ <ArrowRight className="size-5" />
230378 </Link>
231379 </M3Button>
232232- </M3CardContent>
233233- </M3Card>
234234- </div>
380380+ </div>
381381+ <div className="mt-6 flex flex-wrap justify-center gap-6">
382382+ <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)">
383383+ <Clock3 className="size-4 text-(--md-sys-color-primary)" />
384384+ <span className="md-body-medium">Keep a real watch history</span>
385385+ </div>
386386+ <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)">
387387+ <ListChecks className="size-4 text-(--md-sys-color-primary)" />
388388+ <span className="md-body-medium">
389389+ Build lists that stay useful
390390+ </span>
391391+ </div>
392392+ <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)">
393393+ <Search className="size-4 text-(--md-sys-color-primary)" />
394394+ <span className="md-body-medium">
395395+ Discover without signing in
396396+ </span>
397397+ </div>
398398+ </div>
399399+ </div>
400400+ </section>
235401 </div>
236402 );
237403}
+2-2
apps/web/src/routes/index.tsx
···88export const Route = createFileRoute("/")({
99 head: () => ({
1010 meta: [
1111- { title: "Track Movies and Shows | OpnShelf" },
1111+ { title: "OpnShelf | Discover, Curate, and Own Your Watch History" },
1212 {
1313 name: "description",
1414 content:
1515- "Track movies and shows at movie, season, and episode level with watch history, lists, and AT Protocol account portability.",
1515+ "Browse movies and shows, build custom lists, import your history, and keep your data portable with AT Protocol identity.",
1616 },
1717 ],
1818 }),