personal web client for Bluesky
typescript solidjs bluesky atcute
at trunk 3.3 kB view raw
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;