siftlode/frontend/src/components/Feed.tsx

80 lines
2.5 KiB
TypeScript
Raw Normal View History

import { useEffect, useRef, useState } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { api, type FeedFilters, type Video } from "../lib/api";
import VideoCard from "./VideoCard";
const PAGE = 60;
export default function Feed({
filters,
view,
}: {
filters: FeedFilters;
view: "grid" | "list";
}) {
const [overrides, setOverrides] = useState<Record<string, string>>({});
const query = useInfiniteQuery({
queryKey: ["feed", filters],
queryFn: ({ pageParam }) => api.feed(filters, pageParam as number, PAGE),
initialPageParam: 0,
getNextPageParam: (last) => (last.has_more ? last.offset + last.limit : undefined),
});
useEffect(() => setOverrides({}), [filters]);
const sentinel = useRef<HTMLDivElement | null>(null);
const { hasNextPage, isFetchingNextPage, fetchNextPage } = query;
useEffect(() => {
const el = sentinel.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ rootMargin: "800px" }
);
io.observe(el);
return () => io.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
function onState(id: string, status: string) {
setOverrides((o) => ({ ...o, [id]: status }));
api.setState(id, status).catch(() => {});
}
const items: Video[] = (query.data?.pages ?? [])
.flatMap((p) => p.items)
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
.filter((v) => overrides[v.id] !== "hidden");
if (query.isLoading) return <div className="p-8 text-muted">Loading feed</div>;
if (query.isError) return <div className="p-8 text-muted">Couldn't load the feed.</div>;
if (items.length === 0)
return <div className="p-8 text-muted">No videos match these filters.</div>;
return (
<div className="p-4">
{view === "grid" ? (
<div className="grid gap-x-4 gap-y-6 grid-cols-[repeat(auto-fill,minmax(260px,1fr))]">
{items.map((v) => (
<VideoCard key={v.id} video={v} view="grid" onState={onState} />
))}
</div>
) : (
<div className="max-w-4xl mx-auto flex flex-col gap-1">
{items.map((v) => (
<VideoCard key={v.id} video={v} view="list" onState={onState} />
))}
</div>
)}
<div ref={sentinel} className="h-10" />
{isFetchingNextPage && (
<div className="text-center text-muted py-4">Loading more</div>
)}
</div>
);
}