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

✨ feat(#20) 내가 받은 초대 목록 컴포넌트 제작 #99

Merged
merged 15 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@tanstack/react-query": "^5.48.0",
"@tanstack/react-query-devtools": "^5.48.0",
"axios": "^1.7.2",
"lodash": "^4.17.21",
"next": "14.2.4",
"react": "^18",
"react-dom": "^18",
Expand All @@ -32,6 +33,7 @@
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@types/lodash": "^4.17.5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions public/icons/invitations.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 6 additions & 10 deletions src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux';

import DashboardItem from './DashboardItem';

import plus from '@/../public/icons/plus.svg';
import { useFetchDashboards } from '@/hooks/useFetchDashboards';
import { RootState } from '@/store/store';

Expand All @@ -23,15 +22,12 @@ export default function Sidebar() {
) : (
<>
<Link href='/' className='flex items-center justify-center md:block md:px-3'>
<Image src={'/icons/logo.svg'} alt='logo' priority className='hidden md:block' width={110} height={33} />
<Image
src={'/icons/logo-small.svg'}
alt='logo'
priority
className='block md:hidden'
width={27}
height={27}
/>
<div className='relative hidden h-[33px] w-[110px] md:block'>
<Image src={'/icons/logo.svg'} alt='logo' priority className='' fill />
</div>
<div className='relative block h-[27px] w-[27px] md:hidden'>
<Image src={'/icons/logo-small.svg'} alt='logo' priority className='' fill />
</div>
</Link>

<div className='flex flex-col gap-2'>
Expand Down
14 changes: 5 additions & 9 deletions src/containers/mydashboard/DashboardList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function DashboardList() {

return (
<section className='w-max'>
<ul className='grid gap-3 font-semibold text-black-33 md:min-h-[216px] md:grid-cols-2 lg:min-h-[140px] lg:grid-cols-3'>
<ul className='grid grid-rows-1 gap-3 font-semibold text-black-33 md:min-h-[216px] md:grid-cols-2 md:grid-rows-3 lg:min-h-[140px] lg:grid-cols-3 lg:grid-rows-2'>
<li className='h-16 w-64 rounded-lg border border-gray-d9 bg-white md:w-60 lg:w-80'>
<button className='btn-violet-light size-full gap-4'>
새로운 대시보드
Expand All @@ -63,7 +63,7 @@ export default function DashboardList() {
))}
</ul>

<div className='rou flex items-center justify-end pt-3'>
<div className='flex items-center justify-end pt-3'>
<span className='pr-4 text-sm text-black-33'>
{totalPage} 페이지 중 {currentChunk}
</span>
Expand All @@ -87,12 +87,8 @@ const NavButton = ({ direction, onClick, isDisable }: NavButtonProps) => (
onClick={onClick}
disabled={isDisable}
>
<Image
src={'/icons/arrow-white.svg'}
alt={`arrow-${direction}`}
className={`${direction === 'left' ? 'rotate-180' : ''}`}
width={8}
height={12}
/>
<div className={`${direction === 'left' ? 'rotate-180' : ''} relative h-[12px] w-[8px]`}>
<Image src={'/icons/arrow-white.svg'} alt={`arrow-${direction}`} fill />
</div>
</button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Image from 'next/image';

import ActionButton from '@/components/Button/ActionButton';
import CancelButton from '@/components/Button/CancelButton';
import { Invitation } from '@/types/Invitation.interface';

interface InvitationListProps {
invitations: Invitation[];
handleAcceptInvitation: (invitationId: number, inviteAccepted: boolean) => void;
observerRef: React.RefObject<HTMLDivElement>;
}

export default function InvitationItemList({ invitations, handleAcceptInvitation, observerRef }: InvitationListProps) {
return (
<div className='h-[calc(100%-130px)] pt-6 md:h-[calc(100%-170px)]'>
<div className='hidden grid-cols-3 pb-6 pl-7 text-gray-9f md:grid md:pr-7'>
<p>이름</p>
<p>초대자</p>
<p className='w-44'>수락 여부</p>
</div>
{invitations.length === 0 && (
<div className='flex h-full flex-col items-center justify-center'>
<div className='relative size-[60px] md:size-[100px]'>
<Image src={'/icons/invitations.svg'} alt='invitations' fill />
</div>
<p className='px-7 py-5 text-gray-78'>검색 결과가 없습니다.</p>
</div>
)}
<ul className='h-full overflow-y-scroll'>
{invitations.map((invitation: Invitation) => (
<li
key={invitation.id}
className='grid h-max grid-cols-1 gap-[10px] border-b border-gray-ee p-4 text-sm text-black-33 md:h-16 md:grid-cols-3 md:gap-0 md:px-7 md:py-0 md:text-base'
>
<div className='grid grid-cols-3 items-center md:flex'>
<p className='flex items-center md:hidden'>이름</p>
<p className='col-span-2 flex items-center'>{invitation.dashboard.title}</p>
</div>
<div className='grid grid-cols-3 items-center md:flex'>
<p className='flex items-center md:hidden'>초대자</p>
<p className='col-span-2 flex items-center md:min-w-28'>{invitation.inviter.nickname}</p>
</div>
<div className='flex items-center gap-[10px]'>
<ActionButton onClick={() => handleAcceptInvitation(invitation.id, true)} className='w-20 grow lg:grow-0'>
수락
</ActionButton>
<CancelButton
onClick={() => handleAcceptInvitation(invitation.id, false)}
className='w-20 grow lg:grow-0'
>
거절
</CancelButton>
</div>
</li>
))}

<div ref={observerRef} className='md:h-3 lg:h-5' />
</ul>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

이름이 명확하지 않은 것 같아요!
InvitationSearchBar.tsx 나 상위에서 이미 Invitation관련인 거 같다면 SearchBar.tsx 어떨까요??
후자는 다른 import 할 때 헷갈리려나요..?

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Image from 'next/image';

interface SearchBarProps {
handleChangeSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export default function SearchBar({ handleChangeSearch }: SearchBarProps) {
return (
<div className='relative px-7'>
<div className='absolute left-11 top-2 size-[24px]'>
<Image src={'/icons/search.svg'} alt='search' fill />
</div>
<input
placeholder='검색'
className='size-full rounded-md border border-gray-d9 bg-white py-[8px] pl-12 pr-4'
onChange={handleChangeSearch}
/>
</div>
);
}
145 changes: 145 additions & 0 deletions src/containers/mydashboard/InvitedDashboardList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useQueryClient } from '@tanstack/react-query';
import { debounce } from 'lodash';
import Image from 'next/image';
import { useState, useEffect, useCallback, useRef } from 'react';

import InvitationItemList from './InvitationItemList';
import SearchBar from './InvitationSearch';

import useFetchData from '@/hooks/useFetchData';
import { getInvitationsList } from '@/services/getService';
import { putAcceptInvitation } from '@/services/putService';
import { Invitation, InvitationsResponse } from '@/types/Invitation.interface';

export default function InvitedDashboardList() {
const [invitations, setInvitations] = useState<Invitation[]>([]);
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
const [cursorId, setCursorId] = useState<number>(0);

const queryClient = useQueryClient();

const { data, error, isLoading } = useFetchData<InvitationsResponse>(['invitations'], () => getInvitationsList());

useEffect(() => {
if (data) {
setInvitations(data.invitations);
setCursorId(data.cursorId ? data.cursorId : 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

data.cursorId ?? 0 해도 될 것 같아요!
null이나 undefined면 0 넣는건가요??

}
}, [data]);

const handleMoreInvitations = async (currentCursorId: number, searchValue: string = '') => {
if (currentCursorId !== 0 || searchValue) {
try {
setIsFetchingNextPage(true);
const { data: nextData } = await getInvitationsList(10, currentCursorId, searchValue);

if (nextData.invitations.length > 0) {
setInvitations((prevInvitations) => [...prevInvitations, ...nextData.invitations]);
}
setCursorId(nextData.cursorId || 0);
} catch (err) {
console.error('데이터를 가져오는 중 오류가 발생했습니다:', err);
} finally {
setIsFetchingNextPage(false);
}
}
};

const handleObserver = useCallback(
async (entries: IntersectionObserverEntry[]) => {
const target = entries[0];
if (target.isIntersecting && !isFetchingNextPage && cursorId && !isSearching) {
handleMoreInvitations(cursorId);
}
},
[cursorId, isFetchingNextPage, isSearching],
);

useEffect(() => {
const observer = new IntersectionObserver(handleObserver, {
threshold: 1.0,
});

const currentObserverRef = observerRef.current;

if (currentObserverRef) {
observer.observe(currentObserverRef);
}

return () => {
if (currentObserverRef) {
observer.unobserve(currentObserverRef);
}
};
}, [handleObserver]);

const handleAcceptInvitation = async (invitationId: number, inviteAccepted: boolean) => {
try {
await putAcceptInvitation(invitationId, inviteAccepted);
setInvitations((prevInvitations) => prevInvitations.filter((invitation) => invitation.id !== invitationId));
Copy link
Contributor

Choose a reason for hiding this comment

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

inviteAccepted true일 때만 대시보드 목록 다시 가져오면 좋을 것 같습니다.

queryClient.invalidateQueries({ queryKey: ['dashboards'] });
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 좋은데, 그러면 사이드바만 다시 가져오고 대시보드 목록은 그대로 아닌가요??
-> 대시보드 목록도 같은 페이지 새로 불러오긴 해야할 것 같아요!

대시보드 목록에 새로 만든것일수록 아래로 가나요 위로 가나요??

} catch (err) {
console.error('초대 업데이트 중 오류 발생:', err);
}
};

const handleChangeSearch = debounce(async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = e.target.value;
setIsSearching(!!searchValue);

try {
const { data: searchData } = await getInvitationsList(10, 0, searchValue);
setInvitations(searchData.invitations);
} catch (err) {
console.error('데이터를 가져오는 중 오류가 발생했습니다:', err);
}
}, 300);

if (isLoading) {
return (
<div className='h-full max-w-screen-lg overflow-hidden rounded-lg border-0 bg-white'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33'>초대받은 대시보드</p>
<div className='flex items-center justify-center'>
<p>불러오는 중...</p>
</div>
</div>
);
}

if (error) {
return (
<div className='h-full max-w-screen-lg overflow-hidden rounded-lg border-0 bg-white'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33'>초대받은 대시보드</p>
<div className='flex items-center justify-center'>
<p>데이터를 가져오는 중 오류가 발생했습니다.</p>
<p>{error.message}</p>
</div>
</div>
);
}

return (
<section className='h-full min-h-80 overflow-hidden rounded-lg border-0 bg-white'>
<p className='px-7 pb-5 pt-8 text-base font-bold text-black-33'>초대받은 대시보드</p>
{invitations.length > 0 || isSearching ? (
<>
<SearchBar handleChangeSearch={handleChangeSearch} />
<InvitationItemList
invitations={invitations}
handleAcceptInvitation={handleAcceptInvitation}
observerRef={observerRef}
/>
</>
) : (
<div className='flex h-full flex-col items-center justify-center'>
<div className='relative size-[60px] md:size-[100px]'>
<Image src={'/icons/invitations.svg'} alt='invitations' fill />
</div>
<p className='px-7 py-5 text-gray-78'>초대된 대시보드가 없습니다.</p>
</div>
)}
</section>
);
}
4 changes: 3 additions & 1 deletion src/containers/mydashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import DashboardList from './DashboardList';
import InvitedDashboardList from './InvitedDashboardList';

export default function MyDashboard() {
return (
<div className='p-10'>
<div className='flex h-dvh max-h-[calc(100dvh-70px)] w-full max-w-min flex-col gap-11 bg-gray-fa p-10'>
<DashboardList />
<InvitedDashboardList />
</div>
);
}
Loading