a tool for shared writing and social publishing
1"use client";
2import { PubListing } from "app/discover/PubListing";
3import { ButtonPrimary } from "components/Buttons";
4import { DiscoverSmall } from "components/Icons/DiscoverSmall";
5import { Json } from "supabase/database.types";
6import { PublicationSubscription, getSubscriptions } from "./getSubscriptions";
7import useSWRInfinite from "swr/infinite";
8import { useEffect, useRef } from "react";
9import { Cursor } from "./getReaderFeed";
10
11export const SubscriptionsContent = (props: {
12 publications: PublicationSubscription[];
13 nextCursor: Cursor | null;
14}) => {
15 const getKey = (
16 pageIndex: number,
17 previousPageData: {
18 subscriptions: PublicationSubscription[];
19 nextCursor: Cursor | null;
20 } | null,
21 ) => {
22 // Reached the end
23 if (previousPageData && !previousPageData.nextCursor) return null;
24
25 // First page, we don't have previousPageData
26 if (pageIndex === 0) return ["subscriptions", null] as const;
27
28 // Add the cursor to the key
29 return ["subscriptions", previousPageData?.nextCursor] as const;
30 };
31
32 const { data, error, size, setSize, isValidating } = useSWRInfinite(
33 getKey,
34 ([_, cursor]) => getSubscriptions(cursor),
35 {
36 fallbackData: [
37 { subscriptions: props.publications, nextCursor: props.nextCursor },
38 ],
39 revalidateFirstPage: false,
40 },
41 );
42
43 const loadMoreRef = useRef<HTMLDivElement>(null);
44
45 // Set up intersection observer to load more when trigger element is visible
46 useEffect(() => {
47 const observer = new IntersectionObserver(
48 (entries) => {
49 if (entries[0].isIntersecting && !isValidating) {
50 const hasMore = data && data[data.length - 1]?.nextCursor;
51 if (hasMore) {
52 setSize(size + 1);
53 }
54 }
55 },
56 { threshold: 0.1 },
57 );
58
59 if (loadMoreRef.current) {
60 observer.observe(loadMoreRef.current);
61 }
62
63 return () => observer.disconnect();
64 }, [data, size, setSize, isValidating]);
65
66 const allPublications = data
67 ? data.flatMap((page) => page.subscriptions)
68 : [];
69
70 if (allPublications.length === 0 && !isValidating)
71 return <SubscriptionsEmpty />;
72
73 return (
74 <div className="relative">
75 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
76 {allPublications?.map((p, index) => <PubListing key={p.uri} {...p} />)}
77 </div>
78 {/* Trigger element for loading more subscriptions */}
79 <div
80 ref={loadMoreRef}
81 className="absolute bottom-96 left-0 w-full h-px pointer-events-none"
82 aria-hidden="true"
83 />
84 {isValidating && (
85 <div className="text-center text-tertiary py-4">
86 Loading more subscriptions...
87 </div>
88 )}
89 </div>
90 );
91};
92
93const SubscriptionsEmpty = () => {
94 return (
95 <div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center font-bold text-tertiary">
96 You haven't subscribed to any publications yet!
97 <ButtonPrimary className="mx-auto place-self-center">
98 <DiscoverSmall /> Discover Publications
99 </ButtonPrimary>
100 </div>
101 );
102};