ReviewDetail.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. "use client";
  2. import { Rating } from "@components/common/Rating";
  3. import { GridTileImage } from "@components/theme/ui/grid/Tile";
  4. import { formatDate, getInitials, getReviews } from "@/utils/helper";
  5. import { isArray } from "@/utils/type-guards";
  6. import { Avatar } from "@heroui/avatar";
  7. import { Tooltip } from "@heroui/tooltip";
  8. import clsx from "clsx";
  9. import { FC, useState } from "react";
  10. import ReviewSection from "./ReviewSection";
  11. import { ProductReviewNode } from "@/components/catalog/type";
  12. interface ProductReviewEdge {
  13. __typename: "ProductReviewEdge";
  14. node: ProductReviewNode;
  15. }
  16. interface ReviewDetailProps {
  17. reviewDetails: ProductReviewEdge[];
  18. totalReview: number;
  19. productId: string;
  20. }
  21. const ReviewDetail: FC<ReviewDetailProps> = ({
  22. reviewDetails,
  23. totalReview,
  24. productId,
  25. }) => {
  26. const [visibleCount, setVisibleCount] = useState(5);
  27. const reviews: ProductReviewNode[] =
  28. reviewDetails?.map((edge) => edge.node) || [];
  29. const { reviewAvg, ratingCounts } = getReviews(reviews);
  30. const visibleReviews = reviews.slice(0, visibleCount);
  31. return (
  32. <>
  33. <div className="flex flex-col flex-wrap gap-x-5 sm:gap-x-10">
  34. <div className="my-2 flex w-full flex-col flex-wrap justify-between gap-4 sm:flex-row sm:items-center min-[1350px]:flex-nowrap">
  35. <div className="flex items-center gap-x-2">
  36. <Rating
  37. length={5}
  38. size="size-5"
  39. star={reviewAvg}
  40. reviewCount={totalReview}
  41. />
  42. </div>
  43. <div className="flex w-full max-w-[280px] overflow-hidden rounded-sm">
  44. {Object.entries(ratingCounts)
  45. .reverse()
  46. .filter(([_, count]) => (count as number) > 0)
  47. .map(([star, count]) => (
  48. <div
  49. key={star}
  50. style={{ flex: count as number }}
  51. className="min-h-4"
  52. >
  53. <Tooltip
  54. content={
  55. <p className="text-center">
  56. {star} Star <br /> {count as number}{" "}
  57. {(count as number) >= 2 ? "Reviews" : "Review"}
  58. </p>
  59. }
  60. placement="top"
  61. className="cursor-pointer"
  62. >
  63. <div
  64. className={clsx(
  65. "h-full w-full !cursor-pointer",
  66. star === "5"
  67. ? "bg-green-700"
  68. : star === "4"
  69. ? "bg-cyan-400"
  70. : star === "3"
  71. ? "bg-violet-600"
  72. : star === "2"
  73. ? "bg-yellow-400"
  74. : "bg-red-600",
  75. )}
  76. />
  77. </Tooltip>
  78. </div>
  79. ))}
  80. </div>
  81. </div>
  82. <div className="flex w-full flex-1 flex-col gap-5 py-2 sm:pt-6">
  83. {/* Scrollable Container */}
  84. <div
  85. className="max-h-[380px] overflow-y-auto pr-2
  86. scrollbar-thin scrollbar-track-transparent
  87. scrollbar-thumb-neutral-400 dark:scrollbar-thumb-neutral-600"
  88. >
  89. {visibleReviews &&
  90. visibleReviews.map(
  91. (
  92. {
  93. name,
  94. title,
  95. comment,
  96. createdAt,
  97. rating,
  98. images,
  99. customer,
  100. }: ProductReviewNode,
  101. index: number,
  102. ) => (
  103. <div
  104. key={index}
  105. className={clsx("flex flex-col gap-y-2 py-3", {
  106. "border-b border-neutral-200 dark:border-neutral-700":
  107. visibleReviews.length > 1 &&
  108. index < visibleReviews.length - 1,
  109. })}
  110. >
  111. <h1 className="font-outfit text-xl font-medium text-black/[80%] dark:text-white">
  112. {title}
  113. </h1>
  114. <Rating
  115. className="my-1"
  116. length={5}
  117. size="size-5"
  118. star={rating}
  119. />
  120. <h2 className="font-outfit text-base font-normal text-black/[80%] dark:text-white">
  121. {comment}
  122. </h2>
  123. <div className="flex gap-4">
  124. <div className="flex w-full items-center gap-2">
  125. <Avatar
  126. className="h-[56px] min-w-[56px] border border-solid border-black/10 bg-white text-large dark:bg-neutral-900"
  127. name={getInitials(name)}
  128. src={customer?.imageUrl}
  129. />
  130. <div>
  131. <h1 className="font-outfit text-base font-medium text-black/80 dark:text-white">
  132. {name}
  133. </h1>
  134. <p className="text-base text-black/80 dark:text-white">
  135. {formatDate(createdAt)}
  136. </p>
  137. </div>
  138. </div>
  139. </div>
  140. {isArray(images) && images && images.length > 0 && (
  141. <div className="mt-2 flex h-full min-h-[50px] w-full max-w-[60px] flex-wrap gap-2">
  142. {images.map((img) => (
  143. <GridTileImage
  144. key={img.reviewId}
  145. fill
  146. alt={`${img.reviewId}-review`}
  147. className="rounded-lg"
  148. src={img.url}
  149. />
  150. ))}
  151. </div>
  152. )}
  153. </div>
  154. ),
  155. )}
  156. {reviews.length > visibleCount && (
  157. <div className="py-4">
  158. <button
  159. onClick={() => setVisibleCount((prev) => prev + 5)}
  160. className="flex items-start gap-2 text-primary-600 cursor-pointer hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium transition-colors duration-200 group"
  161. >
  162. <span>Load More</span>
  163. <svg
  164. className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1 mt-1"
  165. fill="none"
  166. stroke="currentColor"
  167. viewBox="0 0 24 24"
  168. xmlns="http://www.w3.org/2000/svg"
  169. >
  170. <path
  171. strokeLinecap="round"
  172. strokeLinejoin="round"
  173. strokeWidth={2}
  174. d="M9 5l7 7-7 7"
  175. />
  176. </svg>
  177. </button>
  178. </div>
  179. )}
  180. </div>
  181. </div>
  182. <div className="mx-auto ">
  183. <ReviewSection productId={productId} totalReview={totalReview} />
  184. </div>
  185. </div>
  186. </>
  187. );
  188. };
  189. export default ReviewDetail;