diff --git a/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx b/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx deleted file mode 100644 index 292e12c06..000000000 --- a/frontend/src/components/Common/MoreButton/MoreButton.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import MoreButton from './MoreButton'; - -const meta: Meta = { - title: 'common/MoreButton', - component: MoreButton, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Common/MoreButton/MoreButton.tsx b/frontend/src/components/Common/MoreButton/MoreButton.tsx deleted file mode 100644 index 479aec96d..000000000 --- a/frontend/src/components/Common/MoreButton/MoreButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Link, Text } from '@fun-eat/design-system'; -import { Link as RouterLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import SvgIcon from '../Svg/SvgIcon'; - -import { PATH } from '@/constants/path'; - -const MoreButton = () => { - return ( - - - - - - - 더보기 - - - - ); -}; - -export default MoreButton; - -const MoreButtonWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 110px; - height: 110px; - border-radius: 5px; - background: ${({ theme }) => theme.colors.gray1}; -`; - -const PlusIconWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 40px; - height: 40px; - margin-bottom: 5px; - border-radius: 50%; - background: ${({ theme }) => theme.colors.white}; -`; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx b/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..e8952c316 --- /dev/null +++ b/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Skeleton from './Skeleton'; + +const meta: Meta = { + title: 'common/Skeleton', + component: Skeleton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + width: 100, + height: 100, + }, +}; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.tsx b/frontend/src/components/Common/Skeleton/Skeleton.tsx new file mode 100644 index 000000000..857d03079 --- /dev/null +++ b/frontend/src/components/Common/Skeleton/Skeleton.tsx @@ -0,0 +1,36 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import styled from 'styled-components'; + +interface SkeletonProps extends ComponentPropsWithoutRef<'div'> { + width?: string | number; + height?: string | number; +} + +const Skeleton = ({ width, height }: SkeletonProps) => { + return ; +}; + +export default Skeleton; + +export const SkeletonContainer = styled.div` + position: absolute; + width: ${({ width }) => (typeof width === 'number' ? width + 'px' : width)}; + height: ${({ height }) => (typeof height === 'number' ? height + 'px' : height)}; + border-radius: 8px; + background: linear-gradient(-90deg, #dddddd, #f7f7f7, #dddddd, #f7f7f7); + background-size: 400%; + overflow: hidden; + animation: skeleton-gradient 5s infinite ease-out; + + @keyframes skeleton-gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } +`; diff --git a/frontend/src/components/Common/index.ts b/frontend/src/components/Common/index.ts index 493fe72bb..7144a82ba 100644 --- a/frontend/src/components/Common/index.ts +++ b/frontend/src/components/Common/index.ts @@ -15,9 +15,9 @@ export { default as ErrorBoundary } from './ErrorBoundary/ErrorBoundary'; export { default as ErrorComponent } from './ErrorComponent/ErrorComponent'; export { default as Loading } from './Loading/Loading'; export { default as MarkedText } from './MarkedText/MarkedText'; -export { default as MoreButton } from './MoreButton/MoreButton'; export { default as NavigableSectionTitle } from './NavigableSectionTitle/NavigableSectionTitle'; export { default as Carousel } from './Carousel/Carousel'; export { default as RegisterButton } from './RegisterButton/RegisterButton'; export { default as CategoryItem } from './CategoryItem/CategoryItem'; export { default as CategoryList } from './CategoryList/CategoryList'; +export { default as Skeleton } from './Skeleton/Skeleton'; diff --git a/frontend/src/components/Members/MembersInfo/MembersInfo.tsx b/frontend/src/components/Members/MembersInfo/MembersInfo.tsx index 5ed9e89e3..1b2e94839 100644 --- a/frontend/src/components/Members/MembersInfo/MembersInfo.tsx +++ b/frontend/src/components/Members/MembersInfo/MembersInfo.tsx @@ -60,4 +60,5 @@ const MembersImage = styled.img` margin-right: 16px; border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; diff --git a/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx b/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx deleted file mode 100644 index 175918ec5..000000000 --- a/frontend/src/components/Product/PBProductItem/PBProductItem.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import PBProductItem from './PBProductItem'; - -import pbProducts from '@/mocks/data/pbProducts.json'; - -const meta: Meta = { - title: 'product/PBProductItem', - component: PBProductItem, - args: { - pbProduct: pbProducts.products[0], - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Product/PBProductItem/PBProductItem.tsx b/frontend/src/components/Product/PBProductItem/PBProductItem.tsx deleted file mode 100644 index 63cf774ef..000000000 --- a/frontend/src/components/Product/PBProductItem/PBProductItem.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Text, theme } from '@fun-eat/design-system'; -import styled from 'styled-components'; - -import PBPreviewImage from '@/assets/samgakgimbab.svg'; -import { SvgIcon } from '@/components/Common'; -import type { PBProduct } from '@/types/product'; - -interface PBProductItemProps { - pbProduct: PBProduct; -} - -const PBProductItem = ({ pbProduct }: PBProductItemProps) => { - const { name, price, image, averageRating } = pbProduct; - - return ( - - {image !== null ? ( - - ) : ( - - )} - - {name} - - - - - {averageRating} - - - - {price.toLocaleString('ko-KR')}원 - - - - - ); -}; - -export default PBProductItem; - -const PBProductItemContainer = styled.div` - width: 110px; -`; - -const PBProductImage = styled.img` - object-fit: cover; -`; - -const PBProductInfoWrapper = styled.div` - height: 50%; - margin-top: 10px; -`; - -const PBProductName = styled(Text)` - display: inline-block; - width: 100%; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -`; - -const PBProductReviewWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - margin: 5px 0; -`; - -const RatingWrapper = styled.div` - display: flex; - align-items: center; - column-gap: 4px; - - & > svg { - padding-bottom: 2px; - } -`; diff --git a/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx b/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx deleted file mode 100644 index dc1804f08..000000000 --- a/frontend/src/components/Product/PBProductList/PBProductList.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import PBProductList from './PBProductList'; - -const meta: Meta = { - title: 'product/PBProductList', - component: PBProductList, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/frontend/src/components/Product/PBProductList/PBProductList.tsx b/frontend/src/components/Product/PBProductList/PBProductList.tsx deleted file mode 100644 index 576ea05a4..000000000 --- a/frontend/src/components/Product/PBProductList/PBProductList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Link } from '@fun-eat/design-system'; -import { Link as RouterLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import PBProductItem from '../PBProductItem/PBProductItem'; - -import { MoreButton } from '@/components/Common'; -import { PATH } from '@/constants/path'; -import { useCategoryValueContext } from '@/hooks/context'; -import { useInfiniteProductsQuery } from '@/hooks/queries/product'; - -const PBProductList = () => { - const { categoryIds } = useCategoryValueContext(); - - const { data: pbProductListResponse } = useInfiniteProductsQuery(categoryIds.store); - const pbProducts = pbProductListResponse.pages.flatMap((page) => page.products); - - return ( - <> - - {pbProducts.map((pbProduct) => ( -
  • - - - -
  • - ))} -
  • - -
  • -
    - - ); -}; - -export default PBProductList; - -const PBProductListContainer = styled.ul` - display: flex; - gap: 40px; - overflow-x: auto; - overflow-y: hidden; - - &::-webkit-scrollbar { - display: none; - } -`; diff --git a/frontend/src/components/Product/ProductItem/ProductItem.tsx b/frontend/src/components/Product/ProductItem/ProductItem.tsx index 6fb9ce673..ecdf8b030 100644 --- a/frontend/src/components/Product/ProductItem/ProductItem.tsx +++ b/frontend/src/components/Product/ProductItem/ProductItem.tsx @@ -1,8 +1,9 @@ import { Text, useTheme } from '@fun-eat/design-system'; +import { useState } from 'react'; import styled from 'styled-components'; import PreviewImage from '@/assets/characters.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { Product } from '@/types/product'; interface ProductItemProps { @@ -12,11 +13,22 @@ interface ProductItemProps { const ProductItem = ({ product }: ProductItemProps) => { const theme = useTheme(); const { name, price, image, averageRating, reviewCount } = product; + const [isImageLoading, setIsImageLoading] = useState(true); return ( {image !== null ? ( - + <> + setIsImageLoading(false)} + /> + {isImageLoading && } + ) : ( )} diff --git a/frontend/src/components/Product/index.ts b/frontend/src/components/Product/index.ts index 47ce2703d..25a06f28f 100644 --- a/frontend/src/components/Product/index.ts +++ b/frontend/src/components/Product/index.ts @@ -2,6 +2,5 @@ export { default as ProductDetailItem } from './ProductDetailItem/ProductDetailI export { default as ProductItem } from './ProductItem/ProductItem'; export { default as ProductList } from './ProductList/ProductList'; export { default as ProductOverviewItem } from './ProductOverviewItem/ProductOverviewItem'; -export { default as PBProductList } from './PBProductList/PBProductList'; export { default as ProductRecipeList } from './ProductRecipeList/ProductRecipeList'; export { default as ProductTitle } from './ProductTitle/ProductTitle'; diff --git a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx index b9ac0cab6..81086851d 100644 --- a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx +++ b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx @@ -1,8 +1,9 @@ import { Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { useState } from 'react'; import styled from 'styled-components'; import RecipePreviewImage from '@/assets/plate.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { RecipeRanking } from '@/types/ranking'; interface RecipeRankingItemProps { @@ -18,6 +19,7 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { author: { nickname, profileImage }, favoriteCount, } = recipe; + const [isImageLoading, setIsImageLoading] = useState(true); return ( @@ -26,7 +28,16 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { {image !== null ? ( - + <> + setIsImageLoading(false)} + /> + {isImageLoading && } + ) : ( )} @@ -74,6 +85,7 @@ const RankingRecipeWrapper = styled.div` const RecipeImage = styled.img` border-radius: 5px; + object-fit: cover; `; const TitleFavoriteWrapper = styled.div` @@ -100,4 +112,5 @@ const AuthorWrapper = styled.div` const AuthorImage = styled.img` border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; diff --git a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx index 2ad70c90a..da7c51781 100644 --- a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx +++ b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx @@ -1,9 +1,9 @@ import { Heading, Text, useTheme } from '@fun-eat/design-system'; -import { Fragment } from 'react'; +import { Fragment, useState } from 'react'; import styled from 'styled-components'; import PreviewImage from '@/assets/plate.svg'; -import { SvgIcon } from '@/components/Common'; +import { Skeleton, SvgIcon } from '@/components/Common'; import type { MemberRecipe, Recipe } from '@/types/recipe'; import { getFormattedDate } from '@/utils/date'; @@ -15,6 +15,7 @@ interface RecipeItemProps { const RecipeItem = ({ recipe, isMemberPage = false }: RecipeItemProps) => { const { image, title, createdAt, favoriteCount, products } = recipe; const author = 'author' in recipe ? recipe.author : null; + const [isImageLoading, setIsImageLoading] = useState(true); const theme = useTheme(); return ( @@ -22,7 +23,10 @@ const RecipeItem = ({ recipe, isMemberPage = false }: RecipeItemProps) => { {!isMemberPage && ( {image !== null ? ( - + <> + setIsImageLoading(false)} /> + {isImageLoading && } + ) : ( )} diff --git a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx index 782bdd928..1ec031690 100644 --- a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx +++ b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx @@ -118,6 +118,7 @@ const RebuyBadge = styled(Badge)` const ReviewerImage = styled.img` border: 2px solid ${({ theme }) => theme.colors.primary}; border-radius: 50%; + object-fit: cover; `; const RatingIconWrapper = styled.div` diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index e6e701b07..4b236673e 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -21,7 +21,7 @@ const HomePage = () => { <>
    - +
    @@ -81,6 +81,7 @@ export default HomePage; const Banner = styled.img` width: 100%; + height: auto; `; const SectionWrapper = styled.section`