-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from 14 commits
5aa6faa
0e09de8
07d6fc9
f748cb8
d0b2e85
54297c6
3ee0f75
dd46375
b21866c
d7d8ebf
b34c8d9
e06c000
90110f9
66d3f4d
0fd8ac9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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> | ||
); | ||
} |
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> | ||
); | ||
} |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. data.cursorId ?? 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inviteAccepted true일 때만 대시보드 목록 다시 가져오면 좋을 것 같습니다. |
||
queryClient.invalidateQueries({ queryKey: ['dashboards'] }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} |
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> | ||
); | ||
} |
There was a problem hiding this comment.
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 할 때 헷갈리려나요..?