diff --git a/src/client/src/api/ApiStore/store.ts b/src/client/src/api/ApiStore/store.ts index 8d2d1e4..1b75404 100644 --- a/src/client/src/api/ApiStore/store.ts +++ b/src/client/src/api/ApiStore/store.ts @@ -47,6 +47,16 @@ function darkenHex(hex: string, amount: number): string { return `#${dr.toString(16).padStart(2, "0")}${dg.toString(16).padStart(2, "0")}${db.toString(16).padStart(2, "0")}`; } +function lightenRgbString(hex: string, amount: number): string | null { + const rgb = hexToRgbString(hex); + if (!rgb) return null; + const [r, g, b] = rgb.split(",").map(Number); + const lr = Math.round(r + (255 - r) * amount); + const lg = Math.round(g + (255 - g) * amount); + const lb = Math.round(b + (255 - b) * amount); + return `${lr}, ${lg}, ${lb}`; +} + function applyCarrierColors(carrier: { main_color?: string; left_color?: string; right_color?: string }) { const mainColor = carrier.main_color || "#006f3a"; const leftColor = carrier.left_color || "#006f3a"; @@ -60,6 +70,7 @@ function applyCarrierColors(carrier: { main_color?: string; left_color?: string; document.documentElement.style.setProperty("--carrier-left-rgb", hexToRgbString(leftColor) ?? "0, 111, 58"); document.documentElement.style.setProperty("--carrier-right", rightColor); document.documentElement.style.setProperty("--carrier-right-rgb", hexToRgbString(rightColor) ?? "0, 111, 58"); + document.documentElement.style.setProperty("--carrier-right-menu-rgb", lightenRgbString(rightColor, 0.38) ?? "179, 165, 152"); } class ApiStore { diff --git a/src/client/src/components/ListOfSights/ListHeader.jsx b/src/client/src/components/ListOfSights/ListHeader.jsx index 9b8a4bb..d026881 100644 --- a/src/client/src/components/ListOfSights/ListHeader.jsx +++ b/src/client/src/components/ListOfSights/ListHeader.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; const LANGUAGES = { EN: "en", @@ -18,6 +18,26 @@ const ListHeader = function ListHeader({ isTransferWidgetOpen, onBackToNearest, }) { + const [isIdle, setIsIdle] = useState(false); + const timerRef = useRef(null); + + useEffect(() => { + const resetTimer = () => { + setIsIdle(false); + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setIsIdle(true), 15000); + }; + + const events = ["pointermove", "pointerdown", "touchstart", "keydown"]; + events.forEach((e) => window.addEventListener(e, resetTimer)); + resetTimer(); + + return () => { + clearTimeout(timerRef.current); + events.forEach((e) => window.removeEventListener(e, resetTimer)); + }; + }, []); + const getTitle = () => { return selectedLanguageRight === LANGUAGES.RU ? "Достопримечательности" @@ -41,14 +61,11 @@ const ListHeader = function ListHeader({ > { )} -
+
{selectedSection !== 0 && (
{ const rx = cluster.longitude; const ry = cluster.latitude; - const iconSizePercent = resolveSightIconSizePercent(); - const iconSize = - SIGHT_ICON_BASE_SIZE * clamp(iconSizePercent / 100, 0.1, 10); - const screenX = (rx * scale + position.x) / dpr; const screenY = (ry * scale + position.y) / dpr; - const iconLeft = screenX - iconSize / 2; - const iconTop = screenY - iconSize / 2; const isExpanded = activeClusterId === cluster.id; const selectedSightInCluster = cluster.sights.find( @@ -2570,6 +2563,20 @@ export const WebGLMap = observer(() => { const selectedIsCustomInCluster = selectedSightInCluster?.is_default_icon === false && selectedHasIconInCluster; + + const iconSizePercent = resolveSightIconSizePercent( + cluster.sights[0], + ); + const clusterCustomScaleFactor = selectedIsCustomInCluster + ? scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6) + : 1; + const iconSize = + SIGHT_ICON_BASE_SIZE * + clamp(iconSizePercent / 100, 0.1, 10) * + clusterCustomScaleFactor; + + const iconLeft = screenX - iconSize; + const iconTop = screenY - iconSize; const hasSelectedAltIconInCluster = selectedSightInCluster != null && selectedIsCustomInCluster && @@ -2643,13 +2650,10 @@ export const WebGLMap = observer(() => { onTouchEnd={handleClusterClick} style={{ position: "absolute", - left: iconLeft - CLUSTER_RADIUS_BASE - 10, - top: iconTop - CLUSTER_RADIUS_BASE - 10, - width: iconSize + CLUSTER_RADIUS_BASE * 2 + 20, - height: iconSize + CLUSTER_RADIUS_BASE * 2 + 20, - display: "flex", - alignItems: "center", - justifyContent: "center", + left: iconLeft - 25, + top: iconTop - 25, + width: iconSize + 50, + height: iconSize + 50, pointerEvents: "auto", cursor: "pointer", userSelect: "none", @@ -2662,15 +2666,24 @@ export const WebGLMap = observer(() => { }), }} > -
+
{ position: "absolute", top: -6, right: -6, + zIndex: 1, width: 15, height: 15, borderRadius: "10px", @@ -2708,8 +2722,8 @@ export const WebGLMap = observer(() => { onMouseMove={handleCircleInteraction} style={{ position: "absolute", - left: screenX - iconSize / 2, - top: screenY - iconSize / 2, + left: screenX - iconSize, + top: screenY - iconSize, display: "flex", alignItems: "flex-start", pointerEvents: "auto", diff --git a/src/client/src/components/side-menu/SideMenu.jsx b/src/client/src/components/side-menu/SideMenu.jsx index a575323..d8adcd6 100644 --- a/src/client/src/components/side-menu/SideMenu.jsx +++ b/src/client/src/components/side-menu/SideMenu.jsx @@ -2,7 +2,6 @@ import "../../styles/SideMenu.css"; import AppealWidget from "../widgets/AppealWidget"; import { useEffect, useState, useCallback, useRef } from "react"; import { observer } from "mobx-react-lite"; -import gouvermentImage from "../../assets/images/test-image.png"; import sideMenuPhoto from "/side-menu-photo.png"; import RouteWidget from "../widgets/RouteWidget"; import ContentAPI from "../../api/content/content.api"; @@ -455,7 +454,7 @@ const SideMenu = observer(({ onMenuToggle }) => { : "Governor's appeal"}
)} -
+
0 ? '40px' : '260px' }}>
{ if (!isSightsOpen) { @@ -583,11 +582,17 @@ const SideMenu = observer(({ onMenuToggle }) => { { + const m = sightArticles.get(route?.governor_appeal + "_ru")?.media; + const mediaId = Array.isArray(m) ? m[0]?.id : m?.id; + return mediaId ? getMediaUrl(mediaId) : undefined; + })()} + isOpen={isWidgetOpen} style={{ transform: isWidgetOpen ? "translateX(0)" : "translateX(-200%)", transition: "transform 0.5s ease", zIndex: -1, + pointerEvents: isWidgetOpen ? "auto" : "none", }} widgetLabel={ selectedLanguage == "ru" diff --git a/src/client/src/components/widgets/AppealWidget.jsx b/src/client/src/components/widgets/AppealWidget.jsx index 9b46505..d060c08 100644 --- a/src/client/src/components/widgets/AppealWidget.jsx +++ b/src/client/src/components/widgets/AppealWidget.jsx @@ -1,11 +1,29 @@ +import { useRef, useEffect } from 'react' import '../../styles/AppealWidget.css' +import { TouchableLayout } from '../TouchableLayout' + +function AppealWidget({widgetImgPath, widgetLabel, widgetText, style, isOpen}) { + const stopProp = (e) => { e.stopPropagation(); e.preventDefault(); }; + const layoutRef = useRef(null); + + useEffect(() => { + if (isOpen && layoutRef.current) { + const scrollable = layoutRef.current.querySelector('.scrollable'); + if (scrollable) scrollable.scrollTop = 0; + } + }, [isOpen]); -function AppealWidget({widgetImgPath, widgetLabel, widgetText, style}) { return ( -
- +
+ {widgetImgPath && }
{widgetLabel}
-
{widgetText}
+ +
{widgetText}
+
); } diff --git a/src/client/src/styles/AppealWidget.css b/src/client/src/styles/AppealWidget.css index fd6bf47..e750481 100644 --- a/src/client/src/styles/AppealWidget.css +++ b/src/client/src/styles/AppealWidget.css @@ -5,37 +5,57 @@ flex-direction: column; align-items: center; width: 420px; + max-height: calc(100vh - 150px - 100px); border-radius: 10px; - background: linear-gradient( + background: + linear-gradient( 114deg, rgba(255, 255, 255, 0) 8.71%, rgba(255, 255, 255, 0.16) 69.69% ), - var(--carrier-main, #006F3A); + var(--carrier-left, #006F3A); box-sizing: border-box; + overflow: hidden; + touch-action: none; } .dynamic-widget-image { - border-radius-top-left: 10px; - border-radius-top-right: 10px; padding-top: 4px; margin-left: 4px; margin-right: 4px; width: 412px; + border-radius: 6px 6px 0 0; + object-fit: cover; } .dynamic-widget-label { width: 380px; - margin-top: 29px; + margin-top: 20px; + margin-bottom: 5px; font-size: 20px; font-weight: 500; } -.dynamic-widget-text { - margin-top: 16px; +.dynamic-widget-text-scroll { + margin-top: 0; margin-bottom: 25px; - width: 380px; - font-size: 16px; - font-weight: 300; - line-height: 150%; + flex: 1; + min-height: 0; +} + +.dynamic-widget-text-scroll.scrollable-container { + margin: 0 20px; +} + +.dynamic-widget-text-scroll .scrollable-viewport { + padding-bottom: 20px; + box-sizing: border-box; +} + +.dynamic-widget-text { + font-size: 14px; + font-weight: 400; + line-height: 190%; + padding-bottom: 10px; + padding-right: 5px; } diff --git a/src/client/src/styles/ListOfSights.css b/src/client/src/styles/ListOfSights.css index 9893ac4..371d125 100644 --- a/src/client/src/styles/ListOfSights.css +++ b/src/client/src/styles/ListOfSights.css @@ -1,3 +1,25 @@ +@keyframes pulse-chevron { + 0% { transform: rotate(var(--r, 0deg)) translateY(0px) scale(1); } + 40% { transform: rotate(var(--r, 0deg)) translateY(-4px) scale(1.12); } + 60% { transform: rotate(var(--r, 0deg)) translateY(-5px) scale(1.14); } + 100% { transform: rotate(var(--r, 0deg)) translateY(0px) scale(1); } +} + +.chevron-svg { + font-size: 20px; + animation: pulse-chevron 1.2s ease-in-out infinite; + animation-play-state: paused; + will-change: transform; +} + +.chevron-svg.is-idle { + animation-play-state: running; +} + +.chevron-svg.is-open { + --r: 180deg; +} + .right-widget { position: fixed; right: 32px; @@ -17,7 +39,7 @@ rgba(255, 255, 255, 0) 8.71%, rgba(255, 255, 255, 0.16) 69.69% ), - var(--carrier-right, #006f3a); + var(--carrier-right, #806C59); color: white; max-height: 68px; @@ -65,7 +87,7 @@ background-color: color-mix( in srgb, - var(--carrier-right, #006f3a) 80%, + var(--carrier-right, #806C59) 80%, black ); } @@ -198,7 +220,7 @@ rgba(255, 255, 255, 0) 8.71%, rgba(255, 255, 255, 0.16) 69.69% ), - var(--carrier-right, #006f3a); + var(--carrier-right, #806C59); max-height: calc(100vh - 128px); } @@ -241,7 +263,7 @@ rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.04) 100% ), - rgba(var(--carrier-right-rgb, 0, 111, 58), 0.72); + rgba(var(--carrier-right-rgb, 128, 108, 89), 0.72); box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset; box-sizing: border-box; color: white; @@ -250,6 +272,16 @@ min-width: 0; } +.sight-frame-title:not(.intro-title) { + background: + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 100% + ), + rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4); +} + .sight-frame-title p { word-wrap: break-word; overflow-wrap: break-word; @@ -308,7 +340,7 @@ background: linear-gradient( to right, transparent 35%, - color-mix(in srgb, var(--carrier-right, #006f3a) 80%, black) 50%, + color-mix(in srgb, var(--carrier-right, #806C59) 80%, black) 50%, transparent 65% ); border-radius: 3px; @@ -336,7 +368,7 @@ width: 100%; display: flex; align-items: center; - justify-content: space-around; + justify-content: space-evenly; border-radius: 0px 0px 10px 10px; border-top: 1px solid rgba(255, 255, 255, 0.8); background: @@ -345,7 +377,7 @@ rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100% ), - rgba(var(--carrier-right-rgb, 0, 111, 58), 0.4); + rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4); box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset; backdrop-filter: blur(10px); box-sizing: border-box; @@ -360,7 +392,6 @@ font-style: normal; font-weight: 400; padding: 8px 12px; - white-space: nowrap; transition: background-color 0.1s ease, @@ -368,8 +399,8 @@ } .sight-frame-menu-point.active { - border-bottom: 2px solid #fff; font-weight: 600; + border-bottom: 2px solid #fff; } .sight-frame-text-wrapper::-webkit-scrollbar-track { @@ -751,7 +782,7 @@ border-radius: 32px; right: 20px; bottom: 20px; - background: var(--carrier-right, #006f3a); + background: var(--carrier-right, #806C59); z-index: 9999; display: flex; } diff --git a/src/client/src/styles/SideMenu.css b/src/client/src/styles/SideMenu.css index ea3bc15..4d64958 100644 --- a/src/client/src/styles/SideMenu.css +++ b/src/client/src/styles/SideMenu.css @@ -39,7 +39,7 @@ .side-menu-buttons { width: 220px; - margin-top: 260px; + margin-top: 40px; } .side-menu-button { diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index ee1d3cf..6ffcfbd 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -288,7 +288,7 @@ export const CarrierCreatePage = observer(() => { } />

- Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет + Используется в: виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет

diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx index 05d232e..84ad32c 100644 --- a/src/pages/Carrier/CarrierEditPage/index.tsx +++ b/src/pages/Carrier/CarrierEditPage/index.tsx @@ -30,7 +30,11 @@ import { UploadMediaDialog, } from "@shared"; -type ColorFields = { main_color: string; left_color: string; right_color: string }; +type ColorFields = { + main_color: string; + left_color: string; + right_color: string; +}; const colorFields = (data: ColorFields) => ({ main_color: data.main_color, @@ -130,7 +134,7 @@ export const CarrierEditPage = observer(() => { carrierData.ru?.slogan || "", carrierData.ru?.logo || "", "ru", - colors + colors, ); setEditCarrierData( carrierData.en?.full_name || "", @@ -138,7 +142,7 @@ export const CarrierEditPage = observer(() => { carrierData.en?.city_id || 0, carrierData.en?.slogan || "", carrierData.en?.logo || "", - "en" + "en", ); setEditCarrierData( carrierData.zh?.full_name || "", @@ -146,7 +150,7 @@ export const CarrierEditPage = observer(() => { carrierData.zh?.city_id || 0, carrierData.zh?.slogan || "", carrierData.zh?.logo || "", - "zh" + "zh", ); setInitialCityName(carrierData.ru?.city || ""); } @@ -185,7 +189,7 @@ export const CarrierEditPage = observer(() => { editCarrierData.city_id, editCarrierData[language].slogan, media.id, - language + language, ); }; @@ -267,7 +271,7 @@ export const CarrierEditPage = observer(() => { Number(e.target.value), editCarrierData[language].slogan, editCarrierData.logo, - language + language, ) } > @@ -291,7 +295,7 @@ export const CarrierEditPage = observer(() => { editCarrierData.city_id, editCarrierData[language].slogan, editCarrierData.logo, - language + language, ) } /> @@ -308,7 +312,7 @@ export const CarrierEditPage = observer(() => { editCarrierData.city_id, editCarrierData[language].slogan, editCarrierData.logo, - language + language, ) } /> @@ -324,7 +328,7 @@ export const CarrierEditPage = observer(() => { editCarrierData.city_id, e.target.value, editCarrierData.logo, - language + language, ) } /> @@ -342,12 +346,13 @@ export const CarrierEditPage = observer(() => { editCarrierData[language].slogan, editCarrierData.logo, language, - { ...colorFields(editCarrierData), main_color: val } + { ...colorFields(editCarrierData), main_color: val }, ) } />

- Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет + Используется в: значки на карте, скопление достопримечательностей + на карте, информационный виджет

@@ -362,12 +367,13 @@ export const CarrierEditPage = observer(() => { editCarrierData[language].slogan, editCarrierData.logo, language, - { ...colorFields(editCarrierData), left_color: val } + { ...colorFields(editCarrierData), left_color: val }, ) } />

- Используется в: боковое меню, левый виджет достопримечательности + Используется в: виджет обращений, боковое меню (фон, список + остановок), левый виджет достопримечательности

@@ -382,12 +388,13 @@ export const CarrierEditPage = observer(() => { editCarrierData[language].slogan, editCarrierData.logo, language, - { ...colorFields(editCarrierData), right_color: val } + { ...colorFields(editCarrierData), right_color: val }, ) } />

- Используется в: список достопримечательностей, страница достопримечательности + Используется в: список достопримечательностей (фон, карточки), + правый виджет достопримечательности

@@ -465,7 +472,7 @@ export const CarrierEditPage = observer(() => { editCarrierData.city_id, editCarrierData[language].slogan, "", - language + language, ); setIsDeleteLogoModalOpen(false); }} diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx index ee67058..ca6a1e7 100644 --- a/src/pages/Route/route-preview/LeftSidebar.tsx +++ b/src/pages/Route/route-preview/LeftSidebar.tsx @@ -125,7 +125,12 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { )} {/* Кнопки — .side-menu-buttons */} -
+
0 ? 40 : 260, + }} + >