diff --git a/src/components/AdCard.jsx b/src/components/AdCard.jsx index 94f1577..a59634c 100644 --- a/src/components/AdCard.jsx +++ b/src/components/AdCard.jsx @@ -4,7 +4,8 @@ import PhoneIcon from "./icons/PhoneIcon"; import MapIcon from "./icons/MapIcon"; import HeartIcon from "./icons/HeartIcon"; -export default function AdCard({ ad, liked, onLike }) { +export default function AdCard({ ad, liked, onLike, onEdit }) { + console.log("ad object:", ad); return (
@@ -13,8 +14,8 @@ export default function AdCard({ ad, liked, onLike }) { : {ad.emoji || "📋"} } {ad.category &&
{ad.category}
} -
- {ad.type === "loss" ? "Потеря" : "Находка"} +
+ {ad.type === "LOSS" ? "Потеря" : "Находка"}
@@ -37,13 +38,23 @@ export default function AdCard({ ad, liked, onLike }) { : } {ad.date || ""} - +
+ {onEdit && ( + + )} + +
diff --git a/src/components/PostModal.jsx b/src/components/PostModal.jsx index d37b895..5dfef07 100644 --- a/src/components/PostModal.jsx +++ b/src/components/PostModal.jsx @@ -1,13 +1,27 @@ -import {useCallback, useState} from "react"; +import { useCallback, useState } from "react"; import PinIcon from "./icons/PinIcon"; import CloseIcon from "./icons/CloseIcon"; import { EMPTY_FORM, CATEGORIES } from "../config/feedConstants"; import { useCreatePost } from "../hooks/useCreatePost"; +import { useUpdatePost } from "../hooks/useUpdatePost"; + +export default function PostModal({ onClose, onSuccess, initialData }) { + const isEdit = !!initialData; + + const [form, setForm] = useState(isEdit ? { + type: initialData.type || "loss", + title: initialData.title || "", + description: initialData.description || "", + category: initialData.category || "", + date: initialData.date || "", + phone: initialData.phone || "", + } : EMPTY_FORM); -export default function PostModal({ onClose, onSuccess }) { - const [form, setForm] = useState(EMPTY_FORM); const [errors, setErrors] = useState({}); - const { createAd, loading, error: apiError } = useCreatePost(); + const { createAd, loading: creating, error: createError } = useCreatePost(); + const { updateAd, loading: updating, error: updateError } = useUpdatePost(); + const loading = creating || updating; + const apiError = createError || updateError; const set = useCallback((k, v) => { setForm(p => ({ ...p, [k]: v })); @@ -26,9 +40,8 @@ export default function PostModal({ onClose, onSuccess }) { const handleSubmit = async () => { if (!validate()) return; - const payload = { - type: form.type, // "loss" | "found" + type: form.type, title: form.title.trim(), description: form.description.trim(), category: form.category, @@ -36,9 +49,12 @@ export default function PostModal({ onClose, onSuccess }) { phone: form.phone.trim(), }; - const newAd = await createAd(payload); - if (newAd) { - onSuccess(newAd); + const result = isEdit + ? await updateAd(initialData.id, payload) + : await createAd(payload); + + if (result) { + onSuccess(result); onClose(); } }; @@ -47,17 +63,25 @@ export default function PostModal({ onClose, onSuccess }) {
e.target === e.currentTarget && onClose()}>
- Новое объявление + + {isEdit ? "Редактировать объявление" : "Новое объявление"} +
- -
@@ -100,7 +124,6 @@ export default function PostModal({ onClose, onSuccess }) { {errors.category &&

{errors.category}

}
-
@@ -124,16 +147,12 @@ export default function PostModal({ onClose, onSuccess }) { {errors.phone &&

{errors.phone}

}
- {apiError && ( -

- {apiError} -

- )} + {apiError &&

{apiError}

}
diff --git a/src/hooks/useUpdatePost.js b/src/hooks/useUpdatePost.js new file mode 100644 index 0000000..fa4f57d --- /dev/null +++ b/src/hooks/useUpdatePost.js @@ -0,0 +1,28 @@ +import { useState } from "react"; +import { API_URL } from "../config/constants"; + +export function useUpdatePost() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const updateAd = async (id, payload) => { + setLoading(true); + setError(""); + try { + const res = await fetch(`${API_URL}/api/v1/posts/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) throw new Error(`Ошибка ${res.status}`); + return await res.json(); + } catch (err) { + setError(err.message || "Не удалось сохранить"); + return null; + } finally { + setLoading(false); + } + }; + + return { updateAd, loading, error }; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index ec2585e..e8af213 100644 --- a/src/index.css +++ b/src/index.css @@ -11,3 +11,10 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +html, body, #root { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/src/pages/FeedPage.jsx b/src/pages/FeedPage.jsx index 434581d..9402f5a 100644 --- a/src/pages/FeedPage.jsx +++ b/src/pages/FeedPage.jsx @@ -1,16 +1,17 @@ -import {useState, useEffect, useCallback} from "react"; +import { useState, useEffect, useCallback } from "react"; import "../styles/feed.css"; -import {API_URL} from "../config/constants"; +import { API_URL } from "../config/constants"; import Sidebar from "../components/Sidebar"; import RightSidebar from "../components/RightSidebar"; import AdCard from "../components/AdCard"; import PostModal from "../components/PostModal"; -import MenuIcon from "../components/icons/MenuIcon" +import MenuIcon from "../components/icons/MenuIcon"; export default function FeedPage() { const [sidebarOpen, setSidebarOpen] = useState(false); const [activeNav, setActiveNav] = useState("Главная"); const [modalOpen, setModalOpen] = useState(false); + const [editAd, setEditAd] = useState(null); const [ads, setAds] = useState([]); const [liked, setLiked] = useState(new Set()); const [loading, setLoading] = useState(true); @@ -22,11 +23,8 @@ export default function FeedPage() { setError(""); try { const res = await fetch(`${API_URL}/api/v1/posts`); - if (!res.ok) { - throw new Error(`Ошибка ${res.status}`); - } + if (!res.ok) throw new Error(`Ошибка ${res.status}`); const data = await res.json(); - // поддерживаем { items: [] } и просто [] setAds(Array.isArray(data) ? data : (data.items ?? [])); } catch (err) { setError(err.message || "Не удалось загрузить объявления"); @@ -45,17 +43,20 @@ export default function FeedPage() { }); }, []); - // Новое объявление — добавляем в начало ленты const handleNewAd = useCallback((ad) => { setAds(prev => [ad, ...prev]); }, []); + const handleEditSave = useCallback((updated) => { + setAds(prev => prev.map(a => a.id === updated.id ? updated : a)); + }, []); + const renderContent = () => { if (loading) { return (
-
+
Загружаем объявления...
@@ -76,64 +77,67 @@ export default function FeedPage() {
🔍
Объявлений пока нет
- Будьте первым — разместите объявление
о потере или находке + Будьте первым — разместите объявление
о потере или находке
); } return ads.map(ad => ( - + )); }; return ( - <> -
+
+ setSidebarOpen(false)} + onNavClick={setActiveNav} + /> - setSidebarOpen(false)} - onNavClick={setActiveNav} - /> - -
-
- +
+ -
- -
-
-
- -
-
- {renderContent()} -
-
+
- {modalOpen && ( - setModalOpen(false)} - onSuccess={handleNewAd} - /> - )} - +
+
+ {renderContent()} +
+ +
- + + {modalOpen && ( + setModalOpen(false)} + onSuccess={handleNewAd} + /> + )} + + {editAd && ( + setEditAd(null)} + onSuccess={handleEditSave} + /> + )} +
); -} +} \ No newline at end of file diff --git a/src/styles/feed.css b/src/styles/feed.css index ffc79df..e07506f 100644 --- a/src/styles/feed.css +++ b/src/styles/feed.css @@ -62,8 +62,14 @@ .f-sb-item.active { background: rgba(45,224,200,.1); color: var(--teal); border-left-color: var(--teal); } /* Main */ -.f-main { flex: 1; display: flex; flex-direction: column; min-width: 0; } - +.f-main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + height: 100vh; + overflow: hidden; +} /* Topbar */ .f-topbar { display: flex; align-items: center; gap: 12px; @@ -169,15 +175,25 @@ transform: translateY(-2px); } +@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} } @keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} } /* Feed wrap */ -.f-wrap { display: flex; flex: 1; overflow: hidden; } - +.f-wrap { + display: flex; + flex: 1; + overflow: hidden; + min-height: 0; +} /* Feed */ .f-feed { - flex: 1; overflow-y: auto; - padding: 16px; display: flex; flex-direction: column; gap: 14px; + flex: 1; + overflow-y: scroll; + min-height: 0; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; } .f-feed::-webkit-scrollbar { width: 4px; } .f-feed::-webkit-scrollbar-track { background: transparent; } @@ -202,6 +218,7 @@ /* Ad Card */ .f-card { + flex-shrink: 0; background: linear-gradient(155deg, #1d2436, #131720); border: 1px solid var(--border); border-radius: 20px; overflow: hidden; transition: border-color .2s, transform .2s; cursor: pointer;