personal web client for Bluesky
typescript
solidjs
bluesky
atcute
1import { For, type JSX, Match, Switch } from 'solid-js';
2
3import { getQueryErrorInfo } from '~/api/utils/query';
4
5import { ifIntersect } from '~/lib/element-refs';
6import { useIsFocused } from '~/lib/navigation/router';
7
8import CircularProgress from './circular-progress';
9import ErrorView from './error-view';
10
11export interface PagedListProps<T> {
12 data?: T[][];
13 error?: unknown;
14 render: (item: T, index: number) => JSX.Element;
15 fallback?: JSX.Element;
16 manualScroll?: boolean;
17 hasNewData?: boolean;
18 hasNextPage?: boolean;
19 isRefreshing?: boolean;
20 isFetchingNextPage?: boolean;
21 onEndReached?: () => void;
22 onRefresh?: () => void;
23 extraBottomGutter?: boolean;
24}
25
26const PagedList = <T,>(props: PagedListProps<T>) => {
27 const render = props.render;
28 const extraBottomGutter = props.extraBottomGutter;
29
30 const onEndReached = props.onEndReached;
31 const onRefresh = props.onRefresh;
32
33 const hasFallback = 'fallback' in props;
34
35 const isEmpty = () => {
36 const data = props.data;
37 return !data || data.length === 0 || (data.length === 1 && data[0].length === 0);
38 };
39
40 return (
41 <div class={'flex flex-col' + (extraBottomGutter ? ` pb-4` : ``)}>
42 <Switch>
43 <Match when={props.isFetchingNextPage}>{null}</Match>
44
45 <Match when={props.isRefreshing}>
46 <div class="grid h-13 shrink-0 place-items-center border-b border-outline">
47 <CircularProgress />
48 </div>
49 </Match>
50
51 <Match when={props.hasNewData}>
52 <button
53 onClick={onRefresh}
54 class="hover:bg-border-outline-25 grid h-13 shrink-0 place-items-center border-b border-outline text-sm text-accent"
55 >
56 Show new items
57 </button>
58 </Match>
59 </Switch>
60
61 <For each={props.data}>{(array) => array.map(render)}</For>
62
63 <Switch>
64 <Match when={props.isRefreshing}>{null}</Match>
65
66 <Match when={props.error}>
67 {(err) => (
68 <ErrorView
69 error={err()}
70 onRetry={() => {
71 const info = getQueryErrorInfo(err());
72
73 if (info && info.pageParam === undefined) {
74 onRefresh?.();
75 } else {
76 onEndReached?.();
77 }
78 }}
79 />
80 )}
81 </Match>
82
83 <Match when={props.manualScroll && !props.isFetchingNextPage && props.hasNextPage}>
84 <button
85 onClick={onEndReached}
86 class="grid h-13 shrink-0 place-items-center text-sm text-accent hover:bg-contrast/sm"
87 >
88 Show more
89 </button>
90 </Match>
91
92 <Match when={props.isFetchingNextPage || props.hasNextPage}>
93 <div
94 ref={(node) => {
95 if (onEndReached) {
96 const isFocused = useIsFocused();
97
98 ifIntersect(
99 node,
100 () => !props.isFetchingNextPage && !props.isRefreshing && props.hasNextPage && isFocused(),
101 onEndReached,
102 { rootMargin: '200% 0%' },
103 );
104 }
105 }}
106 class="h-[50svh] shrink-0"
107 >
108 <div class="grid place-items-center py-8">
109 <CircularProgress />
110 </div>
111 </div>
112 </Match>
113
114 <Match when={hasFallback && isEmpty()}>{props.fallback}</Match>
115
116 <Match when={props.data}>
117 <div class="h-[50svh] shrink-0">
118 <div class="grid place-items-center py-8">
119 <div class="h-1 w-1 rounded-full bg-contrast-muted"></div>
120 </div>
121 </div>
122 </Match>
123 </Switch>
124 </div>
125 );
126};
127
128export default PagedList;