mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
fix: address review feedback on API reorganization
- Fix updateRating invalidation in native app — was only invalidating title queries, now calls invalidateTitleQueries() to also refresh tracking.userInfo (drives the rating UI) - Make unwatchMovie status revert consistent with unwatchSeries — revert any non-watchlist status, not just "completed" - Add sync invariant comment on handleWatch/handleUnwatch loops - Remove unused queryClient import in native use-title-actions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import type { DashboardStats, HistoryBucket, TimePeriod } from "@sofa/api/schemas";
|
||||
import type { HistoryBucket, LibraryStats, TimePeriod } from "@sofa/api/schemas";
|
||||
|
||||
import { Sparkline } from "./sparkline";
|
||||
|
||||
@@ -152,27 +152,38 @@ function PeriodSelector({
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
interface WatchStats {
|
||||
count: number;
|
||||
history: HistoryBucket[];
|
||||
}
|
||||
|
||||
interface StatsDisplayProps {
|
||||
movieStats: WatchStats;
|
||||
episodeStats: WatchStats;
|
||||
libraryStats: LibraryStats;
|
||||
}
|
||||
|
||||
export function StatsDisplay({ movieStats, episodeStats, libraryStats }: StatsDisplayProps) {
|
||||
const { t } = useLingui();
|
||||
const [moviePeriod, setMoviePeriod] = useState<TimePeriod>("this_month");
|
||||
const [episodePeriod, setEpisodePeriod] = useState<TimePeriod>("this_week");
|
||||
|
||||
const { data: movieStats } = useQuery(
|
||||
orpc.tracking.history.queryOptions({
|
||||
const { data: moviePeriodStats } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "movie", period: moviePeriod },
|
||||
}),
|
||||
);
|
||||
const { data: episodeStats } = useQuery(
|
||||
orpc.tracking.history.queryOptions({
|
||||
const { data: episodePeriodStats } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "episode", period: episodePeriod },
|
||||
}),
|
||||
);
|
||||
|
||||
const movieCount = movieStats?.count ?? stats.moviesThisMonth;
|
||||
const movieHistory = movieStats?.history;
|
||||
const movieCount = moviePeriodStats?.count ?? movieStats.count;
|
||||
const movieHistory = moviePeriodStats?.history ?? movieStats.history;
|
||||
|
||||
const episodeCount = episodeStats?.count ?? stats.episodesThisWeek;
|
||||
const episodeHistory = episodeStats?.history;
|
||||
const episodeCount = episodePeriodStats?.count ?? episodeStats.count;
|
||||
const episodeHistory = episodePeriodStats?.history ?? episodeStats.history;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
@@ -206,7 +217,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
icon={IconBooks}
|
||||
color="text-status-watchlist"
|
||||
bgColor="bg-status-watchlist/10"
|
||||
value={stats.librarySize}
|
||||
value={libraryStats.size}
|
||||
index={2}
|
||||
label={t`In Library`}
|
||||
/>
|
||||
@@ -214,7 +225,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
icon={IconCheck}
|
||||
color="text-status-completed"
|
||||
bgColor="bg-status-completed/10"
|
||||
value={stats.completed}
|
||||
value={libraryStats.completed}
|
||||
index={3}
|
||||
label={t`Completed`}
|
||||
/>
|
||||
|
||||
@@ -8,20 +8,34 @@ import { orpc } from "@/lib/orpc/client";
|
||||
import { StatsDisplay, StatsSectionSkeleton } from "./stats-display";
|
||||
|
||||
export function StatsSection() {
|
||||
const { data: stats, isPending } = useQuery(orpc.tracking.stats.queryOptions());
|
||||
const { data: movieStats, isPending: moviesPending } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "movie", period: "this_month" } }),
|
||||
);
|
||||
const { data: episodeStats, isPending: episodesPending } = useQuery(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "episode", period: "this_week" } }),
|
||||
);
|
||||
const { data: libraryStats, isPending: libraryPending } = useQuery(
|
||||
orpc.library.stats.queryOptions(),
|
||||
);
|
||||
|
||||
const isPending = moviesPending || episodesPending || libraryPending;
|
||||
|
||||
if (isPending) return <StatsSectionSkeleton />;
|
||||
if (!stats) return null;
|
||||
if (!movieStats || !episodeStats || !libraryStats) return null;
|
||||
|
||||
const isEmpty =
|
||||
stats.moviesThisMonth === 0 &&
|
||||
stats.episodesThisWeek === 0 &&
|
||||
stats.librarySize === 0 &&
|
||||
stats.completed === 0;
|
||||
movieStats.count === 0 &&
|
||||
episodeStats.count === 0 &&
|
||||
libraryStats.size === 0 &&
|
||||
libraryStats.completed === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatsDisplay stats={stats} />
|
||||
<StatsDisplay
|
||||
movieStats={movieStats}
|
||||
episodeStats={episodeStats}
|
||||
libraryStats={libraryStats}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<div className="border-border/50 flex flex-col items-center gap-4 rounded-xl border border-dashed py-16 text-center">
|
||||
<div className="bg-primary/10 rounded-full p-4">
|
||||
|
||||
@@ -88,8 +88,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
const statusConfig = useStatusConfig();
|
||||
const [optimisticStatus, setOptimisticStatus] = useState<TitleStatus | null>(null);
|
||||
|
||||
const quickAddMutation = useMutation(
|
||||
orpc.tracking.quickAdd.mutationOptions({
|
||||
const addToWatchlistMutation = useMutation(
|
||||
orpc.tracking.updateStatus.mutationOptions({
|
||||
onSuccess: () => setOptimisticStatus("in_watchlist"),
|
||||
}),
|
||||
);
|
||||
@@ -101,8 +101,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
function handleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (quickAddMutation.isPending || isAdded) return;
|
||||
quickAddMutation.mutate({ id });
|
||||
if (addToWatchlistMutation.isPending || isAdded) return;
|
||||
addToWatchlistMutation.mutate({ id, status: "watchlist" });
|
||||
}
|
||||
|
||||
if (isAdded && config) {
|
||||
@@ -127,8 +127,8 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
className="absolute end-2 top-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
render={<button type="button" />}
|
||||
>
|
||||
{!quickAddMutation.isPending && <IconPlus className="size-4" />}
|
||||
{quickAddMutation.isPending && <IconLoader className="size-4 animate-spin" />}
|
||||
{!addToWatchlistMutation.isPending && <IconPlus className="size-4" />}
|
||||
{addToWatchlistMutation.isPending && <IconLoader className="size-4 animate-spin" />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{t`Add to Watchlist`}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -17,7 +17,13 @@ export const Route = createFileRoute("/_app/dashboard")({
|
||||
staleTime: 30_000,
|
||||
loader: async ({ context }) => {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(orpc.tracking.stats.queryOptions()),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "movie", period: "this_month" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(
|
||||
orpc.tracking.stats.queryOptions({ input: { type: "episode", period: "this_week" } }),
|
||||
),
|
||||
context.queryClient.ensureQueryData(orpc.library.stats.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.library.continueWatching.queryOptions()),
|
||||
context.queryClient.ensureQueryData(orpc.discover.recommendations.queryOptions()),
|
||||
context.queryClient.ensureQueryData(
|
||||
|
||||
Reference in New Issue
Block a user