diff --git a/src/app/(routes)/space/[spaceId]/page.tsx b/src/app/(routes)/space/[spaceId]/page.tsx index d28105ba..2eaf4934 100644 --- a/src/app/(routes)/space/[spaceId]/page.tsx +++ b/src/app/(routes)/space/[spaceId]/page.tsx @@ -5,6 +5,7 @@ import Button from '@/components/common/Button/Button' import DeferredComponent from '@/components/common/DeferedComponent/DeferedComponent' import useViewLink from '@/components/common/LinkList/hooks/useViewLink' import Space from '@/components/common/Space/Space' +import SpaceSkeleton from '@/components/common/Space/SpaceSkeleton' import useGetSpace from '@/components/common/Space/hooks/useGetSpace' import useGetTags from '@/components/common/Space/hooks/useGetTags' import Tab from '@/components/common/Tab/Tab' @@ -36,27 +37,29 @@ const SpacePage = ({ params }: { params: { spaceId: number } }) => { }) const { tag, tagIndex, handleTagChange } = useTagParam({ tags }) - return isSpaceLoading || isTagsLoading ? ( - <DeferredComponent> - <Spinner /> - </DeferredComponent> - ) : ( + return ( <> - {space && ( - <Space - type="Header" - userName={space.memberDetailInfos[0].nickname} - spaceId={space.spaceId} - spaceName={space.spaceName} - spaceImage={space.spaceImagePath} - description={space.description} - category={CATEGORIES_RENDER[space.category]} - scrap={space.scrapCount} - favorite={space.favoriteCount} - hasFavorite={space.hasFavorite} - hasScrap={space.hasScrap} - isVisible={space.isVisible} - /> + {isSpaceLoading ? ( + <DeferredComponent> + <SpaceSkeleton /> + </DeferredComponent> + ) : ( + space && ( + <Space + type="Header" + userName={space.memberDetailInfos[0].nickname} + spaceId={space.spaceId} + spaceName={space.spaceName} + spaceImage={space.spaceImagePath} + description={space.description} + category={CATEGORIES_RENDER[space.category]} + scrap={space.scrapCount} + favorite={space.favoriteCount} + hasFavorite={space.hasFavorite} + hasScrap={space.hasScrap} + isVisible={space.isVisible} + /> + ) )} {tabList.length > MIN_TAB_NUMBER && ( <Tab> diff --git a/src/app/page.tsx b/src/app/page.tsx index 1d4d0e4d..37f507e2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import { CategoryList, Dropdown, LinkItem, Spinner } from '@/components' import FloatingButton from '@/components/FloatingButton/FloatingButton' import { ChipColors } from '@/components/common/Chip/Chip' import DeferredComponent from '@/components/common/DeferedComponent/DeferedComponent' +import LinkItemSkeleton from '@/components/common/LinkItem/LinkItemSkeleton' import MainSpaceList from '@/components/common/MainSpaceList/MainSpaceList' import { useCategoryParam, useSortParam } from '@/hooks' import useGetPopularLinks from '@/hooks/useGetPopularLinks' @@ -23,67 +24,65 @@ export default function Home() { return ( <> - {isPopularLinksLoading ? ( - <DeferredComponent> - <Spinner /> - </DeferredComponent> - ) : ( - <> - <section className="px-4 pb-8"> - <h2 className="py-4 font-bold text-gray9">인기있는 링크</h2> - {links && ( - <Swiper - slidesPerView={2.1} - spaceBetween={16} - freeMode={true} - pagination={{ - clickable: true, - }} - modules={[FreeMode]} - className="mySwiper"> - {links.map((link: PopularLinkResBody) => ( - <SwiperSlide key={link.linkId}> - <LinkItem - linkId={link.linkId} - title={link.title} - url={link.url} - tagName={link.tagName} - tagColor={link.tagColor as ChipColors} - isInitLiked={link.isLiked} - likeInitCount={link.likeCount} - type="card" - /> - </SwiperSlide> - ))} - </Swiper> - )} - </section> - <section> - <div className="sticky top-[53px] z-40 bg-bgColor"> - <div className="flex items-center justify-between px-4 pt-2"> - <h2 className="font-bold text-gray9">스페이스 모음</h2> - <Dropdown - type="space" - placement="right" - defaultIndex={sortIndex} - onChange={handleSortChange} - /> - </div> - <CategoryList - type="all" - defaultIndex={categoryIndex} - onChange={handleCategoryChange} - /> - </div> - <MainSpaceList - queryKey="main" - sort={sort ?? ''} - category={category ?? ''} - fetchFn={fetchGetSpaces} + <section className="px-4 pb-8"> + <h2 className="py-4 font-bold text-gray9">인기있는 링크</h2> + {isPopularLinksLoading ? ( + <DeferredComponent> + <LinkItemSkeleton type="card" /> + </DeferredComponent> + ) : ( + links && ( + <Swiper + slidesPerView={2.1} + spaceBetween={16} + freeMode={true} + pagination={{ + clickable: true, + }} + modules={[FreeMode]} + className="mySwiper"> + {links.map((link: PopularLinkResBody) => ( + <SwiperSlide key={link.linkId}> + <LinkItem + linkId={link.linkId} + title={link.title} + url={link.url} + tagName={link.tagName} + tagColor={link.tagColor as ChipColors} + isInitLiked={link.isLiked} + likeInitCount={link.likeCount} + type="card" + /> + </SwiperSlide> + ))} + </Swiper> + ) + )} + </section> + <section> + <div className="sticky top-[53px] z-40 bg-bgColor"> + <div className="flex items-center justify-between px-4 pt-2"> + <h2 className="font-bold text-gray9">스페이스 모음</h2> + <Dropdown + type="space" + placement="right" + defaultIndex={sortIndex} + onChange={handleSortChange} /> - </section> - </> - )} + </div> + <CategoryList + type="all" + defaultIndex={categoryIndex} + onChange={handleCategoryChange} + /> + </div> + <MainSpaceList + queryKey="main" + sort={sort ?? ''} + category={category ?? ''} + fetchFn={fetchGetSpaces} + /> + </section> <FloatingButton /> </> ) diff --git a/src/components/common/LinkItem/LinkItemSkeleton.tsx b/src/components/common/LinkItem/LinkItemSkeleton.tsx new file mode 100644 index 00000000..50759235 --- /dev/null +++ b/src/components/common/LinkItem/LinkItemSkeleton.tsx @@ -0,0 +1,36 @@ +import Skeleton from '../Skeleton/Skeleton' + +export interface LinkItemSkeletonProps { + type?: 'list' | 'card' +} + +const LinkItemSkeleton = ({ type }: LinkItemSkeletonProps) => { + return ( + <> + {type === 'list' ? ( + <div className="flex items-center justify-between gap-2 border-t border-slate3 px-3 py-2 last:border-b"> + <Skeleton className="h-5 w-3/4" /> + </div> + ) : ( + <div className="flex"> + <div className="mr-4 flex min-h-[101.5px] w-[214.47px] flex-col justify-between gap-1 rounded-md border border-slate3 px-3 py-2.5"> + <Skeleton className="h-5 w-4/5" /> + <Skeleton className="h-5 w-10" /> + <div className="flex items-center justify-end"> + <Skeleton className="h-5 w-10" /> + </div> + </div> + <div className="mr-4 flex min-h-[101.5px] w-[214.47px] flex-col justify-between gap-1 rounded-md border border-slate3 px-3 py-2.5"> + <Skeleton className="h-5 w-4/5" /> + <Skeleton className="h-5 w-10" /> + <div className="flex items-center justify-end"> + <Skeleton className="h-5 w-10" /> + </div> + </div> + </div> + )} + </> + ) +} + +export default LinkItemSkeleton diff --git a/src/components/common/MainSpaceList/MainSpaceList.tsx b/src/components/common/MainSpaceList/MainSpaceList.tsx index bf34e221..6c821a87 100644 --- a/src/components/common/MainSpaceList/MainSpaceList.tsx +++ b/src/components/common/MainSpaceList/MainSpaceList.tsx @@ -6,9 +6,9 @@ import useMainSpacesQuery from '@/components/SpaceList/hooks/useMainSpacesQuery' import { CATEGORIES_RENDER } from '@/constants' import useInfiniteScroll from '@/hooks/useInfiniteScroll' import { SearchSpaceReqBody, SpaceResBody } from '@/types' -import { Spinner } from '../..' import DeferredComponent from '../DeferedComponent/DeferedComponent' import Space from '../Space/Space' +import MainSpaceSkeleton from './MainSpaceSkeleton' export interface SpaceListProps { memberId?: number @@ -50,7 +50,7 @@ const MainSpaceList = ({ return isSpacesLoading ? ( <DeferredComponent> - <Spinner /> + <MainSpaceSkeleton /> </DeferredComponent> ) : ( <> diff --git a/src/components/common/MainSpaceList/MainSpaceSkeleton.tsx b/src/components/common/MainSpaceList/MainSpaceSkeleton.tsx new file mode 100644 index 00000000..172501d4 --- /dev/null +++ b/src/components/common/MainSpaceList/MainSpaceSkeleton.tsx @@ -0,0 +1,25 @@ +import SpaceSkeleton from '../Space/SpaceSkeleton' + +const MainSpaceSkeleton = () => { + return ( + <ul className="flex flex-col gap-y-2 px-4 pt-2"> + <li> + <SpaceSkeleton type="Card" /> + </li> + <li> + <SpaceSkeleton type="Card" /> + </li> + <li> + <SpaceSkeleton type="Card" /> + </li> + <li> + <SpaceSkeleton type="Card" /> + </li> + <li> + <SpaceSkeleton type="Card" /> + </li> + </ul> + ) +} + +export default MainSpaceSkeleton diff --git a/src/components/common/Skeleton/Skeleton.tsx b/src/components/common/Skeleton/Skeleton.tsx new file mode 100644 index 00000000..f5442735 --- /dev/null +++ b/src/components/common/Skeleton/Skeleton.tsx @@ -0,0 +1,14 @@ +interface SkeletonProps { + className?: string +} + +const Skeleton = ({ className }: SkeletonProps) => { + return ( + <div + className={ + className + ' rounded-xl bg-slate-100 dark:bg-slate-800 ' + }></div> + ) +} + +export default Skeleton diff --git a/src/components/common/Space/SpaceSkeleton.tsx b/src/components/common/Space/SpaceSkeleton.tsx new file mode 100644 index 00000000..bc1b5468 --- /dev/null +++ b/src/components/common/Space/SpaceSkeleton.tsx @@ -0,0 +1,38 @@ +import Skeleton from '../Skeleton/Skeleton' + +interface SpaceSkeletonProps { + type?: 'Card' | 'Header' +} + +const SpaceSkeleton = ({ type }: SpaceSkeletonProps) => { + return ( + <> + {type === 'Card' ? ( + <div className="relative flex gap-3 rounded-md border border-slate3 p-2"> + <div className="flex grow flex-col justify-center gap-1 rounded-md bg-white bg-opacity-60 px-3 py-1.5 dark:bg-gray-900 dark:bg-opacity-60"> + <Skeleton className="h-5 w-1/2" /> + <Skeleton className="h-5 w-1/2" /> + <div className="flex justify-between"> + <Skeleton className="h-5 w-20" /> + <Skeleton className="h-5 w-20" /> + </div> + </div> + </div> + ) : ( + <div className="relative flex flex-col gap-10 rounded-md border border-slate3 p-4"> + <div className="flex justify-end gap-2"> + <Skeleton className="h-5 w-20" /> + <Skeleton className="h-5 w-20" /> + </div> + <div className="flex flex-col gap-1.5 rounded-md border border-slate3 bg-white bg-opacity-60 px-3 py-1.5 dark:bg-gray-900 dark:bg-opacity-60"> + <Skeleton className="h-5 w-1/2" /> + <Skeleton className="h-4 w-1/3" /> + <Skeleton className="h-5 w-20" /> + </div> + </div> + )} + </> + ) +} + +export default SpaceSkeleton