mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
Revert "Persist dashboard periods and explore genre filters in URL via nuqs"
This reverts commit 2bb5af042b.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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 />}>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user