diff --git a/frontend/src/components/pages/main/MainPage.tsx b/frontend/src/components/pages/main/MainPage.tsx index 65884814..aaba6701 100644 --- a/frontend/src/components/pages/main/MainPage.tsx +++ b/frontend/src/components/pages/main/MainPage.tsx @@ -8,8 +8,10 @@ import TravelogueCard from "@components/pages/main/TravelogueCard/TravelogueCard import useIntersectionObserver from "@hooks/useIntersectionObserver"; import * as S from "./MainPage.styled"; +import TravelogueCardSkeleton from "./TravelogueCard/skeleton/TravelogueCardSkeleton"; const MainPage = () => { + const SKELETON_COUNT = 5; const { travelogues, status, fetchNextPage } = useInfiniteTravelogues(); const { lastElementRef } = useIntersectionObserver(fetchNextPage); @@ -20,7 +22,13 @@ const MainPage = () => {

지금 뜨고 있는 여행기

다른 이들의 여행을 한 번 구경해보세요.

- {status === "pending" && <>로딩 ...} + {status === "pending" && ( + + {Array.from({ length: SKELETON_COUNT }, (_, index) => ( + + ))} + + )} {travelogues.map(({ userAvatar, id, title, thumbnail, likes }) => ( { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts new file mode 100644 index 00000000..5bddedfa --- /dev/null +++ b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts @@ -0,0 +1,67 @@ +import { keyframes } from "@emotion/react"; +import styled from "@emotion/styled"; + +const skeletonAnimation = keyframes` + 0% { + background-position: -100% 0; + } + 100% { + background-position: 100% 0; + } +`; + +const SkeletonBase = styled.div` + background-color: ${(props) => props.theme.colors.skeleton.base}; + background-image: linear-gradient( + 90deg, + ${(props) => props.theme.colors.skeleton.base} 6rem, + ${(props) => props.theme.colors.skeleton.highlight} 12rem, + ${(props) => props.theme.colors.skeleton.base} 18rem + ); + background-size: 200% 200%; + background-repeat: no-repeat; + + animation: ${skeletonAnimation} 1.2s ease-in-out infinite; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.m}; +`; + +export const ThumbnailCard = styled(SkeletonBase)` + width: 100%; + height: 25rem; + border-radius: 10px; +`; + +export const BottomBarContainer = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + padding: 0 ${(props) => props.theme.spacing.m}; +`; + +export const TitleContainer = styled.div` + display: flex; + gap: ${(props) => props.theme.spacing.s}; +`; + +export const ProfileSkeleton = styled(SkeletonBase)` + width: 2.2rem; + height: 2.2rem; + border-radius: 50%; +`; + +export const TitleSkeleton = styled(SkeletonBase)` + width: 13rem; + height: 2.2rem; + border-radius: 10px; +`; + +export const LikesSkeleton = styled(SkeletonBase)` + width: 5rem; + height: 2.2rem; + border-radius: 10px; +`; diff --git a/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx new file mode 100644 index 00000000..8495c273 --- /dev/null +++ b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx @@ -0,0 +1,20 @@ +import * as S from "./TravelogueCardSkeleton.styled"; + +const TravelogueCardSkeleton = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default TravelogueCardSkeleton; diff --git a/frontend/src/styles/tokens/colors.ts b/frontend/src/styles/tokens/colors.ts index 8d5355ea..19ba37a1 100644 --- a/frontend/src/styles/tokens/colors.ts +++ b/frontend/src/styles/tokens/colors.ts @@ -42,4 +42,8 @@ export const SEMANTIC_COLORS = { danger: "#ea0000", kakao: "#f9e007", dimmed: "#0000004d", + skeleton: { + base: PRIMITIVE_COLORS.gray[200], + highlight: PRIMITIVE_COLORS.gray[100], + }, } as const;