a tool for shared writing and social publishing
at feature/reader 333 lines 10 kB view raw
1"use client"; 2 3import { getHomeDocs, HomeDoc } from "./storage"; 4import useSWR from "swr"; 5import { 6 Fact, 7 PermissionToken, 8 ReplicacheProvider, 9 useEntity, 10} from "src/replicache"; 11import { LeafletListItem } from "./LeafletList/LeafletListItem"; 12import { useIdentityData } from "components/IdentityProvider"; 13import type { Attribute } from "src/replicache/attributes"; 14import { callRPC } from "app/api/rpc/client"; 15import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; 16import { HomeSmall } from "components/Icons/HomeSmall"; 17import { 18 HomeDashboardControls, 19 DashboardLayout, 20 DashboardState, 21 useDashboardState, 22} from "components/PageLayouts/DashboardLayout"; 23import { Actions } from "./Actions/Actions"; 24import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 25import { Json } from "supabase/database.types"; 26import { useTemplateState } from "./Actions/CreateNewButton"; 27import { CreateNewLeafletButton } from "./Actions/CreateNewButton"; 28import { ActionButton } from "components/ActionBar/ActionButton"; 29import { AddTiny } from "components/Icons/AddTiny"; 30import { 31 get_leaflet_data, 32 GetLeafletDataReturnType, 33} from "app/api/rpc/[command]/get_leaflet_data"; 34import { useEffect, useRef, useState } from "react"; 35import { Input } from "components/Input"; 36import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 37import { 38 ButtonPrimary, 39 ButtonSecondary, 40 ButtonTertiary, 41} from "components/Buttons"; 42import { AddSmall } from "components/Icons/AddSmall"; 43import { PublishIllustration } from "app/[leaflet_id]/publish/PublishIllustration/PublishIllustration"; 44import { PubListEmptyIllo } from "components/ActionBar/Publications"; 45import { theme } from "tailwind.config"; 46import Link from "next/link"; 47import { DiscoverIllo } from "./HomeEmpty/DiscoverIllo"; 48import { WelcomeToLeafletIllo } from "./HomeEmpty/WelcomeToLeafletIllo"; 49import { 50 DiscoverBanner, 51 HomeEmptyState, 52 PublicationBanner, 53} from "./HomeEmpty/HomeEmpty"; 54 55type Leaflet = { 56 added_at: string; 57 token: PermissionToken & { 58 leaflets_in_publications?: Exclude< 59 GetLeafletDataReturnType["result"]["data"], 60 null 61 >["leaflets_in_publications"]; 62 }; 63}; 64 65export const HomeLayout = (props: { 66 entityID: string; 67 titles: { [root_entity: string]: string }; 68 initialFacts: { 69 [root_entity: string]: Fact<Attribute>[]; 70 }; 71}) => { 72 let hasBackgroundImage = !!useEntity( 73 props.entityID, 74 "theme/background-image", 75 ); 76 let cardBorderHidden = !!useCardBorderHidden(props.entityID); 77 78 let [searchValue, setSearchValue] = useState(""); 79 let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); 80 81 useDebouncedEffect( 82 () => { 83 setDebouncedSearchValue(searchValue); 84 }, 85 200, 86 [searchValue], 87 ); 88 89 let { identity } = useIdentityData(); 90 91 let hasPubs = !identity || identity.publications.length === 0 ? false : true; 92 let hasTemplates = 93 useTemplateState((s) => s.templates).length === 0 ? false : true; 94 95 return ( 96 <DashboardLayout 97 id="home" 98 cardBorderHidden={cardBorderHidden} 99 currentPage="home" 100 defaultTab="home" 101 actions={<Actions />} 102 tabs={{ 103 home: { 104 controls: ( 105 <HomeDashboardControls 106 defaultDisplay={"grid"} 107 searchValue={searchValue} 108 setSearchValueAction={setSearchValue} 109 hasBackgroundImage={hasBackgroundImage} 110 hasPubs={hasPubs} 111 hasTemplates={hasTemplates} 112 /> 113 ), 114 content: ( 115 <HomeLeafletList 116 titles={props.titles} 117 initialFacts={props.initialFacts} 118 cardBorderHidden={cardBorderHidden} 119 searchValue={debouncedSearchValue} 120 /> 121 ), 122 }, 123 }} 124 /> 125 ); 126}; 127 128export function HomeLeafletList(props: { 129 titles: { [root_entity: string]: string }; 130 initialFacts: { 131 [root_entity: string]: Fact<Attribute>[]; 132 }; 133 searchValue: string; 134 cardBorderHidden: boolean; 135}) { 136 let { identity } = useIdentityData(); 137 let { data: initialFacts } = useSWR( 138 "home-leaflet-data", 139 async () => { 140 if (identity) { 141 let { result } = await callRPC("getFactsFromHomeLeaflets", { 142 tokens: identity.permission_token_on_homepage.map( 143 (ptrh) => ptrh.permission_tokens.root_entity, 144 ), 145 }); 146 let titles = { 147 ...result.titles, 148 ...identity.permission_token_on_homepage.reduce( 149 (acc, tok) => { 150 let title = 151 tok.permission_tokens.leaflets_in_publications[0]?.title; 152 if (title) acc[tok.permission_tokens.root_entity] = title; 153 return acc; 154 }, 155 {} as { [k: string]: string }, 156 ), 157 }; 158 return { ...result, titles }; 159 } 160 }, 161 { fallbackData: { facts: props.initialFacts, titles: props.titles } }, 162 ); 163 164 let { data: localLeaflets } = useSWR("leaflets", () => getHomeDocs(), { 165 fallbackData: [], 166 }); 167 let leaflets: Leaflet[] = identity 168 ? identity.permission_token_on_homepage.map((ptoh) => ({ 169 added_at: ptoh.created_at, 170 token: ptoh.permission_tokens as PermissionToken, 171 })) 172 : localLeaflets 173 .sort((a, b) => (a.added_at > b.added_at ? -1 : 1)) 174 .filter((d) => !d.hidden) 175 .map((ll) => ll); 176 177 return leaflets.length === 0 ? ( 178 <HomeEmptyState /> 179 ) : ( 180 <> 181 <LeafletList 182 defaultDisplay="grid" 183 searchValue={props.searchValue} 184 leaflets={leaflets} 185 titles={initialFacts?.titles || {}} 186 cardBorderHidden={props.cardBorderHidden} 187 initialFacts={initialFacts?.facts || {}} 188 showPreview 189 /> 190 <div className="spacer h-4 w-full bg-transparent shrink-0 " /> 191 192 {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === 193 0 && <PublicationBanner small />} 194 <DiscoverBanner small /> 195 </> 196 ); 197} 198 199export function LeafletList(props: { 200 leaflets: Leaflet[]; 201 titles: { [root_entity: string]: string }; 202 defaultDisplay: Exclude<DashboardState["display"], undefined>; 203 initialFacts: { 204 [root_entity: string]: Fact<Attribute>[]; 205 }; 206 searchValue: string; 207 cardBorderHidden: boolean; 208 showPreview?: boolean; 209}) { 210 let { identity } = useIdentityData(); 211 let { display } = useDashboardState(); 212 213 display = display || props.defaultDisplay; 214 215 let searchedLeaflets = useSearchedLeaflets( 216 props.leaflets, 217 props.titles, 218 props.searchValue, 219 ); 220 221 return ( 222 <div 223 className={` 224 leafletList 225 w-full 226 ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2 pt-2"} `} 227 > 228 {props.leaflets.map(({ token: leaflet, added_at }, index) => ( 229 <ReplicacheProvider 230 disablePull 231 initialFactsOnly={!!identity} 232 key={leaflet.id} 233 rootEntity={leaflet.root_entity} 234 token={leaflet} 235 name={leaflet.root_entity} 236 initialFacts={props.initialFacts?.[leaflet.root_entity] || []} 237 > 238 <StaticLeafletDataContext 239 value={{ 240 ...leaflet, 241 leaflets_in_publications: leaflet.leaflets_in_publications || [], 242 blocked_by_admin: null, 243 custom_domain_routes: [], 244 }} 245 > 246 <LeafletListItem 247 title={props?.titles?.[leaflet.root_entity] || "Untitled"} 248 token={leaflet} 249 draft={!!leaflet.leaflets_in_publications?.length} 250 published={!!leaflet.leaflets_in_publications?.find((l) => l.doc)} 251 publishedAt={ 252 leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents 253 ?.indexed_at 254 } 255 leaflet_id={leaflet.root_entity} 256 loggedIn={!!identity} 257 display={display} 258 added_at={added_at} 259 cardBorderHidden={props.cardBorderHidden} 260 index={index} 261 showPreview={props.showPreview} 262 isHidden={ 263 !searchedLeaflets.some( 264 (sl) => sl.token.root_entity === leaflet.root_entity, 265 ) 266 } 267 /> 268 </StaticLeafletDataContext> 269 </ReplicacheProvider> 270 ))} 271 </div> 272 ); 273} 274 275function useSearchedLeaflets( 276 leaflets: Leaflet[], 277 titles: { [root_entity: string]: string }, 278 searchValue: string, 279) { 280 let { sort, filter } = useDashboardState(); 281 282 let sortedLeaflets = leaflets.sort((a, b) => { 283 if (sort === "alphabetical") { 284 if (titles[a.token.root_entity] === titles[b.token.root_entity]) { 285 return a.added_at > b.added_at ? -1 : 1; 286 } else { 287 return titles[a.token.root_entity].toLocaleLowerCase() > 288 titles[b.token.root_entity].toLocaleLowerCase() 289 ? 1 290 : -1; 291 } 292 } else { 293 return a.added_at === b.added_at 294 ? a.token.root_entity > b.token.root_entity 295 ? -1 296 : 1 297 : a.added_at > b.added_at 298 ? -1 299 : 1; 300 } 301 }); 302 303 let allTemplates = useTemplateState((s) => s.templates); 304 let filteredLeaflets = sortedLeaflets.filter(({ token: leaflet }) => { 305 let published = !!leaflet.leaflets_in_publications?.find((l) => l.doc); 306 let drafts = !!leaflet.leaflets_in_publications?.length && !published; 307 let docs = !leaflet.leaflets_in_publications?.length; 308 let templates = !!allTemplates.find((t) => t.id === leaflet.id); 309 // If no filters are active, show all 310 if ( 311 !filter.drafts && 312 !filter.published && 313 !filter.docs && 314 !filter.templates 315 ) 316 return true; 317 318 return ( 319 (filter.drafts && drafts) || 320 (filter.published && published) || 321 (filter.docs && docs) || 322 (filter.templates && templates) 323 ); 324 }); 325 if (searchValue === "") return filteredLeaflets; 326 let searchedLeaflets = filteredLeaflets.filter(({ token: leaflet }) => { 327 return titles[leaflet.root_entity] 328 ?.toLowerCase() 329 .includes(searchValue.toLowerCase()); 330 }); 331 332 return searchedLeaflets; 333}