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}
}
{loading &&
}
- {loading ? "Размещаем..." : "Разместить объявление"}
+ {isEdit ? "Сохранить изменения" : "Разместить объявление"}
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}
- />
-
-
-
-
setSidebarOpen(v => !v)}>
-
+
+
+
setSidebarOpen(v => !v)}>
+
+
+
+
setModalOpen(true)}>
+ +
+ Разместить объявление
-
- setModalOpen(true)}>
- +
- Разместить объявление
-
-
-
-
-
-
-
- {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;