Skip to content

Commit

Permalink
Merge pull request #3 from nguyentrongbut/task/reviews-tab
Browse files Browse the repository at this point in the history
feat(reviews-tabs): display total rating and display reviews
  • Loading branch information
nguyentrongbut authored Aug 6, 2024
2 parents ba524c0 + 09a7c5d commit 9dfebbe
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 8 deletions.
95 changes: 95 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
Expand Down
9 changes: 6 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
--ring: 222.2 84% 4.9%;

--radius: 0.5rem;

/* customs */
--primary-color: rgb(5 170 240);
}

.dark {
Expand Down Expand Up @@ -133,15 +136,15 @@
}

.primary-color {
color: rgb(5 170 240);
color: var(--primary-color);
}

.bg-primary-color {
background-color: rgb(5 170 240);
background-color: var(--primary-color);
}

.border-primary-color {
border-color: rgb(5 170 240);
border-color: var(--primary-color);
}

::-webkit-scrollbar {
Expand Down
14 changes: 14 additions & 0 deletions src/app/title/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import IconComment from "@/components/icon/icon.comment";
import { Button } from "@/components/ui/button";
import IconShare from "@/components/icon/icon.share";
import Tab from "@/app/title/@component/tab";
import RatingReadOnly from "@/app/title/[slug]/reviews/@component/rating.read.only";

type Props = {
params: { slug: string };
Expand Down Expand Up @@ -142,6 +143,19 @@ export default async function RootLayout({
{formatNumber(data?.totalComment)}
</span>
</span>
<span className="-mt-0.5 flex items-center gap-2">
<RatingReadOnly
stars={data?.rating}
half={true}
></RatingReadOnly>
<span className="mt-0.5 text-sm opacity-70">
(
{data?.rating
? `${data.rating}`
: "0.0"}
)
</span>
</span>
</li>
<li></li>
</ul>
Expand Down
3 changes: 1 addition & 2 deletions src/app/title/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Expand All @@ -34,7 +33,7 @@ const ChaptersTab = async (props: any) => {
const id = temp1[temp1.length - 1];

const data = await sendRequest<IChapter[]>({
url: `${process.env.WEB_COMIC_API}/api//chapters?comicId=${id}&_sort=createdAt&_order=desc`,
url: `${process.env.WEB_COMIC_API}/api/chapters?comicId=${id}&_sort=createdAt&_order=desc`,
method: "GET",
nextOption: {
cache: "no-store",
Expand Down
27 changes: 27 additions & 0 deletions src/app/title/[slug]/reviews/@component/rating.read.only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import IconStar from "@/components/icon/icon.star";
import IconStarBorder from "@/components/icon/icon.star.border";
import IconStarHalf from "@/components/icon/icon.star.half";

const RatingReadOnly = (props: { stars: number, half?: boolean }) => {
const { stars, half = false } = props;

const rattingStar = Array.from({ length: 5 }, (elm, index) => {
let number = index + 0.5;

return (
<span key={index}>
{stars >= index + 1 || (stars > index && stars < index + 1 && stars % 1 > 0.7) ? (
<IconStar className="primary-color size-5" />
) : stars >= number ? (
<IconStarHalf className={half ? "primary-color" : ""} />
) : (
<IconStarBorder className={`size-5 ${half ? "primary-color" : ""}`} />
)}
</span>
);
});

return <div className="flex">{rattingStar}</div>;
};

export default RatingReadOnly;
43 changes: 43 additions & 0 deletions src/app/title/[slug]/reviews/@component/rating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";
import IconStar from "@/components/icon/icon.star";
import IconStarBorder from "@/components/icon/icon.star.border";
import { useState } from "react";

const Rating = () => {
const [rating, setRating] = useState<number | null>(null);
const [hover, setHover] = useState<number | null>(null);

return (
<div className="flex h-8 my-auto">
{[...Array(5)].map((star, index) => {
const currentRating = index + 1;
return (
<label
key={index}
className="relative flex items-center cursor-pointer transition-all py-1 pl-1"
onMouseEnter={() => setHover(currentRating)}
onMouseLeave={() => setHover(null)}
>
<input
type="radio"
name="rating"
value={currentRating}
onClick={() => setRating(currentRating)}
className="hidden"
/>
<IconStarBorder className="cursor-pointer" />
<IconStar
className={`absolute top-0 left-0 z-10 transition-all primary-color w-full h-full py-1 pl-1 ${
currentRating <= (hover ?? rating ?? 0)
? "visible"
: "invisible"
}`}
/>
</label>
);
})}
</div>
);
};

export default Rating;
105 changes: 103 additions & 2 deletions src/app/title/[slug]/reviews/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,106 @@
const ReviewsTab = () => {
return <div>this is reviews tab</div>;
import TabContent from "@/app/title/@component/tab.content.wrapper";
import Rating from "@/app/title/[slug]/reviews/@component/rating";
import IconSend from "@/components/icon/icon.send";
import IconStarBorder from "@/components/icon/icon.star.border";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { sendRequest } from "@/utils/api";
import Link from "next/link";

import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import RatingReadOnly from "@/app/title/[slug]/reviews/@component/rating.read.only";

dayjs.extend(relativeTime);

const ReviewsTab = async (props: any) => {
const { params } = props;
const temp = params?.slug?.split(".html" ?? []);
const temp1 = temp[0]?.split("-" ?? []) as string[];
const id = temp1[temp1.length - 1];

const data = await sendRequest<IReviews[]>({
url: `${process.env.WEB_COMIC_API}/api/reviews?comicId=${id}&_expand=author`,
method: "GET",
nextOption: {
cache: "no-store",
},
});

return (
<TabContent>
<Rating></Rating>
<div className="relative">
<Textarea className="min-h-[7rem] mt-4 bg-neutral-100 border border-[#a3a3a3] focus:border-[var(--primary-color)] focus:outline-none transition-all peer"></Textarea>
<label className="absolute top-2 left-4 text-neutral-700 transition-all rounded text-base pointer-events-none peer-focus-within:text-xs peer-focus-within:-top-2 peer-focus-within:bg-white px-1 -mx-1 max-w-[calc(100% - 2rem)]">
<span className="relative z-[2] overflow-hidden whitespace-nowrap">
Write a review
</span>
<div className="absolute left-0 bottom-0 w-full h-1/2 z-[1] bg-neutral-100"></div>
</label>
</div>
<div className="flex justify-end mt-6">
<Button className="flex gap-1 items-center bg-primary-color p-1.5">
<IconSend></IconSend>
<span>Submit</span>
</Button>
</div>
<Separator className="mt-4 bg-neutral-300"></Separator>
<section>
{data.length >= 1 ? (
<ul>
{data?.map((reviews) => {
return (
<li
className="flex gap-4 mt-4"
key={reviews?.id}
>
<Link href="/author">
<Avatar className="size-12 mb-auto">
<AvatarImage
src={reviews?.author?.avatar}
alt={reviews?.author?.name}
/>
<AvatarFallback>
{reviews?.author?.name}
</AvatarFallback>
</Avatar>
</Link>
<div className="flex flex-col gap-1">
<Link href="/author">
<h3 className="font-medium line-clamp-1 break-all">
{reviews?.author?.name}
<span className="text-sm opacity-70 ml-1">
@{reviews?.author?.name}
</span>
</h3>
</Link>
<div className="flex -ml-0.5">
<RatingReadOnly
stars={reviews?.rated}
></RatingReadOnly>
<div className="text-neutral-500 whitespace-nowrap text-sm mt-1 ml-2">
{dayjs(
reviews?.createdAt
).fromNow()}
</div>
</div>
<p className="mt-2">
{reviews?.content}
</p>
</div>
</li>
);
})}
</ul>
) : (
<div className="mt-4">No Reviews</div>
)}
</section>
</TabContent>
);
};

export default ReviewsTab;
24 changes: 24 additions & 0 deletions src/components/icon/icon.send.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentProps } from "react";

const IconSend = (props: ComponentProps<"svg">) => {
return (
<svg
data-v-b8aef6c3=""
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M10 14l11 -11"></path>
<path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5"></path>
</svg>
);
};

export default IconSend;
Loading

0 comments on commit 9dfebbe

Please sign in to comment.