added english language

This commit is contained in:
SlimusMinus
2026-07-09 00:09:45 +03:00
parent 3b503623f6
commit fffcdaa526
33 changed files with 454 additions and 687 deletions

View File

@@ -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 && (