diff --git a/app/(pages)/dashboard/_components/library-section.tsx b/app/(pages)/dashboard/_components/library-section.tsx index b2f5adc..f8f4c31 100644 --- a/app/(pages)/dashboard/_components/library-section.tsx +++ b/app/(pages)/dashboard/_components/library-section.tsx @@ -1,4 +1,4 @@ -import { IconSparkles } from "@tabler/icons-react"; +import { IconBooks } from "@tabler/icons-react"; import { getNewAvailableFeed } from "@/lib/services/discovery"; import { tmdbImageUrl } from "@/lib/tmdb/image"; import { FeedSection } from "./feed-section"; @@ -21,7 +21,7 @@ export async function LibrarySection({ userId }: { userId: string }) { return ( } + icon={} > diff --git a/app/(pages)/dashboard/_components/stats-display.tsx b/app/(pages)/dashboard/_components/stats-display.tsx index 89cd713..bb2ae2a 100644 --- a/app/(pages)/dashboard/_components/stats-display.tsx +++ b/app/(pages)/dashboard/_components/stats-display.tsx @@ -82,14 +82,11 @@ function StatCard({ {label} - {value} - +

); } diff --git a/app/(pages)/dashboard/_components/stats-section.tsx b/app/(pages)/dashboard/_components/stats-section.tsx index cce6988..ea90fd7 100644 --- a/app/(pages)/dashboard/_components/stats-section.tsx +++ b/app/(pages)/dashboard/_components/stats-section.tsx @@ -17,7 +17,7 @@ export async function StatsSection({ userId }: { userId: string }) { {isEmpty && (
-
+
diff --git a/app/(pages)/explore/filterable-title-row.tsx b/app/(pages)/explore/filterable-title-row.tsx index 7a75c78..93a508b 100644 --- a/app/(pages)/explore/filterable-title-row.tsx +++ b/app/(pages)/explore/filterable-title-row.tsx @@ -137,7 +137,7 @@ function FilterableTitleRowInner({
{/* Genre chips */} -
+
{genres.map((genre) => ( )} diff --git a/app/(pages)/titles/[id]/_components/title-cast.tsx b/app/(pages)/titles/[id]/_components/title-cast.tsx index e4e805d..89a5247 100644 --- a/app/(pages)/titles/[id]/_components/title-cast.tsx +++ b/app/(pages)/titles/[id]/_components/title-cast.tsx @@ -8,9 +8,8 @@ interface TitleCastProps { export function TitleCast({ cast, titleType }: TitleCastProps) { const actors = cast.filter((c) => c.department === "Acting"); - const crew = cast.filter((c) => c.department !== "Acting"); - if (actors.length === 0 && crew.length === 0) return null; + if (actors.length === 0) return null; - return ; + return ; } diff --git a/app/(pages)/titles/[id]/_components/title-hero.tsx b/app/(pages)/titles/[id]/_components/title-hero.tsx index f8d1936..0114b97 100644 --- a/app/(pages)/titles/[id]/_components/title-hero.tsx +++ b/app/(pages)/titles/[id]/_components/title-hero.tsx @@ -57,6 +57,11 @@ export function TitleHero({ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`, }} /> + {trailerVideoKey && ( +
+ +
+ )}
)} @@ -122,7 +127,6 @@ export function TitleHero({ > - {trailerVideoKey && }
diff --git a/app/(pages)/titles/[id]/_components/title-seasons.tsx b/app/(pages)/titles/[id]/_components/title-seasons.tsx index da54f90..f64934e 100644 --- a/app/(pages)/titles/[id]/_components/title-seasons.tsx +++ b/app/(pages)/titles/[id]/_components/title-seasons.tsx @@ -5,6 +5,7 @@ import { IconChecks, IconChevronDown, IconChevronUp, + IconDeviceTvOld, } from "@tabler/icons-react"; import { useAtomValue } from "jotai"; import { AnimatePresence, motion } from "motion/react"; @@ -47,7 +48,10 @@ export function TitleSeasons() { return (
-

Seasons

+
+ +

Seasons

+
{userStatus && userStatus !== "completed" && ( import("youtube-video-element/react"), { + ssr: false, +}); +const MediaThemeSutro = dynamic(() => import("@player.style/sutro/react"), { + ssr: false, +}); - useEffect(() => { - if (open && !loaded) { - import("youtube-video-element"); - import("@player.style/sutro"); - setLoaded(true); - } - }, [open, loaded]); +export function TrailerDialog({ + videoKey, + variant = "badge", +}: { + videoKey: string; + variant?: "badge" | "backdrop"; +}) { + const [open, setOpen] = useState(false); return ( - - + {variant === "backdrop" ? ( + + ) : ( + + )} + Trailer
- {loaded && ( - // @ts-expect-error -- web components not in JSX types - - {/* @ts-expect-error -- web component */} - - {/* @ts-expect-error -- web component */} - - )} + } as React.CSSProperties + } + > + +
diff --git a/app/(pages)/titles/[id]/page.tsx b/app/(pages)/titles/[id]/page.tsx index d349dc2..4a576a1 100644 --- a/app/(pages)/titles/[id]/page.tsx +++ b/app/(pages)/titles/[id]/page.tsx @@ -94,10 +94,10 @@ export default async function TitleDetailPage({ - - {title.type === "tv" && seasons.length > 0 && } + + diff --git a/app/globals.css b/app/globals.css index 2cd7870..a36deae 100644 --- a/app/globals.css +++ b/app/globals.css @@ -95,122 +95,51 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground antialiased; + @apply bg-background text-foreground; + } + /* Film grain texture overlay */ + body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + opacity: 0.015; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); } + /* Restore hand cursor on interactive elements */ /* https://ui.shadcn.com/docs/components/radix/button#cursor */ button:not(:disabled), [role="button"]:not(:disabled), [role="switch"]:not(:disabled) { cursor: pointer; } -} -/* Film grain texture overlay */ -body::before { - content: ""; - position: fixed; - inset: 0; - z-index: 9999; - pointer-events: none; - opacity: 0.015; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); -} - -/* Custom keyframe animations */ -@keyframes card-enter { - from { - opacity: 0; - transform: translateY(12px) scale(0.98); + /* Scrollbar styling */ + ::-webkit-scrollbar { + width: 6px; + height: 6px; } - to { - opacity: 1; - transform: translateY(0) scale(1); + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: oklch(1 0 0 / 15%); + border-radius: 3px; + } + ::-webkit-scrollbar-thumb:hover { + background: oklch(1 0 0 / 25%); } } -@keyframes check-pop { - 0% { - transform: scale(0); +@layer utilities { + /* Horizontal scroll row for feeds */ + .no-scrollbar { + scrollbar-width: none; + -ms-overflow-style: none; } - 60% { - transform: scale(1.15); - } - 100% { - transform: scale(1); + .no-scrollbar::-webkit-scrollbar { + display: none; } } - -@keyframes star-fill { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.25); - } - 100% { - transform: scale(1); - } -} - -@keyframes gentle-float { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-6px); - } -} - -/* Warm skeleton pulse override */ -@keyframes warm-pulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.4; - } -} - -[data-slot="skeleton"] { - animation: warm-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - background: oklch(0.25 0.01 55); -} - -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 6px; - height: 6px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: oklch(1 0 0 / 15%); - border-radius: 3px; -} -::-webkit-scrollbar-thumb:hover { - background: oklch(1 0 0 / 25%); -} - -/* Horizontal scroll row for feeds */ -.feed-scroll { - scrollbar-width: none; - -ms-overflow-style: none; -} -.feed-scroll::-webkit-scrollbar { - display: none; -} - -/* Utility animations */ -.animate-gentle-float { - animation: gentle-float 3s ease-in-out infinite; -} -.animate-check-pop { - animation: check-pop 0.3s ease-out; -} -.animate-star-fill { - animation: star-fill 0.3s ease-out; -} diff --git a/components/command-palette.tsx b/components/command-palette.tsx index 45219d7..0a0f700 100644 --- a/components/command-palette.tsx +++ b/components/command-palette.tsx @@ -91,7 +91,7 @@ export function CommandPalette() { (result: SearchResult) => { setCommandPaletteOpen(false); if (result.type === "person") { - router.push(`/person/tmdb-${result.tmdbId}`); + router.push(`/people/tmdb-${result.tmdbId}`); } else { router.push(`/titles/tmdb-${result.tmdbId}-${result.type}`); } diff --git a/components/title-card.tsx b/components/title-card.tsx index bba12e5..503c12d 100644 --- a/components/title-card.tsx +++ b/components/title-card.tsx @@ -11,7 +11,7 @@ import { IconPlus, IconStarFilled, } from "@tabler/icons-react"; -import { motion } from "motion/react"; + import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; @@ -173,10 +173,8 @@ function CardInner({ : "ring-white/[0.06]"; return ( -
{posterPath ? ( @@ -248,7 +246,7 @@ function CardInner({ total={episodeProgress.total} /> )} - +
); }