added english language
This commit is contained in:
@@ -7,8 +7,10 @@ import { getPostForChat } from "../stores/chatPostStore";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { useDeleteChat } from "../hooks/useDeleteChat";
|
||||
import TrashIcon from "../components/icons/TrashIcon";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ChatListPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { chats, loading, fetchChats } = useChats();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
@@ -28,10 +30,10 @@ export default function ChatListPage() {
|
||||
}, [chats, updateFromChats]);
|
||||
|
||||
const handleDeleteChat = useCallback(async (chatId) => {
|
||||
if (!window.confirm("Вы уверены, что хотите удалить этот чат?")) return;
|
||||
if (!window.confirm(t('chat.confirmDeleteChat'))) return;
|
||||
const ok = await deleteChat(chatId);
|
||||
if (ok) fetchChats();
|
||||
}, [deleteChat, fetchChats]);
|
||||
}, [t, deleteChat, fetchChats]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
@@ -41,10 +43,10 @@ export default function ChatListPage() {
|
||||
<button className="chat-back-btn" onClick={() => navigate("/feed")}>
|
||||
←
|
||||
</button>
|
||||
<div className="chat-header-title">Чаты</div>
|
||||
<div className="chat-header-title">{t('chat.chats')}</div>
|
||||
<div />
|
||||
</div>
|
||||
<div className="chat-empty-full">Войдите, чтобы видеть чаты</div>
|
||||
<div className="chat-empty-full">{t('chat.loginToSee')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -57,22 +59,22 @@ export default function ChatListPage() {
|
||||
<button className="chat-back-btn" onClick={() => navigate("/feed")}>
|
||||
←
|
||||
</button>
|
||||
<div className="chat-header-title">Чаты</div>
|
||||
<div className="chat-header-title">{t('chat.chats')}</div>
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<div className="chat-list">
|
||||
{loading && (
|
||||
<div className="chat-empty-full">Загрузка...</div>
|
||||
<div className="chat-empty-full">{t('chat.loading')}</div>
|
||||
)}
|
||||
|
||||
{!loading && chats.length === 0 && (
|
||||
<div className="chat-empty-full">У вас пока нет чатов</div>
|
||||
<div className="chat-empty-full">{t('chat.noChats')}</div>
|
||||
)}
|
||||
|
||||
{!loading && chats.map((chat) => {
|
||||
const post = getPostForChat(chat.id);
|
||||
const title = post?.title || chat.otherUser?.name || "Пользователь";
|
||||
const title = post?.title || chat.otherUser?.name || t('chat.user');
|
||||
const imgSrc = post?.images?.[0] ? `${API_URL}${post.images[0]}` : null;
|
||||
const emoji = post?.emoji;
|
||||
const unreadCount = unreadMap[chat.id] ?? chat.unreadCount ?? chat.unreadMessages ?? 0;
|
||||
@@ -92,7 +94,7 @@ export default function ChatListPage() {
|
||||
<div className="chat-list-info">
|
||||
<div className="chat-list-name">{title}</div>
|
||||
<div className="chat-list-preview">
|
||||
{chat.lastMessage?.content || "Нет сообщений"}
|
||||
{chat.lastMessage?.content || t('chat.noMessages')}
|
||||
</div>
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
@@ -100,7 +102,7 @@ export default function ChatListPage() {
|
||||
)}
|
||||
<button
|
||||
className="chat-list-delete-btn"
|
||||
title="Удалить чат"
|
||||
title={t('chat.deleteChat')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteChat(chat.id);
|
||||
|
||||
@@ -8,8 +8,10 @@ import { useUnread } from "../context/UnreadContext";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import emojiData from "@emoji-mart/data";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ChatPage() {
|
||||
const { t } = useTranslation();
|
||||
const { chatId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { fetchMessages, markAsRead } = useChats();
|
||||
@@ -83,15 +85,15 @@ export default function ChatPage() {
|
||||
post?.emoji || "📋"
|
||||
)}
|
||||
</div>
|
||||
<div className="chat-header-title">{post?.title || "Чат"}</div>
|
||||
<div className="chat-header-title">{post?.title || t('chat.title')}</div>
|
||||
<div className={`chat-status${connected ? " connected" : ""}`}>
|
||||
{connected ? "В сети" : "Отключено"}
|
||||
{connected ? t('chat.online') : t('chat.offline')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chat-messages">
|
||||
{messages.length === 0 && (
|
||||
<div className="chat-empty">Нет сообщений</div>
|
||||
<div className="chat-empty">{t('chat.noMessages')}</div>
|
||||
)}
|
||||
{messages.map((m) => (
|
||||
<div
|
||||
@@ -123,7 +125,7 @@ export default function ChatPage() {
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Сообщение..."
|
||||
placeholder={t('chat.messagePlaceholder')}
|
||||
className="chat-input"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useAuthContext } from "../context/AuthContext";
|
||||
import { useDeletePost } from "../hooks/useDeletePost";
|
||||
import { useFavorites } from "../hooks/useFavorites";
|
||||
import { usePostStatus } from "../hooks/usePostStatus";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* MainFeed — корневой компонент приложения.
|
||||
@@ -29,11 +30,12 @@ export default function MainFeed() {
|
||||
const { deletePost } = useDeletePost();
|
||||
const { toggleFavorite, loadFavorites } = useFavorites();
|
||||
const { markAsFound } = usePostStatus();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Открыт ли левый сайдбар (мобильный слайд-аут)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
// Активный пункт навигации в сайдбаре
|
||||
const [activeNav, setActiveNav] = useState("Главная");
|
||||
const [activeNav, setActiveNav] = useState("home");
|
||||
// Фильтр по типу объявления: null (все) | "loss" | "found"
|
||||
const [typeFilter, setTypeFilter] = useState(null);
|
||||
// Поисковый запрос (фильтр по title / description)
|
||||
@@ -109,7 +111,7 @@ export default function MainFeed() {
|
||||
if (search) params.set("search", search);
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/posts?${params}`);
|
||||
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
|
||||
if (!res.ok) throw new Error(`Error ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const content = data.content ?? [];
|
||||
@@ -122,7 +124,7 @@ export default function MainFeed() {
|
||||
setHasMore(!data.last); // data.last — стандартное поле Spring Page
|
||||
setPage(pageNum);
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось загрузить объявления");
|
||||
setError(err.message || "Failed to load ads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
@@ -169,7 +171,7 @@ export default function MainFeed() {
|
||||
setMyAds([]);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
|
||||
if (!res.ok) throw new Error(`Error ${res.status}`);
|
||||
const data = await res.json();
|
||||
const posts = Array.isArray(data) ? data : (data.items ?? []);
|
||||
const normalizedPosts = posts.map(ad => ({
|
||||
@@ -178,7 +180,7 @@ export default function MainFeed() {
|
||||
}));
|
||||
setMyAds(normalizedPosts);
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось загрузить ваши объявления");
|
||||
setError(err.message || "Failed to load your ads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -197,7 +199,7 @@ export default function MainFeed() {
|
||||
if (success) {
|
||||
// Перезагружаем ленту для получения обновлённых данных
|
||||
loadFeed();
|
||||
if (activeNav === "Мои объявления") {
|
||||
if (activeNav === "myads") {
|
||||
loadMyPosts();
|
||||
}
|
||||
}
|
||||
@@ -205,7 +207,7 @@ export default function MainFeed() {
|
||||
|
||||
/** Удаляет объявление и перезагружает ленту */
|
||||
const handleDelete = useCallback(async (postId) => {
|
||||
if (!window.confirm("Вы уверены, что хотите удалить это объявление?")) {
|
||||
if (!window.confirm(t('feed.confirmDelete'))) {
|
||||
return;
|
||||
}
|
||||
const success = await deletePost(postId);
|
||||
@@ -213,11 +215,11 @@ export default function MainFeed() {
|
||||
loadFeed();
|
||||
loadMyPosts();
|
||||
}
|
||||
}, [deletePost, loadFeed, loadMyPosts]);
|
||||
}, [t, deletePost, loadFeed, loadMyPosts]);
|
||||
|
||||
/** Отмечает объявление как "Вещь найдена" и перезагружает ленту */
|
||||
const handleMarkFound = useCallback(async (postId) => {
|
||||
if (!window.confirm("Отметить это объявление как \"Вещь найдена\"?")) {
|
||||
if (!window.confirm(t('feed.confirmMarkFound'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ export default function MainFeed() {
|
||||
loadFeed();
|
||||
loadMyPosts();
|
||||
}
|
||||
}, [markAsFound, token, loadFeed, loadMyPosts]);
|
||||
}, [t, markAsFound, token, loadFeed, loadMyPosts]);
|
||||
|
||||
// После создания или редактирования объявления перезагружаем ленту,
|
||||
// чтобы получить актуальные данные (включая изображения) из БД
|
||||
@@ -280,7 +282,7 @@ export default function MainFeed() {
|
||||
|
||||
// Когда переключаемся на «Мои объявления» — подгружаем с бэкенда по userId
|
||||
useEffect(() => {
|
||||
if (activeNav === "Мои объявления" && isAuthenticated && user?.id) {
|
||||
if (activeNav === "myads" && isAuthenticated && user?.id) {
|
||||
loadMyPosts();
|
||||
}
|
||||
}, [activeNav, isAuthenticated, user?.id, loadMyPosts]);
|
||||
@@ -289,16 +291,16 @@ export default function MainFeed() {
|
||||
const filteredAds = (() => {
|
||||
let result;
|
||||
|
||||
if (activeNav === "Мои объявления") {
|
||||
if (activeNav === "myads") {
|
||||
// Используем объявления, загруженные с бэкенда по userId
|
||||
result = myAds;
|
||||
} else if (activeNav === "Главная") {
|
||||
} else if (activeNav === "home") {
|
||||
result = ads;
|
||||
} else {
|
||||
result = ads.filter(ad =>
|
||||
activeNav === "Вещи"
|
||||
activeNav === "items"
|
||||
? ["Вещь", "Другое"].includes(ad.category)
|
||||
: activeNav === "Питомцы"
|
||||
: activeNav === "pets"
|
||||
? ad.category === "Питомец"
|
||||
: false
|
||||
);
|
||||
@@ -329,7 +331,7 @@ export default function MainFeed() {
|
||||
<div className="f-center">
|
||||
<div className="f-spinner-wrap">
|
||||
<div className="f-spinner" />
|
||||
Загружаем объявления...
|
||||
{t('feed.loading')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -338,15 +340,15 @@ export default function MainFeed() {
|
||||
return (
|
||||
<div className="f-center">
|
||||
<div className="f-center-icon">⚠️</div>
|
||||
<div className="f-center-title">Не удалось загрузить ленту</div>
|
||||
<div className="f-center-title">{t('feed.loadError')}</div>
|
||||
<div className="f-center-sub">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!filteredAds.length) {
|
||||
const emptyMsg = activeNav === "Мои объявления"
|
||||
? { title: "У вас нет объявлений", sub: "Создайте первое — нажмите кнопку выше" }
|
||||
: { title: "Объявлений пока нет", sub: "Будьте первым — разместите объявление<br />о потере или находке" };
|
||||
const emptyMsg = activeNav === "myads"
|
||||
? { title: t('feed.emptyMyAdsTitle'), sub: t('feed.emptyMyAdsSub') }
|
||||
: { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') };
|
||||
|
||||
return (
|
||||
<div className="f-center">
|
||||
@@ -364,10 +366,10 @@ export default function MainFeed() {
|
||||
ad={ad}
|
||||
liked={liked.has(ad.id)}
|
||||
onLike={toggleLike}
|
||||
onEdit={activeNav === "Мои объявления" ? handleEdit : undefined}
|
||||
onDelete={activeNav === "Мои объявления" ? handleDelete : undefined}
|
||||
onMarkFound={activeNav === "Мои объявления" ? handleMarkFound : undefined}
|
||||
isOwner={activeNav === "Мои объявления"}
|
||||
onEdit={activeNav === "myads" ? handleEdit : undefined}
|
||||
onDelete={activeNav === "myads" ? handleDelete : undefined}
|
||||
onMarkFound={activeNav === "myads" ? handleMarkFound : undefined}
|
||||
isOwner={activeNav === "myads"}
|
||||
onImageClick={handleImageClick}
|
||||
onAuthRequired={() => setAuthModalOpen(true)}
|
||||
/>
|
||||
@@ -402,7 +404,7 @@ export default function MainFeed() {
|
||||
fontWeight: 900,
|
||||
lineHeight: 1
|
||||
}}>+</span>
|
||||
Разместить объявление
|
||||
{t('feed.createPost')}
|
||||
</button>
|
||||
</div>
|
||||
{/* Иконка профиля в правом углу */}
|
||||
@@ -416,7 +418,7 @@ export default function MainFeed() {
|
||||
) : (
|
||||
<button className="f-login-btn"
|
||||
onClick={() => setAuthModalOpen(true)}>
|
||||
Войти
|
||||
{t('feed.login')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -432,7 +434,7 @@ export default function MainFeed() {
|
||||
<input
|
||||
className="f-search-input"
|
||||
type="text"
|
||||
placeholder="Поиск по названию или описанию..."
|
||||
placeholder={t('feed.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -455,21 +457,21 @@ export default function MainFeed() {
|
||||
: ""}`}
|
||||
onClick={() => setTypeFilter("loss")}
|
||||
>
|
||||
🔴 Потери
|
||||
{t('feed.filterLoss')}
|
||||
</button>
|
||||
<button
|
||||
className={`f-filter-btn${typeFilter === null ? " active"
|
||||
: ""}`}
|
||||
onClick={() => setTypeFilter(null)}
|
||||
>
|
||||
Все объявления
|
||||
{t('feed.filterAll')}
|
||||
</button>
|
||||
<button
|
||||
className={`f-filter-btn${typeFilter === "found" ? " active"
|
||||
: ""}`}
|
||||
onClick={() => setTypeFilter("found")}
|
||||
>
|
||||
🟢 Находки
|
||||
{t('feed.filterFound')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -477,7 +479,7 @@ export default function MainFeed() {
|
||||
<div className="f-wrap">
|
||||
<div className="f-feed">
|
||||
{renderContent()}
|
||||
{activeNav === "Главная" && !loading && (
|
||||
{activeNav === "home" && !loading && (
|
||||
<div ref={sentinelRef} style={{height: "1px"}}/>
|
||||
)}
|
||||
{loadingMore && (
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 }) {
|
||||
@@ -43,6 +44,7 @@ function StarRating({ value, onChange, size = 28, readOnly = false }) {
|
||||
|
||||
/* ── 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",
|
||||
@@ -56,7 +58,7 @@ function ReviewCard({ review }) {
|
||||
{review.authorName ? review.authorName.charAt(0).toUpperCase() : "?"}
|
||||
</div>
|
||||
<div className="rv-card-meta">
|
||||
<span className="rv-card-name">{review.authorName || "Аноним"}</span>
|
||||
<span className="rv-card-name">{review.authorName || t('reviews.anonymous')}</span>
|
||||
{date && <span className="rv-card-date">{date}</span>}
|
||||
</div>
|
||||
<div className="rv-card-rating">
|
||||
@@ -71,6 +73,7 @@ function ReviewCard({ review }) {
|
||||
|
||||
/* ── Review form ── */
|
||||
function ReviewForm({ onSubmit, submitting }) {
|
||||
const { t } = useTranslation();
|
||||
const [rating, setRating] = useState(0);
|
||||
const [text, setText] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@@ -78,7 +81,7 @@ function ReviewForm({ onSubmit, submitting }) {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (rating === 0) {
|
||||
setError("Поставьте оценку");
|
||||
setError(t('reviews.ratePrompt'));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
@@ -89,15 +92,15 @@ function ReviewForm({ onSubmit, submitting }) {
|
||||
|
||||
return (
|
||||
<form className="rv-form" onSubmit={handleSubmit}>
|
||||
<h3 className="rv-form-title">Оставить отзыв</h3>
|
||||
<h3 className="rv-form-title">{t('reviews.leaveReview')}</h3>
|
||||
<div className="rv-form-rating">
|
||||
<span className="rv-form-label">Оценка:</span>
|
||||
<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="Напишите ваш отзыв (необязательно)..."
|
||||
placeholder={t('reviews.reviewPlaceholder')}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
maxLength={500}
|
||||
@@ -106,7 +109,7 @@ function ReviewForm({ onSubmit, submitting }) {
|
||||
<div className="rv-form-footer">
|
||||
<span className="rv-form-counter">{text.length}/500</span>
|
||||
<button className="rv-form-submit" type="submit" disabled={submitting}>
|
||||
{submitting ? "Отправка..." : "Отправить"}
|
||||
{submitting ? t('reviews.sending') : t('reviews.send')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -115,13 +118,14 @@ function ReviewForm({ onSubmit, submitting }) {
|
||||
|
||||
/* ── 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("Отзывы");
|
||||
const [activeNav] = useState("reviews");
|
||||
|
||||
const [reviews, setReviews] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -149,7 +153,7 @@ export default function ReviewsPage() {
|
||||
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(`Ошибка ${res.status}`);
|
||||
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 ?? [];
|
||||
@@ -157,7 +161,7 @@ export default function ReviewsPage() {
|
||||
setReviews(list);
|
||||
} catch (err) {
|
||||
console.error('[Reviews] Error:', err);
|
||||
setError(err.message || "Не удалось загрузить отзывы");
|
||||
setError(err.message || "Failed to load reviews");
|
||||
// Show empty state with mock data hint
|
||||
setReviews([]);
|
||||
} finally {
|
||||
@@ -190,7 +194,7 @@ export default function ReviewsPage() {
|
||||
const responseData = await res.json();
|
||||
console.log('[Reviews] POST response data:', responseData);
|
||||
}
|
||||
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
|
||||
if (!res.ok) throw new Error(`Error ${res.status}`);
|
||||
// Reload reviews after successful submit
|
||||
await loadReviews();
|
||||
} catch (err) {
|
||||
@@ -198,7 +202,7 @@ export default function ReviewsPage() {
|
||||
setReviews((prev) => [
|
||||
{
|
||||
id: Date.now(),
|
||||
authorName: profileData.firstName || "Вы",
|
||||
authorName: profileData.firstName || t('reviews.you'),
|
||||
rating,
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -208,7 +212,7 @@ export default function ReviewsPage() {
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [isAuthenticated, loadReviews, profileData.firstName]);
|
||||
}, [t, isAuthenticated, loadReviews, profileData.firstName]);
|
||||
|
||||
/** Average rating */
|
||||
const avgRating = reviews.length
|
||||
@@ -245,7 +249,7 @@ export default function ReviewsPage() {
|
||||
<MenuIcon />
|
||||
</button>
|
||||
<div className="f-topbar-center">
|
||||
<h1 className="rv-page-title">Отзывы</h1>
|
||||
<h1 className="rv-page-title">{t('reviews.title')}</h1>
|
||||
</div>
|
||||
{/* Иконка профиля в правом углу */}
|
||||
{isAuthenticated ? (
|
||||
@@ -254,7 +258,7 @@ export default function ReviewsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<button className="f-login-btn" onClick={() => setAuthModalOpen(true)}>
|
||||
Войти
|
||||
{t('reviews.login')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -271,11 +275,11 @@ export default function ReviewsPage() {
|
||||
<StarRating value={Math.round(Number(avgRating))} readOnly size={22} />
|
||||
</>
|
||||
) : (
|
||||
<span className="rv-summary-none">Пока нет отзывов</span>
|
||||
<span className="rv-summary-none">{t('reviews.noReviews')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="rv-summary-total">
|
||||
{reviews.length} {reviews.length === 1 ? "отзыв" : reviews.length < 5 ? "отзыва" : "отзывов"}
|
||||
{t('reviews.title')} ({reviews.length})
|
||||
</span>
|
||||
|
||||
{/* Distribution bars */}
|
||||
@@ -299,9 +303,9 @@ export default function ReviewsPage() {
|
||||
<ReviewForm onSubmit={handleSubmitReview} submitting={submitting} />
|
||||
) : (
|
||||
<div className="rv-login-prompt">
|
||||
<p>Войдите, чтобы оставить отзыв</p>
|
||||
<p>{t('reviews.loginPrompt')}</p>
|
||||
<button className="rv-login-btn" onClick={() => setAuthModalOpen(true)}>
|
||||
Войти
|
||||
{t('reviews.login')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -313,14 +317,14 @@ export default function ReviewsPage() {
|
||||
<div className="f-center">
|
||||
<div className="f-spinner-wrap">
|
||||
<div className="f-spinner" />
|
||||
Загружаем отзывы...
|
||||
{t('reviews.loading')}
|
||||
</div>
|
||||
</div>
|
||||
) : error && reviews.length === 0 ? (
|
||||
<div className="f-center">
|
||||
<div className="f-center-icon">💬</div>
|
||||
<div className="f-center-title">Отзывов пока нет</div>
|
||||
<div className="f-center-sub">Станьте первым — оставьте отзыв о площадке</div>
|
||||
<div className="f-center-title">{t('reviews.emptyTitle')}</div>
|
||||
<div className="f-center-sub">{t('reviews.emptySub')}</div>
|
||||
</div>
|
||||
) : (
|
||||
reviews.map((review) => <ReviewCard key={review.id} review={review} />)
|
||||
|
||||
Reference in New Issue
Block a user