From 2ab0e2880255dda6223ef0f5155c046940aa9b8b Mon Sep 17 00:00:00 2001 From: Lucieo Date: Thu, 14 Sep 2023 11:26:36 +0200 Subject: [PATCH] Share bookmark component between lists (#14) * Share bookmark component between lists * Fix delete btn click inside Link * Fix align content un bookmark --- src/app/(routes)/teams/[teamSlug]/page.tsx | 2 + src/components/Popover.tsx | 5 +- src/components/bookmark/BookmarkAddButton.tsx | 65 +++++++ src/components/bookmark/BookmarkItem.tsx | 178 ++++++++++++++++++ .../{digests => bookmark}/BookmarkListDnd.tsx | 11 +- src/components/bookmark/Bookmarks.tsx | 161 ---------------- ...Controls.tsx => BookmarksListControls.tsx} | 2 +- src/components/bookmark/BookmarksTeamList.tsx | 59 ++++++ src/components/digests/AddBookmarkItem.tsx | 115 ----------- src/components/pages/DigestEditPage.tsx | 5 +- src/components/pages/Team.tsx | 10 +- src/lib/queries.ts | 45 +++-- 12 files changed, 356 insertions(+), 302 deletions(-) create mode 100644 src/components/bookmark/BookmarkAddButton.tsx create mode 100644 src/components/bookmark/BookmarkItem.tsx rename src/components/{digests => bookmark}/BookmarkListDnd.tsx (85%) delete mode 100644 src/components/bookmark/Bookmarks.tsx rename src/components/bookmark/{BookmarksControls.tsx => BookmarksListControls.tsx} (96%) create mode 100644 src/components/bookmark/BookmarksTeamList.tsx delete mode 100644 src/components/digests/AddBookmarkItem.tsx diff --git a/src/app/(routes)/teams/[teamSlug]/page.tsx b/src/app/(routes)/teams/[teamSlug]/page.tsx index 75db70b..b43d496 100644 --- a/src/app/(routes)/teams/[teamSlug]/page.tsx +++ b/src/app/(routes)/teams/[teamSlug]/page.tsx @@ -38,9 +38,11 @@ const TeamPage = async ({ params, searchParams }: TeamPageProps) => { await updateDefaultTeam(user.id, team.id); const page = Number(searchParams?.page || 1); + const search = searchParams?.search || ''; const { totalCount, bookmarks } = await getTeamBookmarks(team.id, { page, onlyNotInDigest: !searchParams?.all, + search, }); const digests = await getTeamDigests(team.id, 1, 11); diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx index e64e2cc..fd8489f 100644 --- a/src/components/Popover.tsx +++ b/src/components/Popover.tsx @@ -45,7 +45,10 @@ export const DeletePopover = forwardRef( + Delete ) diff --git a/src/components/bookmark/BookmarkAddButton.tsx b/src/components/bookmark/BookmarkAddButton.tsx new file mode 100644 index 0000000..cd1b59d --- /dev/null +++ b/src/components/bookmark/BookmarkAddButton.tsx @@ -0,0 +1,65 @@ +import useAddAndRemoveBlockOnDigest from '@/hooks/useAddAndRemoveBlockOnDigest'; +import { getTeamBookmarksNotInDigest } from '@/lib/queries'; +import Link from 'next/link'; +import React from 'react'; +import BookmarkImage from '../bookmark/BookmarkImage'; +import { AiOutlineLoading3Quarters as LoadingIcon } from '@react-icons/all-files/ai/AiOutlineLoading3Quarters'; +import { PlusCircleIcon } from '@heroicons/react/24/solid'; +import { getRelativeDate } from '@/utils/date'; +import { getDomainFromUrl } from '@/utils/url'; +import { BookmarkDigestStyle, DigestBlockType } from '@prisma/client'; +import { getEnvHost } from '@/lib/server'; +import { isTwitterLink } from '@/utils/link'; + +interface Props { + bookmark: Awaited< + ReturnType + >['bookmarks'][0]; + teamId: string; + digestId: string; +} + +/** + * Bookmark item with an add to a (specific) digest button + */ +export default function BookmarkAddButton({ + bookmark, + teamId, + digestId, +}: Props) { + const { add, isRefreshing } = useAddAndRemoveBlockOnDigest({ + teamId, + digestId, + }); + + return ( +
+ +
+ ); +} diff --git a/src/components/bookmark/BookmarkItem.tsx b/src/components/bookmark/BookmarkItem.tsx new file mode 100644 index 0000000..70d0e11 --- /dev/null +++ b/src/components/bookmark/BookmarkItem.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { + TeamBookmarksNotInDigestResult, + TeamBookmarksResult, +} from '@/lib/queries'; +import { getRelativeDate } from '@/utils/date'; +import { getDomainFromUrl } from '@/utils/url'; +import clsx from 'clsx'; +import BookmarkImage from './BookmarkImage'; +import { DeletePopover } from '../Popover'; +import { getEnvHost } from '@/lib/server'; + +import api from '@/lib/api'; +import { ApiBookmarkResponseSuccess } from '@/pages/api/teams/[teamId]/bookmark'; + +import { AxiosError, AxiosResponse } from 'axios'; + +import { useMutation } from 'react-query'; + +import message from '../../messages/en'; +import useCustomToast from '@/hooks/useCustomToast'; +import useTransitionRefresh from '@/hooks/useTransitionRefresh'; +import BookmarkAddButton from './BookmarkAddButton'; +import Link from 'next/link'; + +type Props = { + bookmark: + | TeamBookmarksResult + | TeamBookmarksNotInDigestResult['bookmarks'][0]; + teamSlug: string; + teamId: string; + digestId?: string; + nbOfTimesUsed?: number; + editMode?: boolean; +}; + +export const BookmarkItem = ({ + bookmark, + teamSlug, + teamId, + digestId, + nbOfTimesUsed, + editMode, +}: Props) => { + const { successToast, errorToast } = useCustomToast(); + const { isRefreshing, refresh } = useTransitionRefresh(); + + const { mutate: deleteBookmark, isLoading: isDeleting } = useMutation< + AxiosResponse, + AxiosError, + { bookmarkId: string } + >( + 'delete-bookmarks', + ({ bookmarkId }) => { + console.log(bookmarkId, teamId); + return api.delete(`/teams/${teamId}/bookmark/${bookmarkId}`); + }, + { + onSuccess: () => { + successToast(message.bookmark.delete.success); + refresh(); + }, + onError: (error: AxiosError) => { + errorToast( + error.response?.data?.error || + error.response?.statusText || + error.message + ); + }, + } + ); + + const isLoading = isRefreshing || isDeleting; + const isUsed = !!nbOfTimesUsed && !editMode; + + return ( +
+ {isUsed && ( + + Bookmarked {nbOfTimesUsed > 1 ? nbOfTimesUsed : ''}{' '} + + )} +
+
+
+ +
+
+
+ + {bookmark.link.title || bookmark.link.url} + + +
+ {bookmark.membership ? ( +
+ {bookmark.membership.user?.name || + bookmark.membership.user?.email?.split('@')[0]}{' '} + {bookmark.createdAt && getRelativeDate(bookmark.createdAt)} +
+ ) : ( +
+ {bookmark.provider === 'SLACK' && ( + <> + From Slack{' '} + {bookmark.createdAt && + getRelativeDate(bookmark.createdAt)} + + )} +
+ )} +
-
+
+ {editMode ? ( + + {getDomainFromUrl(bookmark.link.url)} + + ) : ( + getDomainFromUrl(bookmark.link.url) + )} +
+
-
+
e.preventDefault()} + > + + deleteBookmark({ bookmarkId: bookmark?.id }) + } + isLoading={isLoading} + /> +
+
+
+ {bookmark.link.description && ( +

+ {bookmark.link.description} +

+ )} +
+
+ {editMode && digestId && ( + + )} +
+
+ ); +}; diff --git a/src/components/digests/BookmarkListDnd.tsx b/src/components/bookmark/BookmarkListDnd.tsx similarity index 85% rename from src/components/digests/BookmarkListDnd.tsx rename to src/components/bookmark/BookmarkListDnd.tsx index 38d81be..913fd1c 100644 --- a/src/components/digests/BookmarkListDnd.tsx +++ b/src/components/bookmark/BookmarkListDnd.tsx @@ -1,17 +1,16 @@ import { + TeamBookmarksNotInDigestResult, getDigest, getTeamBookmarksNotInDigest, getTeamBySlug, } from '@/lib/queries'; import { Draggable, Droppable } from 'react-beautiful-dnd'; -import AddBookmarkItem from './AddBookmarkItem'; +import { BookmarkItem } from './BookmarkItem'; export type BookmarkListDndProps = { digest: NonNullable>>; team: Awaited>; - bookmarks: Awaited< - ReturnType - >['bookmarks']; + bookmarks: TeamBookmarksNotInDigestResult['bookmarks']; }; const BookmarkListDnd = ({ bookmarks, team, digest }: BookmarkListDndProps) => { @@ -36,11 +35,13 @@ const BookmarkListDnd = ({ bookmarks, team, digest }: BookmarkListDndProps) => { {...provided.dragHandleProps} ref={provided.innerRef} > - )} diff --git a/src/components/bookmark/Bookmarks.tsx b/src/components/bookmark/Bookmarks.tsx deleted file mode 100644 index 16590e2..0000000 --- a/src/components/bookmark/Bookmarks.tsx +++ /dev/null @@ -1,161 +0,0 @@ -'use client'; - -import useCustomToast from '@/hooks/useCustomToast'; -import useTransitionRefresh from '@/hooks/useTransitionRefresh'; -import api from '@/lib/api'; -import { TeamBookmarksResult } from '@/lib/queries'; -import { ApiBookmarkResponseSuccess } from '@/pages/api/teams/[teamId]/bookmark'; -import { getRelativeDate } from '@/utils/date'; -import { getDomainFromUrl } from '@/utils/url'; -import { BsFillBookmarkFill } from '@react-icons/all-files/bs/BsFillBookmarkFill'; -import { AxiosError, AxiosResponse } from 'axios'; -import clsx from 'clsx'; -import Link from 'next/link'; -import { useMutation } from 'react-query'; -import NoContent from '../layout/NoContent'; -import BookmarkImage from './BookmarkImage'; -import { DeletePopover } from '../Popover'; -import message from '../../messages/en'; -import { getEnvHost } from '@/lib/server'; - -type Props = { - bookmarks: TeamBookmarksResult[]; - teamId: string; - teamSlug: string; -}; - -/** - * Displays a list of bookmarks from a team - */ -export const Bookmarks = ({ bookmarks, teamId, teamSlug }: Props) => { - const { successToast, errorToast } = useCustomToast(); - const { isRefreshing, refresh } = useTransitionRefresh(); - - const { mutate: deleteBookmark, isLoading } = useMutation< - AxiosResponse, - AxiosError, - TeamBookmarksResult - >( - 'delete-bookmarks', - (bookmark) => { - return api.delete(`/teams/${teamId}/bookmark/${bookmark.id}`); - }, - { - onSuccess: () => { - successToast(message.invitation.delete.success); - refresh(); - }, - onError: (error: AxiosError) => { - errorToast( - error.response?.data?.error || - error.response?.statusText || - error.message - ); - }, - } - ); - - if (bookmarks.length < 1) { - return ( - } - title="No bookmark" - subtitle="Start bookmarking links to share them with your team" - /> - ); - } - - return ( -
- {bookmarks.map((bookmark) => { - const isUsed = bookmark.digestBlocks.length > 0; - const nbOfTimesUsed = bookmark.digestBlocks.length; - return ( -
- {isUsed && ( - - Bookmarked {nbOfTimesUsed > 1 ? nbOfTimesUsed : ''}{' '} - - )} -
-
-
- -
-
-
- - - {bookmark.link.title || bookmark.link.url} - - -
- {bookmark.membership ? ( -
- {bookmark.membership.user?.name || - bookmark.membership.user?.email?.split('@')[0]}{' '} - {bookmark.createdAt && - getRelativeDate(bookmark.createdAt)} -
- ) : ( -
- {bookmark.provider === 'SLACK' && ( - <> - From Slack{' '} - {bookmark.createdAt && - getRelativeDate(bookmark.createdAt)} - - )} -
- )} -
-
-
- {getDomainFromUrl(bookmark.link.url)} -
-
-
-
- deleteBookmark(bookmark)} - isLoading={isLoading || isRefreshing} - /> -
-
-
-
-
-
-
- ); - })} -
- ); -}; diff --git a/src/components/bookmark/BookmarksControls.tsx b/src/components/bookmark/BookmarksListControls.tsx similarity index 96% rename from src/components/bookmark/BookmarksControls.tsx rename to src/components/bookmark/BookmarksListControls.tsx index 3fe5453..85aaf67 100644 --- a/src/components/bookmark/BookmarksControls.tsx +++ b/src/components/bookmark/BookmarksListControls.tsx @@ -5,7 +5,7 @@ import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import { useTransition } from 'react'; import { Switch } from '../Input'; -export const BookmarksControls = ({ +export const BookmarksListControls = ({ linkCount, }: { linkCount: number } & PropsWithChildren) => { const searchParams = useSearchParams(); diff --git a/src/components/bookmark/BookmarksTeamList.tsx b/src/components/bookmark/BookmarksTeamList.tsx new file mode 100644 index 0000000..634292f --- /dev/null +++ b/src/components/bookmark/BookmarksTeamList.tsx @@ -0,0 +1,59 @@ +'use client'; + +import useCustomToast from '@/hooks/useCustomToast'; +import useTransitionRefresh from '@/hooks/useTransitionRefresh'; +import api from '@/lib/api'; +import { TeamBookmarksResult } from '@/lib/queries'; +import { ApiBookmarkResponseSuccess } from '@/pages/api/teams/[teamId]/bookmark'; +import { BsFillBookmarkFill } from '@react-icons/all-files/bs/BsFillBookmarkFill'; +import { AxiosError, AxiosResponse } from 'axios'; +import clsx from 'clsx'; +import { useMutation } from 'react-query'; +import NoContent from '../layout/NoContent'; +import message from '../../messages/en'; +import { BookmarkItem } from './BookmarkItem'; +import Link from 'next/link'; +import SearchInput from '../digests/SearchInput'; + +type Props = { + bookmarks: TeamBookmarksResult[]; + teamId: string; + teamSlug: string; +}; + +export const BookmarksTeamList = ({ bookmarks, teamId, teamSlug }: Props) => { + if (bookmarks.length < 1) { + return ( + } + title="No bookmark" + subtitle="Start bookmarking links to share them with your team" + /> + ); + } + + return ( +
+ + +
+ {bookmarks.map((bookmark) => ( + + + + ))} +
+
+ ); +}; diff --git a/src/components/digests/AddBookmarkItem.tsx b/src/components/digests/AddBookmarkItem.tsx deleted file mode 100644 index 1325ca4..0000000 --- a/src/components/digests/AddBookmarkItem.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import useAddAndRemoveBlockOnDigest from '@/hooks/useAddAndRemoveBlockOnDigest'; -import { getTeamBookmarksNotInDigest } from '@/lib/queries'; -import Link from 'next/link'; -import React from 'react'; -import BookmarkImage from '../bookmark/BookmarkImage'; -import { AiOutlineLoading3Quarters as LoadingIcon } from '@react-icons/all-files/ai/AiOutlineLoading3Quarters'; -import { PlusCircleIcon } from '@heroicons/react/24/solid'; -import { getRelativeDate } from '@/utils/date'; -import { getDomainFromUrl } from '@/utils/url'; -import { BookmarkDigestStyle, DigestBlockType } from '@prisma/client'; -import { getEnvHost } from '@/lib/server'; -import { isTwitterLink } from '@/utils/link'; - -interface Props { - bookmark: Awaited< - ReturnType - >['bookmarks'][0]; - teamId: string; - digestId: string; -} - -/** - * Bookmark item with an add to a (specific) digest button - */ -export default function AddBookmarkItem({ bookmark, teamId, digestId }: Props) { - const { add, isRefreshing } = useAddAndRemoveBlockOnDigest({ - teamId, - digestId, - }); - - return ( -
-
-
-
- -
-
-
-

- {bookmark.link.title} -

-
- {bookmark.membership ? ( -
- {bookmark.membership.user?.name || - bookmark.membership.user?.email?.split('@')[0]}{' '} - {bookmark.createdAt && getRelativeDate(bookmark.createdAt)} -
- ) : ( -
- {bookmark.provider === 'SLACK' && ( - <> - From Slack{' '} - {bookmark.createdAt && - getRelativeDate(bookmark.createdAt)} - - )} -
- )} -
-
-
- - {getDomainFromUrl(bookmark.link.url)} - -
-
-
-
-
- -
- -
-
- {bookmark.link.description && ( -

{bookmark.link.description}

- )} -
- ); -} diff --git a/src/components/pages/DigestEditPage.tsx b/src/components/pages/DigestEditPage.tsx index d67847c..9a44b4d 100644 --- a/src/components/pages/DigestEditPage.tsx +++ b/src/components/pages/DigestEditPage.tsx @@ -8,6 +8,7 @@ import api from '@/lib/api'; import useAddAndRemoveBlockOnDigest from '@/hooks/useAddAndRemoveBlockOnDigest'; import { + TeamBookmarksNotInDigestResult, getDigest, getTeamBookmarksNotInDigest, getTeamBySlug, @@ -32,7 +33,7 @@ import Button from '../Button'; import { Input, TextArea } from '../Input'; import { DeletePopover } from '../Popover'; import { BlockListDnd } from '../digests/BlockListDnd'; -import BookmarkListDnd from '../digests/BookmarkListDnd'; +import BookmarkListDnd from '../bookmark/BookmarkListDnd'; import SearchInput from '../digests/SearchInput'; import NoContent from '../layout/NoContent'; import SectionContainer from '../layout/SectionContainer'; @@ -43,7 +44,7 @@ import DigestEditTypefully from './DigestEditTypefully'; import DigestEditSendNewsletter from './DigestEditSendNewsletter'; type Props = { - dataBookmarks: Awaited>; + dataBookmarks: TeamBookmarksNotInDigestResult; digest: NonNullable>>; team: Awaited>; }; diff --git a/src/components/pages/Team.tsx b/src/components/pages/Team.tsx index c4c684d..a4e590f 100644 --- a/src/components/pages/Team.tsx +++ b/src/components/pages/Team.tsx @@ -6,14 +6,14 @@ import { } from '@/lib/queries'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid'; import Link from 'next/link'; -import { Bookmarks } from '../bookmark/Bookmarks'; +import { BookmarksTeamList } from '../bookmark/BookmarksTeamList'; import Card from '../Card'; import { CounterTag } from '../CounterTag'; import { DigestCreateInput } from '../digests/DigestCreateInput'; import { Digests } from '../digests/Digests'; import PageContainer from '../layout/PageContainer'; import { Tooltip } from '../Tooltip'; -import { BookmarksControls } from '../bookmark/BookmarksControls'; +import { BookmarksListControls } from '../bookmark/BookmarksListControls'; type Props = { linkCount: number; @@ -34,16 +34,16 @@ const Team = ({ team, linkCount, bookmarks, digests }: Props) => {

Bookmarks

- + } footer={
- +
} > - { const { page, perPage = 10 } = options; + + const where: Prisma.BookmarkFindManyArgs['where'] = { + ...(options.onlyNotInDigest && { + digestBlocks: { none: {} }, + }), + teamId, + ...(options.search && { + link: { + OR: [ + { + title: { + contains: options.search, + mode: 'insensitive', + }, + }, + { + description: { + contains: options.search, + mode: 'insensitive', + }, + }, + ], + }, + }), + }; + const totalCount = await db.bookmark.count({ - where: { - teamId, - ...(options.onlyNotInDigest && { - digestBlocks: { none: {} }, - }), - }, + where, }); const bookmarks = await db.bookmark.findMany({ take: perPage, skip: page ? (page - 1) * perPage : 0, - where: { - teamId, - ...(options.onlyNotInDigest && { - digestBlocks: { none: {} }, - }), - }, + where, orderBy: { createdAt: 'desc', }, @@ -501,6 +518,10 @@ export type TeamBookmarksResult = Awaited< ReturnType >['bookmarks'][number]; +export type TeamBookmarksNotInDigestResult = Awaited< + ReturnType +>; + export type TeamDigestsResult = Awaited< ReturnType >[number];