added many images in post

This commit is contained in:
SlimusMinus
2026-07-01 00:23:27 +03:00
parent e9183fd165
commit 139621be0a
5 changed files with 256 additions and 112 deletions

View File

@@ -1,5 +1,4 @@
import {useCallback, useEffect, useState} from "react";
import PinIcon from "./icons/PinIcon";
import CloseIcon from "./icons/CloseIcon";
import { EMPTY_FORM, CATEGORIES } from "../config/feedConstants";
import { usePostMutation } from "../hooks/usePostMutation";
@@ -7,7 +6,6 @@ import { useImageUpload, ACCEPT } from "../hooks/useImageUpload";
import { validatePostForm } from "../utils/validatePostForm";
import { formatPhone } from "../utils/formatPhone";
import { useYandexMap } from "../hooks/useYandexMap";
import { API_URL } from "../config/authConstants";
const MAX_SIZE_MB = 5;
@@ -40,21 +38,16 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) {
// Ошибки валидации — ключ совпадает с именем поля
const [errors, setErrors] = useState({});
// URL существующей картинки с сервера (только при редактировании)
const existingImageUrl = isEdit && initialData.images?.[0]
? `${API_URL}${initialData.images[0]}`
: null;
// Вся логика работы с фото вынесена в хук
const {
imageFile,
displayImage,
images: uploadedImages,
imgError,
compressing,
fileInputRef,
handleFileChange,
removeImage,
} = useImageUpload(existingImageUrl);
getFilesForUpload,
getExistingUrlsToKeep,
} = useImageUpload(isEdit ? initialData.images : []);
// Единый хук для создания и обновления
const { save, loading: saving, error: apiError } = usePostMutation();
@@ -106,7 +99,15 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) {
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 files = getFilesForUpload();
files.forEach(f => data.append("images", f));
if (isEdit) {
const existing = getExistingUrlsToKeep();
if (existing.length > 0) {
data.append("existingImages", JSON.stringify(existing));
}
}
if (coords) {
data.append("latitude", coords.latitude);
@@ -189,41 +190,50 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) {
className="f-err-msg">{errors.description}</p>}
</div>
{/* Фотография: превью + удаление или загрузчик */}
{/* Фотографии: сетка превью + кнопка добавления */}
<div className="f-m-group">
<label className="f-m-label">Фотография</label>
{displayImage ? (
<div className="f-img-preview-wrap">
<img src={displayImage} alt="preview"
className="f-img-preview"/>
<button
className="f-img-remove"
onClick={removeImage}
type="button"
aria-label="Удалить фото"
>
<CloseIcon/>
</button>
<label className="f-m-label">Фотографии</label>
{uploadedImages.length > 0 && (
<div className="f-img-grid">
{uploadedImages.map(img => (
<div key={img.id} className="f-img-grid-item">
<img src={img.preview} alt="" className="f-img-grid-preview"/>
<button
className="f-img-remove"
onClick={() => removeImage(img.id)}
type="button"
aria-label="Удалить фото"
>
<CloseIcon/>
</button>
</div>
))}
</div>
) : (
<label className="f-img-upload-label"
aria-disabled={compressing}>
<input
ref={fileInputRef}
type="file"
accept={ACCEPT}
// На мобильных открываем камеру напрямую
capture={/Mobi|Android|iPhone/i.test(navigator.userAgent)
? "environment" : undefined}
onChange={handleFileChange}
className="f-img-input"
disabled={compressing}
/>
{compressing ? <span>Обработка...</span> : <span>📷 Добавить фото</span>}
</label>
)}
<label className="f-img-upload-label"
aria-disabled={compressing}>
<input
ref={fileInputRef}
type="file"
accept={ACCEPT}
multiple
capture={/Mobi|Android|iPhone/i.test(navigator.userAgent)
? "environment" : undefined}
onChange={handleFileChange}
className="f-img-input"
disabled={compressing}
/>
{compressing ? (
<span>Обработка...</span>
) : (
<span>{uploadedImages.length > 0 ? "➕ Добавить ещё" : "📷 Добавить фото"}</span>
)}
</label>
{imgError && <p className="f-err-msg">{imgError}</p>}
<p className="f-hint">JPEG / PNG / WebP · до {MAX_SIZE_MB} МБ</p>
<p className="f-hint">JPEG / PNG / WebP · до {MAX_SIZE_MB} МБ · можно несколько</p>
</div>
{/* Категория */}

View File

@@ -6,6 +6,7 @@ import Sidebar from "./Sidebar";
import RightSidebar from "./RightSidebar";
import PostOnFeed from "./PostOnFeed";
import CreateUpdatePost from "./CreateUpdatePost";
import ImageViewer from "./ImageViewer";
import MenuIcon from "./icons/MenuIcon";
import ProfileAvatar from "./profile/ProfileAvatar";
import { ProfileModal } from "./profile";
@@ -44,6 +45,8 @@ export default function MainFeed() {
const [authModalOpen, setAuthModalOpen] = useState(false);
// Открыто ли модальное окно правил площадки
const [rulesModalOpen, setRulesModalOpen] = useState(false);
// Просмотр изображений: null — закрыто, { images, index } — открыто
const [viewerData, setViewerData] = useState(null);
// Массив объявлений из API (основная лента)
const [ads, setAds] = useState([]);
// Объявления текущего пользователя (загружаются отдельно по userId)
@@ -193,6 +196,12 @@ export default function MainFeed() {
const handleNewAd = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
const handleEditSave = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
const handleImageClick = useCallback((images, index) => {
if (images?.length) {
setViewerData({ images, index });
}
}, []);
/**
* Обработчик нажатия «Разместить объявление».
* Если пользователь не авторизован — открывает модалку входа.
@@ -301,6 +310,7 @@ export default function MainFeed() {
onDelete={activeNav === "Мои объявления" ? handleDelete : undefined}
onMarkFound={activeNav === "Мои объявления" ? handleMarkFound : undefined}
isOwner={activeNav === "Мои объявления"}
onImageClick={handleImageClick}
/>
));
};
@@ -411,6 +421,15 @@ export default function MainFeed() {
{rulesModalOpen && (
<RulesModal onClose={() => setRulesModalOpen(false)} />
)}
{/* Просмотр изображений */}
{viewerData && (
<ImageViewer
images={viewerData.images}
initialIndex={viewerData.index}
onClose={() => setViewerData(null)}
/>
)}
</div>
);
}

View File

@@ -9,7 +9,7 @@ import { useChats } from "../hooks/useChats";
import { useAuthContext } from "../context/AuthContext";
import { API_URL } from "../config/authConstants";
export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMarkFound, isOwner }) {
export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMarkFound, isOwner, onImageClick }) {
const navigate = useNavigate();
const { createOrGetChat } = useChats();
const { isAuthenticated } = useAuthContext();
@@ -55,7 +55,7 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMark
return (
<div className={`f-card${isClosed ? ' f-card-closed' : ''}${roleClass ? ' ' + roleClass : ''}`}>
<div className="f-card-inner">
<div className="f-card-img">
<div className="f-card-img" onClick={e => { e.stopPropagation(); if (onImageClick) onImageClick(ad.images, 0); }}>
{imageUrl
? <img src={imageUrl} alt={ad.title} />
: <span className="f-card-emoji">{ad.emoji || "📋"}</span>