-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 상품 상세 컴포넌트 개편 * feat: 상품 상세 아이템 스타일 수정 * refactor: 상품 상세 페이지 폴더 추가 * feat: 리뷰 등록 버튼 구현 * feat: 상품 상세 응답 카테고리 추가 * feat: 상품 상세 아이템 카테고리 추가 * feat: 리뷰 좋아요 버튼 수정 * feat: 리뷰 작성자 정보 디자인 수정 * feat: 리뷰 별점 디자인 수정 * feat: 태그 리스트 컴포넌트 개편 * feat: 리뷰 아이템 개편 * feat: 리뷰 리스트 컴포넌트 개편 * feat: 상품 상세 페이지 리뷰 섹션 개편 * feat: 평점 높이 수정 * feat: 상품 상세 페이지 category param 삭제 * refactor: 꿀조합 상품 바텀시트 버튼 컴포넌트로 이동 * refactor: 꿀조합 아이템 props 추가 * feat: 꿀조합 이미지 비율 수정 * feat: 상품 상세 꿀조합 리스트 개편 * refactor: Text 컴포넌트로 수정 * feat: 리뷰 정렬 바텀시트 추가 * feat: 리뷰 반영 * refactor: 좋아요 색상 이름 수정
- Loading branch information
1 parent
a56add6
commit 4196d6b
Showing
30 changed files
with
642 additions
and
483 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,24 @@ | ||
import { Badge } from '@fun-eat/design-system'; | ||
import styled from 'styled-components'; | ||
import { tag, tagList } from './tagList.css'; | ||
import Text from '../Text/Text'; | ||
|
||
import type { Tag } from '@/types/common'; | ||
import { convertTagColor } from '@/utils/convertTagColor'; | ||
|
||
interface TagListProps { | ||
tags: Tag[]; | ||
} | ||
|
||
const TagList = ({ tags }: TagListProps) => { | ||
return ( | ||
<TagListContainer> | ||
{tags.map((tag) => { | ||
const tagColor = convertTagColor(tag.tagType); | ||
return ( | ||
<li key={tag.id}> | ||
<TagBadge element="p" color={tagColor} textColor="black"> | ||
{tag.name} | ||
</TagBadge> | ||
</li> | ||
); | ||
})} | ||
</TagListContainer> | ||
<ul className={tagList}> | ||
{tags.map(({ id, name }) => ( | ||
<li key={id} className={tag}> | ||
<Text as="span" color="info" size="caption2" weight="medium"> | ||
{name} | ||
</Text> | ||
</li> | ||
))} | ||
</ul> | ||
); | ||
}; | ||
|
||
export default TagList; | ||
|
||
const TagListContainer = styled.ul` | ||
display: flex; | ||
margin: 12px 0; | ||
column-gap: 8px; | ||
`; | ||
|
||
const TagBadge = styled(Badge)` | ||
font-weight: bold; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { vars } from '@/styles/theme.css'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const tagList = style({ | ||
display: 'flex', | ||
gap: 4, | ||
}); | ||
|
||
export const tag = style({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
height: 23, | ||
padding: '0 6px', | ||
textAlign: 'center', | ||
borderRadius: 4, | ||
backgroundColor: vars.colors.background.category, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 49 additions & 74 deletions
123
src/components/Product/ProductDetailItem/ProductDetailItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,89 +1,64 @@ | ||
import { Text, useTheme } from '@fun-eat/design-system'; | ||
import styled from 'styled-components'; | ||
import { | ||
previewWrapper, | ||
productContent, | ||
productDetails, | ||
productImage, | ||
productInfo, | ||
productName, | ||
productOverview, | ||
summaryWrapper, | ||
} from './productDetailItem.css'; | ||
|
||
import PreviewImage from '@/assets/characters.svg'; | ||
import { SvgIcon, TagList } from '@/components/Common'; | ||
import { SvgIcon, TagList, Text } from '@/components/Common'; | ||
import type { ProductDetail } from '@/types/product'; | ||
|
||
interface ProductDetailItemProps { | ||
productDetail: ProductDetail; | ||
} | ||
|
||
const ProductDetailItem = ({ productDetail }: ProductDetailItemProps) => { | ||
const { name, price, image, content, averageRating, tags } = productDetail; | ||
|
||
const theme = useTheme(); | ||
const { name, price, image, content, averageRating, tags, reviewCount, category } = productDetail; | ||
|
||
return ( | ||
<ProductDetailContainer> | ||
<ImageWrapper>{image ? <img src={image} width={300} alt={name} /> : <PreviewImage width={300} />}</ImageWrapper> | ||
<DetailInfoWrapper> | ||
<TagList tags={tags} /> | ||
<DescriptionWrapper> | ||
<Text weight="bold">가격</Text> | ||
<Text>{price.toLocaleString('ko-KR')}원</Text> | ||
</DescriptionWrapper> | ||
<DescriptionWrapper> | ||
<Text weight="bold">상품 설명</Text> | ||
<ProductContent>{content}</ProductContent> | ||
</DescriptionWrapper> | ||
<DescriptionWrapper aria-label={`평균 평점 ${averageRating}점`}> | ||
<Text weight="bold">평균 평점</Text> | ||
<RatingIconWrapper> | ||
<SvgIcon variant="star" width={20} height={20} fill={theme.colors.secondary} /> | ||
<Text as="span">{averageRating.toFixed(1)}</Text> | ||
</RatingIconWrapper> | ||
</DescriptionWrapper> | ||
</DetailInfoWrapper> | ||
</ProductDetailContainer> | ||
); | ||
}; | ||
|
||
export default ProductDetailItem; | ||
|
||
const ProductDetailContainer = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
row-gap: 30px; | ||
g & > img, | ||
svg { | ||
align-self: center; | ||
} | ||
`; | ||
<section> | ||
<img src={image} className={productImage} height={328} alt={name} /> | ||
|
||
const ImageWrapper = styled.div` | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
`; | ||
<div className={productOverview}> | ||
<div className={productInfo}> | ||
<div className={productDetails}> | ||
<Text size="caption1" weight="medium"> | ||
{category.name} | ||
</Text> | ||
<h2 className={productName}>{name}</h2> | ||
<Text weight="semiBold" size="display1"> | ||
{price.toLocaleString('ko-KR')}원 | ||
</Text> | ||
</div> | ||
|
||
const DetailInfoWrapper = styled.div` | ||
& > div + div { | ||
margin-top: 10px; | ||
} | ||
`; | ||
<div className={summaryWrapper}> | ||
<div className={previewWrapper}> | ||
<SvgIcon variant="star2" width={14} height={14} fill="#ffc14a" /> | ||
<Text as="span" size="caption1" weight="medium" aria-label={`${averageRating}점`}> | ||
{averageRating.toFixed(1)} | ||
</Text> | ||
</div> | ||
<div className={previewWrapper}> | ||
<SvgIcon variant="review2" width={14} height={14} fill="#ddd" /> | ||
<Text as="span" size="caption1" weight="medium" aria-label={`리뷰 ${reviewCount}개`}> | ||
{reviewCount} | ||
</Text> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
const DescriptionWrapper = styled.div` | ||
display: flex; | ||
column-gap: 20px; | ||
<Text color="info" size="caption2" className={productContent}> | ||
{content} | ||
</Text> | ||
|
||
& > p:first-of-type { | ||
flex-shrink: 0; | ||
width: 60px; | ||
} | ||
`; | ||
|
||
const ProductContent = styled(Text)` | ||
white-space: pre-wrap; | ||
`; | ||
|
||
const RatingIconWrapper = styled.div` | ||
display: flex; | ||
align-items: center; | ||
margin-left: -4px; | ||
column-gap: 4px; | ||
<TagList tags={tags} /> | ||
</div> | ||
</section> | ||
); | ||
}; | ||
|
||
& > svg { | ||
padding-bottom: 2px; | ||
} | ||
`; | ||
export default ProductDetailItem; |
57 changes: 57 additions & 0 deletions
57
src/components/Product/ProductDetailItem/productDetailItem.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { vars } from '@/styles/theme.css'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const container = style({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
rowGap: '30px', | ||
}); | ||
|
||
export const productImage = style({ | ||
width: '100%', | ||
objectFit: 'cover', | ||
}); | ||
|
||
export const productOverview = style({ | ||
margin: '20px 0 25px', | ||
padding: '0 20px', | ||
}); | ||
|
||
export const productInfo = style({ | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
alignItems: 'flex-start', | ||
}); | ||
|
||
export const productDetails = style({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 4, | ||
}); | ||
|
||
export const categoryName = style({ | ||
color: vars.colors.gray4, | ||
}); | ||
|
||
export const productName = style({ | ||
fontSize: '1.6rem', | ||
fontWeight: 600, | ||
lineHeight: 1.4, | ||
}); | ||
|
||
export const summaryWrapper = style({ | ||
display: 'flex', | ||
gap: 12, | ||
}); | ||
|
||
export const previewWrapper = style({ | ||
display: 'flex', | ||
alignItems: 'flex-start', | ||
justifyContent: 'center', | ||
gap: 4, | ||
}); | ||
|
||
export const productContent = style({ | ||
margin: '16px 0 8px', | ||
whiteSpace: 'pre-line', | ||
}); |
89 changes: 31 additions & 58 deletions
89
src/components/Product/ProductRecipeList/ProductRecipeList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,50 @@ | ||
import { Link, Text } from '@fun-eat/design-system'; | ||
import { useRef } from 'react'; | ||
import { Link as RouterLink } from 'react-router-dom'; | ||
import styled from 'styled-components'; | ||
import { Link } from 'react-router-dom'; | ||
|
||
import { container, moreIcon, moreIconWrapper, moreItem, moreLink } from './productRecipeList.css'; | ||
|
||
import { SvgIcon, Text } from '@/components/Common'; | ||
import { RecipeItem } from '@/components/Recipe'; | ||
import { PATH } from '@/constants/path'; | ||
import { useIntersectionObserver } from '@/hooks/common'; | ||
import { useInfiniteProductRecipesQuery } from '@/hooks/queries/product'; | ||
import type { SortOption } from '@/types/common'; | ||
import { vars } from '@/styles/theme.css'; | ||
import displaySlice from '@/utils/displaySlice'; | ||
|
||
interface ProductRecipeListProps { | ||
productId: number; | ||
productName: string; | ||
selectedOption: SortOption; | ||
} | ||
|
||
const ProductRecipeList = ({ productId, productName, selectedOption }: ProductRecipeListProps) => { | ||
const scrollRef = useRef<HTMLDivElement>(null); | ||
const { fetchNextPage, hasNextPage, data } = useInfiniteProductRecipesQuery(productId, selectedOption.value); | ||
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage); | ||
const ProductRecipeList = ({ productId }: ProductRecipeListProps) => { | ||
// 상품에서 보여줄 꿀조합 정렬 조건 | ||
const { data } = useInfiniteProductRecipesQuery(productId, 'favoriteCount,desc'); | ||
|
||
const recipes = data.pages.flatMap((page) => page.recipes); | ||
const recipeToDisplay = displaySlice(true, recipes, 3); | ||
|
||
if (recipes.length === 0) { | ||
return ( | ||
<ErrorContainer> | ||
<ErrorDescription align="center" weight="bold" size="lg"> | ||
{productName}을/를 {'\n'}사용한 꿀조합을 만들어보세요 🍯 | ||
</ErrorDescription> | ||
<RecipeLink as={RouterLink} to={`${PATH.RECIPE}`} block> | ||
꿀조합 작성하러 가기 | ||
</RecipeLink> | ||
</ErrorContainer> | ||
); | ||
return null; | ||
} | ||
|
||
return ( | ||
<> | ||
<ProductRecipeListContainer> | ||
{recipes.map((recipe) => ( | ||
<li key={recipe.id}> | ||
<Link as={RouterLink} to={`${PATH.RECIPE}/${recipe.id}`}> | ||
<RecipeItem recipe={recipe} /> | ||
</Link> | ||
</li> | ||
))} | ||
</ProductRecipeListContainer> | ||
<div ref={scrollRef} aria-hidden /> | ||
</> | ||
<ul className={container}> | ||
{recipeToDisplay.map((recipe) => ( | ||
<li key={recipe.id}> | ||
<RecipeItem recipe={recipe} hasFavoriteButton /> | ||
</li> | ||
))} | ||
{recipeToDisplay.length < recipes.length && ( | ||
<li className={moreItem}> | ||
{/*링크는 상품이 포함된 꿀조합 검색결과로 가는 것이 맞을듯?*/} | ||
<Link to={''} className={moreLink}> | ||
<div className={moreIconWrapper}> | ||
<SvgIcon variant="arrowLeft" className={moreIcon} fill="none" stroke={vars.colors.gray5} /> | ||
</div> | ||
<Text as="span" color="info" weight="semiBold" size="caption2"> | ||
전체보기 | ||
</Text> | ||
</Link> | ||
</li> | ||
)} | ||
</ul> | ||
); | ||
}; | ||
|
||
export default ProductRecipeList; | ||
|
||
const ProductRecipeListContainer = styled.ul` | ||
& > li + li { | ||
margin-top: 40px; | ||
} | ||
`; | ||
|
||
const ErrorContainer = styled.div` | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
`; | ||
|
||
const ErrorDescription = styled(Text)` | ||
padding: 20px 0; | ||
white-space: pre-wrap; | ||
`; | ||
|
||
const RecipeLink = styled(Link)` | ||
padding: 16px 24px; | ||
border: 1px solid ${({ theme }) => theme.colors.gray4}; | ||
border-radius: 8px; | ||
`; |
Oops, something went wrong.