diff --git a/web/src/components/Action/Action.tsx b/web/src/components/Action/Action.tsx index 9bbe3b9dd..120f41e5c 100644 --- a/web/src/components/Action/Action.tsx +++ b/web/src/components/Action/Action.tsx @@ -10,12 +10,14 @@ import { ArrowLeftIcon, Bars3Icon, BellIcon, + BookmarkIcon, CloudArrowUpIcon, EllipsisHorizontalIcon, HomeIcon, PlusIcon, XMarkIcon, } from "@heroicons/react/24/outline"; +import { BookmarkIcon as BookmarkSolidIcon } from "@heroicons/react/24/solid"; import { MouseEvent, MouseEventHandler, useCallback } from "react"; import { LoginIcon } from "../graphics/LoginIcon"; @@ -250,3 +252,33 @@ export const More = forwardRef( ); } ); + +export const Bookmark = forwardRef( + ({ "aria-label": al, ...props }: WithOptionalARIALabel, ref) => { + return ( + } + /> + ); + } +); + +export const BookmarkSolid = forwardRef( + ({ "aria-label": al, ...props }: WithOptionalARIALabel, ref) => { + return ( + } + /> + ); + } +); diff --git a/web/src/components/CollectionMenu/CollectionMenu.tsx b/web/src/components/CollectionMenu/CollectionMenu.tsx new file mode 100644 index 000000000..db25ca981 --- /dev/null +++ b/web/src/components/CollectionMenu/CollectionMenu.tsx @@ -0,0 +1,59 @@ +import { + Menu, + MenuButton, + MenuDivider, + MenuGroup, + MenuItem, + MenuList, +} from "@chakra-ui/react"; +import { MinusIcon, PlusIcon } from "@heroicons/react/24/solid"; + +import { Bookmark, BookmarkSolid } from "../Action/Action"; + +import { Props, useCollectionMenu } from "./useCollectionMenu"; + +export function CollectionMenu(props: Props) { + const { collections, isAlreadySaved, onSelect } = useCollectionMenu(props); + + if (!collections) return null; + + return ( + + + + + + {collections.map((c) => ( + + ) : ( + + ) + } + onClick={onSelect(c)} + > + {c.name} + + ))} + + + + ); +} diff --git a/web/src/components/CollectionMenu/useCollectionMenu.ts b/web/src/components/CollectionMenu/useCollectionMenu.ts new file mode 100644 index 000000000..13ee399c0 --- /dev/null +++ b/web/src/components/CollectionMenu/useCollectionMenu.ts @@ -0,0 +1,58 @@ +import { useToast } from "@chakra-ui/react"; +import { mutate } from "swr"; + +import { + collectionAddPost, + collectionRemovePost, + useCollectionList, +} from "src/api/openapi/collections"; +import { ThreadReference } from "src/api/openapi/schemas"; +import { getThreadListKey } from "src/api/openapi/threads"; +import { useSession } from "src/auth"; + +export type Props = { + thread: ThreadReference; +}; + +type CollectionState = { + id: string; + name: string; + hasPost: boolean; +}; + +export function useCollectionMenu(props: Props) { + const account = useSession(); + const toast = useToast(); + const collectionList = useCollectionList(); + + const postCollections = new Set(props.thread.collections.map((c) => c.id)); + const isAlreadySaved = Boolean( + props.thread.collections.filter((c) => c.owner.id === account?.id).length + ); + + const collections: CollectionState[] = + collectionList.data?.collections.map((c) => ({ + id: c.id, + name: c.name, + hasPost: postCollections.has(c.id), + })) ?? []; + + const onSelect = (c: CollectionState) => async () => { + if (postCollections.has(c.id)) { + await collectionRemovePost(c.id, props.thread.id); + toast({ title: `Removed from ${c.name}` }); + } else { + await collectionAddPost(c.id, props.thread.id); + toast({ title: `Added to ${c.name}` }); + } + console.log(getThreadListKey()); + await mutate(getThreadListKey({})); + }; + + return { + error: collectionList.error, + collections: collections, + isAlreadySaved, + onSelect, + }; +} diff --git a/web/src/screens/collection/components/Collection.tsx b/web/src/screens/collection/components/Collection.tsx index aab86a19b..87b7787ea 100644 --- a/web/src/screens/collection/components/Collection.tsx +++ b/web/src/screens/collection/components/Collection.tsx @@ -1,8 +1,8 @@ import { Heading, Text } from "@chakra-ui/react"; import { Collection, CollectionWithItems } from "src/api/openapi/schemas"; -import { ThreadList } from "src/screens/home/components/ThreadList"; import { Byline } from "src/screens/thread/components/Byline"; +import { CollectionItemList } from "./CollectionItemList"; export function Collection(props: CollectionWithItems) { return ( @@ -16,7 +16,7 @@ export function Collection(props: CollectionWithItems) { /> {props.description} - + ); } diff --git a/web/src/screens/collection/components/CollectionItem.tsx b/web/src/screens/collection/components/CollectionItem.tsx new file mode 100644 index 000000000..243d9f6bd --- /dev/null +++ b/web/src/screens/collection/components/CollectionItem.tsx @@ -0,0 +1,37 @@ +import { Flex, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react"; +import NextLink from "next/link"; + +import { CollectionItem } from "src/api/openapi/schemas"; +import { Byline } from "src/screens/thread/components/Byline"; + +export function CollectionItem(props: { item: CollectionItem }) { + const permalink = `/t/${props.item.slug}`; + + return ( + + + + + + {props.item.title} + + + + + {props.item.short} + + + + } + /> + + {/* Tags list */} + + + ); +} diff --git a/web/src/screens/collection/components/CollectionItemList.tsx b/web/src/screens/collection/components/CollectionItemList.tsx new file mode 100644 index 000000000..06660e092 --- /dev/null +++ b/web/src/screens/collection/components/CollectionItemList.tsx @@ -0,0 +1,21 @@ +import { Divider, List } from "@chakra-ui/react"; +import { Fragment } from "react"; + +import { CollectionItem as CollectionItemSchema } from "src/api/openapi/schemas"; + +import { CollectionItem } from "./CollectionItem"; + +type Props = { items: CollectionItemSchema[] }; + +export function CollectionItemList(props: Props) { + return ( + + {props.items.map((t) => ( + + + + + ))} + + ); +} diff --git a/web/src/screens/home/components/ThreadListItem.tsx b/web/src/screens/home/components/ThreadListItem.tsx index bca147eac..6eab04f02 100644 --- a/web/src/screens/home/components/ThreadListItem.tsx +++ b/web/src/screens/home/components/ThreadListItem.tsx @@ -1,8 +1,18 @@ -import { Flex, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react"; +import { + Flex, + HStack, + Heading, + LinkBox, + LinkOverlay, + Text, +} from "@chakra-ui/react"; +import NextLink from "next/link"; + import { ThreadReference } from "src/api/openapi/schemas"; +import { CollectionMenu } from "src/components/CollectionMenu/CollectionMenu"; import { Byline } from "src/screens/thread/components/Byline"; + import { ThreadMenu } from "./ThreadMenu/ThreadMenu"; -import NextLink from "next/link"; export function ThreadListItem(props: { thread: ThreadReference }) { const permalink = `/t/${props.thread.slug}`; @@ -27,10 +37,14 @@ export function ThreadListItem(props: { thread: ThreadReference }) { author={props.thread.author.handle} time={new Date(props.thread.createdAt)} updated={new Date(props.thread.updatedAt)} - more={} /> {/* Tags list */} + + + + + ); diff --git a/web/src/screens/profile/components/Content/Content.tsx b/web/src/screens/profile/components/Content/Content.tsx index 2fdc87029..5947aaf38 100644 --- a/web/src/screens/profile/components/Content/Content.tsx +++ b/web/src/screens/profile/components/Content/Content.tsx @@ -26,8 +26,8 @@ export function Content(props: PublicProfile) { - Threads Posts + Replies Collections diff --git a/web/src/screens/thread/components/Byline.tsx b/web/src/screens/thread/components/Byline.tsx index a86b4f80f..f495154eb 100644 --- a/web/src/screens/thread/components/Byline.tsx +++ b/web/src/screens/thread/components/Byline.tsx @@ -1,5 +1,6 @@ import { Flex, HStack, Text } from "@chakra-ui/react"; import { differenceInSeconds, formatDistanceToNow } from "date-fns"; + import { ProfileReference } from "src/components/ProfileReference/ProfileReference"; import { Timestamp } from "src/components/Timestamp"; import { formatDistanceDefaults } from "src/utils/date"; @@ -20,32 +21,28 @@ export function Byline(props: Props) { : undefined; return ( - - - - - - - + + - - - - + + + - {props.more} - + + + + ); }