8000 Feature: 메모 선택 by guesung · Pull Request #113 · guesung/Web-Memo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Feature: 메모 선택 #113

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

Merged
merged 18 commits into from
Dec 3, 2024
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
8 changes: 8 additions & 0 deletions packages/shared/src/constants/MotionVariants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const MOTION_VARIANTS = {
fadeInAndOut: {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.3 },
exit: { opacity: 0, y: 10 },
},
};
1 change: 1 addition & 0 deletions packages/shared/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './Cookie';
export * from './Error';
export * from './Extension';
export * from './Language';
export * from './MotionVariants';
export * from './Path';
export * from './QueryKey';
export * from './Storage';
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './supabase';
export { default as useCloseOnEscape } from './useCloseOnEscape';
export { default as useDidMount } from './useDidMount';
export { default as useError } from './useError';
export { default as useFetch } from './useFetch';
export { default as useKeyboardBind } from './useKeyboardBind';
export { default as useThrottle } from './useThrottle';
export { default as useUserPreferDarkMode } from './useUserPreferDarkMode';
1 change: 1 addition & 0 deletions packages/shared/src/hooks/supabase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { default as useMemoPatchMutation } from './useMemoPatchMutation';
export { default as useMemoPostMutation } from './useMemoPostMutation';
export { default as useMemoQuery } from './useMemoQuery';
export { default as useMemosQuery } from './useMemosQuery';
export { default as useMemosUpsertMutation } from './useMemosUpsertMutation';
export { default as useSupabaseUser } from './useSupabaseUser';
10 changes: 6 additions & 4 deletions packages/shared/src/hooks/supabase/useMemoPatchMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ export default function useMemoPatchMutation({ supabaseClient, ...useMutationPro

if (!previousMemosData) throw new NoMemosError();

const currentMemoIndex = previousMemosData.findIndex(memo => memo.id === id);
const currentMemoBase = previousMemosData.find(memo => memo.id === id);
const updatedMemosData = [...previousMemosData];

const currentMemoIndex = updatedMemosData.findIndex(memo => memo.id === id);
const currentMemoBase = updatedMemosData.find(memo => memo.id === id);

if (currentMemoIndex === -1 || !currentMemoBase) throw new NoMemoError();

previousMemosData.splice(currentMemoIndex, 1, { ...currentMemoBase, ...memoRequest });
updatedMemosData.splice(currentMemoIndex, 1, { ...currentMemoBase, ...memoRequest });

await queryClient.setQueryData(QUERY_KEY.memos(), { ...previousMemos, data: previousMemosData });
await queryClient.setQueryData(QUERY_KEY.memos(), { ...previousMemos, data: updatedMemosData });

return { previousMemos };
},
Expand Down
44 changes: 44 additions & 0 deletions packages/shared/src/hooks/supabase/useMemosUpsertMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NoMemosError, QUERY_KEY } from '@src/constants';
import { MemoRow, MemoSupabaseClient, MemoSupabaseResponse, MemoTable } from '@src/types';
import { upsertMemos } from '@src/utils';
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';

type MutationVariables = MemoTable['Insert'][];
type MutationData = Awaited<ReturnType<typeof upsertMemos>>;
type MutationError = Error;

interface UseMemosUpsertMutationProps extends UseMutationOptions<MutationData, MutationError, MutationVariables> {
supabaseClient: MemoSupabaseClient;
}

export default function useMemosUpsertMutation({ supabaseClient, ...useMutationProps }: UseMemosUpsertMutationProps) {
const queryClient = useQueryClient();
return useMutation<MutationData, MutationError, MutationVariables>({
...useMutationProps,
mutationFn: async memoRequest => await upsertMemos(supabaseClient, memoRequest),
onMutate: async memoRequest => {
await queryClient.cancelQueries({ queryKey: QUERY_KEY.memos() });
const previousMemos = queryClient.getQueryData<MemoSupabaseResponse>(QUERY_KEY.memos());

if (!previousMemos) throw new NoMemosError();

const { data: previousMemosData } = previousMemos;

if (!previousMemosData) throw new NoMemosError();

const updatedMemosData = [...previousMemosData];

memoRequest.forEach(memo => {
const currentMemoIndex = updatedMemosData.findIndex(previousMemo => previousMemo.id === memo.id);
const currentMemoBase = updatedMemosData.find(previousMemo => previousMemo.id === memo.id);

if (currentMemoIndex === -1 || !currentMemoBase) updatedMemosData.unshift(memo as MemoRow);
else updatedMemosData.splice(currentMemoIndex, 1, { ...currentMemoBase, ...memo });
});

await queryClient.setQueryData(QUERY_KEY.memos(), { ...previousMemos, data: updatedMemosData });

return { previousMemos };
},
});
}
12 changes: 0 additions & 12 deletions packages/shared/src/hooks/useCloseOnEscape.ts

This file was deleted.

19 changes: 19 additions & 0 deletions packages/shared/src/hooks/useKeyboardBind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react';

type KeyboardEventKey = 'Backspace' | 'Escape';

interface UseKeyboardBindProps {
key: KeyboardEventKey;
callback: () => void;
}

export default function useKeyboardBind({ key, callback }: UseKeyboardBindProps) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === key) callback();
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [key, callback]);
}
5 changes: 4 additions & 1 deletion packages/shared/src/utils/Supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export const updateMemo = async (
export const deleteMemo = async (supabaseClient: MemoSupabaseClient, id: number) =>
supabaseClient.from(SUPABASE.schemaMemo).delete().eq('id', id).select();

export const upsertMemo = async (supabaseClient: MemoSupabaseClient, memoRequest: MemoTable['Insert']) =>
export const deleteMemos = async (supabaseClient: MemoSupabaseClient, idList: number[]) =>
supabaseClient.from(SUPABASE.schemaMemo).delete().in('id', idList).select();

export const upsertMemos = async (supabaseClient: MemoSupabaseClient, memoRequest: MemoTable['Insert'][]) =>
supabaseClient.from(SUPABASE.schemaMemo).upsert(memoRequest).select();

export const getUser = (supabaseClient: MemoSupabaseClient) => supabaseClient.auth.getUser();
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './Date';
export * from './Download';
export * from './Environment';
export * from './Error';
export * from './isAllSame';
export * from './Notion';
export * from './Sentry';
export * from './String';
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/utils/isAllSame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isAllSame = (array: unknown[]) => new Set(array).size === 1;
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { QUERY_KEY } from '@extension/shared/constants';
import { useCategoryQuery, useMemoPatchMutation, useMemoPostMutation, useMemoQuery } from '@extension/shared/hooks';
import { useCategoryQuery, useMemosUpsertMutation } from '@extension/shared/hooks';
import { useSearchParams } from '@extension/shared/modules/search-params';
import { MemoRow } from '@extension/shared/types';
import { isAllSame } from '@extension/shared/utils';
import { requestRefetchTheMemos } from '@extension/shared/utils/extension';
import { Button } from '@src/components/ui/button';
import {
Expand All @@ -12,65 +14,72 @@ import {
} from '@src/components/ui/dropdown-menu';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@src/components/ui/select';
import { ToastAction } from '@src/components/ui/toast';
import { useMemoDeleteMutation, useSupabaseClient } from '@src/hooks';
import { useDeleteMemosMutation, useSupabaseClient } from '@src/hooks';
import { useToast } from '@src/hooks/use-toast';
import { LanguageType } from '@src/modules/i18n';
import useTranslation from '@src/modules/i18n/client';
import { useQueryClient } from '@tanstack/react-query';
import { EllipsisVerticalIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { MouseEventHandler, useState } from 'react';
import { MouseEvent, useState } from 'react';

interface MemoOptionProps extends LanguageType {
memoId: number;
memos: MemoRow[];
closeMemoOption?: () => void;
}

export default function MemoOption({ lng, memoId }: MemoOptionProps) {
export default function MemoOption({ lng, memos, closeMemoOption }: MemoOptionProps) {
const { t } = useTranslation(lng);
const supabaseClient = useSupabaseClient();
const searchParams = useSearchParams();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const { categories } = useCategoryQuery({ supabaseClient });
const queryClient = useQueryClient();

const { memo: memoData } = useMemoQuery({ supabaseClient, id: memoId });
const { mutate: mutatePatchMemo } = useMemoPatchMutation({
const { mutate: mutateUpsertMemo } = useMemosUpsertMutation({
supabaseClient,
});
const { mutate: mutatePostMemo } = useMemoPostMutation({
supabaseClient,
});
const { mutate: mutateDeleteMemo } = useMemoDeleteMutation();
const { mutate: mutateDeleteMemo } = useDeleteMemosMutation({ supabaseClient });

const [isOpen, setIsOpen] = useState(false);
const defaultCategoryId = isAllSame(memos.map(memo => memo.category_id)) ? String(memos.at(0)?.category_id) : '';

const handleDeleteMemo: MouseEventHandler<HTMLDivElement> = event => {
event.stopPropagation();
const handleDeleteMemo = (event?: MouseEvent<HTMLDivElement>) => {
event?.stopPropagation();

mutateDeleteMemo(memoId, {
onSuccess: ({ data }) => {
if (!data) return;

const deletedMemo = data[0];
const handlePostMemo = () => mutatePostMemo(deletedMemo);
mutateDeleteMemo(
memos.map(memo => memo.id),
{
onSuccess: () => {
const handleToastActionClick = () => {
mutateUpsertMemo(memos, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY.memos() });
requestRefetchTheMemos();
},
});
};

toast({
title: t('toastTitle.memoDeleted'),
action: (
<ToastAction altText={t('toastActionMessage.undo')} >
{t('toastActionMessage.undo')}
</ToastAction>
),
});
requestRefetchTheMemos();
toast({
title: t('toastTitle.memoDeleted'),
action: (
<ToastAction altText={t('toastActionMessage.undo')} >
{t('toastActionMessage.undo')}
</ToastAction>
),
});
},
onSettled: () => {
setIsOpen(false);
closeMemoOption?.();
},
},
});
);
};

const handleCategoryChange = (categoryId: string) => {
mutatePatchMemo(
{ id: memoId, memoRequest: { category_id: Number(categoryId) } },
mutateUpsertMemo(
memos.map(memo => ({ ...memo, category_id: Number(categoryId) })),
{
onSuccess: () => {
const category = categories?.find(category => category.id === Number(categoryId));
Expand All @@ -83,7 +92,6 @@ export default function MemoOption({ lng, memoId }: MemoOptionProps) {
altText={t('toastActionMessage.goTo')}
=> {
searchParams.set('category', category.name);
searchParams.set('id', memoId.toString());

router.push(searchParams.getUrl());
}}>
Expand All @@ -93,10 +101,11 @@ export default function MemoOption({ lng, memoId }: MemoOptionProps) {
});
queryClient.invalidateQueries({ queryKey: QUERY_KEY.memos() });
},
onSettled: () => {
setIsOpen(false);
},
},
);

setIsOpen(false);
};

return (
Expand All @@ -112,7 +121,7 @@ export default function MemoOption({ lng, memoId }: MemoOptionProps) {
{t('option.deleteMemo')}
</DropdownMenuItem>
<DropdownMenuItem>
<Select defaultValue={String(memoData?.category_id)}>
<Select defaultValue={defaultCategoryId}>
<SelectTrigger>
<SelectValue placeholder={t('option.changeCategory')} />
</SelectTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@ import MemoOption from './MemoOption';

interface MemoCardFooterProps extends LanguageType, React.HTMLAttributes<HTMLDivElement>, PropsWithChildren {
memo: GetMemoResponse;
isHovered: boolean;
isOptionShown: boolean;
}
export default function MemoCardFooter({ memo, lng, isHovered, children, ...props }: MemoCardFooterProps) {
export default function MemoCardFooter({ memo, lng, isOptionShown, children, ...props }: MemoCardFooterProps) {
const { t } = useTranslation(lng);
const { toast } = useToast();
const supabaseClient = useSupabaseClient();
const searchParams = useSearchParams();
const router = useRouter();
const { mutate: mutateMemoPatch } = useMemoPatchMutation({
supabaseClient,
});
const { toast } = useToast();

const handleCategoryClick = (event: MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
if (!memo.category?.name) return;

searchParams.set('category', memo.category?.name);
router.replace(searchParams.getUrl(), { scroll: false });
router.push(searchParams.getUrl(), { scroll: false });
};

const handleIsWishClick = (event: MouseEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -87,8 +87,7 @@ export default function MemoCardFooter({ memo, lng, isHovered, children, ...prop
</div>
<div
className={cn('flex items-center transition', {
'opacity-0': !isHovered,
'opacity-100': isHovered,
'opacity-0': !isOptionShown,
})}>
<Button variant="ghost" size="icon" >
<HeartIcon
Expand All @@ -100,7 +99,7 @@ export default function MemoCardFooter({ memo, lng, isHovered, children, ...prop
})}
/>
</Button>
<MemoOption memoId={memo.id} lng={lng} />
<MemoOption memos={[memo]} lng={lng} />
{children}
</div>
</CardFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { GetMemoResponse } from '@extension/shared/utils';
import { cn, GetMemoResponse } from '@extension/shared/utils';
import { Button } from '@src/components/ui/button';
import { CardHeader } from '@src/components/ui/card';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@src/components/ui/tooltip';
import { CheckIcon } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { KeyboardEvent, MouseEvent } from 'react';

interface MemoCardHeaderProps {
memo: GetMemoResponse;
onSelect?: (e: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void;
tooltip?: boolean;
isHovered?: boolean;
isSelected?: boolean;
}

export default function MemoCardHeader({ memo }: MemoCardHeaderProps) {
export default function MemoCardHeader({ memo, isHovered, isSelected, onSelect }: MemoCardHeaderProps) {
return (
<CardHeader className="py-4 font-normal">
<CardHeader className="relative py-4 font-normal">
<Button
id={String(memo.id)}
variant="outline"
size="sm"
className={cn('absolute -left-4 -top-4 z-10 rounded-full px-2', {
'opacity-100': isHovered || isSelected,
'opacity-0': !isHovered && !isSelected,
})}
>
<CheckIcon size={8} />
</Button>
<Link href={memo.url} target="_blank" className="flex gap-2" => e.stopPropagation()}>
{memo?.favIconUrl && (
<Image
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function MemoDialog({ lng, id }: MemoDialog) {
{t('common.updatedAt')} {formatDate(memoData.updated_at, 'yyyy.mm.dd')}
</span>
</CardContent>
<MemoCardFooter memo={memoData as GetMemoResponse} lng={lng} isHovered>
<MemoCardFooter memo={memoData as GetMemoResponse} lng={lng} isOptionShown>
<div className="flex gap-2">
<Button variant="outline" type="button" >
{t('common.close')}
Expand Down
Loading
0