Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
6.9 kB · 205 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206import { useMemo } from "react";import { useUIState } from "src/useUIState";import { useEntity, useReplicache } from "src/replicache";import { BlockProps, BlockLayout } from "./Block";import { usePublicationData, useNormalizedPublicationRecord,} from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider";import { PublicationPostsList } from "app/lish/[did]/[publication]/PublicationPostsList";import { Popover } from "components/Popover";import { Toggle } from "components/Toggle";import { ToggleGroup } from "components/ToggleGroup";import { SettingsTriggerButton } from "./SettingsTriggerButton";
type PostsListView = "compact" | "full";
export const PostsListBlock = (props: BlockProps & { preview?: boolean }) => { let isSelected = useUIState((s) => !!s.selectedBlocks.find((b) => b.value === props.value), );
if (props.preview) { return ( <BlockLayout isSelected={isSelected} className="border-none!"> <PostsListPlaceholder /> </BlockLayout> ); }
return ( <BlockLayout isSelected={isSelected} className="border-none!" extraOptions={<PostsListSettingsButton entityID={props.entityID} />} > <PostsListBlockContent entityID={props.entityID} /> </BlockLayout> );};
function PostsListBlockContent({ entityID }: { entityID: string }) { let { data } = usePublicationData(); let publicationRecord = useNormalizedPublicationRecord();
let viewFact = useEntity(entityID, "posts-list/view"); let view: PostsListView = viewFact?.data.value ?? "full";
let highlightFirstFact = useEntity( entityID, "posts-list/highlight-first-post", ); let highlightFirst = highlightFirstFact?.data.value ?? false;
let filterTagFact = useEntity(entityID, "posts-list/filter-tag"); let filterTag = filterTagFact?.data.value;
let filteredPosts = useMemo(() => { if (!data?.documents) return data?.documents; if (!filterTag) return data.documents; return data.documents.filter((d) => d.record.tags?.includes(filterTag)); }, [data?.documents, filterTag]);
if (data === undefined) return <PostsListPlaceholder />; if (!data?.publication) return <PostsListPlaceholder />;
return ( <PublicationPostsList publication={data.publication} publicationRecord={publicationRecord} posts={filteredPosts} view={view} highlightFirstPost={highlightFirst} /> );}
function PostsListPlaceholder() { return ( <div className="publicationPostList w-full flex flex-col gap-4"> {[0, 1, 2].map((i) => ( <div key={i} className="flex w-full grow flex-col"> <div className="flex flex-col gap-2"> <div className="h-5 w-2/3 bg-border-light rounded animate-pulse" /> <div className="h-4 w-full bg-border-light rounded animate-pulse" /> </div> <div className="pt-2"> <div className="h-3 w-24 bg-border-light rounded animate-pulse" /> </div> <hr className="last:hidden border-border-light mt-4" /> </div> ))} </div> );}
function PostsListSettingsButton(props: { entityID: string }) { let { rep } = useReplicache(); let { data } = usePublicationData();
let viewFact = useEntity(props.entityID, "posts-list/view"); let view: PostsListView = viewFact?.data.value ?? "full";
let highlightFirstFact = useEntity( props.entityID, "posts-list/highlight-first-post", ); let highlightFirst = highlightFirstFact?.data.value ?? false;
let filterTagFact = useEntity(props.entityID, "posts-list/filter-tag"); let filterTag = filterTagFact?.data.value;
let allTags = useMemo(() => { let tagSet = new Set<string>(); for (let doc of data?.documents ?? []) { for (let tag of doc.record.tags ?? []) tagSet.add(tag); } return Array.from(tagSet).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }), ); }, [data?.documents]);
return ( <Popover asChild side="top" align="end" sideOffset={6} trigger={<SettingsTriggerButton aria-label="Posts List Settings" />} > <div className="flex flex-col gap-3 text-primary py-1 min-w-[220px]"> <div className="flex flex-col gap-1"> <div className="font-bold text-sm">Post View</div> <ToggleGroup<PostsListView> fullWidth value={view} onChange={(value) => { if (!rep) return; rep.mutate.assertFact({ entity: props.entityID, attribute: "posts-list/view", data: { type: "posts-list-view-union", value }, }); }} options={[ { value: "compact", label: "Compact" }, { value: "full", label: "Full" }, ]} /> </div> <Toggle toggle={highlightFirst} onToggle={() => { if (!rep) return; rep.mutate.assertFact({ entity: props.entityID, attribute: "posts-list/highlight-first-post", data: { type: "boolean", value: !highlightFirst }, }); }} > <div className="font-bold">Highlight First Post</div> </Toggle> <div className="flex flex-col gap-1"> <div className="font-bold text-sm">Filter by Tag</div> {allTags.length === 0 ? ( <div className="text-tertiary italic text-sm">no tags yet</div> ) : ( <div className="flex flex-col max-h-40 overflow-y-auto border border-border-light rounded-md"> {allTags.map((tag) => { let isSelected = filterTag === tag; return ( <button key={tag} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => { if (!rep) return; if (isSelected) { if (filterTagFact) rep.mutate.retractFact({ factID: filterTagFact.id, }); } else { rep.mutate.assertFact({ entity: props.entityID, attribute: "posts-list/filter-tag", data: { type: "string", value: tag }, }); } }} className={`text-left px-2 py-1 text-sm hover:bg-border-light ${ isSelected ? "bg-accent-1 text-accent-2 font-bold" : "" }`} > {tag} </button> ); })} </div> )} </div> </div> </Popover> );}