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

리뷰 작성 구현 및 컴포넌트 수정 #65

Merged
merged 9 commits into from
Nov 15, 2024
145 changes: 145 additions & 0 deletions src/components/CreateReview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* eslint-disable @typescript-eslint/naming-convention */
import styled from "@emotion/styled";
import { useState } from "react";

export default function CreateReview({
onSubmit,
}: {
onSubmit: (rating: number, reviewText: string) => void;
}) {
const [rating, setRating] = useState(0);
const [hoverRating, setHoverRating] = useState<number | null>(null);
const [reviewText, setReviewText] = useState("");
const [isDragging, setIsDragging] = useState(false);

const handleMouseDown = (index: number) => {
setIsDragging(true);
setRating(index + 1);
};

const handleMouseMove = (e: React.MouseEvent, index: number) => {
if (isDragging) {
const rect = e.currentTarget.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const width = rect.width;
const newRating = index + Math.min(Math.max(offsetX / width, 0), 1);
setHoverRating(Math.round(newRating * 10) / 10);
}
};

const handleMouseUp = () => {
if (hoverRating !== null) {
setRating(hoverRating);
}
setIsDragging(false);
setHoverRating(null);
};

const handleSubmit = () => {
onSubmit(rating, reviewText);
setReviewText("");
setRating(0);
};

const displayRating = hoverRating !== null ? hoverRating : rating;

return (
<ReviewForm>
<h3>리뷰 작성</h3>
<StarRatingContainer>
<StarRating onMouseLeave={() => setHoverRating(null)}>
{[...Array(5)].map((_, index) => (
<StarWrapper
key={index}
onMouseDown={() => handleMouseDown(index)}
onMouseMove={(e) => handleMouseMove(e, index)}
onMouseUp={handleMouseUp}
>
<Star selected={displayRating > index}>★</Star>
<StarPartial width={`${((displayRating || rating) - index) * 100}%`} />
</StarWrapper>
))}
</StarRating>
<RatingValue>{displayRating.toFixed(1)}점</RatingValue>
</StarRatingContainer>
<ReviewInput
value={reviewText}
onChange={(e) => setReviewText(e.target.value)}
placeholder="리뷰를 입력하세요"
/>
<SubmitButton onClick={handleSubmit}>등록</SubmitButton>
</ReviewForm>
);
}

const ReviewForm = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: bold;
}
`;

const StarRatingContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 10px;
`;

const StarRating = styled.div`
display: flex;
gap: 5px;
position: relative;
user-select: none;
`;

const StarWrapper = styled.div`
position: relative;
font-size: 24px;
cursor: pointer;
display: inline-block;
width: 24px;
`;

const Star = styled.span<{ selected: boolean }>`
font-size: 30px;
color: ${(props) => (props.selected ? "#ffd700" : "#ddd")};
`;

const StarPartial = styled.div<{ width: string }>`
position: absolute;
top: 0;
left: 0;
height: 100%;
overflow: hidden;
width: ${(props) => props.width};
color: #ffd700;
`;

const RatingValue = styled.span`
font-size: 18px;
color: #333;
margin-top: 8px;
margin-left: 16px;
`;

const ReviewInput = styled.textarea`
height: 80px;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 5px;
resize: none;
`;

const SubmitButton = styled.button`
align-self: flex-end;
padding: 8px 16px;
background-color: #89a06b;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
`;
71 changes: 52 additions & 19 deletions src/components/Review.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import styled from "@emotion/styled";
import { format } from "date-fns";
import { useState } from "react";
import { useNavigate } from "react-router-dom";

import { RouterPath } from "@/utils/path";

type ReviewProps = {
user: {
info: {
name: string;
avatarUrl: string;
img: string;
};
rate: number;
content: string;
date: Date;
infoIsProduct: boolean;
productId?: number;
};

export default function Review({ user, rate, content, date }: ReviewProps) {
export default function Review({
info,
rate,
content,
date,
infoIsProduct,
productId,
}: ReviewProps) {
const [isExpanded, setIsExpanded] = useState(false);
const navigate = useNavigate();

const handleToggleContent = () => setIsExpanded(!isExpanded);

Expand All @@ -26,11 +39,19 @@ export default function Review({ user, rate, content, date }: ReviewProps) {
<StarRating>
<Stars rate={rate} />
</StarRating>
<UserInfo>
<Avatar src={user.avatarUrl} alt={`${user.name}'s avatar`} />
<UserName>{user.name}</UserName>
<ReviewDate>{format(date, "yyyy-MM-dd HH:mm")}</ReviewDate>
</UserInfo>
<Info
onClick={
infoIsProduct
? () => navigate(RouterPath.productDetail.getPath(String(productId)))
: undefined
}
>
<InfoImg src={info.img} alt={`${info.name}'s avatar`} infoIsProduct={infoIsProduct} />
<InfoRight infoIsProduct={infoIsProduct}>
<InfoName>{info.name}</InfoName>
<ReviewDate>{format(date, "yyyy-MM-dd HH:mm")}</ReviewDate>
</InfoRight>
</Info>
</Header>
<Content>
{displayContent}
Expand Down Expand Up @@ -74,16 +95,15 @@ const ReviewContainer = styled.div`
`;

const Header = styled.div`
display: flex;
align-items: center;
margin-bottom: 8px;
margin-bottom: 15px;
`;

const StarRating = styled.div`
display: flex;
color: #9bc678;
font-size: 18px;
margin-right: 10px;
margin-bottom: 10px;
`;

const Star = styled.span`
Expand Down Expand Up @@ -112,26 +132,39 @@ const StarEmpty = styled.span`
font-size: 18px;
`;

const UserInfo = styled.div`
const Info = styled.div`
display: flex;
align-items: center;
gap: 3px;
`;

const Avatar = styled.img`
width: 24px;
height: 24px;
const InfoImg = styled.img<{ infoIsProduct: boolean }>`
width: 30px;
height: 30px;
border-radius: 50%;
${(props) =>
props.infoIsProduct
? `
border-radius: 0.3rem;
width: 60px;
height: 60px;
`
: ""}
margin-right: 8px;
`;

const UserName = styled.span`
const InfoRight = styled.div<{ infoIsProduct: boolean }>`
display: flex;
flex-direction: column;
gap: ${(props) => (props.infoIsProduct ? "10px" : "4px")};
`;
const InfoName = styled.span`
font-weight: bold;
margin-right: 6px;
font-size: var(--font-size-small);
`;

const ReviewDate = styled.span`
color: #888;
font-size: 12px;
font-size: var(--font-size-small);
`;

const Content = styled.div`
Expand Down
80 changes: 23 additions & 57 deletions src/pages/MyReviews/index.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,43 @@
import styled from "@emotion/styled";
import { useQuery } from "@tanstack/react-query";

// import { useQuery } from "@tanstack/react-query";
import Background from "@/components/Background";
import Review from "@/components/Review";
// import { fetchInstance } from "@/utils/axiosInstance";
import { fetchInstance } from "@/utils/axiosInstance";

type Reviews = {
reviews: {
id: number;
rate: number;
content: string;
productId: number;
productName: string;
productImgUrl: string;
date: Date;
}[];
};
id: number;
rate: number;
content: string;
productId: number;
productName: string;
productImgUrl: string;
date: Date;
}[];

export default function MyReviews() {
// const { data, isPending, isError } = useQuery<Reviews>({
// queryKey: ["myReviews"],
// queryFn: async () => {
// const response = await fetchInstance().get("/api/reviews/my");
// return response.data;
// },
// });
const { data, isPending, isError } = useQuery<Reviews>({
queryKey: ["myReviews"],
queryFn: async () => {
const response = await fetchInstance().get("/api/reviews/my");
return response.data;
},
});

// if (isPending) return <div>Loading...</div>;
// if (isError) return <div>Error</div>;
const data: Reviews = {
reviews: [
{
id: 1,
rate: 4.5,
content: "너무 맛있어요. 다음에 또 시킬게요.",
productId: 1,
productName: "짜장면",
productImgUrl:
"https://cdn.pixabay.com/photo/2016/11/29/12/54/asian-food-1869623_960_720.jpg",
date: new Date(),
},
{
id: 2,
rate: 3.5,
content: "맛이 좀 아쉬웠어요. 다음에는 다른 메뉴를 시켜봐야겠어요.",
productId: 2,
productName: "짬뽕",
productImgUrl:
"https://cdn.pixabay.com/photo/2016/11/29/12/54/asian-food-1869623_960_720.jpg",
date: new Date(),
},
{
id: 3,
rate: 5,
content: "완전 대박입니다. 다음에 또 시킬게요.",
productId: 3,
productName: "탕수육",
productImgUrl:
"https://cdn.pixabay.com/photo/2016/11/29/12/54/asian-food-1869623_960_720.jpg",
date: new Date(),
},
],
};
if (isPending) return <div>Loading...</div>;
if (isError) return <div>Error</div>;

return (
<Background>
<H1>나의 리뷰</H1>
<Container>
{data?.reviews.map((review) => (
{data.map((review) => (
<Review
key={review.id}
{...review}
user={{ name: review.productName, avatarUrl: review.productImgUrl }}
info={{ name: review.productName, img: review.productImgUrl }}
infoIsProduct={true}
productId={review.productId}
/>
))}
</Container>
Expand Down
Loading