diff --git a/src/components/AdCard.jsx b/src/components/AdCard.jsx index a59634c..0924c91 100644 --- a/src/components/AdCard.jsx +++ b/src/components/AdCard.jsx @@ -3,59 +3,79 @@ import CalendarIcon from "./icons/CalendarIcon"; import PhoneIcon from "./icons/PhoneIcon"; import MapIcon from "./icons/MapIcon"; import HeartIcon from "./icons/HeartIcon"; +import { API_URL } from "../config/constants"; export default function AdCard({ ad, liked, onLike, onEdit }) { - console.log("ad object:", ad); + const imageUrl = ad.images?.[0] ? `${API_URL}${ad.images[0]}` : null; + return (
-
- {ad.image - ? {ad.title} - : {ad.emoji || "📋"} - } - {ad.category &&
{ad.category}
} -
- {ad.type === "LOSS" ? "Потеря" : "Находка"} -
-
-
-
{ad.title}
-
{ad.description}
-
- {ad.location &&
{ad.location}
} - {ad.date &&
{ad.date}
} -
- {ad.location && ( - - )} -
- {ad.phone - ? e.stopPropagation()}> - {ad.phone} - - : +
+ + {/* Левая колонка — картинка */} +
+ {imageUrl + ? {ad.title} + : {ad.emoji || "📋"} } - {ad.date || ""} -
- {onEdit && ( - - )} - +
+ + {/* Правая колонка — контент */} +
+
+ {ad.category &&
{ad.category}
} +
+ {ad.type === "loss" ? "Потеря" : "Находка"} +
+
+
{ad.title}
+
{ad.description}
+
+ {ad.location &&
+ {ad.location}
} + {ad.date &&
{ad.date} +
} +
+ {ad.location && ( + + )} +
+
+ {ad.phone && ( + e.stopPropagation()}> + {ad.phone} + + )} +
+
+ {onEdit && ( + + )} + +
+
); diff --git a/src/components/PostModal.jsx b/src/components/PostModal.jsx index 5dfef07..7be2703 100644 --- a/src/components/PostModal.jsx +++ b/src/components/PostModal.jsx @@ -1,9 +1,46 @@ -import { useCallback, useState } from "react"; +import { useCallback, useState, useRef } 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"; +import { API_URL } from "../config/constants"; + +const MAX_SIZE_MB = 5; +const ACCEPT = "image/jpeg,image/png,image/webp"; + +function compressImage(file, maxW = 1200, quality = 0.82) { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { + URL.revokeObjectURL(url); + const scale = Math.min(1, maxW / img.width); + const canvas = document.createElement("canvas"); + canvas.width = Math.round(img.width * scale); + canvas.height = Math.round(img.height * scale); + canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height); + canvas.toBlob( + blob => blob ? resolve(new File([blob], file.name, { type: "image/jpeg" })) : reject(new Error("Blob error")), + "image/jpeg", + quality + ); + }; + img.onerror = reject; + img.src = url; + }); +} + +const formatPhone = (val) => { + const digits = val.replace(/\D/g, ""); + const local = digits.startsWith("7") ? digits.slice(1) : digits; + let result = "+7"; + if (local.length > 0) result += " (" + local.slice(0, 3); + if (local.length >= 3) result += ") " + local.slice(3, 6); + if (local.length >= 6) result += "-" + local.slice(6, 8); + if (local.length >= 8) result += "-" + local.slice(8, 10); + return result; +}; export default function PostModal({ onClose, onSuccess, initialData }) { const isEdit = !!initialData; @@ -17,10 +54,21 @@ export default function PostModal({ onClose, onSuccess, initialData }) { phone: initialData.phone || "", } : EMPTY_FORM); - const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState({}); + const [imageFile, setImageFile] = useState(null); + // при редактировании показываем существующую картинку + const existingImage = isEdit && initialData.images?.[0] + ? `${API_URL}${initialData.images[0]}` + : null; + const [preview, setPreview] = useState(null); + const [imgRemoved, setImgRemoved] = useState(false); // пользователь удалил старую + const [imgError, setImgError] = useState(""); + const [compressing, setCompressing] = useState(false); + const fileInputRef = useRef(null); + const { createAd, loading: creating, error: createError } = useCreatePost(); const { updateAd, loading: updating, error: updateError } = useUpdatePost(); - const loading = creating || updating; + const loading = creating || updating || compressing; const apiError = createError || updateError; const set = useCallback((k, v) => { @@ -28,6 +76,43 @@ export default function PostModal({ onClose, onSuccess, initialData }) { setErrors(p => ({ ...p, [k]: "" })); }, []); + const handleFileChange = async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + setImgError(""); + if (!file.type.startsWith("image/")) { + setImgError("Только изображения (JPEG, PNG, WebP)"); + return; + } + if (file.size > MAX_SIZE_MB * 1024 * 1024) { + setImgError(`Файл слишком большой — максимум ${MAX_SIZE_MB} МБ`); + return; + } + setCompressing(true); + try { + const compressed = await compressImage(file); + setImageFile(compressed); + setPreview(URL.createObjectURL(compressed)); + setImgRemoved(false); + } catch { + setImgError("Не удалось обработать изображение"); + } finally { + setCompressing(false); + } + }; + + const removeImage = () => { + setImageFile(null); + if (preview) URL.revokeObjectURL(preview); + setPreview(null); + setImgRemoved(true); + setImgError(""); + if (fileInputRef.current) fileInputRef.current.value = ""; + }; + + // что показывать в блоке превью + const displayImage = preview || (!imgRemoved ? existingImage : null); + const validate = () => { const e = {}; if (!form.title.trim()) e.title = "Введите название"; @@ -40,18 +125,19 @@ export default function PostModal({ onClose, onSuccess, initialData }) { const handleSubmit = async () => { if (!validate()) return; - const payload = { - type: form.type, - title: form.title.trim(), - description: form.description.trim(), - category: form.category, - date: form.date || null, - phone: form.phone.trim(), - }; + + const data = new FormData(); + data.append("type", form.type); + data.append("title", form.title.trim()); + data.append("description", form.description.trim()); + data.append("category", form.category); + data.append("phone", form.phone.trim()); + if (form.date) data.append("date", form.date); + if (imageFile) data.append("image", imageFile); const result = isEdit - ? await updateAd(initialData.id, payload) - : await createAd(payload); + ? await updateAd(initialData.id, data) + : await createAd(data); if (result) { onSuccess(result); @@ -69,24 +155,22 @@ export default function PostModal({ onClose, onSuccess, initialData }) {
+ {/* тип */}
+ >Потеря + >Находка
+ {/* название */}
{errors.title}

}
+ {/* описание */}