Files
nahodka-front/src/components/MainFeed.jsx
2026-06-11 01:00:19 +03:00

220 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from "react";
import "../styles/index.css";
import { API_URL } from "../config/authConstants";
import Sidebar from "./Sidebar";
import RightSidebar from "./RightSidebar";
import PostOnFeed from "./PostOnFeed";
import CreateUpdatePost from "./CreateUpdatePost";
import MenuIcon from "./icons/MenuIcon";
/**
* MainFeed — корневой компонент приложения.
* Управляет состоянием ленты, модалок и сайдбара;
* остальные компоненты получают данные через пропсы.
*/
export default function MainFeed() {
// Открыт ли левый сайдбар (мобильный слайд-аут)
const [sidebarOpen, setSidebarOpen] = useState(false);
// Активный пункт навигации в сайдбаре
const [activeNav, setActiveNav] = useState("Главная");
// Фильтр по типу объявления: null (все) | "loss" | "found"
const [typeFilter, setTypeFilter] = useState(null);
// Открыта ли модалка создания нового объявления
const [modalOpen, setModalOpen] = useState(false);
// Объявление, которое сейчас редактируется (null — модалка закрыта)
const [editAd, setEditAd] = useState(null);
// Массив объявлений из API
const [ads, setAds] = useState([]);
// Set id объявлений, которые пользователь лайкнул (только локально)
const [liked, setLiked] = useState(new Set());
// Состояния загрузки и ошибки для ленты
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
/** Загружает ленту объявлений с бэкенда. */
const loadFeed = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await fetch(`${API_URL}/api/v1/posts`);
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
const data = await res.json();
// API может вернуть массив напрямую или объект { items: [] }
const ads = Array.isArray(data) ? data : (data.items ?? []);
// Нормализуем type: enum LOSS/FOUND → "loss"/"found"
const normalizedAds = ads.map(ad => ({
...ad,
type: ad.type ? ad.type.toLowerCase() : "loss"
}));
setAds(normalizedAds);
} catch (err) {
setError(err.message || "Не удалось загрузить объявления");
} finally {
setLoading(false);
}
}, []);
// Первичная загрузка при монтировании компонента
useEffect(() => { loadFeed(); }, [loadFeed]);
/**
* Переключает лайк на объявлении.
* Состояние хранится только на клиенте — без запроса к API.
*/
const toggleLike = useCallback((id) => {
setLiked(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}, []);
// После создания или редактирования объявления перезагружаем ленту,
// чтобы получить актуальные данные (включая изображения) из БД
const handleNewAd = useCallback(() => loadFeed(), [loadFeed]);
const handleEditSave = useCallback(() => loadFeed(), [loadFeed]);
// Фильтруем объявления в зависимости от выбранной вкладки и фильтра типа
const filteredAds = (() => {
let result = activeNav === "Главная"
? ads
: ads.filter(ad =>
activeNav === "Вещи"
? ["Вещь", "Другое"].includes(ad.category)
: activeNav === "Питомцы"
? ad.category === "Питомец"
: false
);
// Применяем фильтр по типу объявления
if (typeFilter) {
result = result.filter(ad => ad.type === typeFilter);
}
return result;
})();
/**
* Рендерит содержимое ленты в зависимости от текущего состояния:
* — спиннер во время загрузки
* — сообщение об ошибке
* — заглушка при пустой ленте
* — список карточек объявлений
*/
const renderContent = () => {
if (loading) {
return (
<div className="f-center">
<div className="f-spinner-wrap">
<div className="f-spinner" />
Загружаем объявления...
</div>
</div>
);
}
if (error) {
return (
<div className="f-center">
<div className="f-center-icon">⚠️</div>
<div className="f-center-title">Не удалось загрузить ленту</div>
<div className="f-center-sub">{error}</div>
</div>
);
}
if (!filteredAds.length) {
return (
<div className="f-center">
<div className="f-center-icon">🔍</div>
<div className="f-center-title">Объявлений пока нет</div>
<div className="f-center-sub">
Будьте первым — разместите объявление<br />о потере или находке
</div>
</div>
);
}
return filteredAds.map(ad => (
<PostOnFeed
key={ad.id}
ad={ad}
liked={liked.has(ad.id)} // передаём локальное состояние лайка
onLike={toggleLike} // колбэк для переключения лайка
onEdit={setEditAd} // колбэк открывает модалку редактирования
/>
));
};
return (
<div className="f-app">
{/* Левый сайдбар с навигацией */}
<Sidebar
open={sidebarOpen}
activeNav={activeNav}
onClose={() => setSidebarOpen(false)}
onNavClick={setActiveNav}
/>
<div className="f-main">
{/* Верхняя панель: бургер-меню + кнопка создания объявления */}
<div className="f-topbar">
<button className="f-menu-btn" onClick={() => setSidebarOpen(v => !v)}>
<MenuIcon />
</button>
<div className="f-topbar-center">
<button className="f-post-btn" onClick={() => setModalOpen(true)}>
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
Разместить объявление
</button>
</div>
{/* Пустой спейсер симметрично уравновешивает бургер слева */}
<div className="f-topbar-spacer" />
</div>
{/* Фильтр по типу объявления */}
<div className="f-filter-bar">
<button
className={`f-filter-btn${typeFilter === null ? " active" : ""}`}
onClick={() => setTypeFilter(null)}
>
Все объявления
</button>
<button
className={`f-filter-btn${typeFilter === "loss" ? " active" : ""}`}
onClick={() => setTypeFilter("loss")}
>
🔴 Потери
</button>
<button
className={`f-filter-btn${typeFilter === "found" ? " active" : ""}`}
onClick={() => setTypeFilter("found")}
>
🟢 Находки
</button>
</div>
{/* Основная область: лента + правый сайдбар */}
<div className="f-wrap">
<div className="f-feed">
{renderContent()}
</div>
{/* Правый сайдбар: промо-блоки / фильтры */}
<RightSidebar />
</div>
</div>
{/* Модалка создания нового объявления */}
{modalOpen && (
<CreateUpdatePost
onClose={() => setModalOpen(false)}
onSuccess={handleNewAd}
/>
)}
{/* Модалка редактирования — открывается когда editAd !== null */}
{editAd && (
<CreateUpdatePost
initialData={editAd}
onClose={() => setEditAd(null)}
onSuccess={handleEditSave}
/>
)}
</div>
);
}