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.
)}
)
}
/>
>
);
}