diff --git a/src/components/CreateUpdatePost.jsx b/src/components/CreateUpdatePost.jsx index 8be4a61..3d433ff 100644 --- a/src/components/CreateUpdatePost.jsx +++ b/src/components/CreateUpdatePost.jsx @@ -54,6 +54,9 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) { // Единый хук для создания и обновления const { save, loading: saving, error: apiError } = usePostMutation(); + // Состояние для модалки-результата (успех / блокировка контента) + const [resultModal, setResultModal] = useState(null); // null | "success" | "blocked" + // Внутри компонента, рядом с другими хуками const initialCoords = isEdit && initialData.latitude && initialData.longitude ? { latitude: initialData.latitude, longitude: initialData.longitude } @@ -124,14 +127,22 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) { // save(id, data): id = null → POST (создание), id = string → PUT (обновление) const result = await save(isEdit ? initialData.id : null, data); - if (result) { - onSuccess(result); - onClose(); + if (!result) { + // Ошибка уже записана в apiError, либо null если неизвестная + return; + } + if (result.blocked) { + setResultModal("blocked"); + return; + } + if (result.data) { + setResultModal("success"); } }; return ( - // Клик по фону (не по самой модалке) закрывает её + <> + {/* Клик по фону (не по самой модалке) закрывает её */}
e.target === e.currentTarget && onClose()}>
@@ -309,5 +320,45 @@ export default function CreateUpdatePost({ onClose, onSuccess, initialData }) {
+ + {/* ── Модалка-результат после отправки ── */} + {resultModal && ( +
{ + if (e.target === e.currentTarget) { + if (resultModal === "success") { + onSuccess?.(); + } + onClose(); + } + }}> +
+
+ {resultModal === "blocked" ? "✕" : "✓"} +
+

+ {resultModal === "blocked" + ? t('post.blockedTitle') + : t('post.successTitle')} +

+

+ {resultModal === "blocked" + ? t('post.blockedText') + : t('post.successText')} +

+ +
+
+ )} + ); } \ No newline at end of file diff --git a/src/config/authConstants.js b/src/config/authConstants.js index 377a919..d4f1eda 100644 --- a/src/config/authConstants.js +++ b/src/config/authConstants.js @@ -9,7 +9,7 @@ import headphonesIcon from '../assets/headphones.png'; import watchIcon from '../assets/watch.png'; import glassesIcon from '../assets/glasses.png'; -export const API_URL = process.env.REACT_APP_API_URL || "http://192.168.1.68:8080"; +export const API_URL = process.env.REACT_APP_API_URL || "http://localhost:8080"; export function getImageUrl(filename) { if (!filename) return null; diff --git a/src/hooks/usePostMutation.js b/src/hooks/usePostMutation.js index fe2bf5f..2828386 100644 --- a/src/hooks/usePostMutation.js +++ b/src/hooks/usePostMutation.js @@ -11,11 +11,10 @@ import { API_URL } from "../config/authConstants"; * * Автоматически добавляет заголовок Authorization с JWT-токеном. * - * @returns {{ - * save: (id: string|null, data: FormData) => Promise, - * loading: boolean, - * error: string, - * }} + * save() возвращает: + * { data } — успешный ответ сервера + * { blocked: true } — контент заблокирован модерацией + * null — серверная ошибка (свойство error содержит текст) */ export function usePostMutation() { const [loading, setLoading] = useState(false); @@ -24,7 +23,7 @@ export function usePostMutation() { /** * @param {string|null} id — id объявления при обновлении, null при создании * @param {FormData} data — данные формы - * @returns {object|null} — ответ сервера или null при ошибке + * @returns {Promise<{data:object}|{blocked:boolean, reason:string}|null>} */ const save = async (id, data) => { setLoading(true); @@ -45,8 +44,18 @@ export function usePostMutation() { throw new Error("Требуется авторизация. Войдите в аккаунт."); } - if (!res.ok) throw new Error(`Ошибка сервера: ${res.status}`); - return await res.json(); + if (!res.ok) { + let body; + try { body = await res.json(); } catch { body = null; } + + if (body?.error === "CONTENT_BLOCKED") { + return { blocked: true, reason: body.reason || "" }; + } + + throw new Error(body?.message || `Ошибка сервера: ${res.status}`); + } + + return { data: await res.json() }; } catch (e) { setError(e.message || "Неизвестная ошибка"); return null; diff --git a/src/styles/modal.css b/src/styles/modal.css index 939c0c6..3619b91 100644 --- a/src/styles/modal.css +++ b/src/styles/modal.css @@ -251,4 +251,55 @@ border-radius: 20px 20px 0 0; padding: 20px 16px 32px; /* 32px снизу — отступ от home indicator */ } +} + +/* ── Result Modal (success / blocked) ── */ +.f-modal-result { + text-align: center; + padding: 36px 28px 28px; + max-width: 380px; +} +.f-result-icon { + width: 56px; height: 56px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + margin: 0 auto 16px; + font-size: 1.5rem; font-weight: 900; +} +.f-result-icon.success { + background: rgba(var(--teal-rgb), .15); + color: var(--teal); +} +.f-result-icon.blocked { + background: rgba(255, 107, 107, .15); + color: #ff6b6b; +} +.f-result-title { + margin: 0 0 8px; + font-size: 1.05rem; font-weight: 800; + color: var(--text-primary); +} +.f-result-text { + margin: 0 0 22px; + font-size: .85rem; line-height: 1.5; + color: var(--muted); +} +.f-result-btn { + width: 100%; border: none; border-radius: 12px; + padding: 13px; font-family: var(--font); + font-size: .9rem; font-weight: 800; cursor: pointer; + transition: opacity .15s, transform .15s; +} +.f-result-btn:hover { opacity: .88; transform: translateY(-1px); } +.f-result-btn:active { transform: translateY(0); } +.f-result-btn.ok { + background: linear-gradient(90deg, var(--teal2), var(--teal), var(--teal2)); + background-size: 200% 100%; + color: #082320; + animation: shimmer 3s linear infinite; +} +.f-result-btn.err { + background: rgba(255, 107, 107, .12); + border: 1.5px solid rgba(255, 107, 107, .35); + color: #ff6b6b; } \ No newline at end of file