added button back from map and fix post on map

This commit is contained in:
SlimusMinus
2026-09-08 23:20:27 +03:00
parent ee5c219707
commit 204627565f
9 changed files with 238 additions and 344 deletions

View File

@@ -1,8 +1,11 @@
import React from "react"; import React from "react";
/**
* Логотип «Находка» — одно слово, без лупы (лупа заменена буквой «о»).
*/
export default function NahodkaLogo({ export default function NahodkaLogo({
width = 760, width = 600,
height = 360, height = 200,
className, className,
}) { }) {
return ( return (
@@ -10,15 +13,11 @@ export default function NahodkaLogo({
className={className} className={className}
width={width} width={width}
height={height} height={height}
viewBox="-40 115 760 205" viewBox="0 0 600 200"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<defs> <defs>
{/* ========================= */}
{/* ЦВЕТ БУКВ */}
{/* ========================= */}
<linearGradient <linearGradient
id="coral" id="coral"
x1="0" x1="0"
@@ -26,53 +25,10 @@ export default function NahodkaLogo({
x2="0" x2="0"
y2="1" y2="1"
> >
<stop offset="0%" stopColor="#E99A78" /> <stop offset="0%" stopColor="#FF8E6B" />
<stop offset="100%" stopColor="#D97956" /> <stop offset="100%" stopColor="#F1503A" />
</linearGradient> </linearGradient>
{/* ========================= */}
{/* РАМКА ЛУПЫ */}
{/* ========================= */}
<linearGradient
id="magnifier"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#EA9B78" />
<stop offset="100%" stopColor="#D57250" />
</linearGradient>
{/* ========================= */}
{/* СТЕКЛО */}
{/* ========================= */}
<radialGradient id="glass">
<stop offset="0%" stopColor="#FFFDF8" />
<stop offset="100%" stopColor="#F3E9DA" />
</radialGradient>
{/* ========================= */}
{/* РУЧКА */}
{/* ========================= */}
<linearGradient
id="handle"
x1="0"
y1="0"
x2="1"
y2="1"
>
<stop offset="0%" stopColor="#86A383" />
<stop offset="100%" stopColor="#617D68" />
</linearGradient>
{/* ========================= */}
{/* ТЕНЬ */}
{/* ========================= */}
<filter <filter
id="shadow" id="shadow"
x="-30%" x="-30%"
@@ -88,181 +44,21 @@ export default function NahodkaLogo({
floodOpacity="0.25" floodOpacity="0.25"
/> />
</filter> </filter>
<filter
id="magnifierShadow"
x="-40%"
y="-40%"
width="180%"
height="200%"
>
<feDropShadow
dx="0"
dy="5"
stdDeviation="4"
floodColor="#8F4938"
floodOpacity="0.3"
/>
</filter>
</defs> </defs>
{/* ===================================================== */} <text
{/* ЛЕВАЯ ЧАСТЬ — "НАХ" */} x="300"
{/* ===================================================== */} y="134"
textAnchor="middle"
<g
fill="url(#coral)" fill="url(#coral)"
filter="url(#shadow)" filter="url(#shadow)"
fontFamily="Nunito, Arial Rounded MT Bold, Arial, sans-serif" fontFamily="Nunito, Arial Rounded MT Bold, Arial, sans-serif"
fontSize="135" fontSize="130"
fontWeight="800" fontWeight="800"
>
<text
x="30"
y="225"
letterSpacing="1" letterSpacing="1"
> >
Нах Находка
</text> </text>
</g>
{/* ===================================================== */}
{/* ЛУПА — "О" */}
{/* ===================================================== */}
<g
transform="translate(352 190)"
filter="url(#magnifierShadow)"
>
{/* Ручка */}
<rect
x="34"
y="70"
width="28"
height="105"
rx="14"
transform="rotate(-43 34 70)"
fill="url(#handle)"
stroke="#D97956"
strokeWidth="6"
/>
{/* Внешняя рамка */}
<circle
cx="0"
cy="0"
r="70"
fill="none"
stroke="url(#magnifier)"
strokeWidth="12"
/>
{/* Внутренняя рамка */}
<circle
cx="0"
cy="0"
r="58"
fill="none"
/>
{/* Стекло */}
<circle
cx="0"
cy="0"
r="53"
fill="none"
/>
{/* ================================================= */}
{/* ЛАПКА */}
{/* ================================================= */}
<g transform="translate(0 15)">
<path
d="
M 0 25
C -8 18 -25 12 -25 -2
C -25 -13 -14 -18 -5 -12
L 0 -8
L 5 -12
C 14 -18 25 -13 25 -2
C 25 12 8 18 0 25
Z
"
fill="#E8D3B0"
stroke="#D97956"
strokeWidth="4"
/>
<ellipse
cx="-22"
cy="-20"
rx="7"
ry="10"
fill="#E8D3B0"
stroke="#D97956"
strokeWidth="4"
transform="rotate(-25 -22 -20)"
/>
<ellipse
cx="-7"
cy="-29"
rx="7"
ry="11"
fill="#E8D3B0"
stroke="#D97956"
strokeWidth="4"
/>
<ellipse
cx="9"
cy="-29"
rx="7"
ry="11"
fill="#E8D3B0"
stroke="#D97956"
strokeWidth="4"
/>
<ellipse
cx="23"
cy="-20"
rx="7"
ry="10"
fill="#E8D3B0"
stroke="#D97956"
strokeWidth="4"
transform="rotate(25 23 -20)"
/>
</g>
</g>
{/* ===================================================== */}
{/* ПРАВАЯ ЧАСТЬ — "ДКА" */}
{/* ===================================================== */}
{/* ПРАВАЯ ЧАСТЬ — "ДКА" */}
<g
fill="url(#coral)"
filter="url(#shadow)"
fontFamily="Nunito, Arial Rounded MT Bold, Arial, sans-serif"
fontSize="135"
fontWeight="800"
>
<text
x="436"
y="225"
letterSpacing="1"
>
дка
</text>
</g>
</svg> </svg>
); );
} }

View File

@@ -1,9 +1,14 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import AppTopBar from "../layout/AppTopBar";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MenuIcon from "../icons/MenuIcon";
import NahodkaLogo from "../NahodkaLogo"; import NahodkaLogo from "../NahodkaLogo";
/**
* TopBar ленты (Telegram-style):
* слева — круглая стеклянная кнопка-логотип (открывает фильтр типа),
* справа — squircle-кнопка-гамбургер (открывает SideBar).
*/
export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange }) { export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -37,17 +42,26 @@ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange
return ( return (
<div className="f-topbar-dropdown-wrap"> <div className="f-topbar-dropdown-wrap">
<AppTopBar <div className="f-topbar">
onMenuClick={onMenuClick} <button
hideProfile type="button"
hideWordmark className="f-menu-btn"
centerContent={ onClick={onMenuClick}
aria-label={t("feed.openMenu")}
>
<MenuIcon />
</button>
<button <button
ref={btnRef} ref={btnRef}
className="f-topbar-dropdown-btn" type="button"
className="f-logo-btn"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
aria-label={t("feed.openFilter")}
aria-haspopup="menu"
aria-expanded={open}
> >
<NahodkaLogo className="f-topbar-logo" width={130} height={52} /> <NahodkaLogo className="f-topbar-logo" />
<svg <svg
className={`f-topbar-chevron${open ? " open" : ""}`} className={`f-topbar-chevron${open ? " open" : ""}`}
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -56,12 +70,14 @@ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange
strokeWidth="2.5" strokeWidth="2.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
aria-hidden="true"
> >
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</button> </button>
}
/> <span className="f-topbar-spacer" />
</div>
<AnimatePresence> <AnimatePresence>
{open && ( {open && (
@@ -89,6 +105,7 @@ export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange
{options.map((opt) => ( {options.map((opt) => (
<button <button
key={opt.key ?? "all"} key={opt.key ?? "all"}
type="button"
className={`f-topbar-dropdown-item${typeFilter === opt.key ? " active" : ""}`} className={`f-topbar-dropdown-item${typeFilter === opt.key ? " active" : ""}`}
onClick={() => handleSelect(opt.key)} onClick={() => handleSelect(opt.key)}
> >

View File

@@ -1,9 +1,11 @@
export default function MenuIcon () { export default function MenuIcon ({ className }) {
return ( return (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" opacity="0.8"> <span className={className || undefined}>
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round">
<line x1="3" y1="6" x2="21" y2="6" /> <line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" /> <line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" /> <line x1="3" y1="18" x2="21" y2="18" />
</svg> </svg>
</span>
); );
} }

View File

@@ -149,6 +149,8 @@
"filterLoss": "Lost", "filterLoss": "Lost",
"filterAll": "All ads", "filterAll": "All ads",
"filterFound": "Found", "filterFound": "Found",
"openFilter": "Filter by ad type",
"openMenu": "Open menu",
"filterCitiesPlaceholder": "Select city…", "filterCitiesPlaceholder": "Select city…",
"filterCitiesEmpty": "No cities found", "filterCitiesEmpty": "No cities found",
"filterCitiesAdd": "Add \"{{city}}\"", "filterCitiesAdd": "Add \"{{city}}\"",

View File

@@ -150,6 +150,8 @@
"filterLoss": "Потери", "filterLoss": "Потери",
"filterAll": "Все объявления", "filterAll": "Все объявления",
"filterFound": "Находки", "filterFound": "Находки",
"openFilter": "Фильтр по типу объявления",
"openMenu": "Открыть меню",
"filterCitiesPlaceholder": "Выберите город…", "filterCitiesPlaceholder": "Выберите город…",
"filterCitiesEmpty": "Города не найдены", "filterCitiesEmpty": "Города не найдены",
"filterCitiesAdd": "Добавить «{{city}}»", "filterCitiesAdd": "Добавить «{{city}}»",

View File

@@ -321,7 +321,7 @@ export default function MainFeed() {
: feed.error; : feed.error;
return ( return (
<div className={`f-app${isSearchView ? " f-search-view" : ""}${isMapView ? " f-map-view" : ""}`}> <div className={`f-app${sidebarOpen ? " f-sb-open" : ""}${isSearchView ? " f-search-view" : ""}${isMapView ? " f-map-view" : ""}`}>
<Sidebar <Sidebar
open={sidebarOpen} open={sidebarOpen}
activeNav={activeNav} activeNav={activeNav}
@@ -342,12 +342,13 @@ export default function MainFeed() {
/> />
<div className="f-main f-main-feed"> <div className="f-main f-main-feed">
{isSearchView && (
<FeedTopBar <FeedTopBar
onMenuClick={() => setSidebarOpen(v => !v)} onMenuClick={() => setSidebarOpen(v => !v)}
typeFilter={typeFilter} typeFilter={typeFilter}
onTypeFilterChange={setTypeFilter} onTypeFilterChange={setTypeFilter}
/> />
)}
<div className="f-search-panel"> <div className="f-search-panel">
<FeedSearchBar value={searchQuery} onChange={setSearchQuery} t={t} /> <FeedSearchBar value={searchQuery} onChange={setSearchQuery} t={t} />
@@ -370,6 +371,13 @@ export default function MainFeed() {
) : ( ) : (
<> <>
<div className="f-feed" ref={feedRef}> <div className="f-feed" ref={feedRef}>
{!isSearchView && (
<FeedTopBar
onMenuClick={() => setSidebarOpen(v => !v)}
typeFilter={typeFilter}
onTypeFilterChange={setTypeFilter}
/>
)}
<FeedContent <FeedContent
loading={listLoading} loading={listLoading}
cityLoading={cityFeed.cityLoading} cityLoading={cityFeed.cityLoading}

View File

@@ -78,7 +78,7 @@ export default function ReviewsPage() {
}, [profileModalOpen, isAuthenticated, user?.id]); }, [profileModalOpen, isAuthenticated, user?.id]);
return ( return (
<div className="f-app"> <div className={`f-app${sidebarOpen ? " f-sb-open" : ""}`}>
<Sidebar <Sidebar
open={sidebarOpen} open={sidebarOpen}
activeNav="reviews" activeNav="reviews"

View File

@@ -18,27 +18,108 @@
min-width: 0; height: 100vh; height: 100dvh; overflow: hidden; min-width: 0; height: 100vh; height: 100dvh; overflow: hidden;
} }
/* ── Topbar (mobile only; desktop uses the nav sidebar) ── */ /* ── Topbar (telegram-style; mobile only; desktop uses the nav sidebar) ──
Фон отсутствует полностью: стеклянными выглядят только сами кнопки. */
.f-topbar { .f-topbar {
display: flex; align-items: center; gap: 8px; position: sticky;
padding: 0 16px; top: 0;
border-bottom: 1px solid var(--border); z-index: 100;
background: var(--bg-surface); display: flex;
z-index: 10; align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
padding-top: calc(8px + env(safe-area-inset-top));
} }
.f-menu-btn {
background: none; border: none; cursor: pointer; /* Кнопка-логотип по центру: wordmark «Находка» в округлой стеклянной капсуле */
padding: 6px; border-radius: 8px; transition: background .15s; .f-logo-btn {
flex-shrink: 0; line-height: 0; width: 32px; position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 1 auto;
min-width: 0;
height: 38px;
padding: 0 30px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px) saturate(150%);
-webkit-backdrop-filter: blur(8px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: pointer;
line-height: 0;
transition: background .15s;
}
.f-logo-btn:hover { background: rgba(255, 255, 255, 0.22); }
.f-logo-btn .f-topbar-logo {
height: 34px;
width: auto;
display: block;
min-width: 0;
}
/* Галочка-индикатор фильтра на кнопке-логотипе: поворачивается при открытии */
.f-topbar-chevron {
position: absolute;
right: 8px;
top: 50%;
width: 14px;
height: 14px;
color: var(--text-primary); color: var(--text-primary);
transform: translateY(-50%);
transition: transform .25s ease;
} }
.f-menu-btn:hover { background: var(--hover-bg); } .f-topbar-chevron.open {
transform: translateY(-50%) rotate(180deg);
}
/* Кнопка вызова SideBar: стеклянный squircle (приближение iOS) */
.f-menu-btn {
width: 38px;
height: 38px;
border-radius: 22%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
padding: 0;
cursor: pointer;
line-height: 0;
color: var(--text-primary);
transition: background .15s, transform .1s;
}
.f-menu-btn:hover { background: rgba(255, 255, 255, 0.22); }
.f-menu-btn:active { transform: scale(.92); }
.f-menu-btn svg { width: 45%; height: 45%; }
.f-topbar-center { flex: 1; display: flex; justify-content: center; min-width: 0; } .f-topbar-center { flex: 1; display: flex; justify-content: center; min-width: 0; }
.f-topbar-wordmark { .f-topbar-wordmark {
color: var(--text-primary); font-weight: 900; font-size: 1.05rem; color: var(--text-primary); font-weight: 900; font-size: 1.05rem;
letter-spacing: -.4px; white-space: nowrap; margin-left: 2px; letter-spacing: -.4px; white-space: nowrap; margin-left: 2px;
} }
.f-topbar-spacer { width: 32px; flex-shrink: 0; } .f-topbar img,
.f-topbar svg { flex-shrink: 0; }
/* Когда открыт левый SideBar — верхний TopBar уходит за него (ниже по z-index) */
.f-app.f-sb-open .f-topbar,
.f-app.f-sb-open .f-feed > .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 ── */ /* ── Topbar Profile ── */
.f-topbar-profile { .f-topbar-profile {
@@ -121,30 +202,23 @@
color: var(--teal); font-weight: 700; color: var(--teal); font-weight: 700;
} }
/* ── Topbar Dropdown (Instagram-style) ── */ /* ── Topbar Dropdown (фильтр типа привязан к кнопке-логотипу) ── */
.f-topbar-dropdown-wrap { .f-topbar-dropdown-wrap {
position: relative; 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 { @media (max-width: 900px) {
display: inline-flex; align-items: center; justify-content: center; gap: 4px; .f-feed > .f-topbar-dropdown-wrap {
background: none; border: none; cursor: pointer; margin: -10px -10px 0;
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;
} }
.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 { .f-topbar-dropdown-overlay {
position: fixed; inset: 0; z-index: 45; position: fixed; inset: 0; z-index: 45;
@@ -504,7 +578,7 @@
.f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; } .f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; }
.f-feed { .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; padding: 20px 16px; display: flex; flex-direction: column; gap: 16px;
width: 100%; max-width: 660px; margin: 0 auto; width: 100%; max-width: 660px; margin: 0 auto;
} }
@@ -525,6 +599,13 @@
} }
@media (max-width: 900px) { @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 { .f-mapview-back {
/* Поднимаем кнопку выше плавающей нижней навигации */ /* Поднимаем кнопку выше плавающей нижней навигации */
bottom: calc(84px + env(safe-area-inset-bottom)); bottom: calc(84px + env(safe-area-inset-bottom));
@@ -535,10 +616,6 @@
gap: 12px; gap: 12px;
} }
.f-topbar {
padding: 0 12px;
}
.f-post-btn { .f-post-btn {
padding: 8px 14px; padding: 8px 14px;
font-size: 0.78rem; font-size: 0.78rem;
@@ -563,8 +640,6 @@
.f-city-ms-input { font-size: 0.85rem; } .f-city-ms-input { font-size: 0.85rem; }
.f-city-ms-tag { font-size: 0.75rem; padding: 2px 8px; } .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 { .f-topbar-dropdown-menu {
padding: 6px 10px; gap: 5px; padding: 6px 10px; gap: 5px;
margin-top: 4px; border-radius: 999px; margin-top: 4px; border-radius: 999px;

View File

@@ -1,49 +1,41 @@
/** export async function compressImage(file, maxW = 2048, quality = 0.9) {
* Сжимает изображение до maxW пикселей по ширине и конвертирует в JPEG. // если файл и так маленький и лёгкий — не трогаем вообще
* Используется перед отправкой, чтобы не гнать тяжёлые файлы на сервер. if (file.size < 300 * 1024) return file;
* Маленькие картинки (< maxW) не увеличиваются.
*
* @param {File} file — оригинальный файл
* @param {number} maxW — максимальная ширина в пикселях (default 1200)
* @param {number} quality — качество JPEG 0..1 (default 0.82)
* @returns {Promise<File>}
*/
export function compressImage(file, maxW = 1200, quality = 0.82) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => { const bitmap = await createImageBitmap(file, {
URL.revokeObjectURL(url); // освобождаем память сразу после загрузки 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 scale = Math.min(1, maxW / img.width);
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = Math.round(img.width * scale); canvas.width = targetW;
canvas.height = Math.round(img.height * scale); canvas.height = targetH;
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height); const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(resized, 0, 0);
return new Promise((resolve, reject) => {
canvas.toBlob( canvas.toBlob(
blob => { blob => {
if (!blob) { if (!blob) return reject(new Error("canvas.toBlob вернул null"));
reject(new Error("canvas.toBlob вернул null")); // используем WebP, если формат исходно не требовал прозрачности/точности
return; const outName = file.name.replace(/\.[^.]+$/, "") + ".webp";
} resolve(new File([blob], outName, { type: "image/webp" }));
// Имя приводим к .jpg — результат всегда JPEG, независимо от
// исходного расширения (png/webp/etc), иначе имя файла не
// соответствует его реальному содержимому.
const jpegName = file.name.replace(/\.[^.]+$/, "") + ".jpg";
resolve(new File([blob], jpegName, { type: "image/jpeg" }));
}, },
"image/jpeg", "image/webp",
quality quality
); );
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("Не удалось загрузить изображение"));
};
img.src = url;
}); });
} }