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:
2026-03-26 12:58:26 -04:00
co-authored by Claude Opus 4.6
parent 73e5c96bd8
commit d2ddc0fd34
23 changed files with 188 additions and 386 deletions
@@ -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">
+6 -6
View File
@@ -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>
+7 -1
View File
@@ -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(