global refactoring

This commit is contained in:
SlimusMinus
2026-06-02 01:10:56 +03:00
parent b53207166b
commit fbdcada3d2
26 changed files with 738 additions and 559 deletions

172
src/components/MainFeed.jsx Normal file
View File

@@ -0,0 +1,172 @@
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("Главная");
// Открыта ли модалка создания нового объявления
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: [] }
setAds(Array.isArray(data) ? data : (data.items ?? []));
} 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 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 (!ads.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 ads.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-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>
);
}