Merge pull request #8 from SlimusMinus/add-edit-card
added edit card and get number
This commit is contained in:
@@ -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 (
|
||||
<div className="f-card">
|
||||
<div className="f-card-img">
|
||||
@@ -13,8 +14,8 @@ export default function AdCard({ ad, liked, onLike }) {
|
||||
: <span className="f-card-emoji">{ad.emoji || "📋"}</span>
|
||||
}
|
||||
{ad.category && <div className="f-cat-badge">{ad.category}</div>}
|
||||
<div className={`f-type-badge ${ad.type === "loss" ? "f-type-loss" : "f-type-found"}`}>
|
||||
{ad.type === "loss" ? "Потеря" : "Находка"}
|
||||
<div className={`f-type-badge ${ad.type === "LOSS" ? "f-type-loss" : "f-type-found"}`}>
|
||||
{ad.type === "LOSS" ? "Потеря" : "Находка"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="f-card-body">
|
||||
@@ -37,6 +38,15 @@ export default function AdCard({ ad, liked, onLike }) {
|
||||
: <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
|
||||
className={`f-like-btn${liked ? " liked" : ""}`}
|
||||
onClick={e => { e.stopPropagation(); onLike(ad.id); }}
|
||||
@@ -47,5 +57,6 @@ export default function AdCard({ ad, liked, onLike }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,25 @@ 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 }) {
|
||||
<div className="f-modal-bg" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
<div className="f-modal">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="f-m-group">
|
||||
<label className="f-m-label">Тип объявления</label>
|
||||
<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 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>
|
||||
</div>
|
||||
@@ -100,7 +124,6 @@ export default function PostModal({ onClose, onSuccess }) {
|
||||
</select>
|
||||
{errors.category && <p className="f-err-msg">{errors.category}</p>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<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>}
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
<p className="f-err-msg" style={{ textAlign: "center", marginBottom: 10 }}>
|
||||
{apiError}
|
||||
</p>
|
||||
)}
|
||||
{apiError && <p className="f-err-msg">{apiError}</p>}
|
||||
|
||||
<button className="f-m-submit" onClick={handleSubmit} disabled={loading}>
|
||||
<div className="f-m-submit-row">
|
||||
{loading && <div className="f-m-spinner" />}
|
||||
{loading ? "Размещаем..." : "Разместить объявление"}
|
||||
{isEdit ? "Сохранить изменения" : "Разместить объявление"}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
28
src/hooks/useUpdatePost.js
Normal file
28
src/hooks/useUpdatePost.js
Normal 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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -5,12 +5,13 @@ 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,11 +43,14 @@ 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 (
|
||||
@@ -82,15 +83,18 @@ export default function FeedPage() {
|
||||
);
|
||||
}
|
||||
return ads.map(ad => (
|
||||
<AdCard key={ad.id} ad={ad} liked={liked.has(ad.id)}
|
||||
onLike={toggleLike}/>
|
||||
<AdCard
|
||||
key={ad.id}
|
||||
ad={ad}
|
||||
liked={liked.has(ad.id)}
|
||||
onLike={toggleLike}
|
||||
onEdit={setEditAd}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="f-app">
|
||||
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
activeNav={activeNav}
|
||||
@@ -100,18 +104,12 @@ export default function FeedPage() {
|
||||
|
||||
<div className="f-main">
|
||||
<div className="f-topbar">
|
||||
<button className="f-menu-btn"
|
||||
onClick={() => setSidebarOpen(v => !v)}>
|
||||
<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 className="f-post-btn" onClick={() => setModalOpen(true)}>
|
||||
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
|
||||
Разместить объявление
|
||||
</button>
|
||||
</div>
|
||||
@@ -133,7 +131,13 @@ export default function FeedPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{editAd && (
|
||||
<PostModal
|
||||
initialData={editAd}
|
||||
onClose={() => setEditAd(null)}
|
||||
onSuccess={handleEditSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user