Revert "Persist dashboard periods and explore genre filters in URL via nuqs"

This reverts commit 2bb5af042b.
This commit is contained in:
2026-03-08 15:14:03 -04:00
parent 2bb5af042b
commit 5fdc7ebd97
12 changed files with 70 additions and 213 deletions
@@ -6,7 +6,6 @@ import {
IconMovie,
IconPlayerPlay,
} from "@tabler/icons-react";
import { useQueryStates } from "nuqs";
import { useEffect, useState } from "react";
import {
Select,
@@ -21,7 +20,6 @@ import type {
HistoryBucket,
TimePeriod,
} from "@/lib/services/discovery";
import { dashboardSearchParams, periods } from "../search-params";
import { Sparkline } from "./sparkline";
const periodLabels: Record<TimePeriod, string> = {
@@ -31,6 +29,8 @@ const periodLabels: Record<TimePeriod, string> = {
this_year: "This Year",
};
const periods: TimePeriod[] = ["today", "this_week", "this_month", "this_year"];
interface StatCardProps {
icon: React.ComponentType<{ className?: string }>;
color: string;
@@ -120,9 +120,8 @@ function PeriodSelector({
}
export function StatsDisplay({ stats }: { stats: DashboardStats }) {
const [{ moviePeriod, episodePeriod }, setPeriods] = useQueryStates(
dashboardSearchParams,
);
const [moviePeriod, setMoviePeriod] = useState<TimePeriod>("this_month");
const [episodePeriod, setEpisodePeriod] = useState<TimePeriod>("this_week");
const [movieStats, setMovieStats] = useState<{
count: number;
history: HistoryBucket[];
@@ -140,10 +139,10 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
void getStatsAction("episodes", episodePeriod).then(setEpisodeStats);
}, [episodePeriod]);
const movieCount = movieStats?.count ?? stats.movieCount;
const movieCount = movieStats?.count ?? stats.moviesThisMonth;
const movieHistory = movieStats?.history;
const episodeCount = episodeStats?.count ?? stats.episodeCount;
const episodeCount = episodeStats?.count ?? stats.episodesThisWeek;
const episodeHistory = episodeStats?.history;
return (
@@ -159,7 +158,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
<PeriodSelector
noun="Movies"
period={moviePeriod}
onPeriodChange={(p) => setPeriods({ moviePeriod: p })}
onPeriodChange={setMoviePeriod}
/>
}
/>
@@ -174,7 +173,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
<PeriodSelector
noun="Episodes"
period={episodePeriod}
onPeriodChange={(p) => setPeriods({ episodePeriod: p })}
onPeriodChange={setEpisodePeriod}
/>
}
/>
@@ -1,22 +1,14 @@
import { IconDeviceTv } from "@tabler/icons-react";
import Link from "next/link";
import { getUserStats, type TimePeriod } from "@/lib/services/discovery";
import { getUserStats } from "@/lib/services/discovery";
import { StatsDisplay } from "./stats-display";
export async function StatsSection({
userId,
moviePeriod,
episodePeriod,
}: {
userId: string;
moviePeriod: TimePeriod;
episodePeriod: TimePeriod;
}) {
const stats = getUserStats(userId, moviePeriod, episodePeriod);
export async function StatsSection({ userId }: { userId: string }) {
const stats = await getUserStats(userId);
const isEmpty =
stats.movieCount === 0 &&
stats.episodeCount === 0 &&
stats.moviesThisMonth === 0 &&
stats.episodesThisWeek === 0 &&
stats.librarySize === 0 &&
stats.completed === 0;
+2 -15
View File
@@ -1,4 +1,3 @@
import type { SearchParams } from "nuqs/server";
import { Suspense } from "react";
import {
ContinueWatchingSectionSkeleton,
@@ -11,29 +10,17 @@ import { LibrarySection } from "./_components/library-section";
import { RecommendationsSection } from "./_components/recommendations-section";
import { StatsSection } from "./_components/stats-section";
import { WelcomeHeader } from "./_components/welcome-header";
import { loadDashboardSearchParams } from "./search-params";
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
export default async function DashboardPage() {
const session = await getSession();
if (!session) return null;
const { moviePeriod, episodePeriod } =
await loadDashboardSearchParams(searchParams);
return (
<div className="space-y-10">
<WelcomeHeader name={session.user.name} />
<Suspense fallback={<StatsSectionSkeleton />}>
<StatsSection
userId={session.user.id}
moviePeriod={moviePeriod}
episodePeriod={episodePeriod}
/>
<StatsSection userId={session.user.id} />
</Suspense>
<Suspense fallback={<ContinueWatchingSectionSkeleton />}>
-15
View File
@@ -1,15 +0,0 @@
import { createLoader, parseAsStringLiteral } from "nuqs/server";
export const periods = [
"today",
"this_week",
"this_month",
"this_year",
] as const;
export const dashboardSearchParams = {
moviePeriod: parseAsStringLiteral(periods).withDefault("this_month"),
episodePeriod: parseAsStringLiteral(periods).withDefault("this_week"),
};
export const loadDashboardSearchParams = createLoader(dashboardSearchParams);
@@ -1,7 +1,5 @@
"use client";
import Link from "next/link";
import { useQueryStates } from "nuqs";
import { useState, useTransition } from "react";
import { TitleCardSkeleton } from "@/components/skeletons";
import { TitleCard } from "@/components/title-card";
@@ -12,7 +10,6 @@ import {
fetchEpisodeProgress,
fetchUserStatuses,
} from "@/lib/actions/watchlist";
import { exploreSearchParams, serializeExploreParams } from "../search-params";
interface Genre {
id: number;
@@ -35,7 +32,6 @@ interface FilterableTitleRowProps {
icon: React.ReactNode;
mediaType: "movie" | "tv";
defaultItems: TitleRowItem[];
initialGenreItems: TitleRowItem[] | null;
genres: Genre[];
userStatuses?: Record<string, TitleStatus>;
episodeProgress?: Record<string, { watched: number; total: number }>;
@@ -46,28 +42,18 @@ export function FilterableTitleRow({
icon,
mediaType,
defaultItems,
initialGenreItems,
genres,
userStatuses: initialStatuses = {},
episodeProgress: initialProgress = {},
}: FilterableTitleRowProps) {
const paramKey = mediaType === "movie" ? "movieGenre" : ("tvGenre" as const);
const [params, setParams] = useQueryStates(exploreSearchParams, {
history: "push",
});
const selectedGenre = params[paramKey];
const setSelectedGenre = (value: number | null) =>
setParams({ [paramKey]: value });
const [genreResults, setGenreResults] = useState<TitleRowItem[] | null>(
initialGenreItems,
);
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
const [genreResults, setGenreResults] = useState<TitleRowItem[] | null>(null);
const [genreStatuses, setGenreStatuses] = useState<
Record<string, TitleStatus>
>(initialGenreItems ? initialStatuses : {});
>({});
const [genreProgress, setGenreProgress] = useState<
Record<string, { watched: number; total: number }>
>(initialGenreItems ? initialProgress : {});
>({});
const [isPending, startTransition] = useTransition();
const items = selectedGenre === null ? defaultItems : (genreResults ?? []);
@@ -117,37 +103,26 @@ export function FilterableTitleRow({
{/* Genre chips */}
<ScrollArea scrollFade hideScrollbar>
<div className="flex gap-2">
{genres.map((genre) => {
const isSelected = selectedGenre === genre.id;
const href = serializeExploreParams("/explore", {
...params,
[paramKey]: isSelected ? null : genre.id,
});
return (
<Button
key={genre.id}
render={<Link href={href} prefetch={false} />}
variant={isSelected ? "default" : "outline"}
size="sm"
onClick={(e) => {
e.preventDefault();
toggleGenre(genre.id);
}}
className={`shrink-0 rounded-full ${
isSelected
? "border-primary bg-primary/10 text-primary hover:bg-primary/20"
: "border-border/50 bg-card/50 text-muted-foreground hover:border-primary/20 hover:text-foreground"
}`}
>
{genre.name}
</Button>
);
})}
{genres.map((genre) => (
<Button
key={genre.id}
variant={selectedGenre === genre.id ? "default" : "outline"}
size="sm"
onClick={() => toggleGenre(genre.id)}
className={`shrink-0 rounded-full ${
selectedGenre === genre.id
? "border-primary bg-primary/10 text-primary hover:bg-primary/20"
: "border-border/50 bg-card/50 text-muted-foreground hover:border-primary/20 hover:text-foreground"
}`}
>
{genre.name}
</Button>
))}
</div>
</ScrollArea>
{/* Loading skeleton */}
{(isPending || (selectedGenre !== null && genreResults === null)) && (
{isPending && (
<div className="-mx-4 flex gap-4 overflow-hidden px-4 sm:-mx-0 sm:px-0">
{Array.from({ length: 8 }).map((_, i) => (
<div
@@ -162,14 +137,11 @@ export function FilterableTitleRow({
)}
{/* Empty state */}
{!isPending &&
selectedGenre !== null &&
genreResults !== null &&
items.length === 0 && (
<p className="py-8 text-center text-muted-foreground text-sm">
No titles found for this genre.
</p>
)}
{!isPending && selectedGenre !== null && items.length === 0 && (
<p className="py-8 text-center text-muted-foreground text-sm">
No titles found for this genre.
</p>
)}
{/* Title cards */}
{!isPending && items.length > 0 && (
+12 -64
View File
@@ -1,21 +1,14 @@
import { IconDeviceTv, IconFlame, IconMovie } from "@tabler/icons-react";
import type { SearchParams } from "nuqs/server";
import { getSession } from "@/lib/auth/session";
import {
getEpisodeProgressByTmdbIds,
getUserStatusesByTmdbIds,
} from "@/lib/services/tracking";
import {
discover,
getGenres,
getPopular,
getTrending,
} from "@/lib/tmdb/client";
import { getGenres, getPopular, getTrending } from "@/lib/tmdb/client";
import { tmdbImageUrl } from "@/lib/tmdb/image";
import { FilterableTitleRow } from "./_components/filterable-title-row";
import { HeroBanner } from "./_components/hero-banner";
import { TitleRow } from "./_components/title-row";
import { loadExploreSearchParams } from "./search-params";
function mapResults(
results: {
@@ -44,33 +37,6 @@ function mapResults(
}));
}
async function discoverByGenreServer(
mediaType: "movie" | "tv",
genreId: number,
) {
const results = await discover(mediaType, {
sort_by: "popularity.desc",
"vote_count.gte": "50",
with_genres: String(genreId),
});
type DiscoverResult = NonNullable<typeof results.results>[number] & {
title?: string;
name?: string;
release_date?: string;
first_air_date?: string;
};
return ((results.results ?? []) as DiscoverResult[])
.filter((r) => r.poster_path)
.map((r) => ({
tmdbId: r.id,
type: mediaType,
title: r.title ?? r.name ?? "",
posterPath: tmdbImageUrl(r.poster_path ?? null, "posters"),
releaseDate: r.release_date ?? r.first_air_date ?? null,
voteAverage: r.vote_average,
}));
}
async function getExploreTmdbData() {
const [trending, popularMovies, popularTv, movieGenres, tvGenres] =
await Promise.all([
@@ -97,35 +63,19 @@ async function getExploreTmdbData() {
};
}
export default async function ExplorePage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
export default async function ExplorePage() {
// Await session first — its headers() call triggers the dynamic bailout during
// PPR static generation, preventing TMDB fetches from firing at build time.
const session = await getSession();
const [
{ movieGenre, tvGenre },
{
trending,
trendingItems,
popularMovieItems,
popularTvItems,
movieGenres,
tvGenres,
},
] = await Promise.all([
loadExploreSearchParams(searchParams),
getExploreTmdbData(),
]);
// Pre-fetch genre-filtered results server-side when URL has genre params
const [movieGenreItems, tvGenreItems] = await Promise.all([
movieGenre ? discoverByGenreServer("movie", movieGenre) : null,
tvGenre ? discoverByGenreServer("tv", tvGenre) : null,
]);
const {
trending,
trendingItems,
popularMovieItems,
popularTvItems,
movieGenres,
tvGenres,
} = await getExploreTmdbData();
// Fetch user statuses and episode progress for all visible TMDB IDs
let userStatuses: Record<string, "watchlist" | "in_progress" | "completed"> =
@@ -134,8 +84,8 @@ export default async function ExplorePage({
if (session) {
const allItems = [
...trendingItems,
...(movieGenreItems ?? popularMovieItems),
...(tvGenreItems ?? popularTvItems),
...popularMovieItems,
...popularTvItems,
];
const tmdbLookups = allItems.map((i) => ({
tmdbId: i.tmdbId,
@@ -183,7 +133,6 @@ export default async function ExplorePage({
icon={<IconMovie aria-hidden={true} className="size-5 text-primary" />}
mediaType="movie"
defaultItems={popularMovieItems.slice(0, 20)}
initialGenreItems={movieGenreItems?.slice(0, 20) ?? null}
genres={movieGenres}
userStatuses={userStatuses}
episodeProgress={episodeProgress}
@@ -196,7 +145,6 @@ export default async function ExplorePage({
}
mediaType="tv"
defaultItems={popularTvItems.slice(0, 20)}
initialGenreItems={tvGenreItems?.slice(0, 20) ?? null}
genres={tvGenres}
userStatuses={userStatuses}
episodeProgress={episodeProgress}
-9
View File
@@ -1,9 +0,0 @@
import { createLoader, createSerializer, parseAsInteger } from "nuqs/server";
export const exploreSearchParams = {
movieGenre: parseAsInteger,
tvGenre: parseAsInteger,
};
export const loadExploreSearchParams = createLoader(exploreSearchParams);
export const serializeExploreParams = createSerializer(exploreSearchParams);
+6 -9
View File
@@ -2,7 +2,6 @@ import { Provider as StoreProvider } from "jotai";
import { MotionConfig } from "motion/react";
import type { Metadata, Viewport } from "next";
import { DM_Sans, DM_Serif_Display, Geist_Mono } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
@@ -55,14 +54,12 @@ export default function RootLayout({
>
Skip to main content
</a>
<NuqsAdapter>
<StoreProvider>
<MotionConfig reducedMotion="user">
<TooltipProvider>{children}</TooltipProvider>
<Toaster position="bottom-right" />
</MotionConfig>
</StoreProvider>
</NuqsAdapter>
<StoreProvider>
<MotionConfig reducedMotion="user">
<TooltipProvider>{children}</TooltipProvider>
<Toaster position="bottom-right" />
</MotionConfig>
</StoreProvider>
</body>
</html>
);