added edit card and get number

This commit is contained in:
SlimusMinus
2026-05-29 01:16:45 +03:00
parent 684ce8f56b
commit 5198e284a3
6 changed files with 175 additions and 89 deletions

View File

@@ -4,7 +4,8 @@ import PhoneIcon from "./icons/PhoneIcon";
import MapIcon from "./icons/MapIcon"; import MapIcon from "./icons/MapIcon";
import HeartIcon from "./icons/HeartIcon"; 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 ( return (
<div className="f-card"> <div className="f-card">
<div className="f-card-img"> <div className="f-card-img">
@@ -13,8 +14,8 @@ export default function AdCard({ ad, liked, onLike }) {
: <span className="f-card-emoji">{ad.emoji || "📋"}</span> : <span className="f-card-emoji">{ad.emoji || "📋"}</span>
} }
{ad.category && <div className="f-cat-badge">{ad.category}</div>} {ad.category && <div className="f-cat-badge">{ad.category}</div>}
<div className={`f-type-badge ${ad.type === "loss" ? "f-type-loss" : "f-type-found"}`}> <div className={`f-type-badge ${ad.type === "LOSS" ? "f-type-loss" : "f-type-found"}`}>
{ad.type === "loss" ? "Потеря" : "Находка"} {ad.type === "LOSS" ? "Потеря" : "Находка"}
</div> </div>
</div> </div>
<div className="f-card-body"> <div className="f-card-body">
@@ -37,6 +38,15 @@ export default function AdCard({ ad, liked, onLike }) {
: <span /> : <span />
} }
<span className="f-card-date">{ad.date || ""}</span> <span className="f-card-date">{ad.date || ""}</span>
<div style={{ display: "flex", gap: 6 }}>
{onEdit && (
<button
className="f-like-btn"
onClick={e => { e.stopPropagation(); onEdit(ad); }}
>
✏️
</button>
)}
<button <button
className={`f-like-btn${liked ? " liked" : ""}`} className={`f-like-btn${liked ? " liked" : ""}`}
onClick={e => { e.stopPropagation(); onLike(ad.id); }} onClick={e => { e.stopPropagation(); onLike(ad.id); }}
@@ -47,5 +57,6 @@ export default function AdCard({ ad, liked, onLike }) {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -3,11 +3,25 @@ import PinIcon from "./icons/PinIcon";
import CloseIcon from "./icons/CloseIcon"; import CloseIcon from "./icons/CloseIcon";
import { EMPTY_FORM, CATEGORIES } from "../config/feedConstants"; import { EMPTY_FORM, CATEGORIES } from "../config/feedConstants";
import { useCreatePost } from "../hooks/useCreatePost"; 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 [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) => { const set = useCallback((k, v) => {
setForm(p => ({ ...p, [k]: v })); setForm(p => ({ ...p, [k]: v }));
@@ -26,9 +40,8 @@ export default function PostModal({ onClose, onSuccess }) {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!validate()) return; if (!validate()) return;
const payload = { const payload = {
type: form.type, // "loss" | "found" type: form.type,
title: form.title.trim(), title: form.title.trim(),
description: form.description.trim(), description: form.description.trim(),
category: form.category, category: form.category,
@@ -36,9 +49,12 @@ export default function PostModal({ onClose, onSuccess }) {
phone: form.phone.trim(), phone: form.phone.trim(),
}; };
const newAd = await createAd(payload); const result = isEdit
if (newAd) { ? await updateAd(initialData.id, payload)
onSuccess(newAd); : await createAd(payload);
if (result) {
onSuccess(result);
onClose(); onClose();
} }
}; };
@@ -47,17 +63,25 @@ export default function PostModal({ onClose, onSuccess }) {
<div className="f-modal-bg" onClick={e => e.target === e.currentTarget && onClose()}> <div className="f-modal-bg" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="f-modal"> <div className="f-modal">
<div className="f-modal-head"> <div className="f-modal-head">
<span className="f-modal-title">Новое объявление</span> <span className="f-modal-title">
{isEdit ? "Редактировать объявление" : "Новое объявление"}
</span>
<button className="f-close-btn" onClick={onClose}><CloseIcon /></button> <button className="f-close-btn" onClick={onClose}><CloseIcon /></button>
</div> </div>
<div className="f-m-group"> <div className="f-m-group">
<label className="f-m-label">Тип объявления</label> <label className="f-m-label">Тип объявления</label>
<div className="f-type-toggle"> <div className="f-type-toggle">
<button className={`f-type-opt${form.type === "loss" ? " loss" : ""}`} onClick={() => set("type", "loss")}> <button
className={`f-type-opt${form.type === "loss" ? " loss" : ""}`}
onClick={() => set("type", "loss")}
>
Потеря Потеря
</button> </button>
<button className={`f-type-opt${form.type === "found" ? " found" : ""}`} onClick={() => set("type", "found")}> <button
className={`f-type-opt${form.type === "found" ? " found" : ""}`}
onClick={() => set("type", "found")}
>
Находка Находка
</button> </button>
</div> </div>
@@ -100,7 +124,6 @@ export default function PostModal({ onClose, onSuccess }) {
</select> </select>
{errors.category && <p className="f-err-msg">{errors.category}</p>} {errors.category && <p className="f-err-msg">{errors.category}</p>}
</div> </div>
</div> </div>
<div className="f-m-group"> <div className="f-m-group">
@@ -124,16 +147,12 @@ export default function PostModal({ onClose, onSuccess }) {
{errors.phone && <p className="f-err-msg">{errors.phone}</p>} {errors.phone && <p className="f-err-msg">{errors.phone}</p>}
</div> </div>
{apiError && ( {apiError && <p className="f-err-msg">{apiError}</p>}
<p className="f-err-msg" style={{ textAlign: "center", marginBottom: 10 }}>
{apiError}
</p>
)}
<button className="f-m-submit" onClick={handleSubmit} disabled={loading}> <button className="f-m-submit" onClick={handleSubmit} disabled={loading}>
<div className="f-m-submit-row"> <div className="f-m-submit-row">
{loading && <div className="f-m-spinner" />} {loading && <div className="f-m-spinner" />}
{loading ? "Размещаем..." : "Разместить объявление"} {isEdit ? "Сохранить изменения" : "Разместить объявление"}
</div> </div>
</button> </button>
</div> </div>

View File

@@ -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 };
}

View File

@@ -11,3 +11,10 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}

View File

@@ -5,12 +5,13 @@ import Sidebar from "../components/Sidebar";
import RightSidebar from "../components/RightSidebar"; import RightSidebar from "../components/RightSidebar";
import AdCard from "../components/AdCard"; import AdCard from "../components/AdCard";
import PostModal from "../components/PostModal"; import PostModal from "../components/PostModal";
import MenuIcon from "../components/icons/MenuIcon" import MenuIcon from "../components/icons/MenuIcon";
export default function FeedPage() { export default function FeedPage() {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeNav, setActiveNav] = useState("Главная"); const [activeNav, setActiveNav] = useState("Главная");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editAd, setEditAd] = useState(null);
const [ads, setAds] = useState([]); const [ads, setAds] = useState([]);
const [liked, setLiked] = useState(new Set()); const [liked, setLiked] = useState(new Set());
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -22,11 +23,8 @@ export default function FeedPage() {
setError(""); setError("");
try { try {
const res = await fetch(`${API_URL}/api/v1/posts`); const res = await fetch(`${API_URL}/api/v1/posts`);
if (!res.ok) { if (!res.ok) throw new Error(`Ошибка ${res.status}`);
throw new Error(`Ошибка ${res.status}`);
}
const data = await res.json(); const data = await res.json();
// поддерживаем { items: [] } и просто []
setAds(Array.isArray(data) ? data : (data.items ?? [])); setAds(Array.isArray(data) ? data : (data.items ?? []));
} catch (err) { } catch (err) {
setError(err.message || "Не удалось загрузить объявления"); setError(err.message || "Не удалось загрузить объявления");
@@ -45,11 +43,14 @@ export default function FeedPage() {
}); });
}, []); }, []);
// Новое объявление — добавляем в начало ленты
const handleNewAd = useCallback((ad) => { const handleNewAd = useCallback((ad) => {
setAds(prev => [ad, ...prev]); setAds(prev => [ad, ...prev]);
}, []); }, []);
const handleEditSave = useCallback((updated) => {
setAds(prev => prev.map(a => a.id === updated.id ? updated : a));
}, []);
const renderContent = () => { const renderContent = () => {
if (loading) { if (loading) {
return ( return (
@@ -82,15 +83,18 @@ export default function FeedPage() {
); );
} }
return ads.map(ad => ( return ads.map(ad => (
<AdCard key={ad.id} ad={ad} liked={liked.has(ad.id)} <AdCard
onLike={toggleLike}/> key={ad.id}
ad={ad}
liked={liked.has(ad.id)}
onLike={toggleLike}
onEdit={setEditAd}
/>
)); ));
}; };
return ( return (
<>
<div className="f-app"> <div className="f-app">
<Sidebar <Sidebar
open={sidebarOpen} open={sidebarOpen}
activeNav={activeNav} activeNav={activeNav}
@@ -100,18 +104,12 @@ export default function FeedPage() {
<div className="f-main"> <div className="f-main">
<div className="f-topbar"> <div className="f-topbar">
<button className="f-menu-btn" <button className="f-menu-btn" onClick={() => setSidebarOpen(v => !v)}>
onClick={() => setSidebarOpen(v => !v)}>
<MenuIcon /> <MenuIcon />
</button> </button>
<div className="f-topbar-center"> <div className="f-topbar-center">
<button className="f-post-btn" <button className="f-post-btn" onClick={() => setModalOpen(true)}>
onClick={() => setModalOpen(true)}> <span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
<span style={{
fontSize: "1.15rem",
fontWeight: 900,
lineHeight: 1
}}>+</span>
Разместить объявление Разместить объявление
</button> </button>
</div> </div>
@@ -133,7 +131,13 @@ export default function FeedPage() {
/> />
)} )}
{editAd && (
<PostModal
initialData={editAd}
onClose={() => setEditAd(null)}
onSuccess={handleEditSave}
/>
)}
</div> </div>
</>
); );
} }

View File

@@ -62,8 +62,14 @@
.f-sb-item.active { background: rgba(45,224,200,.1); color: var(--teal); border-left-color: var(--teal); } .f-sb-item.active { background: rgba(45,224,200,.1); color: var(--teal); border-left-color: var(--teal); }
/* Main */ /* 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 */ /* Topbar */
.f-topbar { .f-topbar {
display: flex; align-items: center; gap: 12px; display: flex; align-items: center; gap: 12px;
@@ -169,15 +175,25 @@
transform: translateY(-2px); 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} } @keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
/* Feed wrap */ /* Feed wrap */
.f-wrap { display: flex; flex: 1; overflow: hidden; } .f-wrap {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
/* Feed */ /* Feed */
.f-feed { .f-feed {
flex: 1; overflow-y: auto; flex: 1;
padding: 16px; display: flex; flex-direction: column; gap: 14px; 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 { width: 4px; }
.f-feed::-webkit-scrollbar-track { background: transparent; } .f-feed::-webkit-scrollbar-track { background: transparent; }
@@ -202,6 +218,7 @@
/* Ad Card */ /* Ad Card */
.f-card { .f-card {
flex-shrink: 0;
background: linear-gradient(155deg, #1d2436, #131720); background: linear-gradient(155deg, #1d2436, #131720);
border: 1px solid var(--border); border-radius: 20px; overflow: hidden; border: 1px solid var(--border); border-radius: 20px; overflow: hidden;
transition: border-color .2s, transform .2s; cursor: pointer; transition: border-color .2s, transform .2s; cursor: pointer;