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>
);
+1 -10
View File
@@ -22,7 +22,6 @@
"motion": "12.35.1",
"next": "16.1.6",
"node-vibrant": "4.0.4",
"nuqs": "2.8.9",
"openapi-fetch": "0.17.0",
"react": "19.2.4",
"react-day-picker": "9.14.0",
@@ -476,7 +475,7 @@
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -1176,8 +1175,6 @@
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="],
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -1536,8 +1533,6 @@
"@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -1568,8 +1563,6 @@
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@reduxjs/toolkit/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
@@ -1598,8 +1591,6 @@
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
+4 -4
View File
@@ -122,8 +122,8 @@ describe("getUserStats", () => {
insertStatus("user-1", titleId, "in_progress");
const stats = getUserStats("user-1");
expect(stats.movieCount).toBe(2);
expect(stats.episodeCount).toBe(1);
expect(stats.moviesThisMonth).toBe(2);
expect(stats.episodesThisWeek).toBe(1);
expect(stats.librarySize).toBe(3);
expect(stats.completed).toBe(2);
});
@@ -131,8 +131,8 @@ describe("getUserStats", () => {
test("returns zeros when no data", () => {
insertUser();
const stats = getUserStats("user-1");
expect(stats.movieCount).toBe(0);
expect(stats.episodeCount).toBe(0);
expect(stats.moviesThisMonth).toBe(0);
expect(stats.episodesThisWeek).toBe(0);
expect(stats.librarySize).toBe(0);
expect(stats.completed).toBe(0);
});
+7 -11
View File
@@ -144,19 +144,15 @@ export function getWatchHistory(
}
export interface DashboardStats {
movieCount: number;
episodeCount: number;
moviesThisMonth: number;
episodesThisWeek: number;
librarySize: number;
completed: number;
}
export function getUserStats(
userId: string,
moviePeriod: TimePeriod = "this_month",
episodePeriod: TimePeriod = "this_week",
): DashboardStats {
const movieCount = getWatchCount(userId, "movies", moviePeriod);
const episodeCount = getWatchCount(userId, "episodes", episodePeriod);
export function getUserStats(userId: string): DashboardStats {
const moviesThisMonth = getWatchCount(userId, "movies", "this_month");
const episodesThisWeek = getWatchCount(userId, "episodes", "this_week");
const [statusCounts] = db
.select({
@@ -168,8 +164,8 @@ export function getUserStats(
.all();
return {
movieCount,
episodeCount,
moviesThisMonth,
episodesThisWeek,
librarySize: statusCounts?.librarySize ?? 0,
completed: statusCounts?.completed ?? 0,
};
-1
View File
@@ -36,7 +36,6 @@
"motion": "12.35.1",
"next": "16.1.6",
"node-vibrant": "4.0.4",
"nuqs": "2.8.9",
"openapi-fetch": "0.17.0",
"react": "19.2.4",
"react-day-picker": "9.14.0",