|
1 | | -import React from 'react'; |
2 | | - |
3 | 1 | import ShowMoreBtn from '@/assets/icons/showMoreBtn.svg'; |
| 2 | +import Star from '@/assets/icons/star.svg'; |
4 | 3 | import { Badge } from '@/components/ui/badge'; |
5 | 4 | import { Button } from '@/components/ui/button'; |
6 | | -import useClickToggle from '@/hooks/useClickToggle'; |
7 | 5 | import { cn } from '@/lib/utils'; |
| 6 | +import useReviewCardStore from '@/stores/reviewCardStore'; |
| 7 | + |
| 8 | +import { |
| 9 | + ReviewCardProps, |
| 10 | + UserHeaderProps, |
| 11 | + TagAndRatingProps, |
| 12 | + ReviewBodyProps, |
| 13 | + ToggleButtonProps, |
| 14 | +} from './ReviewCardTypes'; |
8 | 15 |
|
9 | 16 | /* |
10 | 17 | 여기 저 혼자만 사용하는 것 같아서 |
11 | | - 제가 임의로 간격이나 이런 수치들 조금 건드렸습니다. |
| 18 | + 제가 임의로 건드렸습니다. |
12 | 19 | 혹시 다른 분들도 쓰게 된다면 말씀해주세요 |
13 | 20 | */ |
14 | | - |
15 | | -interface ReviewCardProps { |
16 | | - userIcon: React.ReactNode; // 유저 아이콘 |
17 | | - username: string; // 유저 이름 |
18 | | - timeAgo: string; // 작성 시간 |
19 | | - tags: string[]; // 태그 목록 |
20 | | - rating: React.ReactNode; // 별점 영역 slot |
21 | | - likeSlot: React.ReactNode; // 좋아요 버튼 slot |
22 | | - menuSlot: React.ReactNode; // 메뉴 버튼 slot |
23 | | - reviewText?: string; // 리뷰 목록 |
24 | | - flavorSliderSlot?: React.ReactNode; // 슬라이더 |
25 | | - className?: string; // 컨테이너 클래스 |
| 21 | +export function ReviewCard({ children }: ReviewCardProps) { |
| 22 | + return ( |
| 23 | + <div className='rounded-xl bg-white p-4 md:p-8 xl:p-4 xl:px-6 shadow-sm border border-gray-300 md:pb-6 xl:pb-5 w-full xl:w-[800px]'> |
| 24 | + {children} |
| 25 | + </div> |
| 26 | + ); |
26 | 27 | } |
27 | 28 |
|
28 | | -export function ReviewCard({ |
29 | | - userIcon, |
30 | | - username, |
31 | | - timeAgo, |
32 | | - tags, |
33 | | - rating, |
34 | | - likeSlot, |
35 | | - menuSlot, |
36 | | - reviewText, |
37 | | - flavorSliderSlot, |
38 | | - className, |
39 | | -}: ReviewCardProps) { |
40 | | - const { isOpen, onToggle } = useClickToggle(); |
41 | | - const cardTransition = cn('overflow-hidden transition-all duration-500 ease-in-out', { |
42 | | - 'opacity-100 translate-y-0 max-h-[1000px]': isOpen, |
43 | | - 'opacity-0 -translate-y-4 max-h-0': !isOpen, |
44 | | - }); |
| 29 | +ReviewCard.UserHeader = function UserHeader({ userIcon, reviewId, children }: UserHeaderProps) { |
| 30 | + const username = useReviewCardStore((state) => state.allReviews[reviewId]?.user.name); |
| 31 | + const timeAgo = useReviewCardStore((state) => state.allReviews[reviewId]?.updatedAt); |
45 | 32 |
|
46 | 33 | return ( |
47 | | - <div |
48 | | - className={cn( |
49 | | - 'w-full rounded-xl bg-white p-4 md:p-8 xl:p-4 xl:px-6 shadow-sm border border-gray-300 md:pb-6 xl:pb-5', |
50 | | - className, |
51 | | - )} |
52 | | - > |
53 | | - {/* 상단: 유저 정보 & 우측 slot */} |
54 | | - <div className='flex justify-between items-start'> |
55 | | - <div className='flex items-center gap-4'> |
56 | | - <div className='w-10 h-10 md:w-16 md:h-16 rounded-full bg-gray-200 overflow-hidden'> |
57 | | - {userIcon} |
58 | | - </div> |
59 | | - <div className='flex flex-col'> |
60 | | - <span className='custom-text-lg-semibold text-gray-900'>{username}</span> |
61 | | - <span className='custom-text-md-regular text-gray-500'>{timeAgo}</span> |
62 | | - </div> |
| 34 | + <div className='flex justify-between items-start'> |
| 35 | + <div className='flex items-center gap-4'> |
| 36 | + <div className='w-10 h-10 md:w-16 md:h-16 rounded-full bg-gray-200 overflow-hidden'> |
| 37 | + {userIcon} |
63 | 38 | </div> |
64 | | - |
65 | | - {/* 좋아요 & 메뉴 */} |
66 | | - <div className='flex items-center gap-2'> |
67 | | - {likeSlot} |
68 | | - {menuSlot} |
| 39 | + <div className='flex flex-col'> |
| 40 | + <span className='custom-text-lg-semibold text-gray-900'>{username}</span> |
| 41 | + <span className='custom-text-md-regular text-gray-500'>{timeAgo}</span> |
69 | 42 | </div> |
70 | 43 | </div> |
71 | 44 |
|
72 | | - {/* 태그 & 별점 */} |
73 | | - <div className='mt-3 flex justify-between items-center'> |
74 | | - <div className='flex flex-wrap gap-2'> |
75 | | - {tags.map((tag) => ( |
76 | | - <Badge |
77 | | - key={tag + username} |
78 | | - className='mt-4 rounded-full bg-white border-gray-300 px-[10px] py-[6px] md:px-[15px] md:py-2 custom-text-lg-regular text-gray-700' |
79 | | - variant='flavor' |
80 | | - > |
81 | | - {tag} |
82 | | - </Badge> |
83 | | - ))} |
84 | | - </div> |
85 | | - <Badge variant='star'>{rating}</Badge> |
| 45 | + {/* 좋아요 & 메뉴 */} |
| 46 | + <div className='flex items-center gap-2'>{children}</div> |
| 47 | + </div> |
| 48 | + ); |
| 49 | +}; |
| 50 | + |
| 51 | +ReviewCard.TagAndRating = function TagAndRating({ reviewId }: TagAndRatingProps) { |
| 52 | + const tags = useReviewCardStore((state) => state.allReviews[reviewId]?.aroma ?? []); |
| 53 | + const rating = useReviewCardStore((state) => state.allReviews[reviewId]?.rating); |
| 54 | + return ( |
| 55 | + <div className='mt-3 flex justify-between items-center'> |
| 56 | + <div className='flex flex-wrap gap-2'> |
| 57 | + {tags.map((tag) => ( |
| 58 | + <Badge |
| 59 | + key={tag} |
| 60 | + className='mt-4 rounded-full bg-white border-gray-300 px-[10px] py-[6px] md:px-[15px] md:py-2 custom-text-lg-regular text-gray-700' |
| 61 | + variant='flavor' |
| 62 | + > |
| 63 | + {tag} |
| 64 | + </Badge> |
| 65 | + ))} |
86 | 66 | </div> |
| 67 | + <Badge variant='star' className='inline-flex gap-1 items-center'> |
| 68 | + <Star className='size-3 md:size-4 md:mt-[-2px]' /> {rating} |
| 69 | + </Badge> |
| 70 | + </div> |
| 71 | + ); |
| 72 | +}; |
87 | 73 |
|
88 | | - {/* 리뷰 텍스트 */} |
89 | | - {reviewText && ( |
90 | | - <p |
91 | | - className={cn( |
92 | | - 'mt-9 text-[14px] md:text-[16px] leading-6 md:leading-[26px] text-gray-800', |
93 | | - cardTransition, |
94 | | - )} |
95 | | - > |
96 | | - {reviewText} |
97 | | - </p> |
98 | | - )} |
| 74 | +ReviewCard.ReviewBody = function ReviewBody({ reviewId, flavorSliderSlot }: ReviewBodyProps) { |
| 75 | + const reviewText = useReviewCardStore((state) => state.allReviews[reviewId]?.content); |
| 76 | + const isOpen = useReviewCardStore((state) => state.allReviews[reviewId]?.isOpen); |
99 | 77 |
|
100 | | - {/* 슬라이더 */} |
101 | | - {flavorSliderSlot && ( |
102 | | - <div className={cn('mt-4 md:mt-6 xl:mt-5', cardTransition)}>{flavorSliderSlot}</div> |
103 | | - )} |
104 | | - <Button |
105 | | - size={null} //버튼 디폴트 덮어씌우기 |
106 | | - width={null} |
107 | | - variant='onlyCancel' |
108 | | - onClick={onToggle} |
| 78 | + const cardTransition = cn('overflow-hidden transition-all duration-500 ease-in-out', { |
| 79 | + 'opacity-100 translate-y-0 max-h-[1000px]': isOpen, |
| 80 | + 'opacity-0 -translate-y-4 max-h-0': !isOpen, |
| 81 | + }); |
| 82 | + |
| 83 | + if (!reviewText && !flavorSliderSlot) null; |
| 84 | + |
| 85 | + return ( |
| 86 | + <> |
| 87 | + <p |
109 | 88 | className={cn( |
110 | | - 'border-0 mx-auto [&_svg]:w-[30px] [&_svg]:h-[30px] block transition-all duration-500 ease-in-out mt-[-30px]', |
111 | | - { |
112 | | - 'scale-y-[-1] mt-0': isOpen, |
113 | | - }, |
| 89 | + 'mt-9 text-[14px] md:text-[16px] leading-6 md:leading-[26px] text-gray-800', |
| 90 | + cardTransition, |
114 | 91 | )} |
115 | 92 | > |
116 | | - <ShowMoreBtn /> |
117 | | - </Button> |
118 | | - </div> |
| 93 | + {reviewText} |
| 94 | + </p> |
| 95 | + <div className={cn('mt-4 md:mt-6 xl:mt-5', cardTransition)}>{flavorSliderSlot}</div> |
| 96 | + </> |
119 | 97 | ); |
120 | | -} |
| 98 | +}; |
| 99 | + |
| 100 | +ReviewCard.ToggleButton = function TogleButton({ reviewId }: ToggleButtonProps) { |
| 101 | + const isOpen = useReviewCardStore((state) => state.allReviews[reviewId]?.isOpen); |
| 102 | + const toggleReviewOpen = useReviewCardStore((state) => state.toggleReviewOpen); |
| 103 | + return ( |
| 104 | + <Button |
| 105 | + size={null} //버튼 디폴트 덮어씌우기 |
| 106 | + width={null} |
| 107 | + variant='onlyCancel' |
| 108 | + onClick={() => toggleReviewOpen(reviewId)} |
| 109 | + className={cn( |
| 110 | + 'border-0 mx-auto [&_svg]:w-[30px] [&_svg]:h-[30px] block transition-all duration-500 ease-in-out mt-[-30px]', |
| 111 | + { |
| 112 | + 'scale-y-[-1] mt-0': isOpen, |
| 113 | + }, |
| 114 | + )} |
| 115 | + > |
| 116 | + <ShowMoreBtn /> |
| 117 | + </Button> |
| 118 | + ); |
| 119 | +}; |
0 commit comments