fixed code
This commit is contained in:
38
src/components/reviews/RatingSummary.jsx
Normal file
38
src/components/reviews/RatingSummary.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StarRating from "./StarRating";
|
||||
|
||||
export default function RatingSummary({ avgRating, totalCount, distribution }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rv-summary">
|
||||
<div className="rv-summary-score">
|
||||
{avgRating ? (
|
||||
<>
|
||||
<span className="rv-summary-num">{avgRating}</span>
|
||||
<StarRating value={Math.round(Number(avgRating))} readOnly size={22} />
|
||||
</>
|
||||
) : (
|
||||
<span className="rv-summary-none">{t('reviews.noReviews')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="rv-summary-total">
|
||||
{t('reviews.title')} ({totalCount})
|
||||
</span>
|
||||
|
||||
{totalCount > 0 && (
|
||||
<div className="rv-distribution">
|
||||
{distribution.map(({ star, count, pct }) => (
|
||||
<div key={star} className="rv-dist-row">
|
||||
<span className="rv-dist-star">{star} ★</span>
|
||||
<div className="rv-dist-bar">
|
||||
<div className="rv-dist-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="rv-dist-count">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
src/components/reviews/ReviewCard.jsx
Normal file
33
src/components/reviews/ReviewCard.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StarRating from "./StarRating";
|
||||
|
||||
export default function ReviewCard({ review }) {
|
||||
const { t } = useTranslation();
|
||||
const date = review.createdAt
|
||||
? new Date(review.createdAt).toLocaleDateString("ru-RU", {
|
||||
day: "numeric", month: "long", year: "numeric",
|
||||
})
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className={`rv-card${review.pending ? " rv-card-pending" : ""}`}>
|
||||
<div className="rv-card-head">
|
||||
<div className="rv-card-avatar">
|
||||
{review.authorName ? review.authorName.charAt(0).toUpperCase() : "?"}
|
||||
</div>
|
||||
<div className="rv-card-meta">
|
||||
<span className="rv-card-name">{review.authorName || t('reviews.anonymous')}</span>
|
||||
{date && <span className="rv-card-date">{date}</span>}
|
||||
{review.pending && (
|
||||
<span className="rv-card-pending-badge">{t('reviews.notSaved')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rv-card-rating">
|
||||
<StarRating value={review.rating} readOnly size={18} />
|
||||
<span className="rv-card-rating-num">{review.rating}/5</span>
|
||||
</div>
|
||||
</div>
|
||||
{review.text && <p className="rv-card-text">{review.text}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
src/components/reviews/ReviewForm.jsx
Normal file
47
src/components/reviews/ReviewForm.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StarRating from "./StarRating";
|
||||
|
||||
export default function ReviewForm({ onSubmit, submitting }) {
|
||||
const { t } = useTranslation();
|
||||
const [rating, setRating] = useState(0);
|
||||
const [text, setText] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (rating === 0) {
|
||||
setError(t('reviews.ratePrompt'));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
onSubmit({ rating, text: text.trim() });
|
||||
setRating(0);
|
||||
setText("");
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="rv-form" onSubmit={handleSubmit}>
|
||||
<h3 className="rv-form-title">{t('reviews.leaveReview')}</h3>
|
||||
<div className="rv-form-rating">
|
||||
<span className="rv-form-label">{t('reviews.rating')}</span>
|
||||
<StarRating value={rating} onChange={setRating} size={32} />
|
||||
</div>
|
||||
{error && <div className="rv-form-error">{error}</div>}
|
||||
<textarea
|
||||
className="rv-form-textarea"
|
||||
placeholder={t('reviews.reviewPlaceholder')}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
maxLength={500}
|
||||
rows={4}
|
||||
/>
|
||||
<div className="rv-form-footer">
|
||||
<span className="rv-form-counter">{text.length}/500</span>
|
||||
<button className="rv-form-submit" type="submit" disabled={submitting}>
|
||||
{submitting ? t('reviews.sending') : t('reviews.send')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
32
src/components/reviews/StarRating.jsx
Normal file
32
src/components/reviews/StarRating.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function StarRating({ value, onChange, size = 28, readOnly = false }) {
|
||||
const [hovered, setHovered] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="rv-stars">
|
||||
{[1, 2, 3, 4, 5].map((star) => {
|
||||
const filled = star <= (hovered || value);
|
||||
return (
|
||||
<svg
|
||||
key={star}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
className={`rv-star${filled ? " filled" : ""}${!readOnly ? " interactive" : ""}`}
|
||||
onClick={() => !readOnly && onChange(star)}
|
||||
onMouseEnter={() => !readOnly && setHovered(star)}
|
||||
onMouseLeave={() => !readOnly && setHovered(0)}
|
||||
>
|
||||
<polygon
|
||||
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
||||
fill={filled ? "#f5a623" : "none"}
|
||||
stroke={filled ? "#f5a623" : "currentColor"}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user