Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share bookmark component between lists #14

Merged
merged 3 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading