A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refresh landing page messaging and hero layout

- Rework the homepage into a more narrative, feature-driven landing page
- Update metadata to match the new discovery, curation, and portability focus

+335 -169
+333 -167
apps/web/src/components/home/LandingHomePage.tsx
··· 1 1 import { Link } from "@tanstack/react-router"; 2 2 import { 3 - CalendarRange, 3 + ArrowRight, 4 4 Clock3, 5 5 Database, 6 - Film, 7 6 ListChecks, 8 7 LogIn, 9 8 Search, 10 9 ShieldCheck, 11 - Tv, 12 10 } from "lucide-react"; 13 11 import { M3Button } from "@/components/ui/m3-button"; 14 12 import { ··· 19 17 M3CardTitle, 20 18 } from "@/components/ui/m3-card"; 21 19 22 - const featureCards = [ 20 + const comparisonItems = [ 23 21 { 24 - icon: Tv, 25 - title: "Movie, show, season, episode", 26 - description: 27 - "Track at exactly the level you want, from full-series completion down to single episodes.", 22 + current: "Most apps help you mark something watched.", 23 + better: "OpnShelf helps you discover what belongs on your shelf next.", 28 24 }, 29 25 { 30 - icon: Clock3, 31 - title: "Full watch history", 32 - description: 33 - "Log rewatches, keep each watch date, and build a complete timeline of your viewing activity.", 26 + current: "Most apps give you one watchlist and call it enough.", 27 + better: "OpnShelf lets you build lists that stay useful over time.", 34 28 }, 35 29 { 36 - icon: ListChecks, 37 - title: "Powerful list workflows", 38 - description: 39 - "Use default lists and custom lists to organize favorites, queues, themes, and deep cuts.", 30 + current: "Most apps own your identity.", 31 + better: "OpnShelf is built around portable identity and owning your data.", 40 32 }, 33 + ]; 34 + 35 + const narrativeSections = [ 41 36 { 42 - icon: Database, 43 - title: "Import your history", 37 + icon: Search, 38 + title: "Discover beyond the algorithm", 44 39 description: 45 - "Import history from a public Trakt username or CSV to start with real data instead of a blank slate.", 40 + "Browse movies and shows, follow what catches your eye, and turn discovery into something you will actually come back to.", 41 + points: [ 42 + "Browse movies and shows in one place", 43 + "Turn discoveries into saved intent", 44 + "Keep browsing without signing in", 45 + ], 46 46 }, 47 47 { 48 - icon: CalendarRange, 49 - title: "Timezone-aware activity", 48 + icon: ListChecks, 49 + title: "Build lists that stay useful", 50 50 description: 51 - "Keep your watch dates accurate with timezone and 12h/24h preferences built into your profile.", 51 + "Keep watchlists, favorites, themed collections, and deep-cut queues in a structure that still makes sense months later.", 52 + points: [ 53 + "Default lists plus custom lists", 54 + "Queue, favorites, and themed collections", 55 + "Organize around how you actually decide what to watch", 56 + ], 52 57 }, 53 58 { 54 - icon: ShieldCheck, 55 - title: "AT Protocol identity", 59 + icon: Database, 60 + title: "Own your record", 56 61 description: 57 - "Sign in with your Atmosphere account and keep your identity and data model portable across apps.", 62 + "Bring over the history you already care about, keep every watch date that matters, and avoid starting from zero.", 63 + points: [ 64 + "Import from a public Trakt username", 65 + "Import from CSV", 66 + "Portable identity on the AT Protocol", 67 + ], 58 68 }, 59 69 ]; 60 70 61 71 export function LandingHomePage() { 62 72 return ( 63 73 <div 64 - className="min-h-screen" 74 + className="min-h-screen overflow-hidden" 65 75 style={{ 66 76 backgroundColor: "var(--md-sys-color-background)", 67 77 color: "var(--md-sys-color-on-background)", 68 78 }} 69 79 > 70 - <div 71 - className="border-b" 72 - style={{ borderColor: "var(--md-sys-color-outline-variant)" }} 73 - > 74 - <div className="container mx-auto px-4 py-14 md:py-20 max-w-6xl"> 75 - <div className="grid gap-10 lg:grid-cols-[1.15fr_0.85fr] lg:items-center"> 76 - <div> 77 - <div className="flex items-center gap-3 mb-6"> 78 - <img 79 - src="/icon.png" 80 - alt="OpnShelf" 81 - className="w-14 h-14 rounded-xl" 82 - /> 83 - <span 84 - className="md-label-large px-3 py-1 rounded-full" 85 - style={{ 86 - backgroundColor: "var(--md-sys-color-secondary-container)", 87 - color: "var(--md-sys-color-on-secondary-container)", 88 - }} 89 - > 90 - Built for serious tracking 91 - </span> 92 - </div> 93 - <h1 className="md-display-medium mb-4"> 94 - Track every watch. Organize every obsession. 95 - </h1> 96 - <p 97 - className="md-title-large mb-6" 98 - style={{ color: "var(--md-sys-color-on-surface-variant)" }} 80 + <section className="relative border-b border-[var(--md-sys-color-outline-variant)]"> 81 + <div 82 + className="absolute inset-0 opacity-90" 83 + style={{ 84 + background: 85 + "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))", 86 + }} 87 + /> 88 + <div className="container relative mx-auto max-w-6xl px-4 py-16 md:py-24"> 89 + <div className="max-w-5xl"> 90 + <div className="mb-6 flex items-center gap-3"> 91 + <img 92 + src="/icon.png" 93 + alt="OpnShelf" 94 + className="size-14 rounded-2xl shadow-[0_12px_30px_rgba(0,0,0,0.28)]" 95 + /> 96 + <span 97 + className="md-label-large rounded-full px-4 py-1.5" 98 + style={{ 99 + backgroundColor: "var(--md-sys-color-secondary-container)", 100 + color: "var(--md-sys-color-on-secondary-container)", 101 + }} 99 102 > 100 - OpnShelf gives you movie and show tracking down to season and 101 - episode level, complete watch history, list organization, and a 102 - portable AT Protocol account. 103 - </p> 104 - <div className="flex flex-wrap gap-3"> 105 - <M3Button variant="filled" size="lg" asChild> 106 - <Link to="/login"> 107 - <LogIn className="w-5 h-5 mr-2" /> 108 - Sign in to start tracking 109 - </Link> 110 - </M3Button> 111 - <M3Button variant="outlined" size="lg" asChild> 112 - <Link to="/search" search={{ q: "", type: "all" }}> 113 - <Search className="w-5 h-5 mr-2" /> 114 - Browse catalog 115 - </Link> 116 - </M3Button> 117 - </div> 103 + For people who want a real record of what they watch 104 + </span> 118 105 </div> 119 106 120 - <M3Card variant="elevated" className="h-fit"> 121 - <M3CardHeader> 122 - <M3CardTitle>Why people use OpnShelf</M3CardTitle> 123 - <M3CardDescription> 124 - Built for people who want more than a single watched toggle. 125 - </M3CardDescription> 126 - </M3CardHeader> 127 - <M3CardContent> 128 - <div className="space-y-4"> 129 - {[ 130 - { 131 - title: "Granular tracking", 132 - description: 133 - "Track movies, shows, seasons, and episodes as separate items.", 134 - }, 135 - { 136 - title: "Real watch history", 137 - description: 138 - "Keep every watch date and rewatch, not just a binary status.", 139 - }, 140 - { 141 - title: "Lists that stay useful", 142 - description: 143 - "Combine default lists with your own lists for any workflow.", 144 - }, 145 - ].map((item, index) => ( 146 - <div key={item.title} className="flex items-start gap-3"> 147 - <span 148 - className="md-label-large w-7 h-7 rounded-full flex items-center justify-center" 149 - style={{ 150 - backgroundColor: 151 - "var(--md-sys-color-primary-container)", 152 - color: "var(--md-sys-color-on-primary-container)", 153 - }} 154 - > 155 - {index + 1} 156 - </span> 157 - <div> 158 - <p className="md-title-small">{item.title}</p> 159 - <p 160 - className="md-body-small" 161 - style={{ 162 - color: "var(--md-sys-color-on-surface-variant)", 163 - }} 164 - > 165 - {item.description} 166 - </p> 167 - </div> 168 - </div> 169 - ))} 170 - </div> 171 - </M3CardContent> 172 - </M3Card> 107 + <h1 className="md-display-medium max-w-4xl text-balance sm:text-[3.4rem] sm:leading-[3.8rem]"> 108 + Discover more. Curate better. Own your watch history. 109 + </h1> 110 + <p 111 + className="md-title-large mt-5 max-w-4xl" 112 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 113 + > 114 + Browse movies and shows, create lists for every mood and 115 + obsession, and keep your data portable with AT Protocol identity. 116 + </p> 117 + <p 118 + className="md-body-large mt-4 max-w-3xl" 119 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 120 + > 121 + Find your next watch, save it to the right list, and keep a watch 122 + record you can take with you. 123 + </p> 124 + 125 + <div className="mt-8 flex flex-wrap gap-3"> 126 + <M3Button 127 + variant="filled" 128 + size="lg" 129 + asChild 130 + className="rounded-full" 131 + > 132 + <Link to="/login"> 133 + <LogIn className="size-5" /> 134 + Sign in and build your shelf 135 + </Link> 136 + </M3Button> 137 + <M3Button 138 + variant="outlined" 139 + size="lg" 140 + asChild 141 + className="rounded-full" 142 + > 143 + <Link to="/search" search={{ q: "", type: "all" }}> 144 + <Search className="size-5" /> 145 + Browse the catalog 146 + </Link> 147 + </M3Button> 148 + </div> 173 149 </div> 174 150 </div> 175 - </div> 151 + </section> 176 152 177 - <div className="container mx-auto px-4 py-12 max-w-6xl"> 178 - <div className="mb-6"> 179 - <h2 className="md-headline-small mb-2">Features</h2> 153 + <section className="border-b border-[var(--md-sys-color-outline-variant)] bg-[color:rgba(255,255,255,0.02)]"> 154 + <div className="container mx-auto max-w-6xl px-4 py-10 md:py-14"> 155 + <div className="mb-8 max-w-2xl"> 156 + <p 157 + className="md-label-large uppercase tracking-[0.12em]" 158 + style={{ color: "var(--md-sys-color-primary)" }} 159 + > 160 + Why OpnShelf 161 + </p> 162 + <h2 className="md-headline-medium mt-2"> 163 + Not just a watched toggle. A place to browse, curate, and keep the 164 + record. 165 + </h2> 166 + </div> 167 + <div className="grid gap-4 md:grid-cols-3"> 168 + {comparisonItems.map((item) => ( 169 + <div 170 + key={item.current} 171 + className="rounded-[24px] border p-6" 172 + style={{ 173 + borderColor: "var(--md-sys-color-outline-variant)", 174 + backgroundColor: "rgba(255, 255, 255, 0.025)", 175 + }} 176 + > 177 + <p 178 + className="md-title-small" 179 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 180 + > 181 + {item.current} 182 + </p> 183 + <p className="md-title-large mt-4 text-(--md-sys-color-on-surface)"> 184 + {item.better} 185 + </p> 186 + </div> 187 + ))} 188 + </div> 189 + </div> 190 + </section> 191 + 192 + <section className="container mx-auto max-w-6xl px-4 py-16 md:py-20"> 193 + <div className="mb-10 max-w-2xl"> 180 194 <p 181 - className="md-body-large" 182 - style={{ color: "var(--md-sys-color-on-surface-variant)" }} 195 + className="md-label-large uppercase tracking-[0.12em]" 196 + style={{ color: "var(--md-sys-color-tertiary)" }} 183 197 > 184 - Everything you need to track and organize what you watch. 198 + How It Works 185 199 </p> 200 + <h2 className="md-headline-medium mt-2"> 201 + Three reasons the product feels better the longer you use it. 202 + </h2> 186 203 </div> 187 - 188 - <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> 189 - {featureCards.map((card) => { 190 - const Icon = card.icon; 204 + <div className="grid gap-5 lg:grid-cols-3"> 205 + {narrativeSections.map((section) => { 206 + const Icon = section.icon; 191 207 192 208 return ( 193 - <M3Card key={card.title} variant="elevated"> 194 - <M3CardHeader> 195 - <M3CardTitle className="flex items-center gap-2"> 196 - <Icon className="w-5 h-5" /> 197 - {card.title} 209 + <M3Card 210 + key={section.title} 211 + variant="elevated" 212 + className="rounded-[28px] border border-[var(--md-sys-color-outline-variant)]" 213 + > 214 + <M3CardHeader className="px-6 pt-6"> 215 + <div 216 + className="mb-3 flex size-12 items-center justify-center rounded-2xl" 217 + style={{ 218 + backgroundColor: 219 + "var(--md-sys-color-surface-container-high)", 220 + color: "var(--md-sys-color-primary)", 221 + }} 222 + > 223 + <Icon className="size-5" /> 224 + </div> 225 + <M3CardTitle className="md-headline-small"> 226 + {section.title} 198 227 </M3CardTitle> 228 + <M3CardDescription className="md-body-large"> 229 + {section.description} 230 + </M3CardDescription> 199 231 </M3CardHeader> 200 - <M3CardContent> 201 - <M3CardDescription>{card.description}</M3CardDescription> 232 + <M3CardContent className="px-6 pb-6"> 233 + <ul className="space-y-3"> 234 + {section.points.map((point) => ( 235 + <li key={point} className="flex items-start gap-3"> 236 + <span 237 + className="mt-1 size-2.5 shrink-0 rounded-full" 238 + style={{ 239 + backgroundColor: "var(--md-sys-color-primary)", 240 + }} 241 + /> 242 + <span className="md-body-large text-(--md-sys-color-on-surface)"> 243 + {point} 244 + </span> 245 + </li> 246 + ))} 247 + </ul> 202 248 </M3CardContent> 203 249 </M3Card> 204 250 ); 205 251 })} 206 252 </div> 253 + </section> 207 254 208 - <M3Card variant="elevated" className="mt-8"> 209 - <M3CardHeader> 210 - <M3CardTitle className="flex items-center gap-2"> 211 - <Film className="w-5 h-5" /> 212 - Explore without signing in 213 - </M3CardTitle> 214 - <M3CardDescription> 215 - Explore movies and shows right away, then sign in when you are 216 - ready to track. 217 - </M3CardDescription> 218 - </M3CardHeader> 219 - <M3CardContent className="flex flex-wrap gap-3"> 220 - <M3Button variant="filled-tonal" asChild> 221 - <Link to="/search" search={{ q: "", type: "all" }}> 222 - <Search className="w-4 h-4 mr-2" /> 223 - Start searching 255 + <section className="container mx-auto max-w-6xl px-4 pb-16"> 256 + <div 257 + 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" 258 + style={{ 259 + borderColor: "rgba(243, 188, 0, 0.34)", 260 + background: 261 + "linear-gradient(135deg, rgba(92, 69, 0, 0.7), rgba(33, 31, 38, 0.95) 48%, rgba(50, 75, 59, 0.7))", 262 + boxShadow: "0 24px 60px rgba(0, 0, 0, 0.28)", 263 + }} 264 + > 265 + <div className="max-w-3xl"> 266 + <p 267 + className="md-label-large uppercase tracking-[0.12em]" 268 + style={{ color: "var(--md-sys-color-primary)" }} 269 + > 270 + Import First 271 + </p> 272 + <h2 className="md-headline-medium mt-3"> 273 + Bring your history with you 274 + </h2> 275 + <p 276 + className="md-body-large mt-4 max-w-2xl" 277 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 278 + > 279 + Starting from zero is the fastest way to abandon a tracker. Import 280 + from Trakt or CSV, then keep building your shelf with lists, 281 + discovery, and full watch history. 282 + </p> 283 + </div> 284 + 285 + <M3Button variant="filled" size="lg" asChild className="rounded-full"> 286 + <Link to="/login"> 287 + <Database className="size-5" /> 288 + Sign in and import your history 289 + </Link> 290 + </M3Button> 291 + </div> 292 + </section> 293 + 294 + <section className="container mx-auto max-w-6xl px-4 pb-16"> 295 + <div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr] lg:items-center"> 296 + <div className="max-w-xl"> 297 + <p 298 + className="md-label-large uppercase tracking-[0.12em]" 299 + style={{ color: "var(--md-sys-color-tertiary)" }} 300 + > 301 + Portable Identity 302 + </p> 303 + <h2 className="md-headline-medium mt-3"> 304 + Built on portable identity 305 + </h2> 306 + </div> 307 + 308 + <div 309 + className="rounded-[28px] border p-6 md:p-8" 310 + style={{ 311 + borderColor: "var(--md-sys-color-outline-variant)", 312 + backgroundColor: "var(--md-sys-color-surface-container-low)", 313 + }} 314 + > 315 + <div className="flex items-start gap-4"> 316 + <div 317 + className="flex size-12 shrink-0 items-center justify-center rounded-2xl" 318 + style={{ 319 + backgroundColor: "var(--md-sys-color-tertiary-container)", 320 + color: "var(--md-sys-color-on-tertiary-container)", 321 + }} 322 + > 323 + <ShieldCheck className="size-5" /> 324 + </div> 325 + <p 326 + className="md-body-large" 327 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 328 + > 329 + OpnShelf uses your AT Protocol account so your identity is not 330 + trapped inside one media app. That matters if you care about 331 + long-term ownership of your data and profile. 332 + </p> 333 + </div> 334 + </div> 335 + </div> 336 + </section> 337 + 338 + <section className="container mx-auto max-w-6xl px-4 pb-20"> 339 + <div 340 + className="rounded-[32px] border px-6 py-10 text-center md:px-10" 341 + style={{ 342 + borderColor: "var(--md-sys-color-outline-variant)", 343 + backgroundColor: "var(--md-sys-color-surface-container)", 344 + }} 345 + > 346 + <h2 className="md-headline-medium"> 347 + Find your next watch and keep the record 348 + </h2> 349 + <p 350 + className="md-body-large mx-auto mt-4 max-w-2xl" 351 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 352 + > 353 + Browse first if you want. Sign in when you are ready to build lists, 354 + track what matters, and keep your data yours. 355 + </p> 356 + <div className="mt-8 flex flex-wrap justify-center gap-3"> 357 + <M3Button 358 + variant="filled" 359 + size="lg" 360 + asChild 361 + className="rounded-full" 362 + > 363 + <Link to="/login"> 364 + <LogIn className="size-5" /> 365 + Start discovering 224 366 </Link> 225 367 </M3Button> 226 - <M3Button variant="filled" asChild> 227 - <Link to="/login"> 228 - <LogIn className="w-4 h-4 mr-2" /> 229 - Unlock full tracking 368 + <M3Button 369 + variant="filled-tonal" 370 + size="lg" 371 + asChild 372 + className="rounded-full" 373 + > 374 + <Link to="/search" search={{ q: "", type: "all" }}> 375 + <Search className="size-5" /> 376 + Browse catalog 377 + <ArrowRight className="size-5" /> 230 378 </Link> 231 379 </M3Button> 232 - </M3CardContent> 233 - </M3Card> 234 - </div> 380 + </div> 381 + <div className="mt-6 flex flex-wrap justify-center gap-6"> 382 + <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)"> 383 + <Clock3 className="size-4 text-(--md-sys-color-primary)" /> 384 + <span className="md-body-medium">Keep a real watch history</span> 385 + </div> 386 + <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)"> 387 + <ListChecks className="size-4 text-(--md-sys-color-primary)" /> 388 + <span className="md-body-medium"> 389 + Build lists that stay useful 390 + </span> 391 + </div> 392 + <div className="flex items-center gap-2 text-(--md-sys-color-on-surface-variant)"> 393 + <Search className="size-4 text-(--md-sys-color-primary)" /> 394 + <span className="md-body-medium"> 395 + Discover without signing in 396 + </span> 397 + </div> 398 + </div> 399 + </div> 400 + </section> 235 401 </div> 236 402 ); 237 403 }
+2 -2
apps/web/src/routes/index.tsx
··· 8 8 export const Route = createFileRoute("/")({ 9 9 head: () => ({ 10 10 meta: [ 11 - { title: "Track Movies and Shows | OpnShelf" }, 11 + { title: "OpnShelf | Discover, Curate, and Own Your Watch History" }, 12 12 { 13 13 name: "description", 14 14 content: 15 - "Track movies and shows at movie, season, and episode level with watch history, lists, and AT Protocol account portability.", 15 + "Browse movies and shows, build custom lists, import your history, and keep your data portable with AT Protocol identity.", 16 16 }, 17 17 ], 18 18 }),