Skip to content

Commit

Permalink
feat: 상품 상세 페이지 개편 (#75)
Browse files Browse the repository at this point in the history
* 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
Leejin-Yang authored Apr 14, 2024
1 parent a56add6 commit 4196d6b
Show file tree
Hide file tree
Showing 30 changed files with 642 additions and 483 deletions.
36 changes: 11 additions & 25 deletions src/components/Common/TagList/TagList.tsx
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;
`;
17 changes: 17 additions & 0 deletions src/components/Common/TagList/tagList.css.ts
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,
});
1 change: 1 addition & 0 deletions src/components/Common/Text/text.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const text = recipe({
caption1: { fontSize: '1.4rem' },
body: { fontSize: '1.6rem' },
headline: { fontSize: '1.8rem' },
display1: { fontSize: '2.2rem' },
},
weight: {
regular: { fontWeight: 400 },
Expand Down
2 changes: 1 addition & 1 deletion src/components/Common/Text/text.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { text } from './text.css';
import type { RecipeVariants } from '@vanilla-extract/recipes';

export const colors = ['default', 'sub', 'info', 'disabled', 'white'] as const;
export const sizes = ['caption4', 'caption3', 'caption2', 'caption1', 'body', 'headline'] as const;
export const sizes = ['caption4', 'caption3', 'caption2', 'caption1', 'body', 'headline', 'display1'] as const;
export const weights = ['regular', 'medium', 'semiBold'] as const;

export type Color = (typeof colors)[number];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Story = StoryObj<typeof ProductDetailItem>;

export const Default: Story = {
render: ({ ...args }) => (
<div style={{ width: '375px', padding: '0 20px' }}>
<div style={{ width: '375px' }}>
<ProductDetailItem {...args} />
</div>
),
Expand Down
123 changes: 49 additions & 74 deletions src/components/Product/ProductDetailItem/ProductDetailItem.tsx
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 src/components/Product/ProductDetailItem/productDetailItem.css.ts
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 src/components/Product/ProductRecipeList/ProductRecipeList.tsx
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;
`;
Loading

0 comments on commit 4196d6b

Please sign in to comment.