global refactoring

This commit is contained in:
SlimusMinus
2026-06-02 01:10:56 +03:00
parent b53207166b
commit fbdcada3d2
26 changed files with 738 additions and 559 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { API_URL } from "../config/constants";
import { API_URL } from "../config/authConstants";
export function useAuth() {
const navigate = useNavigate();

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { API_URL } from "../config/constants";
import { API_URL } from "../config/authConstants";
export function useCreatePost() {
const [loading, setLoading] = useState(false);

View File

@@ -0,0 +1,87 @@
import { useState, useRef } from "react";
import { compressImage } from "../utils/compressImage";
const MAX_SIZE_MB = 5;
export const ACCEPT = "image/jpeg,image/png,image/webp";
/**
* Управляет выбором, сжатием и удалением фотографии в форме объявления.
*
* @param {string|null} existingUrl — URL существующей картинки с сервера
* (передаётся при редактировании)
* @returns {{
* imageFile: File|null,
* displayImage: string|null,
* imgError: string,
* compressing: boolean,
* fileInputRef: React.RefObject,
* handleFileChange: function,
* removeImage: function,
* }}
*/
export function useImageUpload(existingUrl = null) {
// Сжатый файл, готовый к отправке в FormData
const [imageFile, setImageFile] = useState(null);
// ObjectURL для предпросмотра нового файла
const [preview, setPreview] = useState(null);
// true — пользователь явно удалил фото, existingUrl больше не показываем
const [removed, setRemoved] = useState(false);
const [imgError, setImgError] = useState("");
// true пока идёт сжатие — нужен чтобы заблокировать сабмит
const [compressing, setCompressing] = useState(false);
// ref на <input type="file"> — нужен для сброса значения после removeImage
const fileInputRef = useRef(null);
// Что показывать в блоке превью:
// новый preview имеет приоритет, иначе existingUrl (если не удалён)
const displayImage = preview || (!removed ? existingUrl : null);
/** Проверяет файл, сжимает его и создаёт превью. */
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));
setRemoved(false);
} catch {
setImgError("Не удалось обработать изображение");
} finally {
setCompressing(false);
}
};
/** Удаляет фото, освобождает ObjectURL и сбрасывает input. */
const removeImage = () => {
setImageFile(null);
if (preview) URL.revokeObjectURL(preview); // предотвращаем утечку памяти
setPreview(null);
setRemoved(true);
setImgError("");
// Сброс нужен, иначе повторный выбор того же файла не вызовет onChange
if (fileInputRef.current) fileInputRef.current.value = "";
};
return {
imageFile,
displayImage,
imgError,
compressing,
fileInputRef,
handleFileChange,
removeImage,
};
}

View File

@@ -0,0 +1,45 @@
import { useState } from "react";
import { API_URL } from "../config/authConstants";
/**
* Универсальная мутация для создания и обновления объявления.
* Заменяет отдельные хуки useCreatePost и useUpdatePost.
*
* Логика выбора метода:
* id передан → PUT /api/v1/posts/:id (обновление)
* id не передан → POST /api/v1/posts (создание)
*
* @returns {{
* save: (id: string|null, data: FormData) => Promise<object|null>,
* loading: boolean,
* error: string,
* }}
*/
export function usePostMutation() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
/**
* @param {string|null} id — id объявления при обновлении, null при создании
* @param {FormData} data — данные формы
* @returns {object|null} — ответ сервера или null при ошибке
*/
const save = async (id, data) => {
setLoading(true);
setError("");
try {
const url = id ? `${API_URL}/api/v1/posts/${id}` : `${API_URL}/api/v1/posts`;
const method = id ? "PUT" : "POST";
const res = await fetch(url, { method, body: data });
if (!res.ok) throw new Error(`Ошибка сервера: ${res.status}`);
return await res.json();
} catch (e) {
setError(e.message || "Неизвестная ошибка");
return null;
} finally {
setLoading(false);
}
};
return { save, loading, error };
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import {API_URL} from "../config/constants";
import {API_URL} from "../config/authConstants";
export function useCheckLogin(login) {

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { API_URL } from "../config/constants";
import { API_URL } from "../config/authConstants";
export function useUpdatePost() {
const [loading, setLoading] = useState(false);
@@ -14,7 +14,6 @@ export function useUpdatePost() {
const res = await fetch(`${API_URL}/api/v1/posts/${id}`, {
method: "PUT",
headers: {
// Content-Type НЕ указываем — браузер сам выставит multipart/form-data
"Authorization": `Bearer ${token}`,
},
body: formData, // FormData