Merge pull request #66 from SlimusMinus/fix-top_bar
added button back from map and fix post on map
This commit is contained in:
@@ -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="130"
|
||||||
fontSize="135"
|
fontWeight="800"
|
||||||
fontWeight="800"
|
letterSpacing="1"
|
||||||
>
|
|
||||||
<text
|
|
||||||
x="30"
|
|
||||||
y="225"
|
|
||||||
letterSpacing="1"
|
|
||||||
>
|
|
||||||
Нах
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{/* ===================================================== */}
|
|
||||||
{/* ЛУПА — "О" */}
|
|
||||||
{/* ===================================================== */}
|
|
||||||
|
|
||||||
<g
|
|
||||||
transform="translate(352 190)"
|
|
||||||
filter="url(#magnifierShadow)"
|
|
||||||
>
|
>
|
||||||
{/* Ручка */}
|
Находка
|
||||||
|
</text>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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,31 +42,42 @@ 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}
|
||||||
<button
|
aria-label={t("feed.openMenu")}
|
||||||
ref={btnRef}
|
>
|
||||||
className="f-topbar-dropdown-btn"
|
<MenuIcon />
|
||||||
onClick={() => setOpen((v) => !v)}
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
ref={btnRef}
|
||||||
|
type="button"
|
||||||
|
className="f-logo-btn"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
aria-label={t("feed.openFilter")}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<NahodkaLogo className="f-topbar-logo" />
|
||||||
|
<svg
|
||||||
|
className={`f-topbar-chevron${open ? " open" : ""}`}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<NahodkaLogo className="f-topbar-logo" width={130} height={52} />
|
<polyline points="6 9 12 15 18 9" />
|
||||||
<svg
|
</svg>
|
||||||
className={`f-topbar-chevron${open ? " open" : ""}`}
|
</button>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
<span className="f-topbar-spacer" />
|
||||||
stroke="currentColor"
|
</div>
|
||||||
strokeWidth="2.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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}}\"",
|
||||||
|
|||||||
@@ -150,6 +150,8 @@
|
|||||||
"filterLoss": "Потери",
|
"filterLoss": "Потери",
|
||||||
"filterAll": "Все объявления",
|
"filterAll": "Все объявления",
|
||||||
"filterFound": "Находки",
|
"filterFound": "Находки",
|
||||||
|
"openFilter": "Фильтр по типу объявления",
|
||||||
|
"openMenu": "Открыть меню",
|
||||||
"filterCitiesPlaceholder": "Выберите город…",
|
"filterCitiesPlaceholder": "Выберите город…",
|
||||||
"filterCitiesEmpty": "Города не найдены",
|
"filterCitiesEmpty": "Города не найдены",
|
||||||
"filterCitiesAdd": "Добавить «{{city}}»",
|
"filterCitiesAdd": "Добавить «{{city}}»",
|
||||||
|
|||||||
@@ -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">
|
||||||
<FeedTopBar
|
{isSearchView && (
|
||||||
onMenuClick={() => setSidebarOpen(v => !v)}
|
<FeedTopBar
|
||||||
typeFilter={typeFilter}
|
onMenuClick={() => setSidebarOpen(v => !v)}
|
||||||
onTypeFilterChange={setTypeFilter}
|
typeFilter={typeFilter}
|
||||||
/>
|
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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,49 +1,41 @@
|
|||||||
/**
|
export async function compressImage(file, maxW = 2048, quality = 0.9) {
|
||||||
* Сжимает изображение до maxW пикселей по ширине и конвертирует в JPEG.
|
// если файл и так маленький и лёгкий — не трогаем вообще
|
||||||
* Используется перед отправкой, чтобы не гнать тяжёлые файлы на сервер.
|
if (file.size < 300 * 1024) return file;
|
||||||
* Маленькие картинки (< maxW) не увеличиваются.
|
|
||||||
*
|
const bitmap = await createImageBitmap(file, {
|
||||||
* @param {File} file — оригинальный файл
|
resizeWidth: undefined, // посчитаем сами
|
||||||
* @param {number} maxW — максимальная ширина в пикселях (default 1200)
|
});
|
||||||
* @param {number} quality — качество JPEG 0..1 (default 0.82)
|
|
||||||
* @returns {Promise<File>}
|
const scale = Math.min(1, maxW / bitmap.width);
|
||||||
*/
|
if (scale === 1) return file; // не увеличиваем и не пересжимаем без причины
|
||||||
export function compressImage(file, maxW = 1200, quality = 0.82) {
|
|
||||||
|
// ресайз через 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
canvas.toBlob(
|
||||||
const url = URL.createObjectURL(file);
|
blob => {
|
||||||
|
if (!blob) return reject(new Error("canvas.toBlob вернул null"));
|
||||||
img.onload = () => {
|
// используем WebP, если формат исходно не требовал прозрачности/точности
|
||||||
URL.revokeObjectURL(url); // освобождаем память сразу после загрузки
|
const outName = file.name.replace(/\.[^.]+$/, "") + ".webp";
|
||||||
|
resolve(new File([blob], outName, { type: "image/webp" }));
|
||||||
const scale = Math.min(1, maxW / img.width);
|
},
|
||||||
const canvas = document.createElement("canvas");
|
"image/webp",
|
||||||
canvas.width = Math.round(img.width * scale);
|
quality
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user