From 204627565fbe80f4a9f694fd224ef560c279644d Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Tue, 8 Sep 2026 23:20:27 +0300 Subject: [PATCH] added button back from map and fix post on map --- src/components/NahodkaLogo.jsx | 244 +++-------------------------- src/components/feed/FeedTopBar.jsx | 69 +++++--- src/components/icons/MenuIcon.jsx | 6 +- src/locales/en.json | 2 + src/locales/ru.json | 2 + src/pages/MainFeed.jsx | 22 ++- src/pages/ReviewsPage.jsx | 2 +- src/styles/layout.css | 151 +++++++++++++----- src/utils/compressImage.js | 84 +++++----- 9 files changed, 238 insertions(+), 344 deletions(-) diff --git a/src/components/NahodkaLogo.jsx b/src/components/NahodkaLogo.jsx index eb686d3..ba8a3b0 100644 --- a/src/components/NahodkaLogo.jsx +++ b/src/components/NahodkaLogo.jsx @@ -1,8 +1,11 @@ import React from "react"; +/** + * Логотип «Находка» — одно слово, без лупы (лупа заменена буквой «о»). + */ export default function NahodkaLogo({ - width = 760, - height = 360, + width = 600, + height = 200, className, }) { return ( @@ -10,15 +13,11 @@ export default function NahodkaLogo({ className={className} width={width} height={height} - viewBox="-40 115 760 205" + viewBox="0 0 600 200" fill="none" xmlns="http://www.w3.org/2000/svg" > - {/* ========================= */} - {/* ЦВЕТ БУКВ */} - {/* ========================= */} - - - + + - {/* ========================= */} - {/* РАМКА ЛУПЫ */} - {/* ========================= */} - - - - - - - {/* ========================= */} - {/* СТЕКЛО */} - {/* ========================= */} - - - - - - - {/* ========================= */} - {/* РУЧКА */} - {/* ========================= */} - - - - - - - {/* ========================= */} - {/* ТЕНЬ */} - {/* ========================= */} - - - - - - {/* ===================================================== */} - {/* ЛЕВАЯ ЧАСТЬ — "НАХ" */} - {/* ===================================================== */} - - - - Нах - - - - {/* ===================================================== */} - {/* ЛУПА — "О" */} - {/* ===================================================== */} - - - {/* Ручка */} - - - - {/* Внешняя рамка */} - - - - {/* Внутренняя рамка */} - - - - {/* Стекло */} - - - - {/* ================================================= */} - {/* ЛАПКА */} - {/* ================================================= */} - - - - - - - - - - - - - - - {/* ===================================================== */} - {/* ПРАВАЯ ЧАСТЬ — "ДКА" */} - {/* ===================================================== */} - - {/* ПРАВАЯ ЧАСТЬ — "ДКА" */} - - - - дка - - + Находка + ); } \ No newline at end of file diff --git a/src/components/feed/FeedTopBar.jsx b/src/components/feed/FeedTopBar.jsx index a1fa771..13eb3a3 100644 --- a/src/components/feed/FeedTopBar.jsx +++ b/src/components/feed/FeedTopBar.jsx @@ -1,9 +1,14 @@ import { useState, useRef, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import AppTopBar from "../layout/AppTopBar"; import { useTranslation } from "react-i18next"; +import MenuIcon from "../icons/MenuIcon"; import NahodkaLogo from "../NahodkaLogo"; +/** + * TopBar ленты (Telegram-style): + * слева — круглая стеклянная кнопка-логотип (открывает фильтр типа), + * справа — squircle-кнопка-гамбургер (открывает SideBar). + */ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -37,31 +42,42 @@ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange return (
- setOpen((v) => !v)} +
+ + + - } - /> + + + + + +
{open && ( @@ -89,6 +105,7 @@ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange {options.map((opt) => (
); -} +} \ No newline at end of file diff --git a/src/components/icons/MenuIcon.jsx b/src/components/icons/MenuIcon.jsx index ef4c123..bc8868d 100644 --- a/src/components/icons/MenuIcon.jsx +++ b/src/components/icons/MenuIcon.jsx @@ -1,9 +1,11 @@ -export default function MenuIcon () { +export default function MenuIcon ({ className }) { return ( - + + + ); } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index b55cb67..4b76c80 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -149,6 +149,8 @@ "filterLoss": "Lost", "filterAll": "All ads", "filterFound": "Found", + "openFilter": "Filter by ad type", + "openMenu": "Open menu", "filterCitiesPlaceholder": "Select city…", "filterCitiesEmpty": "No cities found", "filterCitiesAdd": "Add \"{{city}}\"", diff --git a/src/locales/ru.json b/src/locales/ru.json index 96608f9..bc0104b 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -150,6 +150,8 @@ "filterLoss": "Потери", "filterAll": "Все объявления", "filterFound": "Находки", + "openFilter": "Фильтр по типу объявления", + "openMenu": "Открыть меню", "filterCitiesPlaceholder": "Выберите город…", "filterCitiesEmpty": "Города не найдены", "filterCitiesAdd": "Добавить «{{city}}»", diff --git a/src/pages/MainFeed.jsx b/src/pages/MainFeed.jsx index da8558e..a63256d 100644 --- a/src/pages/MainFeed.jsx +++ b/src/pages/MainFeed.jsx @@ -321,7 +321,7 @@ export default function MainFeed() { : feed.error; return ( -
+
- setSidebarOpen(v => !v)} - typeFilter={typeFilter} - onTypeFilterChange={setTypeFilter} - /> - + {isSearchView && ( + setSidebarOpen(v => !v)} + typeFilter={typeFilter} + onTypeFilterChange={setTypeFilter} + /> + )}
@@ -370,6 +371,13 @@ export default function MainFeed() { ) : ( <>
+ {!isSearchView && ( + setSidebarOpen(v => !v)} + typeFilter={typeFilter} + onTypeFilterChange={setTypeFilter} + /> + )} +
.f-topbar-dropdown-wrap { + z-index: 10; +} +/* Мобильный drawer делаем полупрозрачным, чтобы TopBar просвечивал за ним */ +@media (max-width: 900px) { + .f-app.f-sb-open .f-sidebar { + background: var(--bg-filter); + -webkit-backdrop-filter: blur(20px) saturate(180%); + backdrop-filter: blur(20px) saturate(180%); + } +} /* ── Topbar Profile ── */ .f-topbar-profile { @@ -121,30 +202,23 @@ color: var(--teal); font-weight: 700; } -/* ── Topbar Dropdown (Instagram-style) ── */ +/* ── Topbar Dropdown (фильтр типа привязан к кнопке-логотипу) ── */ .f-topbar-dropdown-wrap { position: relative; } -.f-topbar-dropdown-wrap .f-topbar { - z-index: 10; +/* В ленте шапка липнет к верху скроллера: контент прокручивается под ней + и «замыливается» через backdrop-filter на .f-topbar */ +.f-feed > .f-topbar-dropdown-wrap { + position: sticky; + top: 0; + z-index: 100; + margin: -20px -16px 0; } -.f-topbar-dropdown-btn { - display: inline-flex; align-items: center; justify-content: center; gap: 4px; - background: none; border: none; cursor: pointer; - color: var(--text-primary); - font-family: var(--font); font-weight: 900; font-size: 1.25rem; - letter-spacing: -.4px; white-space: nowrap; - padding: 0 6px; border-radius: 8px; - transition: background .15s; +@media (max-width: 900px) { + .f-feed > .f-topbar-dropdown-wrap { + margin: -10px -10px 0; + } } -.f-topbar-dropdown-btn:hover { background: var(--hover-bg); } -.f-topbar-logo { height: auto; display: block; min-width: 0; max-width: 100%; flex-shrink: 1; margin-top: 10px; } - -.f-topbar-chevron { - width: 18px; height: 18px; flex-shrink: 0; - transition: transform .25s ease; -} -.f-topbar-chevron.open { transform: rotate(180deg); } .f-topbar-dropdown-overlay { position: fixed; inset: 0; z-index: 45; @@ -504,7 +578,7 @@ .f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; } .f-feed { - flex: 1; overflow-y: scroll; overscroll-behavior: contain; min-height: 0; + flex: 1; overflow-y: scroll; overflow-x: hidden; overscroll-behavior: contain; min-height: 0; padding: 20px 16px; display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 660px; margin: 0 auto; } @@ -525,6 +599,13 @@ } @media (max-width: 900px) { + /* Мобильная версия: прижимаем логотип-капсулу влево (к кнопке-гамбургеру) */ + .f-main-feed .f-topbar { + justify-content: flex-start; + } + .f-main-feed .f-logo-btn { margin-left: 64px; } + .f-main-feed .f-topbar-spacer { flex: 1; } + .f-mapview-back { /* Поднимаем кнопку выше плавающей нижней навигации */ bottom: calc(84px + env(safe-area-inset-bottom)); @@ -535,10 +616,6 @@ gap: 12px; } - .f-topbar { - padding: 0 12px; - } - .f-post-btn { padding: 8px 14px; font-size: 0.78rem; @@ -563,8 +640,6 @@ .f-city-ms-input { font-size: 0.85rem; } .f-city-ms-tag { font-size: 0.75rem; padding: 2px 8px; } - .f-topbar-dropdown-btn { font-size: 1.15rem; padding: 0 4px; } - .f-topbar-chevron { width: 18px; height: 18px; } .f-topbar-dropdown-menu { padding: 6px 10px; gap: 5px; margin-top: 4px; border-radius: 999px; diff --git a/src/utils/compressImage.js b/src/utils/compressImage.js index 5bcfc29..0ac2119 100644 --- a/src/utils/compressImage.js +++ b/src/utils/compressImage.js @@ -1,49 +1,41 @@ -/** - * Сжимает изображение до maxW пикселей по ширине и конвертирует в JPEG. - * Используется перед отправкой, чтобы не гнать тяжёлые файлы на сервер. - * Маленькие картинки (< maxW) не увеличиваются. - * - * @param {File} file — оригинальный файл - * @param {number} maxW — максимальная ширина в пикселях (default 1200) - * @param {number} quality — качество JPEG 0..1 (default 0.82) - * @returns {Promise} - */ -export function compressImage(file, maxW = 1200, quality = 0.82) { +export async function compressImage(file, maxW = 2048, quality = 0.9) { + // если файл и так маленький и лёгкий — не трогаем вообще + if (file.size < 300 * 1024) return file; + + const bitmap = await createImageBitmap(file, { + resizeWidth: undefined, // посчитаем сами + }); + + const scale = Math.min(1, maxW / bitmap.width); + if (scale === 1) return file; // не увеличиваем и не пересжимаем без причины + + // ресайз через createImageBitmap с высоким качеством ресемплинга + const targetW = Math.round(bitmap.width * scale); + const targetH = Math.round(bitmap.height * scale); + const resized = await createImageBitmap(bitmap, { + resizeWidth: targetW, + resizeHeight: targetH, + resizeQuality: "high", + }); + + const canvas = document.createElement("canvas"); + canvas.width = targetW; + canvas.height = targetH; + const ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(resized, 0, 0); + 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 => { - if (!blob) { - reject(new Error("canvas.toBlob вернул null")); - return; - } - // Имя приводим к .jpg — результат всегда JPEG, независимо от - // исходного расширения (png/webp/etc), иначе имя файла не - // соответствует его реальному содержимому. - const jpegName = file.name.replace(/\.[^.]+$/, "") + ".jpg"; - resolve(new File([blob], jpegName, { type: "image/jpeg" })); - }, - "image/jpeg", - quality - ); - }; - - img.onerror = () => { - URL.revokeObjectURL(url); - reject(new Error("Не удалось загрузить изображение")); - }; - - img.src = url; + canvas.toBlob( + blob => { + if (!blob) return reject(new Error("canvas.toBlob вернул null")); + // используем WebP, если формат исходно не требовал прозрачности/точности + const outName = file.name.replace(/\.[^.]+$/, "") + ".webp"; + resolve(new File([blob], outName, { type: "image/webp" })); + }, + "image/webp", + quality + ); }); } \ No newline at end of file