Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 상품 리뷰 작성 구현 #89

Merged
merged 13 commits into from
Apr 20, 2024
Merged
2 changes: 1 addition & 1 deletion src/components/Common/TopBar/TopBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const LeftTitleAndRegister: Story = {
return (
<TopBar {...args}>
<TopBar.LeftNavigationGroup title="타이틀" />
<TopBar.RegisterLink />
<TopBar.RegisterButton />
</TopBar>
);
},
Expand Down
16 changes: 9 additions & 7 deletions src/components/Common/TopBar/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import type { ComponentPropsWithoutRef } from 'react';
import { Link } from 'react-router-dom';

import { LeftNavigationWrapper, container, headerTitle, leftTitle } from './topBar.css';
import { LeftNavigationWrapper, container, headerTitle, leftTitle, register } from './topBar.css';
import SvgIcon from '../Svg/SvgIcon';
import Text from '../Text/Text';

import LogoImage from '@/assets/logo.svg';
import { PATH } from '@/constants/path';
import { vars } from '@/styles/theme.css';


interface TopBarProps {
children?: React.ReactNode;
title?: string;
link?: string;
state?: unknown;
}

type RegisterButtonProps = ComponentPropsWithoutRef<'button'>;

const TopBar = ({ children }: TopBarProps) => {
return <header className={container}>{children}</header>;
};
Expand Down Expand Up @@ -58,13 +60,13 @@ const SearchLink = () => {
);
};

const RegisterLink = ({ link = '' }: TopBarProps) => {
const RegisterButton = ({ ...props }: RegisterButtonProps) => {
return (
<Link to={link}>
<Text size="caption1" color="disabled" weight="semiBold">
<button {...props}>
<Text as="span" size="caption1" weight="semiBold" className={register}>
등록
</Text>
</Link>
</button>
);
};

Expand All @@ -85,7 +87,7 @@ TopBar.BackLink = BackLink;
TopBar.LeftNavigationGroup = LeftNavigationGroup;
TopBar.Title = Title;
TopBar.SearchLink = SearchLink;
TopBar.RegisterLink = RegisterLink;
TopBar.RegisterButton = RegisterButton;
TopBar.CloseButton = CloseButton;
TopBar.Spacer = Spacer;

Expand Down
8 changes: 8 additions & 0 deletions src/components/Common/TopBar/topBar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export const headerTitle = style({
fontSize: 18,
fontWeight: 600,
});

export const register = style({
selectors: {
'button:disabled > &': {
color: vars.colors.text.disabled,
},
},
});
Comment on lines +38 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우아 이거 VE에여 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Creative-Lee 아니 어떻게 들어왔지..?ㅋㅋㅋㅋㅋㅋㅋ 맞아여

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면 스타일드 컴포넌트랑 VE 둘다 쓰는건가요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마이그레이션 중임다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

야무지군요 구웃

1 change: 1 addition & 0 deletions src/components/Common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { default as PageHeader } from './PageHeader/PageHeader';
export { default as Badge } from './Badge/Badge';
export { default as WriteButton } from './WriteButton/WriteButton';
export { default as Text } from './Text/Text';
export { default as TopBar } from './TopBar/TopBar';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 헐 이거 깜박했다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

찾았다 범인

36 changes: 33 additions & 3 deletions src/components/Product/ProductRecipeList/ProductRecipeList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Link } from 'react-router-dom';

import { container, moreIcon, moreIconWrapper, moreItem, moreLink } from './productRecipeList.css';
import {
container,
moreIcon,
moreIconWrapper,
moreItem,
moreLink,
notFound,
recipeLink,
} from './productRecipeList.css';

import SearchNotFoundImage from '@/assets/search-notfound.png';
import { SvgIcon, Text } from '@/components/Common';
import { DefaultRecipeItem } from '@/components/Recipe';
import { PATH } from '@/constants/path';
import { useInfiniteProductRecipesQuery } from '@/hooks/queries/product';
import { vars } from '@/styles/theme.css';
import displaySlice from '@/utils/displaySlice';
Expand All @@ -20,7 +30,20 @@ const ProductRecipeList = ({ productId }: ProductRecipeListProps) => {
const recipeToDisplay = displaySlice(true, recipes, 3);

if (recipes.length === 0) {
return null;
return (
<div className={notFound}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="disabled" size="caption4">
아직 작성된 꿀조합이 없어요
</Text>
<div style={{ height: '6px' }} />
<Link to={PATH.RECIPE} className={recipeLink}>
<Text as="span" color="sub" weight="semiBold" size="caption2">
꿀조합 작성하러 가기
</Text>
</Link>
</div>
);
}

return (
Expand All @@ -35,7 +58,14 @@ const ProductRecipeList = ({ productId }: ProductRecipeListProps) => {
{/*링크는 상품이 포함된 꿀조합 검색결과로 가는 것이 맞을듯?*/}
<Link to={''} className={moreLink}>
<div className={moreIconWrapper}>
<SvgIcon variant="arrowLeft" className={moreIcon} fill="none" stroke={vars.colors.gray5} />
<SvgIcon
variant="arrowLeft"
className={moreIcon}
width={16}
height={16}
fill="none"
stroke={vars.colors.gray5}
/>
</div>
<Text as="span" color="info" weight="semiBold" size="caption2">
전체보기
Expand Down
18 changes: 18 additions & 0 deletions src/components/Product/ProductRecipeList/productRecipeList.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ export const moreIconWrapper = style({
export const moreIcon = style({
transform: 'rotate(180deg)',
});

export const notFound = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 6,
textAlign: 'center',
});

export const recipeLink = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 34,
padding: '0 16px',
backgroundColor: vars.colors.gray2,
borderRadius: 44,
});
21 changes: 6 additions & 15 deletions src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,28 @@ import StarRate from './StarRate/StarRate';
import RebuyCheckbox from '../RebuyCheckbox/RebuyCheckbox';

import { ImageUploader, SvgIcon, Text } from '@/components/Common';
import { MAX_DISPLAYED_TAGS_LENGTH, MIN_DISPLAYED_TAGS_LENGTH } from '@/constants';
import type { TagValue } from '@/contexts/ReviewFormContext';
import { useFormData, useImageUploader } from '@/hooks/common';
import { useReviewFormActionContext, useReviewFormValueContext } from '@/hooks/context';
import { useReviewRegisterFormMutation } from '@/hooks/queries/review';
import { vars } from '@/styles/theme.css';
import type { ReviewRequest } from '@/types/review';

const MIN_RATING_SCORE = 0;
const MIN_CONTENT_LENGTH = 0;

interface ReviewRegisterFormProps {
productId: number;
openBottomSheet: () => void;
}

const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormProps) => {
const { isImageUploading, previewImage, imageFile, uploadImage, deleteImage } = useImageUploader();
const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader();
const navigate = useNavigate();

const reviewFormValue = useReviewFormValueContext();
const { formValue: reviewFormValue } = useReviewFormValueContext();
const { handleReviewFormValue, resetReviewFormValue } = useReviewFormActionContext();
const { toast } = useToastActionContext();

const { mutate } = useReviewRegisterFormMutation(productId);

const isValid =
reviewFormValue.rating > MIN_RATING_SCORE &&
reviewFormValue.tags.length >= MIN_DISPLAYED_TAGS_LENGTH &&
reviewFormValue.tags.length <= MAX_DISPLAYED_TAGS_LENGTH &&
reviewFormValue.content.length > MIN_CONTENT_LENGTH &&
!isImageUploading;

const formValue: ReviewRequest = { ...reviewFormValue, tagIds: reviewFormValue.tags.map(({ id }) => id) };

const formData = useFormData<ReviewRequest>({
Expand Down Expand Up @@ -81,7 +70,7 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr
};

return (
<form onSubmit={handleSubmit}>
<form id="review-form" onSubmit={handleSubmit}>
<div>
<h2 className={itemTitle} tabIndex={0}>
사진 등록
Expand All @@ -105,7 +94,9 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr
{reviewFormValue.tags.map((tag) => (
<li key={tag.id}>
<button type="button" onClick={handleTagSelect(tag)} className={tagButton}>
<Text as="span">{tag.name}</Text>
<Text as="span" size="caption2" weight="medium">
{tag.name}
</Text>
<SvgIcon variant="close2" width={8} height={8} fill="none" stroke={vars.colors.gray4} />
</button>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export const tagButton = style({
gap: 4,
height: 28,
padding: '0 6px',
fontSize: '1.3rem',
fontWeight: 500,
borderRadius: 4,
backgroundColor: vars.colors.gray2,
});
4 changes: 3 additions & 1 deletion src/components/Review/ReviewTagList/ReviewTagList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { useReviewTagsQuery } from '@/hooks/queries/review';

const ReviewTagList = () => {
const { data: tagsData } = useReviewTagsQuery();
const { tags: selectedTags } = useReviewFormValueContext();
const {
formValue: { tags: selectedTags },
} = useReviewFormValueContext();
const { handleReviewFormValue } = useReviewFormActionContext();

const isChecked = (tag: TagValue) => selectedTags.some(({ id }) => id === tag.id);
Expand Down
13 changes: 11 additions & 2 deletions src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { container, registerButton, registerButtonWrapper, section } from './reviewTagSheet.css';
import { closeWrapper, container, registerButton, registerButtonWrapper, section } from './reviewTagSheet.css';
import ReviewTagList from '../ReviewTagList/ReviewTagList';

import { SvgIcon } from '@/components/Common';
import { MAX_DISPLAYED_TAGS_LENGTH, MIN_DISPLAYED_TAGS_LENGTH } from '@/constants';
import { useReviewFormValueContext } from '@/hooks/context';
import { vars } from '@/styles/theme.css';

interface ReviewTagSheetProps {
close: () => void;
}

const ReviewTagSheet = ({ close }: ReviewTagSheetProps) => {
const { tags } = useReviewFormValueContext();
const {
formValue: { tags },
} = useReviewFormValueContext();

const isValid = tags.length >= MIN_DISPLAYED_TAGS_LENGTH && tags.length <= MAX_DISPLAYED_TAGS_LENGTH;

return (
<div className={container}>
<div className={closeWrapper}>
<button type="button" onClick={close}>
<SvgIcon variant="close2" stroke={vars.colors.black} width={20} height={20} />
</button>
</div>
<section className={section}>
<ReviewTagList />
</section>
Expand Down
12 changes: 10 additions & 2 deletions src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ export const container = style({
height: '100vh',
});

export const closeWrapper = style({
padding: '0 20px',
display: 'flex',
height: 50,
alignItems: 'center',
justifyContent: 'flex-end',
});

export const section = style({
padding: '50px 20px 70px',
marginBottom: 32,
padding: '0 20px',
margin: '16px 0 32px',
});

export const registerButtonWrapper = style({
Expand Down
23 changes: 10 additions & 13 deletions src/components/Search/SearchNotFound/SearchNotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ import { container } from './searchNotFound.css';
import SearchNotFoundImage from '@/assets/search-notfound.png';
import { Text } from '@/components/Common';


const SearchNotFound = () => {
return (
<>
<div className={container}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="sub" size="headline" weight="semiBold">
검색 결과가 없어요
</Text>
<div style={{ height: '6px' }} />
<Text color="disabled" size="caption4">
다른 키워드로 검색해보세요!
</Text>
</div>
</>
<div className={container}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="sub" size="headline" weight="semiBold">
검색 결과가 없어요
</Text>
<div style={{ height: '6px' }} />
<Text color="disabled" size="caption4">
다른 키워드로 검색해보세요!
</Text>
</div>
);
};

Expand Down
Loading
Loading