import { useState, useCallback, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { API_URL } from "../config/authConstants";
import { useAuthContext } from "../context/AuthContext";
import MenuIcon from "../components/icons/MenuIcon";
import Sidebar from "../components/Sidebar";
import ProfileAvatar from "../components/profile/ProfileAvatar";
import { ProfileModal } from "../components/profile";
import AuthModal from "../components/AuthModal";
import "../styles/reviews.css";
import { useTranslation } from 'react-i18next';
/* ── Star rating sub-component ── */
function StarRating({ value, onChange, size = 28, readOnly = false }) {
const [hovered, setHovered] = useState(0);
return (
{[1, 2, 3, 4, 5].map((star) => {
const filled = star <= (hovered || value);
return (
);
})}
);
}
/* ── Single review card ── */
function ReviewCard({ review }) {
const { t } = useTranslation();
const date = review.createdAt
? new Date(review.createdAt).toLocaleDateString("ru-RU", {
day: "numeric", month: "long", year: "numeric",
})
: "";
return (
{review.authorName ? review.authorName.charAt(0).toUpperCase() : "?"}
{review.authorName || t('reviews.anonymous')}
{date && {date}}
{review.rating}/5
{review.text &&
{review.text}
}
);
}
/* ── Review form ── */
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 (
);
}
/* ── Main Reviews page ── */
export default function ReviewsPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { user, isAuthenticated } = useAuthContext();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [authModalOpen, setAuthModalOpen] = useState(false);
const [activeNav] = useState("reviews");
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [submitting, setSubmitting] = useState(false);
/* Profile data for sidebar */
const profileData = user
? {
login: user.login || "",
firstName: user.firstName || (user.name || "").trim().split(/\s+/)[0] || "",
lastName: user.lastName || (user.name || "").trim().split(/\s+/).slice(1).join(" ") || "",
email: user.email || "",
phone: user.phone || "",
avatar: user.avatar && user.avatar.startsWith("/") ? `${API_URL}${user.avatar}` : user.avatar || null,
id: user.id,
}
: {};
/** Load reviews from backend */
const loadReviews = useCallback(async () => {
setLoading(true);
setError("");
try {
console.log('[Reviews] GET request to:', `${API_URL}/api/v1/reviews`);
const res = await fetch(`${API_URL}/api/v1/reviews`);
console.log('[Reviews] Response status:', res.status, res.ok);
if (!res.ok) throw new Error(`Error ${res.status}`);
const data = await res.json();
console.log('[Reviews] Response data:', data);
const list = Array.isArray(data) ? data : data.items ?? data.reviews ?? [];
console.log('[Reviews] Parsed list:', list);
setReviews(list);
} catch (err) {
console.error('[Reviews] Error:', err);
setError(err.message || "Failed to load reviews");
// Show empty state with mock data hint
setReviews([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadReviews(); }, [loadReviews]);
/** Submit a new review */
const handleSubmitReview = useCallback(async ({ rating, text }) => {
if (!isAuthenticated) {
setAuthModalOpen(true);
return;
}
setSubmitting(true);
try {
const token = localStorage.getItem("token");
console.log('[Reviews] POST request body:', { rating, text });
const res = await fetch(`${API_URL}/api/v1/reviews`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ rating, text }),
});
console.log('[Reviews] POST response status:', res.status, res.ok);
if (res.ok) {
const responseData = await res.json();
console.log('[Reviews] POST response data:', responseData);
}
if (!res.ok) throw new Error(`Error ${res.status}`);
// Reload reviews after successful submit
await loadReviews();
} catch (err) {
// Fallback: add review locally if API is unavailable
setReviews((prev) => [
{
id: Date.now(),
authorName: profileData.firstName || t('reviews.you'),
rating,
text,
createdAt: new Date().toISOString(),
},
...prev,
]);
} finally {
setSubmitting(false);
}
}, [t, isAuthenticated, loadReviews, profileData.firstName]);
/** Average rating */
const avgRating = reviews.length
? (reviews.reduce((s, r) => s + r.rating, 0) / reviews.length).toFixed(1)
: null;
/** Rating distribution */
const distribution = [5, 4, 3, 2, 1].map((star) => ({
star,
count: reviews.filter((r) => r.rating === star).length,
pct: reviews.length
? Math.round((reviews.filter((r) => r.rating === star).length / reviews.length) * 100)
: 0,
}));
return (
setSidebarOpen(false)}
onNavClick={(label) => {
if (label === "Главная") navigate("/feed");
}}
isAuthenticated={isAuthenticated}
onLoginClick={() => setAuthModalOpen(true)}
onRulesClick={() => {}}
/>
{/* Top bar */}
{t('reviews.title')}
{/* Иконка профиля в правом углу */}
{isAuthenticated ? (
setProfileModalOpen(true)}>
) : (
)}
{/* Left: Summary + form */}
{/* Summary */}
{avgRating ? (
<>
{avgRating}
>
) : (
{t('reviews.noReviews')}
)}
{t('reviews.title')} ({reviews.length})
{/* Distribution bars */}
{reviews.length > 0 && (
{distribution.map(({ star, count, pct }) => (
))}
)}
{/* Add review form */}
{isAuthenticated ? (
) : (
{t('reviews.loginPrompt')}
)}
{/* Right: Reviews list */}
{loading ? (
) : error && reviews.length === 0 ? (
💬
{t('reviews.emptyTitle')}
{t('reviews.emptySub')}
) : (
reviews.map((review) =>
)
)}
{/* Profile modal */}
{profileModalOpen && isAuthenticated && (
setProfileModalOpen(false)} initialData={profileData} />
)}
{/* Auth modal */}
{authModalOpen && (
setAuthModalOpen(false)} />
)}
);
}