Skip to content

Commit

Permalink
Share bookmark component between lists (#14)
Browse files Browse the repository at this point in the history
* Share bookmark component between lists

* Fix delete btn click inside Link

* Fix align content un bookmark
  • Loading branch information
Lucieo authored Sep 14, 2023
1 parent 9666c3a commit 2ab0e28
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 302 deletions.
2 changes: 2 additions & 0 deletions src/app/(routes)/teams/[teamSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export const DeletePopover = forwardRef<HTMLButtonElement, DeletePopoverProps>(
<Popover
trigger={
props.trigger || (
<span ref={ref} className="text-xs font-semibold hover:underline">
<span
ref={ref}
className="text-xs font-semibold hover:underline cursor-pointer"
>
Delete
</span>
)
Expand Down
65 changes: 65 additions & 0 deletions src/components/bookmark/BookmarkAddButton.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof getTeamBookmarksNotInDigest>
>['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 (
<div className="flex h-full items-center w-16">
<button
className="group-hover color-black w-full"
onClick={(e) => {
e.preventDefault();
add.mutate({
bookmarkId: bookmark.id,
type: DigestBlockType.BOOKMARK,
style: isTwitterLink(bookmark.link.url)
? BookmarkDigestStyle.TWEET_EMBED
: BookmarkDigestStyle.BLOCK,
});
}}
disabled={add.isLoading || isRefreshing}
aria-label="Add"
>
{add.isLoading || isRefreshing ? (
<LoadingIcon
className="animate-spin h-6 w-6 m-auto text-gray-400"
aria-hidden="true"
/>
) : (
<span className="h-8 w-8 block m-auto text-gray-400 group-hover:text-gray-500 group-hover:scale-[110%] transition-[transform] duration-400">
<PlusCircleIcon />
</span>
)}
</button>
</div>
);
}
178 changes: 178 additions & 0 deletions src/components/bookmark/BookmarkItem.tsx
Original file line number Diff line number Diff line change
@@ -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<ApiBookmarkResponseSuccess>,
AxiosError<ErrorResponse>,
{ 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<ErrorResponse>) => {
errorToast(
error.response?.data?.error ||
error.response?.statusText ||
error.message
);
},
}
);

const isLoading = isRefreshing || isDeleting;
const isUsed = !!nbOfTimesUsed && !editMode;

return (
<div
key={bookmark.id}
className={clsx(
'group relative flex w-full rounded-md p-2 hover:bg-gray-50 flex-col',
{ 'opacity-60': isRefreshing }
)}
>
{isUsed && (
<a
className="absolute bottom-0 right-2 items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 hover:bg-gray-100 hover:text-gray-700 z-20"
href={`/teams/${teamSlug}/digests/${digestId}/edit`}
target="_blank"
title="Used in digest. Click to edit the digest."
>
Bookmarked {nbOfTimesUsed > 1 ? nbOfTimesUsed : ''}{' '}
</a>
)}
<div
className={clsx('flex w-full justify-between', {
'opacity-60': isUsed,
})}
>
<div className="flex gap-2 overflow-hidden w-[100%] justify-start">
<div className="relative w-16 h-16 overflow-hidden rounded-md border max-w-[4rem]">
<BookmarkImage
link={bookmark.link}
fallbackSrc={`${getEnvHost()}/api/bookmark-og?bookmark=${
bookmark.id
}`}
/>
</div>
<div className="flex flex-col items-start max-w-[100%] overflow-hidden flex-1">
<div className="flex flex-col overflow-hidden max-w-[100%]">
<span className="truncate font-semibold whitespace-nowrap">
{bookmark.link.title || bookmark.link.url}
</span>

<div className="flex items-center text-sm text-gray-500">
{bookmark.membership ? (
<div className="whitespace-nowrap max-w-[33%] sm:max-w-none truncate">
{bookmark.membership.user?.name ||
bookmark.membership.user?.email?.split('@')[0]}{' '}
{bookmark.createdAt && getRelativeDate(bookmark.createdAt)}
</div>
) : (
<div className="whitespace-nowrap max-w-[33%] sm:max-w-none truncate">
{bookmark.provider === 'SLACK' && (
<>
From Slack{' '}
{bookmark.createdAt &&
getRelativeDate(bookmark.createdAt)}
</>
)}
</div>
)}
<div className="mx-1">-</div>
<div className="whitespace-nowrap max-w-[33%] sm:max-w-none truncate">
{editMode ? (
<Link
href={bookmark.link.url}
target="_blank"
className="text-gray-400 whitespace-nowrap overflow-hidden text-ellipsis underline underline-offset-2"
>
{getDomainFromUrl(bookmark.link.url)}
</Link>
) : (
getDomainFromUrl(bookmark.link.url)
)}
</div>
<div className="mx-1">-</div>
<div
className="relative z-20"
onClick={(e) => e.preventDefault()}
>
<DeletePopover
handleDelete={() =>
deleteBookmark({ bookmarkId: bookmark?.id })
}
isLoading={isLoading}
/>
</div>
</div>
</div>
{bookmark.link.description && (
<p className={clsx('pt-2 text-sm', { 'opacity-60': isUsed })}>
{bookmark.link.description}
</p>
)}
</div>
</div>
{editMode && digestId && (
<BookmarkAddButton
bookmark={bookmark}
teamId={teamId}
digestId={digestId}
/>
)}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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<Awaited<ReturnType<typeof getDigest>>>;
team: Awaited<ReturnType<typeof getTeamBySlug>>;
bookmarks: Awaited<
ReturnType<typeof getTeamBookmarksNotInDigest>
>['bookmarks'];
bookmarks: TeamBookmarksNotInDigestResult['bookmarks'];
};

const BookmarkListDnd = ({ bookmarks, team, digest }: BookmarkListDndProps) => {
Expand All @@ -36,11 +35,13 @@ const BookmarkListDnd = ({ bookmarks, team, digest }: BookmarkListDndProps) => {
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<AddBookmarkItem
<BookmarkItem
key={bookmark.id}
bookmark={bookmark}
teamSlug={team.slug}
teamId={team.id}
digestId={digest.id}
editMode
/>
</li>
)}
Expand Down
Loading

1 comment on commit 2ab0e28

@vercel
Copy link

@vercel vercel bot commented on 2ab0e28 Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

digestclub – ./

digestclub-premieroctet.vercel.app
digestclub-git-main-premieroctet.vercel.app
digestclub.vercel.app

Please sign in to comment.