classify web
This commit is contained in:
126
components/PagesComponent/Reviews/MyReviewsCard.jsx
Normal file
126
components/PagesComponent/Reviews/MyReviewsCard.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
import { GoReport } from "react-icons/go";
|
||||
import StarRating from "./StarRating";
|
||||
import { formatDate, t } from "@/utils";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import ReportReviewModal from "./ReportReviewModal";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const MyReviewsCard = ({ rating, setMyReviews }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isTextOverflowing, setIsTextOverflowing] = useState(false);
|
||||
const textRef = useRef(null);
|
||||
const [IsReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [SellerReviewId, setSellerReviewId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const checkTextOverflow = () => {
|
||||
if (textRef.current) {
|
||||
const isOverflowing =
|
||||
textRef.current.scrollHeight > textRef.current.clientHeight;
|
||||
setIsTextOverflowing(isOverflowing);
|
||||
}
|
||||
};
|
||||
|
||||
checkTextOverflow();
|
||||
window.addEventListener("resize", checkTextOverflow);
|
||||
|
||||
return () => window.removeEventListener("resize", checkTextOverflow);
|
||||
}, []);
|
||||
|
||||
const handleReportClick = (id) => {
|
||||
setSellerReviewId(id);
|
||||
setIsReportModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row flex-1 gap-4">
|
||||
<div className="relative w-fit">
|
||||
<CustomImage
|
||||
src={rating?.buyer?.profile}
|
||||
width={72}
|
||||
height={72}
|
||||
alt="Reviewer"
|
||||
className="aspect-square rounded-full object-cover"
|
||||
/>
|
||||
<CustomImage
|
||||
src={rating?.item?.image}
|
||||
width={36}
|
||||
height={36}
|
||||
alt="Reviewer"
|
||||
className="absolute top-12 bottom-[-6px] right-[-6px] w-[36px] h-auto aspect-square rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<p className="font-semibold">{rating?.buyer?.name}</p>
|
||||
{rating?.report_status ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button onClick={() => handleReportClick(rating?.id)}>
|
||||
<GoReport />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" align="center">
|
||||
<p>{t("reportReview")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-sm line-clamp-1">
|
||||
{rating?.item?.translated_name || rating?.item?.name}
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<StarRating rating={Number(rating?.ratings)} size={24} />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{rating?.ratings}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm mt-1 justify-self-end">
|
||||
{formatDate(rating?.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b"></div>
|
||||
|
||||
<div>
|
||||
<p ref={textRef} className={`${!isExpanded ? "line-clamp-2" : ""}`}>
|
||||
{rating?.review}
|
||||
</p>
|
||||
{isTextOverflowing && (
|
||||
<div className="flex justify-center mt-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-primary text-sm hover:underline"
|
||||
>
|
||||
{isExpanded ? "See Less" : "See More"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Report Review Modal */}
|
||||
<ReportReviewModal
|
||||
isOpen={IsReportModalOpen}
|
||||
setIsOpen={setIsReportModalOpen}
|
||||
reviewId={SellerReviewId}
|
||||
setMyReviews={setMyReviews}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MyReviewsCard;
|
||||
60
components/PagesComponent/Reviews/MyReviewsCardSkeleton.jsx
Normal file
60
components/PagesComponent/Reviews/MyReviewsCardSkeleton.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const ReviewCardItem = () => {
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row flex-1 gap-4">
|
||||
{/* Profile image with smaller image overlay */}
|
||||
<div className="relative w-fit">
|
||||
<Skeleton className="w-[72px] h-[72px] rounded-full" />
|
||||
<Skeleton className="w-[36px] h-[36px] rounded-full absolute top-12 bottom-[-6px] right-[-6px]" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
{/* Reviewer name and report icon */}
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<Skeleton className="h-5 w-32 rounded-md" />
|
||||
<Skeleton className="h-5 w-5 rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* Item name */}
|
||||
<Skeleton className="h-4 w-48 mt-1 rounded-md" />
|
||||
|
||||
{/* Star rating */}
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<Skeleton className="h-6 w-32 rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<Skeleton className="h-4 w-24 mt-1 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-b"></div>
|
||||
|
||||
{/* Review text */}
|
||||
<div>
|
||||
<Skeleton className="h-4 w-full rounded-md mb-2" />
|
||||
<Skeleton className="h-4 w-full rounded-md" />
|
||||
|
||||
{/* See More button (optional) */}
|
||||
<div className="flex justify-center mt-1">
|
||||
<Skeleton className="h-4 w-16 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MyReviewsCardSkeleton = () => {
|
||||
return (
|
||||
<div className="mt-[30px] p-2 sm:p-4 bg-muted rounded-xl flex flex-col gap-[30px]">
|
||||
{Array(8).fill(0).map((_, index) => (
|
||||
<ReviewCardItem key={`review-skeleton-${index}`} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyReviewsCardSkeleton;
|
||||
45
components/PagesComponent/Reviews/RatingsSummary.jsx
Normal file
45
components/PagesComponent/Reviews/RatingsSummary.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import StarRating from "./StarRating";
|
||||
import { calculateRatingPercentages, t } from "@/utils";
|
||||
import { TiStarFullOutline } from "react-icons/ti";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
const RatingsSummary = ({ averageRating, reviews }) => {
|
||||
|
||||
|
||||
const { ratingCount, ratingPercentages } = reviews?.length
|
||||
? calculateRatingPercentages(reviews)
|
||||
: { ratingCount: {}, ratingPercentages: {} };
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 p-4 rounded-xl border">
|
||||
{/* Average Rating Section */}
|
||||
<div className="col-span-4 border-b md:border-b-0 ltr:md:border-r rtl:md:border-l pb-4 md:pb-0 ltr:md:pr-4 rtl:md:pl-4">
|
||||
<h1 className="font-bold text-6xl text-center">{Number(averageRating).toFixed(2)}</h1>
|
||||
<div className="mt-4 flex flex-col items-center justify-center gap-1">
|
||||
<StarRating rating={Number(averageRating)} size={40} />
|
||||
<p className="text-sm">{reviews.length} {t('ratings')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating Progress Bars Section */}
|
||||
<div className="col-span-8 pt-4 md:pt-0 ltr:md:pl-8 rtl:md:pr-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
{[5, 4, 3, 2, 1].map((rating) => (
|
||||
<div className="flex items-center gap-2" key={`rating-${rating}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
{rating}
|
||||
<TiStarFullOutline color="black" size={24} />
|
||||
</div>
|
||||
<Progress value={ratingPercentages?.[rating] || 0} className='h-3' />
|
||||
<span>{ratingCount?.[rating] || 0}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RatingsSummary;
|
||||
31
components/PagesComponent/Reviews/RatingsSummarySkeleton.jsx
Normal file
31
components/PagesComponent/Reviews/RatingsSummarySkeleton.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const RatingsSummarySkeleton = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 p-4 rounded-xl border">
|
||||
{/* Average Rating Section Skeleton */}
|
||||
<div className="col-span-4 border-b md:border-b-0 ltr:md:border-r pb-4 md:pb-0 md:pr-4">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Skeleton className="h-24 w-24 rounded-md mb-4" />
|
||||
<Skeleton className="h-6 w-36 rounded-md mb-2" />
|
||||
<Skeleton className="h-4 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating Progress Bars Section Skeleton */}
|
||||
<div className="col-span-8 pt-4 md:pt-0 md:pl-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<div className="flex items-center gap-2" key={`skeleton-rating-${index}`}>
|
||||
<Skeleton className="h-6 w-10 rounded-md" />
|
||||
<Skeleton className="h-3 flex-1 rounded-md" />
|
||||
<Skeleton className="h-4 w-6 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RatingsSummarySkeleton;
|
||||
109
components/PagesComponent/Reviews/ReportReviewModal.jsx
Normal file
109
components/PagesComponent/Reviews/ReportReviewModal.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { addReportReviewApi } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { t } from "@/utils";
|
||||
|
||||
const ReportReviewModal = ({ isOpen, setIsOpen, reviewId, setMyReviews }) => {
|
||||
const [reason, setReason] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [validationError, setValidationError] = useState("");
|
||||
|
||||
// Clear reason and validation error when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setReason("");
|
||||
setValidationError("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!reason.trim()) {
|
||||
setValidationError("Please provide a reason for the report");
|
||||
return;
|
||||
}
|
||||
setValidationError("");
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await addReportReviewApi.addReportReview({
|
||||
seller_review_id: reviewId,
|
||||
report_reason: reason,
|
||||
});
|
||||
|
||||
if (res?.data?.error === false) {
|
||||
toast.success(res?.data?.message)
|
||||
setMyReviews(prevReviews =>
|
||||
prevReviews.map(review =>
|
||||
review.id === reviewId ? { ...review, report_reason: res?.data?.data.report_reason, report_status: res?.data?.data.report_status } : review
|
||||
)
|
||||
);
|
||||
setReason("");
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
toast.error(res?.data?.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reporting review:", error);
|
||||
toast.error(t("somethingWentWrong"))
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear validation error when user types
|
||||
const handleReasonChange = (e) => {
|
||||
setReason(e.target.value);
|
||||
if (validationError) {
|
||||
setValidationError("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()} className="px-6 py-6 sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-medium">
|
||||
{t('reportReview')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm text-gray-600 mt-2">
|
||||
{t('reportReviewDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
<Textarea
|
||||
placeholder="Write your reason here"
|
||||
className={`h-32 resize-none ${validationError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
||||
value={reason}
|
||||
onChange={handleReasonChange}
|
||||
/>
|
||||
{validationError && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t("submitting") : t("report")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportReviewModal;
|
||||
90
components/PagesComponent/Reviews/Reviews.jsx
Normal file
90
components/PagesComponent/Reviews/Reviews.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
import { t } from "@/utils";
|
||||
import RatingsSummary from "./RatingsSummary";
|
||||
import RatingsSummarySkeleton from "./RatingsSummarySkeleton";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getMyReviewsApi } from "@/utils/api";
|
||||
import MyReviewsCard from "./MyReviewsCard.jsx";
|
||||
import MyReviewsCardSkeleton from "@/components/PagesComponent/Reviews/MyReviewsCardSkeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import NoData from "@/components/EmptyStates/NoData";
|
||||
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
|
||||
|
||||
const Reviews = () => {
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const [MyReviews, setMyReviews] = useState([]);
|
||||
const [AverageRating, setAverageRating] = useState("");
|
||||
const [CurrentPage, setCurrentPage] = useState(1);
|
||||
const [ReviewHasMore, setReviewHasMore] = useState(false);
|
||||
const [IsLoading, setIsLoading] = useState(false);
|
||||
const [IsLoadMore, setIsLoadMore] = useState(false);
|
||||
|
||||
const getReveiws = async (page) => {
|
||||
try {
|
||||
if (page === 1) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
const res = await getMyReviewsApi.getMyReviews({ page });
|
||||
setAverageRating(res?.data?.data?.average_rating);
|
||||
setMyReviews(res?.data?.data?.ratings?.data);
|
||||
setCurrentPage(res?.data?.data?.ratings?.current_page);
|
||||
if (
|
||||
res?.data?.data?.ratings?.current_page <
|
||||
res?.data?.data?.ratings?.last_page
|
||||
) {
|
||||
setReviewHasMore(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getReveiws(1);
|
||||
}, [CurrentLanguage?.id]);
|
||||
|
||||
const handleReviewLoadMore = () => {
|
||||
setIsLoadMore(true);
|
||||
getReveiws(CurrentPage + 1);
|
||||
};
|
||||
|
||||
return IsLoading ? (
|
||||
<>
|
||||
<RatingsSummarySkeleton />
|
||||
<MyReviewsCardSkeleton />
|
||||
</>
|
||||
) : MyReviews && MyReviews.length > 0 ? (
|
||||
<>
|
||||
<RatingsSummary averageRating={AverageRating} reviews={MyReviews} />
|
||||
<div className="mt-[30px] p-2 sm:p-4 bg-muted rounded-xl flex flex-col gap-[30px]">
|
||||
{MyReviews?.map((rating) => (
|
||||
<MyReviewsCard
|
||||
rating={rating}
|
||||
key={rating?.id}
|
||||
setMyReviews={setMyReviews}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{ReviewHasMore && (
|
||||
<div className="text-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-sm sm:text-base text-primary w-[256px]"
|
||||
disabled={IsLoading || IsLoadMore}
|
||||
onClick={handleReviewLoadMore}
|
||||
>
|
||||
{IsLoadMore ? t("loading") : t("loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<NoData name={t("reviews")} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Reviews;
|
||||
70
components/PagesComponent/Reviews/SellerReviewCard.jsx
Normal file
70
components/PagesComponent/Reviews/SellerReviewCard.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { formatDate, t } from "@/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import StarRating from "./StarRating";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const SellerReviewCard = ({ rating }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isTextOverflowing, setIsTextOverflowing] = useState(false);
|
||||
const textRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
const content = textRef.current;
|
||||
if (content) {
|
||||
setIsTextOverflowing(content.scrollHeight > content.clientHeight);
|
||||
}
|
||||
};
|
||||
checkOverflow();
|
||||
window.addEventListener("resize", checkOverflow);
|
||||
return () => window.removeEventListener("resize", checkOverflow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg flex flex-col gap-4">
|
||||
<div className="flex sm:items-center gap-4 flex-col sm:flex-row">
|
||||
<CustomImage
|
||||
src={rating?.buyer?.profile}
|
||||
width={72}
|
||||
height={72}
|
||||
alt="Reviewer"
|
||||
className="aspect-square rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<p className="font-semibold">{rating?.buyer?.name}</p>
|
||||
|
||||
<div className="flex items-center justify-between flex-wrap gap-1 w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<StarRating rating={Number(rating?.ratings)} size={24} />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{rating?.ratings}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm justify-self-end">
|
||||
{formatDate(rating?.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b"></div>
|
||||
|
||||
<div>
|
||||
<p ref={textRef} className={`${!isExpanded ? "line-clamp-2" : ""}`}>
|
||||
{rating?.review}
|
||||
</p>
|
||||
{isTextOverflowing && (
|
||||
<div className="flex justify-center mt-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-primary text-sm hover:underline"
|
||||
>
|
||||
{isExpanded ? t("seeLess") : t("seeMore")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SellerReviewCard;
|
||||
38
components/PagesComponent/Reviews/StarRating.jsx
Normal file
38
components/PagesComponent/Reviews/StarRating.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import { TiStarFullOutline, TiStarHalfOutline, TiStarOutline } from "react-icons/ti";
|
||||
|
||||
const StarRating = ({ rating = 0, size = 16, maxStars = 5, showEmpty = true }) => {
|
||||
|
||||
// Get the integer part of the rating (full stars)
|
||||
const fullStars = Math.floor(rating);
|
||||
|
||||
// Check if there's any decimal part at all (0.1, 0.2, ..., 0.9)
|
||||
const hasDecimal = rating % 1 !== 0;
|
||||
|
||||
// If there's any decimal, always show a half star
|
||||
const hasHalfStar = hasDecimal;
|
||||
|
||||
// Calculate empty stars
|
||||
const emptyStars = maxStars - fullStars - (hasHalfStar ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 max-w-full">
|
||||
{/* Render full stars */}
|
||||
{[...Array(fullStars)].map((_, index) => (
|
||||
<TiStarFullOutline key={`full-${index}`} color="#FFD700" size={size} />
|
||||
))}
|
||||
|
||||
{/* Render half star if there's any decimal */}
|
||||
{hasHalfStar && (
|
||||
<TiStarHalfOutline key="half" className="rtl:scale-x-[-1]" color="#FFD700" size={size} />
|
||||
)}
|
||||
|
||||
{/* Render empty stars */}
|
||||
{showEmpty && [...Array(emptyStars)].map((_, index) => (
|
||||
<TiStarOutline key={`empty-${index}`} color="#0000002E" size={size} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarRating;
|
||||
Reference in New Issue
Block a user