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 (
+
+ );
+}
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}
-
+
+
+
+
);
}