import { Trans, useLingui } from "@lingui/react/macro"; import { IconCalendarEvent } from "@tabler/icons-react"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; import { UpcomingRow } from "@/components/dashboard/upcoming-item"; import { RouteError } from "@/components/route-error"; import { Skeleton } from "@/components/ui/skeleton"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; import { orpc } from "@/lib/orpc/client"; import { groupByDateBucket } from "@sofa/i18n/date-buckets"; const upcomingSearchSchema = z.object({ view: z.enum(["upcoming", "recent"]).optional().catch(undefined), type: z.enum(["all", "movie", "tv"]).optional().catch(undefined), status: z.enum(["all", "watching", "watchlist"]).optional().catch(undefined), }); export const Route = createFileRoute("/_app/upcoming")({ validateSearch: zodValidator(upcomingSearchSchema), staleTime: 30_000, loader: async ({ context }) => { await context.queryClient.ensureInfiniteQueryData( orpc.library.upcoming.infiniteOptions({ input: (pageParam: string | undefined) => ({ days: 90, limit: 20, cursor: pageParam, }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, }), ); }, head: () => ({ meta: [{ title: "Upcoming — Sofa" }] }), pendingComponent: UpcomingSkeleton, errorComponent: RouteError, component: UpcomingPage, }); function UpcomingSkeleton() { return (
{Array.from({ length: 5 }, (_, i) => (
))}
); } function UpcomingPage() { const { t } = useLingui(); const search = Route.useSearch(); const navigate = useNavigate({ from: Route.fullPath }); const view = search.view ?? "upcoming"; const isRecent = view === "recent"; const typeFilter = search.type ?? "all"; const statusFilter = search.status ?? "all"; const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage, isFetchNextPageError } = useInfiniteQuery( orpc.library.upcoming.infiniteOptions({ input: (pageParam: string | undefined) => ({ days: 90, limit: 20, cursor: pageParam, direction: isRecent ? "recent" : undefined, mediaType: !isRecent && typeFilter !== "all" ? (typeFilter as "movie" | "tv") : undefined, statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter as "watching" | "watchlist"] : undefined, }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, }), ); const sentinelRef = useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPage, isFetchNextPageError, }); function setView(value: "upcoming" | "recent") { void navigate({ search: (prev) => ({ ...prev, view: value === "upcoming" ? undefined : value, }), replace: true, }); } function setTypeFilter(value: string) { void navigate({ search: (prev) => ({ ...prev, type: value === "all" ? undefined : (value as "movie" | "tv"), }), replace: true, }); } function setStatusFilter(value: string) { void navigate({ search: (prev) => ({ ...prev, status: value === "all" ? undefined : (value as "watching" | "watchlist"), }), replace: true, }); } const filterToggles = (
{ const next = values.find((v) => v !== view); if (next === "upcoming" || next === "recent") setView(next); }} variant="outline" size="sm" > {t`Upcoming`} {t`Recently aired`} {!isRecent && (
{ const next = values.find((v) => v !== typeFilter) ?? "all"; setTypeFilter(next); }} variant="outline" size="sm" > {t`All`} {t`Movies`} {t`TV Shows`} { const next = values.find((v) => v !== statusFilter) ?? "all"; setStatusFilter(next); }} variant="outline" size="sm" > {t`All`} {t`Watching`} {t`Watchlist`}
)}
); if (isPending) return ; const allItems = data?.pages.flatMap((p) => p.items) ?? []; if (allItems.length === 0) { return (
{filterToggles}

{isRecent ? ( You're all caught up on the last 90 days. ) : ( No upcoming episodes or releases in the next 90 days. )}

); } const buckets = groupByDateBucket(allItems, { past: isRecent }); return (
{filterToggles} {buckets.map((bucket) => (

{bucket.label}

{bucket.items.map((item, i) => ( ))}
))}
{isFetchingNextPage && }
); } function UpcomingHeader({ view }: { view: "upcoming" | "recent" }) { const { t } = useLingui(); return (

{view === "recent" ? t`Recently aired` : t`Upcoming`}

{view === "recent" ? ( Unwatched episodes from the last 90 days. ) : ( Episodes and movies coming up in the next 90 days. )}

); }