Replace motion.js entry animations with CSS to eliminate hydration delay

Motion's initial="hidden" rendered opacity:0 in server HTML, making content
invisible until JS hydrated — causing a multi-second blank gap after skeleton
disappearance. Replaced with CSS @keyframes stagger animation that plays
immediately from SSR. Also made color extraction fire-and-forget, merged
duplicate stats queries, and removed redundant dashboard loading.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 16:26:02 -05:00
co-authored by Claude Opus 4.6
parent 9c6da8db53
commit 13d616a456
14 changed files with 184 additions and 331 deletions
@@ -1,7 +1,6 @@
"use client";
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
import { motion } from "motion/react";
import {
Carousel,
CarouselContent,
@@ -12,43 +11,29 @@ import {
type ContinueWatchingItemProps,
} from "./continue-watching-card";
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
export function ContinueWatchingList({
items,
}: {
items: ContinueWatchingItemProps[];
}) {
return (
<motion.div variants={staggerContainer} initial="hidden" animate="visible">
<Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-4 sm:-mx-0"
>
<CarouselContent className="px-4 sm:px-0">
{items.map((item) => (
<CarouselItem key={item.title.id} className="basis-auto pl-4">
<motion.div variants={staggerItem}>
<ContinueWatchingCard item={item} />
</motion.div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</motion.div>
<Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-4 sm:-mx-0"
>
<CarouselContent className="px-4 sm:px-0">
{items.map((item, i) => (
<CarouselItem key={item.title.id} className="basis-auto pl-4">
<div
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<ContinueWatchingCard item={item} />
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
);
}
@@ -8,7 +8,6 @@ import {
IconPlayerPlay,
} from "@tabler/icons-react";
import { useAtom, useAtomValue } from "jotai";
import { motion } from "motion/react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -56,20 +55,12 @@ function StatCard({
value,
index,
label,
loading,
sparklineData,
}: StatCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
stiffness: 300,
damping: 24,
delay: index * 0.08,
}}
className="relative overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4"
<div
className="animate-stagger-item relative overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4"
style={{ "--stagger-index": index } as React.CSSProperties}
>
{sparklineData && <Sparkline data={sparklineData} color={color} />}
<div className="relative z-10 flex items-center gap-2">
@@ -83,11 +74,12 @@ function StatCard({
</span>
</div>
<p
className={`relative z-10 mt-2 font-display text-2xl tabular-nums tracking-tight ${color} transition-opacity duration-300 ${loading ? "opacity-40" : "opacity-100"}`}
suppressHydrationWarning
className={`relative z-10 mt-2 font-display text-2xl tabular-nums tracking-tight ${color} transition-opacity duration-300`}
>
{value}
</p>
</motion.div>
</div>
);
}
@@ -131,7 +123,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
const movieStats = useAtomValue(movieStatsLoadable);
const episodeStats = useAtomValue(episodeStatsLoadable);
const movieLoading = movieStats.state === "loading";
const _movieLoading = movieStats.state === "loading";
const movieCount =
movieStats.state === "hasData"
? movieStats.data.count
@@ -139,7 +131,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
const movieHistory =
movieStats.state === "hasData" ? movieStats.data.history : undefined;
const episodeLoading = episodeStats.state === "loading";
const _episodeLoading = episodeStats.state === "loading";
const episodeCount =
episodeStats.state === "hasData"
? episodeStats.data.count
@@ -155,7 +147,6 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
bgColor="bg-primary/10"
value={movieCount}
index={0}
loading={movieLoading}
sparklineData={movieHistory}
label={
<PeriodSelector
@@ -171,7 +162,6 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
bgColor="bg-status-watching/10"
value={episodeCount}
index={1}
loading={episodeLoading}
sparklineData={episodeHistory}
label={
<PeriodSelector
@@ -1,23 +1,7 @@
"use client";
import { motion } from "motion/react";
import { TitleCard } from "@/components/title-card";
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
interface TitleGridItem {
id: string;
tmdbId: number;
@@ -30,14 +14,13 @@ interface TitleGridItem {
export function TitleGrid({ items }: { items: TitleGridItem[] }) {
return (
<motion.div
className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{items.map((t) => (
<motion.div key={t.id} variants={staggerItem}>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{items.map((t, i) => (
<div
key={t.id}
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<TitleCard
id={t.id}
tmdbId={t.tmdbId}
@@ -47,8 +30,8 @@ export function TitleGrid({ items }: { items: TitleGridItem[] }) {
releaseDate={t.releaseDate}
voteAverage={t.voteAverage}
/>
</motion.div>
</div>
))}
</motion.div>
</div>
);
}
-5
View File
@@ -1,5 +0,0 @@
import { DashboardSkeleton } from "@/components/skeletons";
export default function DashboardLoading() {
return <DashboardSkeleton />;
}
+8 -26
View File
@@ -2,7 +2,6 @@
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
import { createStore, Provider, useAtom, useAtomValue } from "jotai";
import { motion } from "motion/react";
import { useState } from "react";
import { TitleCardSkeleton } from "@/components/skeletons";
import { ExploreTitleCard } from "@/components/title-card";
@@ -46,21 +45,6 @@ interface FilterableTitleRowProps {
episodeProgress?: Record<string, { watched: number; total: number }>;
}
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
export function FilterableTitleRow({
heading,
icon,
@@ -178,12 +162,7 @@ function FilterableTitleRowInner({
{/* Carousel */}
{!loading && items.length > 0 && (
<motion.div
key={selectedGenre ?? "default"}
variants={staggerContainer}
initial="hidden"
animate="visible"
>
<div key={selectedGenre ?? "default"}>
<Carousel
opts={{
align: "start",
@@ -194,12 +173,15 @@ function FilterableTitleRowInner({
className="-mx-6 sm:-mx-2 carousel-tilt"
>
<CarouselContent className="px-6 sm:px-2">
{items.slice(0, 20).map((item) => (
{items.slice(0, 20).map((item, i) => (
<CarouselItem
key={`${item.type}-${item.tmdbId}`}
className="basis-auto pl-4 w-[140px] shrink-0 sm:w-[160px]"
>
<motion.div variants={staggerItem}>
<div
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<ExploreTitleCard
tmdbId={item.tmdbId}
type={item.type}
@@ -213,12 +195,12 @@ function FilterableTitleRowInner({
episodeProgress[`${item.tmdbId}-${item.type}`]
}
/>
</motion.div>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</motion.div>
</div>
)}
</section>
);
+6 -18
View File
@@ -1,7 +1,6 @@
"use client";
import { IconPlus, IconStar } from "@tabler/icons-react";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
@@ -25,12 +24,7 @@ export function HeroBanner({
const href = `/titles/tmdb-${tmdbId}-${type}`;
return (
<motion.div
className="relative -mt-6 mb-4 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)] overflow-hidden"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
<div className="animate-stagger-item relative -mt-6 mb-4 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)] overflow-hidden">
<div className="relative w-full min-h-[280px] max-h-[420px] aspect-[21/9]">
{backdropPath ? (
<Image
@@ -52,15 +46,9 @@ export function HeroBanner({
<div className="absolute inset-0 flex items-end">
<div className="w-full px-4 pb-8 sm:px-6">
<div className="mx-auto max-w-6xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
stiffness: 200,
damping: 24,
delay: 0.2,
}}
<div
className="animate-stagger-item"
style={{ "--stagger-index": 3 } as React.CSSProperties}
>
<div className="mb-3 flex items-center gap-2">
<span className="rounded bg-primary/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary">
@@ -91,11 +79,11 @@ export function HeroBanner({
<IconPlus className="size-4" />
Add to Library
</Link>
</motion.div>
</div>
</div>
</div>
</div>
</div>
</motion.div>
</div>
);
}
+31 -50
View File
@@ -1,7 +1,6 @@
"use client";
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
import { motion } from "motion/react";
import { ExploreTitleCard } from "@/components/title-card";
import {
Carousel,
@@ -26,21 +25,6 @@ interface TitleRowProps {
episodeProgress?: Record<string, { watched: number; total: number }>;
}
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
export function TitleRow({
heading,
icon,
@@ -56,42 +40,39 @@ export function TitleRow({
{icon}
<h2 className="font-display text-xl tracking-tight">{heading}</h2>
</div>
<motion.div
variants={staggerContainer}
initial="hidden"
animate="visible"
<Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-6 sm:-mx-2 carousel-tilt"
>
<Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-6 sm:-mx-2 carousel-tilt"
>
<CarouselContent className="px-6 sm:px-2">
{items.map((item) => (
<CarouselItem
key={`${item.type}-${item.tmdbId}`}
className="basis-auto pl-4 w-[140px] shrink-0 sm:w-[160px]"
<CarouselContent className="px-6 sm:px-2">
{items.map((item, i) => (
<CarouselItem
key={`${item.type}-${item.tmdbId}`}
className="basis-auto pl-4 w-[140px] shrink-0 sm:w-[160px]"
>
<div
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<motion.div variants={staggerItem}>
<ExploreTitleCard
tmdbId={item.tmdbId}
type={item.type}
title={item.title}
posterPath={item.posterPath}
releaseDate={item.releaseDate}
voteAverage={item.voteAverage}
href={`/titles/tmdb-${item.tmdbId}-${item.type}`}
userStatus={userStatuses?.[`${item.tmdbId}-${item.type}`]}
episodeProgress={
episodeProgress?.[`${item.tmdbId}-${item.type}`]
}
/>
</motion.div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</motion.div>
<ExploreTitleCard
tmdbId={item.tmdbId}
type={item.type}
title={item.title}
posterPath={item.posterPath}
releaseDate={item.releaseDate}
voteAverage={item.voteAverage}
href={`/titles/tmdb-${item.tmdbId}-${item.type}`}
userStatus={userStatuses?.[`${item.tmdbId}-${item.type}`]}
episodeProgress={
episodeProgress?.[`${item.tmdbId}-${item.type}`]
}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</section>
);
}
@@ -1,7 +1,6 @@
"use client";
import { IconMovie } from "@tabler/icons-react";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
@@ -10,21 +9,6 @@ import type { PersonCredit } from "@/lib/types/title";
type Filter = "all" | "movie" | "tv";
type Sort = "newest" | "rating";
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.04 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
interface FilmographyGridProps {
credits: PersonCredit[];
}
@@ -103,15 +87,16 @@ export function FilmographyGrid({ credits }: FilmographyGridProps) {
))}
</div>
<motion.div
<div
key={`${filter}-${sort}`}
variants={staggerContainer}
initial="hidden"
animate="visible"
className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
>
{filtered.map((credit) => (
<motion.div key={credit.titleId} variants={staggerItem}>
{filtered.map((credit, i) => (
<div
key={credit.titleId}
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<Link href={`/titles/${credit.titleId}`} className="group">
<div className="overflow-hidden rounded-xl bg-card ring-1 ring-white/[0.06] transition-all group-hover:ring-primary/25">
<div className="aspect-[2/3] w-full bg-muted">
@@ -150,9 +135,9 @@ export function FilmographyGrid({ credits }: FilmographyGridProps) {
</div>
</div>
</Link>
</motion.div>
</div>
))}
</motion.div>
</div>
</section>
);
}
@@ -1,7 +1,6 @@
"use client";
import { IconCalendar, IconMapPin } from "@tabler/icons-react";
import { motion } from "motion/react";
import Image from "next/image";
import { useState } from "react";
import type { ResolvedPerson } from "@/lib/types/title";
@@ -28,12 +27,7 @@ export function PersonHero({ person }: PersonHeroProps) {
: null;
return (
<motion.div
className="flex flex-col gap-6 sm:flex-row sm:gap-8"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 28 }}
>
<div className="animate-stagger-item flex flex-col gap-6 sm:flex-row sm:gap-8">
<div className="size-40 shrink-0 self-center overflow-hidden rounded-2xl ring-1 ring-white/10 shadow-2xl sm:size-56 sm:self-start">
{person.profilePath ? (
<Image
@@ -105,6 +99,6 @@ export function PersonHero({ person }: PersonHeroProps) {
</div>
)}
</div>
</motion.div>
</div>
);
}
@@ -2,7 +2,6 @@
import { IconUser, IconUsers } from "@tabler/icons-react";
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import {
@@ -12,21 +11,6 @@ import {
} from "@/components/ui/carousel";
import type { CastMember } from "@/lib/types/title";
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.04 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
interface CastCarouselProps {
actors: CastMember[];
titleType: "movie" | "tv";
@@ -41,69 +25,66 @@ export function CastCarousel({ actors, titleType }: CastCarouselProps) {
</div>
{actors.length > 0 && (
<motion.div
variants={staggerContainer}
initial="hidden"
animate="visible"
<Carousel
opts={{
align: "start",
dragFree: true,
containScroll: "trimSnaps",
}}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-4 sm:-mx-0"
>
<Carousel
opts={{
align: "start",
dragFree: true,
containScroll: "trimSnaps",
}}
plugins={[WheelGesturesPlugin({ forceWheelAxis: "x" })]}
className="-mx-4 sm:-mx-0"
>
<CarouselContent className="px-4 sm:px-0">
{actors.map((member) => (
<CarouselItem
key={member.id}
className="w-[100px] shrink-0 basis-auto pl-4 sm:w-[120px]"
<CarouselContent className="px-4 sm:px-0">
{actors.map((member, i) => (
<CarouselItem
key={member.id}
className="w-[100px] shrink-0 basis-auto pl-4 sm:w-[120px]"
>
<div
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<motion.div variants={staggerItem}>
<Link
href={`/people/${member.personId}`}
className="group flex flex-col items-center gap-2"
>
<div className="size-20 overflow-hidden rounded-full ring-1 ring-white/10 transition-all group-hover:ring-primary/25 sm:size-24">
{member.profilePath ? (
<Image
src={member.profilePath}
alt={member.name}
width={96}
height={96}
className="h-full w-full object-cover transition-transform group-hover:scale-105"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50">
<IconUser className="size-8 text-muted-foreground/50" />
</div>
)}
</div>
<div className="w-full text-center">
<p className="truncate text-xs font-medium">
{member.name}
<Link
href={`/people/${member.personId}`}
className="group flex flex-col items-center gap-2"
>
<div className="size-20 overflow-hidden rounded-full ring-1 ring-white/10 transition-all group-hover:ring-primary/25 sm:size-24">
{member.profilePath ? (
<Image
src={member.profilePath}
alt={member.name}
width={96}
height={96}
className="h-full w-full object-cover transition-transform group-hover:scale-105"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50">
<IconUser className="size-8 text-muted-foreground/50" />
</div>
)}
</div>
<div className="w-full text-center">
<p className="truncate text-xs font-medium">
{member.name}
</p>
{member.character && (
<p className="truncate text-[10px] text-muted-foreground">
{member.character}
</p>
{member.character && (
<p className="truncate text-[10px] text-muted-foreground">
{member.character}
</p>
)}
{titleType === "tv" && member.episodeCount && (
<p className="text-[10px] text-muted-foreground/70">
{member.episodeCount} ep
{member.episodeCount !== 1 ? "s" : ""}
</p>
)}
</div>
</Link>
</motion.div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</motion.div>
)}
{titleType === "tv" && member.episodeCount && (
<p className="text-[10px] text-muted-foreground/70">
{member.episodeCount} ep
{member.episodeCount !== 1 ? "s" : ""}
</p>
)}
</div>
</Link>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
)}
</section>
);
@@ -1,25 +1,9 @@
"use client";
import { IconSparkles } from "@tabler/icons-react";
import { motion } from "motion/react";
import { TitleCard } from "@/components/title-card";
import type { RecommendedTitle } from "@/lib/types/title";
const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const staggerItem = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 300, damping: 24 },
},
};
export function RecommendationsGrid({
recommendations,
}: {
@@ -31,14 +15,13 @@ export function RecommendationsGrid({
<IconSparkles className="size-5 text-primary" />
<h2 className="font-display text-2xl tracking-tight">Recommended</h2>
</div>
<motion.div
className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{recommendations.slice(0, 12).map((rec) => (
<motion.div key={rec.id} variants={staggerItem}>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{recommendations.slice(0, 12).map((rec, i) => (
<div
key={rec.id}
className="animate-stagger-item"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<TitleCard
id={rec.id}
tmdbId={rec.tmdbId}
@@ -48,9 +31,9 @@ export function RecommendationsGrid({
releaseDate={rec.releaseDate ?? rec.firstAirDate}
voteAverage={rec.voteAverage}
/>
</motion.div>
</div>
))}
</motion.div>
</div>
</div>
);
}
+17
View File
@@ -148,4 +148,21 @@
overflow-x: clip;
overflow-y: visible;
}
/* CSS stagger animation — plays immediately from SSR without waiting for JS hydration */
.animate-stagger-item {
animation: stagger-fade-in 0.35s ease-out both;
animation-delay: calc(var(--stagger-index, 0) * 0.04s);
}
}
@keyframes stagger-fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
+7 -15
View File
@@ -154,28 +154,20 @@ export function getUserStats(userId: string): DashboardStats {
const moviesThisMonth = getWatchCount(userId, "movies", "this_month");
const episodesThisWeek = getWatchCount(userId, "episodes", "this_week");
const [librarySizeRow] = db
.select({ count: sql<number>`count(*)` })
const [statusCounts] = db
.select({
librarySize: sql<number>`count(*)`,
completed: sql<number>`sum(case when ${userTitleStatus.status} = 'completed' then 1 else 0 end)`,
})
.from(userTitleStatus)
.where(eq(userTitleStatus.userId, userId))
.all();
const [completedCount] = db
.select({ count: sql<number>`count(*)` })
.from(userTitleStatus)
.where(
and(
eq(userTitleStatus.userId, userId),
eq(userTitleStatus.status, "completed"),
),
)
.all();
return {
moviesThisMonth,
episodesThisWeek,
librarySize: librarySizeRow?.count ?? 0,
completed: completedCount?.count ?? 0,
librarySize: statusCounts?.librarySize ?? 0,
completed: statusCounts?.completed ?? 0,
};
}
+3 -6
View File
@@ -667,13 +667,10 @@ export async function getTitleWithChildren(id: string): Promise<{
}),
);
// Lazy color extraction — await so the palette is available for theming
let palette = parseColorPalette(title.colorPalette);
// Color extraction — use cached palette if available, otherwise fire-and-forget
const palette = parseColorPalette(title.colorPalette);
if (!palette && title.posterPath) {
palette =
(await extractAndStoreColors(title.id, title.posterPath).catch(
() => null,
)) ?? null;
extractAndStoreColors(title.id, title.posterPath).catch(() => {});
}
const resolvedTitle: ResolvedTitle = {