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 />;
}