Skip to content

Commit

Permalink
Add text block edit functionality (#12)
Browse files Browse the repository at this point in the history
* feat: add text block edit functionality

* feat(EditTextBlockDialog): use the right title and placeholders

* feat(AddTextBlockDialog): change button label from "Save" to "Add" to better convey the intent

* Update src/components/digests/dialog/EditTextBlockDialog.tsx

Co-authored-by: Baptiste Adrien <[email protected]>

* Update src/components/digests/dialog/EditTextBlockDialog.tsx

Co-authored-by: Baptiste Adrien <[email protected]>

* feat: add use effect to register dialog text area default value change

---------

Co-authored-by: Baptiste Adrien <[email protected]>
  • Loading branch information
mrnossiom and baptadn authored Sep 16, 2023
1 parent 5ef35a0 commit 49eedba
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 42 deletions.
24 changes: 20 additions & 4 deletions src/components/digests/block-card/text-card/TextCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { useTeam } from '@/contexts/TeamContext';
import { useParams } from 'next/navigation';
import useAddAndRemoveBlockOnDigest from '@/hooks/useAddAndRemoveBlockOnDigest';
import ActionsBlockPopover from '../../ActionsBlockPopover';
import EditTextBlockDialog from '../../dialog/EditTextBlockDialog';

export interface Props {
block: PublicDigestListProps['digest']['digestBlocks'][number];
isEditable?: boolean;
}

export default function BlockTextCard({ block, isEditable = false }: Props) {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const { id: teamId } = useTeam();
const params = useParams();
const { remove, isRefreshing } = useAddAndRemoveBlockOnDigest({
Expand All @@ -27,6 +29,7 @@ export default function BlockTextCard({ block, isEditable = false }: Props) {
'BlockTextCard: block.text is null, but it should not be null.'
);
}

const htmlContent = remark().use(html).processSync(block.text);

return (
Expand All @@ -50,10 +53,23 @@ export default function BlockTextCard({ block, isEditable = false }: Props) {
</div>
</div>
{isEditable && (
<ActionsBlockPopover
isRemoving={remove.isLoading || isRefreshing}
onRemoveClick={() => remove.mutate(block.id)}
/>
<>
<ActionsBlockPopover
isRemoving={remove.isLoading || isRefreshing}
onRemoveClick={() => remove.mutate(block.id)}
onEditClick={() => {
setIsEditDialogOpen(true);
}}
/>
<EditTextBlockDialog
isOpen={isEditDialogOpen}
setIsOpen={setIsEditDialogOpen}
bookmarkDigest={block}
defaultValues={{
text: block.text,
}}
/>
</>
)}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/digests/dialog/AddTextBlockDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function AddTextBlockDialog({
disabled={!isDirty}
isLoading={isCreating || isRefreshing}
>
Save
Add
</Button>
<Button
type="button"
Expand Down
135 changes: 135 additions & 0 deletions src/components/digests/dialog/EditTextBlockDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useTeam } from '@/contexts/TeamContext';
import useCustomToast from '@/hooks/useCustomToast';
import useTransitionRefresh from '@/hooks/useTransitionRefresh';
import api from '@/lib/api';
import { AxiosError, AxiosResponse } from 'axios';
import { useParams } from 'next/navigation';
import React, { useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useMutation } from 'react-query';
import Button from '../../Button';
import { Dialog, DialogContent } from '../../Dialog';
import { TextArea } from '../../Input';
import { Props as BookmarkCardProps } from '../block-card/BlockCard';

interface FormValues {
text: string;
}
interface Props {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
bookmarkDigest: BookmarkCardProps['block'];
defaultValues?: Partial<FormValues>;
}

export default function EditTextBlockDialog({
isOpen,
setIsOpen,
bookmarkDigest,
defaultValues,
}: Props) {
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
} = useForm<FormValues>({
defaultValues,
});
const { successToast, errorToast } = useCustomToast();
const { isRefreshing, refresh } = useTransitionRefresh();

useEffect(() => {
reset(defaultValues);
}, [defaultValues, reset]);

const params = useParams();
const { id: teamId } = useTeam();

const { mutate: updateBookDigest, isLoading: isRemoving } = useMutation<
AxiosResponse,
AxiosError<ErrorResponse>,
FormValues
>(
'update-bookmarkdigest',
({ text }) => {
return api.patch(
`/teams/${teamId}/digests/${params?.digestId}/block/${bookmarkDigest.id}`,
{ text }
);
},
{
onSuccess: () => {
successToast('Digest updated');
closeDialog();
refresh();
},
onError: (error: AxiosError<ErrorResponse>) => {
errorToast(
error.response?.data?.error ||
error.response?.statusText ||
error.message ||
'Something went wrong'
);
},
}
);

const onSubmit: SubmitHandler<FormValues> = (data: FormValues) => {
updateBookDigest(data);
};

function closeDialog() {
reset();
setIsOpen(false);
}

useEffect(() => {
if (Object.keys(errors).length > 0) {
Object.values(errors).forEach((error) => {
if (error.message) {
errorToast(error.message);
}
});
}
}, [errors, errorToast]);

return (
<Dialog open={isOpen} onOpenChange={() => setIsOpen(!open)}>
<DialogContent
title="Edit a text block"
description="Write something to add to your digest, markdown is supported."
closeIcon
containerClassName="w-full sm:max-w-3xl"
>
<form
className="flex flex-col items-end mt-4 gap-6 w-full max-w-3xl"
onSubmit={handleSubmit(onSubmit)}
>
<TextArea
className="min-h-[10rem]"
placeholder="Write something..."
{...register('text')}
rows={10}
/>
<div className="flex justify-start gap-4 w-full items-center">
<Button
isLoading={isRemoving || isRefreshing}
type="submit"
disabled={!isDirty}
>
Save
</Button>
<Button
type="button"
variant="destructiveGhost"
onClick={closeDialog}
>
Cancel
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ router
const teamId = req.query.teamId as string;
const digestId = req.query.digestId as string;
const blockId = req.query.blockId as string;
const { title, description, style } = req.body;

await client.digest.findFirstOrThrow({
where: {
Expand All @@ -53,51 +52,74 @@ router
},
});

/* Update a bookmark block, other block type are not editable at the moment */
if (block.type !== DigestBlockType.BOOKMARK) return res.end(400);

/* If the update is an update on the style to a tweet style, we need to check if this is a valid tweet url */
if (
block &&
style === BookmarkDigestStyle.TWEET_EMBED &&
block.style !== style
) {
if (!block.bookmarkId)
return res.status(404).json({ error: 'Block not found' });
const bookmark = await client.bookmark.findFirst({
if (block.type === DigestBlockType.BOOKMARK) {
const { title, description, style } = req.body;

/* If the update is an update on the style to a tweet style, we need to check if this is a valid tweet url */
if (
block &&
style === BookmarkDigestStyle.TWEET_EMBED &&
block.style !== style
) {
if (!block.bookmarkId)
return res.status(404).json({ error: 'Block not found' });
const bookmark = await client.bookmark.findFirst({
where: {
id: block.bookmarkId,
},
include: {
link: true,
},
});
if (!bookmark)
return res.status(404).json({ error: 'Bookmark not found' });
const isTweet = isTwitterLink(bookmark?.link?.url);
if (!isTweet)
return res.status(400).json({
error: 'This bookmark is not a tweet, cannot be a tweet embed',
});
}

if (isStringEmpty(title)) {
return res.status(400).json({
error: 'Title cannot be empty',
});
}

const updatedBlock = await client.digestBlock.update({
where: {
id: block.bookmarkId,
id: block.id,
},
include: {
link: true,
data: {
title,
description,
style,
},
});
if (!bookmark)
return res.status(404).json({ error: 'Bookmark not found' });
const isTweet = isTwitterLink(bookmark?.link?.url);
if (!isTweet)

return res.status(200).json(updatedBlock);
} else if (block.type === DigestBlockType.TEXT) {
const { text } = req.body;

if (isStringEmpty(text)) {
return res.status(400).json({
error: 'This bookmark is not a tweet, cannot be a tweet embed',
error: 'Title cannot be empty',
});
}
}

if (isStringEmpty(title)) {
return res.status(400).json({
error: 'Title cannot be empty',
const updatedBlock = await client.digestBlock.update({
where: {
id: block.id,
},
data: {
text,
},
});
}

const updatedBlock = await client.digestBlock.update({
where: {
id: block.id,
},
data: {
title,
description,
style,
},
});
return res.status(200).json(updatedBlock);
return res.status(200).json(updatedBlock);
} else {
return res.end(400);
}
});

export default router.handler({
Expand Down

1 comment on commit 49eedba

@vercel
Copy link

@vercel vercel bot commented on 49eedba Sep 16, 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-git-main-premieroctet.vercel.app
digestclub-premieroctet.vercel.app
digestclub.vercel.app

Please sign in to comment.