import { Trans, useLingui } from "@lingui/react/macro"; import { IconAlertTriangle, IconCalendarEvent } from "@tabler/icons-react-native"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Stack } from "expo-router"; import { useCallback, useMemo, useState } from "react"; import { Pressable, RefreshControl, ScrollView, SectionList, View } from "react-native"; import Animated, { FadeInDown } from "react-native-reanimated"; import { useCSSVariable, useResolveClassNames } from "uniwind"; import { UpcomingRow } from "@/components/dashboard/upcoming-row"; import { EmptyState } from "@/components/ui/empty-state"; import { ScaledIcon } from "@/components/ui/scaled-icon"; import { Text } from "@/components/ui/text"; import { usePullToRefresh } from "@/hooks/use-pull-to-refresh"; import { orpc } from "@/lib/orpc"; import { queryClient } from "@/lib/query-client"; import * as Haptics from "@/utils/haptics"; import { groupByDateBucket } from "@sofa/i18n/date-buckets"; const contentContainerStyle = { paddingBottom: 24 }; function FilterChip({ label, isSelected, onPress, }: { label: string; isSelected: boolean; onPress: () => void; }) { return ( { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onPress(); }} accessibilityRole="button" accessibilityState={{ selected: isSelected }} className={`rounded-full px-3 py-1.5 ${isSelected ? "bg-primary" : "bg-secondary"}`} > {label} ); } export default function UpcomingScreen() { const { t } = useLingui(); const headerTitleStyle = useResolveClassNames("font-display text-foreground text-xl"); const tintColor = useCSSVariable("--color-primary") as string; const backgroundColor = useCSSVariable("--color-background") as string; const mutedColor = useCSSVariable("--color-muted-foreground") as string; const [view, setView] = useState<"upcoming" | "recent">("upcoming"); const isRecent = view === "recent"; const [mediaType, setMediaType] = useState<"all" | "movie" | "tv">("all"); const [statusFilter, setStatusFilter] = useState<"all" | "watching" | "watchlist">("all"); const { data, isPending, isError, refetch, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery( orpc.library.upcoming.infiniteOptions({ input: (pageParam: string | undefined) => ({ days: 90, limit: 20, cursor: pageParam, direction: isRecent ? "recent" : undefined, mediaType: !isRecent && mediaType !== "all" ? mediaType : undefined, statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter] : undefined, }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, }), ); const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]); const sections = useMemo( () => groupByDateBucket(allItems, { past: view === "recent" }).map((b) => ({ key: b.key, title: b.label, data: b.items, })), [allItems, view], ); const refreshUpcoming = useCallback( () => queryClient.invalidateQueries({ queryKey: orpc.library.upcoming.key() }), [], ); const { refreshing, onRefresh } = usePullToRefresh(refreshUpcoming); const onEndReached = useCallback(() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, [hasNextPage, isFetchingNextPage, fetchNextPage]); const filterChips = ( setView("upcoming")} /> setView("recent")} /> {!isRecent && ( <> setMediaType("all")} /> setMediaType("movie")} /> setMediaType("tv")} /> setStatusFilter(statusFilter === "watching" ? "all" : "watching")} /> setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")} /> )} ); const isIOS = process.env.EXPO_OS === "ios"; return ( <> }> {t`Upcoming`} `${item.titleId}-${item.date}-${i}`} ListHeaderComponent={filterChips} renderItem={({ item }) => ( )} renderSectionHeader={({ section: { title } }) => ( {title} )} stickySectionHeadersEnabled contentContainerStyle={contentContainerStyle} contentInsetAdjustmentBehavior="automatic" onEndReached={onEndReached} onEndReachedThreshold={0.5} refreshControl={} ListEmptyComponent={ isPending ? null : isError && allItems.length === 0 ? ( refetch()} /> ) : ( {isRecent ? ( You're all caught up on the last 90 days. ) : ( No upcoming episodes or releases in the next 90 days. )} ) } /> ); }