-
Notifications
You must be signed in to change notification settings - Fork 6
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: 레스토랑 카드에 loading 상태 추가 및 skeleton 반영 #202
The head ref may contain hidden characters: "199-feat-\uB808\uC2A4\uD1A0\uB791-\uCE74\uB4DC\uC5D0-loading-\uC0C1\uD0DC-\uCD94\uAC00-\uBC0F-skeleton-\uBC18\uC601"
Changes from all commits
462bbb7
161d6fe
0838f20
d09af48
11cf5f8
8748b91
11ba021
43ca047
fdeb97a
f731c1d
b4ad157
f2df719
e1b1f3c
7a3cf54
389520b
3a8a10e
e60b230
d11c319
7dc4a3e
fbc4ff1
cfb505f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { styled } from 'styled-components'; | ||
import { paintSkeleton } from '~/styles/common'; | ||
|
||
interface ProfileImageSkeletonProps { | ||
size: number; | ||
} | ||
|
||
function ProfileImageSkeleton({ size }: ProfileImageSkeletonProps) { | ||
return <StyledProfileImageSkeleton size={size} />; | ||
} | ||
|
||
export default ProfileImageSkeleton; | ||
|
||
const StyledProfileImageSkeleton = styled.div<{ size: number }>` | ||
${paintSkeleton} | ||
width: ${({ size }) => (size ? `${size}px` : '100%')}; | ||
height: ${({ size }) => (size ? `${size}px` : 'auto')}; | ||
border-radius: 50%; | ||
background: none; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { styled } from 'styled-components'; | ||
import ProfileImageSkeleton from '../@common/ProfileImage/ProfileImageSkeleton'; | ||
import { BORDER_RADIUS, paintSkeleton } from '~/styles/common'; | ||
|
||
function RestaurantCardSkeleton() { | ||
return ( | ||
<StyledContainer> | ||
<StyledImage /> | ||
<section> | ||
<StyledInfo> | ||
<StyledCategory /> | ||
<StyledName /> | ||
<StyledAddress /> | ||
<StyledAddress /> | ||
</StyledInfo> | ||
<StyledProfileImageSection> | ||
<ProfileImageSkeleton size={42} /> | ||
</StyledProfileImageSection> | ||
</section> | ||
</StyledContainer> | ||
); | ||
} | ||
|
||
export default RestaurantCardSkeleton; | ||
|
||
const StyledContainer = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: start; | ||
gap: 0.8rem; | ||
|
||
width: 100%; | ||
height: 100%; | ||
|
||
& > section { | ||
display: flex; | ||
justify-content: space-between; | ||
} | ||
|
||
cursor: pointer; | ||
`; | ||
|
||
const StyledImage = styled.div` | ||
${paintSkeleton} | ||
width: 100%; | ||
aspect-ratio: 1.05 / 1; | ||
|
||
object-fit: cover; | ||
|
||
border-radius: ${BORDER_RADIUS.md}; | ||
`; | ||
|
||
const StyledInfo = styled.div` | ||
display: flex; | ||
flex: 1; | ||
flex-direction: column; | ||
gap: 0.4rem; | ||
|
||
position: relative; | ||
|
||
width: 100%; | ||
|
||
padding: 0.4rem; | ||
`; | ||
|
||
const StyledName = styled.h5` | ||
${paintSkeleton} | ||
width: 100%; | ||
height: 20px; | ||
|
||
border-radius: ${BORDER_RADIUS.xs}; | ||
`; | ||
|
||
const StyledAddress = styled.span` | ||
${paintSkeleton} | ||
width: 50%; | ||
height: 12px; | ||
|
||
border-radius: ${BORDER_RADIUS.xs}; | ||
`; | ||
|
||
const StyledCategory = styled.span` | ||
${paintSkeleton} | ||
width: 40%; | ||
height: 12px; | ||
|
||
border-radius: ${BORDER_RADIUS.xs}; | ||
`; | ||
|
||
const StyledProfileImageSection = styled.div` | ||
align-self: flex-end; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { styled } from 'styled-components'; | ||
import { useEffect, useState } from 'react'; | ||
import RestaurantCard from '../RestaurantCard/RestaurantCard'; | ||
import { FONT_SIZE } from '~/styles/common'; | ||
import RestaurantCardListSkeleton from './RestaurantCardListSkeleton'; | ||
|
||
import type { RestaurantData, RestaurantListData } from '~/@types/api.types'; | ||
|
||
interface RestaurantCardListProps { | ||
restaurantDataList: RestaurantListData | null; | ||
loading: boolean; | ||
setHoveredId: React.Dispatch<React.SetStateAction<number>>; | ||
} | ||
|
||
function RestaurantCardList({ restaurantDataList, loading, setHoveredId }: RestaurantCardListProps) { | ||
const [prevCardNumber, setPrevCardNumber] = useState(18); | ||
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. restaurant List 개수에 따라 스켈레톤 UI 개수를 지정하는 로직 이군요!! 해당 로직의 경우 커스텀 훅으로 분리를 해도 좋을 거 같아요!! 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. 저도 커스텀 훅 분리를 좋아하는 사람으로써 분리를 하라면 하겠지만, 계속 살펴보니 분리를 해서 얻을 수 있는 장점이 크게 없어 보여요..!
이에 대해서 푸만능은 어떻게 생각하시나요? 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. 다시 보니 저 매직넘버 18 은 상수로 분리할 필요는 있어보이네요! ㅎㅎ |
||
|
||
useEffect(() => { | ||
if (restaurantDataList) setPrevCardNumber(restaurantDataList.currentElementsCount); | ||
}, [restaurantDataList?.currentElementsCount]); | ||
|
||
if (!restaurantDataList || loading) return <RestaurantCardListSkeleton cardNumber={prevCardNumber} />; | ||
|
||
return ( | ||
<div> | ||
<StyledCardListHeader>음식점 수 {restaurantDataList.totalElementsCount} 개</StyledCardListHeader> | ||
<StyledRestaurantCardList> | ||
{restaurantDataList.content?.map(({ celebs, ...restaurant }: RestaurantData) => ( | ||
<RestaurantCard restaurant={restaurant} celebs={celebs} size="42px" setHoveredId={setHoveredId} /> | ||
))} | ||
</StyledRestaurantCardList> | ||
</div> | ||
); | ||
} | ||
|
||
export default RestaurantCardList; | ||
|
||
const StyledCardListHeader = styled.p` | ||
margin: 3.2rem 2.4rem; | ||
font-size: ${FONT_SIZE.md}; | ||
`; | ||
|
||
const StyledRestaurantCardList = styled.div` | ||
display: grid; | ||
gap: 4rem 2.4rem; | ||
height: 100%; | ||
margin: 0 2.4rem; | ||
grid-template-columns: 1fr 1fr 1fr; | ||
@media screen and (width <= 1240px) { | ||
grid-template-columns: 1fr 1fr; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { styled } from 'styled-components'; | ||
import RestaurantCardSkeleton from '../RestaurantCard/RestaurantCardSkeleton'; | ||
import { BORDER_RADIUS, paintSkeleton } from '~/styles/common'; | ||
|
||
interface RestaurantCardListSkeletonProps { | ||
cardNumber: number; | ||
} | ||
|
||
function RestaurantCardListSkeleton({ cardNumber }: RestaurantCardListSkeletonProps) { | ||
return ( | ||
<div> | ||
<StyledCardListHeader /> | ||
<StyledRestaurantCardList> | ||
{Array.from({ length: cardNumber }, () => ( | ||
<RestaurantCardSkeleton /> | ||
))} | ||
</StyledRestaurantCardList> | ||
</div> | ||
); | ||
} | ||
|
||
export default RestaurantCardListSkeleton; | ||
|
||
const StyledCardListHeader = styled.p` | ||
${paintSkeleton} | ||
width: 35%; | ||
height: 16px; | ||
margin: 3.2rem 2.4rem; | ||
border-radius: ${BORDER_RADIUS.xs}; | ||
`; | ||
|
||
const StyledRestaurantCardList = styled.div` | ||
display: grid; | ||
gap: 4rem 2.4rem; | ||
height: 100%; | ||
margin: 0 2.4rem; | ||
grid-template-columns: 1fr 1fr 1fr; | ||
@media screen and (width <= 1240px) { | ||
grid-template-columns: 1fr 1fr; | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import RestaurantCardList from './RestaurantCardList'; | ||
|
||
export default RestaurantCardList; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { useEffect, RefObject } from 'react'; | ||
|
||
export default function useOnClickOutside<T extends HTMLElement = HTMLElement>( | ||
ref: RefObject<T>, | ||
handler: (event?: Event | MouseEvent) => void, | ||
) { | ||
useEffect(() => { | ||
function onClickHandler(event: Event | MouseEvent) { | ||
if (!ref?.current || ref?.current.contains(event?.target as Node)) { | ||
return; | ||
} | ||
handler(event); | ||
} | ||
window.addEventListener('click', onClickHandler); | ||
return () => { | ||
window.removeEventListener('click', onClickHandler); | ||
}; | ||
}, [ref, handler]); | ||
} |
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.
data가 존재하지 않았을 어떤 UI를 보여줄 지 같이 고려를 해보면 좋을 거 같아요!!
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.
맞아요.. 고려해야합니다..
이 부분은 새로 이슈를 파서 추가해보도록 할게요~!