diff --git a/src/pages/api/concierge-landing.ts b/src/pages/api/concierge-landing.ts index 2a2db368..13ebcace 100644 --- a/src/pages/api/concierge-landing.ts +++ b/src/pages/api/concierge-landing.ts @@ -17,6 +17,19 @@ import { type LandingPayload = { text: string; suggestions: string[] }; +/* Human backstop: the organic greeting is a paid call, gated client-side + on the panel being open (a real click). This rejects known crawlers and + header-less scripted requests so we never pay a machine that reaches the + route anyway. Greeting is non-critical — a blocked caller just gets no + line, never an error. */ +const BOT_UA_RE = + /bot|crawl|spider|slurp|mediapartners|ahrefs|semrush|mj12|dotbot|bingpreview|facebookexternalhit|embedly|slackbot|telegrambot|whatsapp|headless|phantomjs|python-requests|curl\/|wget|go-http-client|scrapy|yandex(?:bot)?|baidu|duckduckbot/i; + +function isBotUserAgent(ua: string | undefined): boolean { + if (!ua || !ua.trim()) return true; + return BOT_UA_RE.test(ua); +} + async function callClaude( system: string, user: string, @@ -268,6 +281,14 @@ export default async function handler( return res.status(200).json({ text: '' }); } + const ua = + typeof req.headers['user-agent'] === 'string' + ? req.headers['user-agent'] + : undefined; + if (isBotUserAgent(ua)) { + return res.status(200).json({ text: '' }); + } + const { url, title, prevQuery, prevAnswer, lang, mode } = (req.body ?? {}) as { url?: string; diff --git a/src/pages/uxcg/[slug].tsx b/src/pages/uxcg/[slug].tsx index 99fa8285..0b6ec79e 100644 --- a/src/pages/uxcg/[slug].tsx +++ b/src/pages/uxcg/[slug].tsx @@ -4,8 +4,6 @@ import { getStrapiQuestions } from '@uxcore/api/questions'; import { getTags } from '@uxcore/api/tags'; import SeoGenerator from '@uxcore/components/SeoGenerator'; import UXCGModal from '@uxcore/components/UXCGModal'; -import UXCGModalMobile from '@uxcore/components/UXCGModalMobile'; -import useMobile from '@uxcore/hooks/useMobile'; import UXCGLayout from '@uxcore/layouts/UXCGLayout'; import { getUXCGRedirects } from '@uxcore/lib/getUXCGRedirects'; import { @@ -67,11 +65,9 @@ const Slug: FC = ({ : typeof searchQuery === 'string' ? searchQuery : undefined; - const { isMobile } = useMobile()[1]; const [isModalClosed, setIsModalClosed] = useState(true); const [questionId, setQuestionId] = useState(id); - const [clickedQuestionId, setClickedQuestionId] = useState(null); const [answerId, setAnswerId] = useState(null); const [searchValue, setSearchValue] = useState(searchTerm as string); const [isCopyTooltipVisible, setIsCopyTooltipVisible] = useState(false); @@ -112,7 +108,7 @@ const Slug: FC = ({ router.push(`/uxcg/${newSlug}`, undefined, { scroll: false }); setQuestionId(Number(newId)); }, - [router, isMobile], + [router], ); // Copy link to clipboard @@ -130,7 +126,6 @@ const Slug: FC = ({ const handleQuestionClick = useCallback( (number, slug) => { handleSelectedQuestion(Number(number), slug); - setClickedQuestionId(Number(number)); }, [handleSelectedQuestion], ); @@ -179,48 +174,27 @@ const Slug: FC = ({ createdDate={'2021-07-16'} />

{title}

- {isMobile ? ( - - ) : ( - - )} + {/* UXCGModal carries its own mobile layout now (pills, dark theme, + loaders) — the legacy UXCGModalMobile swap is retired. */} + = ({ }) => { const [activeBiasNumber, setActiveBiasNumber] = useState(null); const [isModalClosed, setIsModalClosed] = useState(true); - const [{ toggleIsProductView }, { isProductView }] = useUXCoreGlobals(); + const [, { isProductView }] = useUXCoreGlobals(); const router = useRouter(); - const { isMobile } = useMobile()[1]; const { locale } = router as TRouter; const slugs = { @@ -149,42 +146,25 @@ const UXCoreIds: FC = ({ modifiedDate={currentActiveBias.updatedAt} />

{currentActiveBias.title}

- {isMobile ? ( - - ) : ( - - )} + {/* UXCoreModal carries its own mobile layout now (pills, dark theme, + loaders) — the legacy UXCoreModalMobile swap is retired. */} + {biases[locale] ? ( = ({ > {!customTip && ( - + {text} diff --git a/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.module.scss b/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.module.scss index 69552693..bb31e361 100644 --- a/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.module.scss +++ b/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.module.scss @@ -3,11 +3,19 @@ position: fixed; top: 0; left: 0; - background-color: rgba(0, 0, 0, 0.5); width: 100vw; height: 100vh; - z-index: 10; + height: 100dvh; + background-color: rgba(0, 0, 0, 0.55); + // Must sit above the UXCG modal overlay (z-index 10). + z-index: 30; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 16px; + box-sizing: border-box; } - width: calc(100% - 10px); + width: 100%; + max-width: 420px; } diff --git a/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.tsx b/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.tsx index 6361e06c..e9ce688d 100644 --- a/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.tsx +++ b/src/uxcore/components/AnswerBiasLink/Mobile/MobileBiasModal/MobileBiasModal.tsx @@ -25,8 +25,11 @@ const MobileBiasModal: FC = ({ return ( <> {createPortal( -
-
+
+
e.stopPropagation()} + > = ({ withText = true, }) => { const router = useRouter(); + // Touch devices open via this state (hover-open is desktop-only in CSS), + // so the dropdown reliably closes after a language is picked. + const [isOpen, setIsOpen] = useState(false); const locales = ['en', 'hy', 'ru']; const { asPath } = router; @@ -62,6 +64,7 @@ const LanguageSwitcher: FC = ({ }; const closeDropdown = () => { + setIsOpen(false); detectingLangSwitch && handleDetectingLangSwitch(); }; @@ -72,11 +75,19 @@ const LanguageSwitcher: FC = ({ return (
-
+ ); }; diff --git a/src/uxcore/components/Tooltip/Tooltip.module.scss b/src/uxcore/components/Tooltip/Tooltip.module.scss index 516ec001..57fa5e2a 100644 --- a/src/uxcore/components/Tooltip/Tooltip.module.scss +++ b/src/uxcore/components/Tooltip/Tooltip.module.scss @@ -70,7 +70,9 @@ .Popup { width: 100%; left: 50%; - max-width: 380px; + // 380px barely fits a 390px phone — leave breathing room for the + // JS clamp (8px margins) so the popup never touches the screen edge. + max-width: calc(100vw - 16px); } } diff --git a/src/uxcore/components/UXCGModal/UXCGModal.module.scss b/src/uxcore/components/UXCGModal/UXCGModal.module.scss index 41aac734..52a795f8 100644 --- a/src/uxcore/components/UXCGModal/UXCGModal.module.scss +++ b/src/uxcore/components/UXCGModal/UXCGModal.module.scss @@ -123,11 +123,11 @@ } & .ModalHeaderCloseButtonContainer { - width: 75px; + flex-shrink: 0; display: flex; justify-content: flex-end; - align-items: flex-end; - gap: 24px; + align-items: center; + gap: 16px; & .ModalHeaderCloseButton { text-align: right; @@ -325,6 +325,86 @@ } } +.MobileNavButtons { + display: none; +} + +@media (max-width: 800px) { + // Full-screen sheet: the centered-card layout leaked the page behind the + // modal at the bottom edge and let the browser bar clip the nav pills. + .ModalOverlay { + height: 100vh; + height: 100dvh; + align-items: stretch; + justify-content: stretch; + } + + .ModalOverlay .Modal { + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + height: 100dvh; + border: none; + border-radius: 0; + box-sizing: border-box; + // Desktop arrows sit at ±100px outside the modal — off-screen on phones. + // Reserve a bottom strip for the pills so they never cover the rating row. + padding-bottom: 56px; + + .ModalHeader { + flex: 0 0 auto; + } + + // Body takes all remaining height and scrolls; the rating row lives + // inside it so only the pill strip stays pinned to the bottom. + .ModalBody { + flex: 1 1 auto; + min-height: 0; + height: auto !important; + } + + .ModalButtons { + display: none; + } + + .MobileNavButtons { + display: flex; + justify-content: space-between; + position: absolute; + bottom: 8px; + left: 16px; + right: 16px; + z-index: 5; + pointer-events: none; + + .MobileNavButton { + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 18px; + border: none; + border-radius: 20px; + background: #ffffff; + color: #1f1d1a; + font-size: 14px; + font-weight: 600; + cursor: pointer; + pointer-events: auto; + box-shadow: + 0 0 0 1px #d9d2c5, + 0 3px 12px rgba(31, 29, 26, 0.18); + + img { + height: 14px; + filter: invert(1) brightness(0.4); + } + } + } + } +} + @media (max-width: 1440px) { .ModalOverlay { & .Modal { @@ -431,3 +511,20 @@ } } } + +// Specificity must beat the 4-class mobile rule above. +:global(body.darkTheme) + .ModalOverlay + .Modal + .MobileNavButtons + .MobileNavButton { + background: #262b36; + color: #e8eaf0; + box-shadow: + 0 0 0 1px #3a4150, + 0 3px 12px rgba(0, 0, 0, 0.45); + + img { + filter: none; + } +} diff --git a/src/uxcore/components/UXCGModal/UXCGModal.tsx b/src/uxcore/components/UXCGModal/UXCGModal.tsx index f27e7689..20019425 100644 --- a/src/uxcore/components/UXCGModal/UXCGModal.tsx +++ b/src/uxcore/components/UXCGModal/UXCGModal.tsx @@ -1,3 +1,26 @@ +import AnswerContentGenerator from '@uxcore/components/AnswerContentGenerator'; +import LanguageSwitcher from '@uxcore/components/LanguageSwitcher'; +import ModalRaiting from '@uxcore/components/ModalRaiting'; +import RouteLoadingOverlay, { + useRouteLoading, +} from '@uxcore/components/RouteLoadingOverlay'; +import Tag from '@uxcore/components/Tag'; +import Tooltip from '@uxcore/components/Tooltip'; +import Share from '@uxcore/components/UXCGModalSubComponents/Share'; +import modalIntl from '@uxcore/data/modal'; +import useMobile from '@uxcore/hooks/useMobile'; +import useTooltip from '@uxcore/hooks/useTooltip'; +import { + copyToClipboard, + generateSocialLinks, + updateVH, +} from '@uxcore/lib/helpers'; +import type { + QuestionType, + StrapiBiasType, + TagType, +} from '@uxcore/local-types/data'; +import type { TRouter } from '@uxcore/local-types/global'; import cn from 'classnames'; import Image from 'next/image'; import { useRouter } from 'next/router'; @@ -12,22 +35,7 @@ import React, { useState, } from 'react'; -import type { QuestionType, StrapiBiasType, TagType } from '@uxcore/local-types/data'; -import type { TRouter } from '@uxcore/local-types/global'; - -import useMobile from '@uxcore/hooks/useMobile'; -import useTooltip from '@uxcore/hooks/useTooltip'; - -import { copyToClipboard, generateSocialLinks, updateVH } from '@uxcore/lib/helpers'; - -import modalIntl from '@uxcore/data/modal'; - -import AnswerContentGenerator from '@uxcore/components/AnswerContentGenerator'; -import LanguageSwitcher from '@uxcore/components/LanguageSwitcher'; -import ModalRaiting from '@uxcore/components/ModalRaiting'; -import Tag from '@uxcore/components/Tag'; -import Tooltip from '@uxcore/components/Tooltip'; -import Share from '@uxcore/components/UXCGModalSubComponents/Share'; +import ThemeToggle from '@components/ThemeToggle'; import styles from './UXCGModal.module.scss'; @@ -85,6 +93,7 @@ const UXCGModal: FC = ({ const [highlightAnswer, setHighlightAnswer] = useState(false); const [headerHeight, setHeaderHeight] = useState(105); + const [isNavigating, startNavigation] = useRouteLoading(); const modalBodyRef = useRef(null); const modalHeaderRef = useRef(null); @@ -103,12 +112,18 @@ const UXCGModal: FC = ({ nextQuestionId = id === 1 ? 63 : id - 1; } const questionSlug = dir === 'next' ? nextQuestion : prevQuestion; + startNavigation(); onChangeQuestionId(nextQuestionId, questionSlug); } }, - [onChangeQuestionId, id, nextQuestion, prevQuestion], + [onChangeQuestionId, id, nextQuestion, prevQuestion, startNavigation], ); + const handleClose = useCallback(() => { + startNavigation(); + closeModal(); + }, [closeModal, startNavigation]); + const handleHeightCalc = useCallback(() => { updateVH(); @@ -158,7 +173,7 @@ const UXCGModal: FC = ({ if (isOpen) { const arrowClickData: any = {}; - if (e.key === 'Escape') closeModal(); + if (e.key === 'Escape') handleClose(); if (e.key === 'ArrowLeft') { arrowClickData.active = String(id >= 1); arrowClickData.dir = 'prev'; @@ -264,8 +279,15 @@ const UXCGModal: FC = ({ hy: '/hy', }; - const { copyLink, copied, share, answersLabel, relatedQuestionsLabel } = - modalIntl[locale]; + const { + copyLink, + copied, + share, + answersLabel, + relatedQuestionsLabel, + nextLabel, + prevLabel, + } = modalIntl[locale]; const { number, @@ -298,7 +320,7 @@ const UXCGModal: FC = ({ ); return ( -
+
= ({ })}
+ = ({
modal close button
@@ -420,9 +443,10 @@ const UXCGModal: FC = ({ key={index} className={styles.QuestionLink} data-cy={'related-question'} - onClick={() => - handleQuestionClick(question.number, slugs[locale]) - } + onClick={() => { + startNavigation(); + handleQuestionClick(question.number, slugs[locale]); + }} > #{question.number}. @@ -434,8 +458,28 @@ const UXCGModal: FC = ({
)} + +
+
+ +
-
= ({
+
); }; diff --git a/src/uxcore/components/UXCoreModal/UXCoreModal.module.scss b/src/uxcore/components/UXCoreModal/UXCoreModal.module.scss index a78a1d7a..c07c8871 100644 --- a/src/uxcore/components/UXCoreModal/UXCoreModal.module.scss +++ b/src/uxcore/components/UXCoreModal/UXCoreModal.module.scss @@ -13,6 +13,9 @@ display: flex; width: 100vw; height: 100vh; + /* iOS Safari: 100vh includes the area behind the URL bar; dvh tracks the + actually visible viewport so the modal bottom is never cut off. */ + height: 100dvh; position: fixed; top: 0; left: 0; @@ -38,6 +41,7 @@ max-height: calc(100vh - 315px); overflow: auto; overflow-y: overlay; + overscroll-behavior: contain; /* width */ &::-webkit-scrollbar { @@ -123,17 +127,18 @@ border-radius: 8px; background-color: #fff; isolation: isolate; + overflow: hidden; &::before { content: ''; position: absolute; - top: -1px; - bottom: -1px; - left: -1px; - width: calc(33.3333% + 1px); + top: 0; + bottom: 0; + left: 0; + width: 33.3333%; background-color: #e8f0fb; border: 1px solid #e0e0e0; - border-radius: 8px; + border-radius: 7px; transform: translateX(0); transition: transform 450ms cubic-bezier(0.22, 0.95, 0.35, 1), @@ -145,13 +150,13 @@ } &:has(.activeHr)::before { - transform: translateX(calc(100% - 1px)); + transform: translateX(100%); } // OffSec uses Hexens-crimson for the sliding indicator so the // mode shift reads as "different domain", not just "different tab". &:has(.activeOffsec)::before { - transform: translateX(calc(200% - 2px)); + transform: translateX(200%); background-color: #fdecea; border-color: #c8412a; } @@ -174,6 +179,18 @@ svg { transition: fill 300ms ease; } + + & + .switcherItem::after { + content: ''; + position: absolute; + left: 0; + top: 9px; + bottom: 9px; + width: 1px; + background-color: #e0e0e0; + z-index: -1; + pointer-events: none; + } } .activeProduct, @@ -243,6 +260,88 @@ } } +// Crossfade while a Prev/Next navigation is in flight: opacity only — cheap, +// GPU-composited, reliable on every mobile browser. Nav controls stay solid. +.Modal > *:not(.MobileNavButtons):not(.ModalButtons) { + transition: opacity 0.18s ease; +} + +.Modal.biasSwitching > *:not(.MobileNavButtons):not(.ModalButtons) { + opacity: 0.15; +} + +// Mobile-only Prev/Next pills; desktop keeps the side arrows. +.MobileNavButtons { + display: none; +} + +// full chain to outweigh the base .ModalOverlay .Modal … rule in the media query +// Cool blue-grey to match the dark modal palette (#1b1e26 surfaces, #303338 +// borders) — the widget's warm brown pill looks alien inside this modal. +:global(body.darkTheme) + .ModalOverlay + .Modal + .MobileNavButtons + .MobileNavButton { + background: #262b36; + color: #e8eaf0; + box-shadow: + 0 0 0 1px #3a4150, + 0 3px 12px rgba(0, 0, 0, 0.45); + + img { + // caret SVGs are white already — keep them white on the dark pill + filter: none; + } +} + +// Closing the modal navigates to the list route; cover the modal while that +// hop resolves so a slow connection doesn't read as a dead tap. +.ClosingOverlay { + position: absolute; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.55); + backdrop-filter: blur(1.5px); + animation: modalCloseOverlayIn 0.15s ease; +} + +.ClosingSpinner { + width: 38px; + height: 38px; + border-radius: 50%; + border: 3px solid rgba(40, 88, 123, 0.25); + border-top-color: #28587b; + animation: modalCloseSpin 0.7s linear infinite; +} + +@keyframes modalCloseSpin { + to { + transform: rotate(360deg); + } +} + +@keyframes modalCloseOverlayIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +:global(body.darkTheme) .ModalOverlay .ClosingOverlay { + background: rgba(15, 17, 22, 0.6); +} + +:global(body.darkTheme) .ModalOverlay .ClosingSpinner { + border-color: rgba(127, 179, 213, 0.25); + border-top-color: #7fb3d5; +} + .hyLang { * { font-family: 'NotoSansArmenian-Regular', sans-serif; @@ -590,6 +689,10 @@ ol { color: rgba(218, 218, 218, 0.7) !important; } + .ModalBodyContent .switcher .switcherItem + .switcherItem::after { + background-color: #303338 !important; + } + .ModalBodyContent .switcher .switcherItem svg { fill: rgba(218, 218, 218, 0.7) !important; } @@ -631,3 +734,109 @@ ol { :global(body.darkTheme) .span { border-left-color: #4d5566; } + +// Mobile: three long labels (RU "Наступательная кибербезопасность" etc.) +// can't share one horizontal row — stack the use-case switcher vertically +// and slide the pill on the Y axis instead. Selector chain mirrors the +// base nesting so this later block wins at equal specificity. +@media (max-width: 800px) { + // Full-screen modal: covers the whole viewport including the navbar strip, + // so no page background peeks through above or below. + .ModalOverlay { + align-items: flex-end; + + .Modal { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + border: none; + border-radius: 0; + + .ModalBody { + max-height: none; + flex: 1 1 auto; + // room for the floating Prev/Next pills + padding-bottom: 76px; + } + + .MobileNavButtons { + display: flex; + justify-content: space-between; + position: absolute; + bottom: 16px; + left: 16px; + right: 16px; + z-index: 5; + pointer-events: none; + + .MobileNavButton { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 18px; + border: none; + border-radius: 20px; + background: #ffffff; + color: #1f1d1a; + font-family: inherit; + font-size: 14px; + font-weight: 600; + box-shadow: + 0 0 0 1px #d9d2c5, + 0 3px 12px rgba(31, 29, 26, 0.18); + cursor: pointer; + + img { + height: 14px; + // caret SVGs are white — ink them dark for the light pill + filter: invert(1) brightness(0.4); + } + } + } + } + } + + .ModalOverlay .Modal .ModalBody .ModalBodyContent .switcher { + flex-direction: column; + + &::before { + width: 100%; + height: 33.3333%; + transform: translateY(0); + } + + &:has(.activeHr)::before { + transform: translateY(100%); + } + + &:has(.activeOffsec)::before { + transform: translateY(200%); + } + + .switcherItem { + width: 100%; + // width:100% + side padding spills past the viewport without border-box + box-sizing: border-box; + justify-content: flex-start; + padding: 10px 14px; + gap: 10px; + text-align: left; + + svg { + flex-shrink: 0; + } + + & + .switcherItem::after { + top: 0; + bottom: auto; + left: 12px; + right: 12px; + width: auto; + height: 1px; + } + } + } +} diff --git a/src/uxcore/components/UXCoreModal/UXCoreModal.tsx b/src/uxcore/components/UXCoreModal/UXCoreModal.tsx index afaa7903..49366b2f 100644 --- a/src/uxcore/components/UXCoreModal/UXCoreModal.tsx +++ b/src/uxcore/components/UXCoreModal/UXCoreModal.tsx @@ -64,15 +64,29 @@ const UXCoreModal: FC = ({ }) => { const router = useRouter(); const [{ setUseCase }, { isOffsecView }] = useUXCoreGlobals(); + // OffSec (Cybersecurity) is a work-in-progress use case: surface it only on + // the dev preview, keep it dark on staging/prod until it's ready. Gating the + // active state too (not just the switch) means a persisted isOffsecView=true + // from a prior dev session can't leak the layer onto a public build. + const offsecEnabled = + (process.env.NEXT_PUBLIC_ENV || '').toLowerCase() === 'dev'; + const offsecActive = offsecEnabled && isOffsecView; const [isCopyTooltipVisible, setIsCopyTooltipVisible] = useState(false); const [isQuestionHovered, setIsQuestionHovered] = useState(false); const [isLoading, setIsLoading] = useState(false); + // Closing the modal navigates back to the list route; on a slow connection + // that hop is visibly delayed, so dim + spin until the route settles. + const [isClosing, setIsClosing] = useState(false); const tooltipTimer: { current: any } = useRef(); const modalBodyRef = useRef(null); const shareUrl = typeof window !== 'undefined' ? window.location.href : ''; const { locale } = router as TRouter; const isOpen = !!biasNumber && data; + // biasNumber flips instantly on Prev/Next tap, while data (and the title) + // arrives only after the route change — dim everything until both agree so + // the swap reads as a single motion instead of a staggered repaint. + const isBiasSwitching = !!data && Number(data.number) !== Number(biasNumber); const handleUseCaseClick = useCallback( e => { @@ -121,12 +135,42 @@ const UXCoreModal: FC = ({ setIsModalClosed(false); }, []); + const handleClose = useCallback(() => { + setIsClosing(true); + onClose(); + }, [onClose]); + + useEffect(() => { + const stop = () => setIsClosing(false); + router.events.on('routeChangeComplete', stop); + router.events.on('routeChangeError', stop); + return () => { + router.events.off('routeChangeComplete', stop); + router.events.off('routeChangeError', stop); + }; + }, [router.events]); + + // Each bias starts reading from the top; keep the page behind the modal + // from scrolling along (iOS scroll chaining). + useEffect(() => { + modalBodyRef.current?.scrollTo?.(0, 0); + }, [biasNumber]); + + useEffect(() => { + if (!isOpen) return; + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prevOverflow; + }; + }, [isOpen]); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isOpen) return; const arrowClickData: any = {}; - if (e.key === 'Escape') onClose(); + if (e.key === 'Escape') handleClose(); if (e.key === 'ArrowLeft') { arrowClickData.active = String(biasNumber >= 1); arrowClickData.dir = 'prev'; @@ -145,7 +189,7 @@ const UXCoreModal: FC = ({ // @ts-ignore document.removeEventListener('keydown', handleKeyDown); }; - }, [biasNumber, handleArrowClick, isOpen]); + }, [biasNumber, handleArrowClick, handleClose, isOpen]); useEffect(() => { data ? setIsLoading(false) : setIsLoading(true); @@ -166,6 +210,8 @@ const UXCoreModal: FC = ({ hrText, offsecText, offsecComingSoon, + nextLabel, + prevLabel, } = modalIntl[locale]; const { linkedIn, facebook, tweeter } = generateSocialLinks( @@ -176,10 +222,11 @@ const UXCoreModal: FC = ({ return isLoading ? ( ) : ( -
+
= ({ number={data.number} productValue={productValue} managementValue={managementValue} - onClose={onClose} + onClose={handleClose} linkedIn={linkedIn} facebook={facebook} tweeter={tweeter} @@ -220,7 +267,7 @@ const UXCoreModal: FC = ({ data-cy="switch-product" data-usecase="product" className={cn(styles.switcherItem, { - [styles.activeProduct]: !isOffsecView && !isProductView, + [styles.activeProduct]: !offsecActive && !isProductView, })} > @@ -231,29 +278,31 @@ const UXCoreModal: FC = ({ data-cy="switch-hr" data-usecase="hr" className={cn(styles.switcherItem, { - [styles.activeHr]: !isOffsecView && isProductView, + [styles.activeHr]: !offsecActive && isProductView, })} > {hrText}
-
- {isOffsecView ? : } - {offsecText} -
+ {offsecEnabled && ( +
+ {offsecActive ? : } + {offsecText} +
+ )}
- {isOffsecView ? ( + {offsecActive ? ( (() => { const offsecContent = getOffsecBiasContent(biasNumber); return offsecContent ? ( @@ -272,7 +321,7 @@ const UXCoreModal: FC = ({ )}
- {!isOffsecView && data.title && ( + {!offsecActive && data.title && ( )} {questions.length > 0 && ( @@ -298,8 +347,28 @@ const UXCoreModal: FC = ({
)} + +
+
+ +
-
= ({
+ {isClosing && ( +
+ +
+ )} ); }; diff --git a/src/uxcore/components/UXCoreModalParts/UXCoreModalHeader/UXCoreModalHeader.module.scss b/src/uxcore/components/UXCoreModalParts/UXCoreModalHeader/UXCoreModalHeader.module.scss index 821fa238..ed7b55ad 100644 --- a/src/uxcore/components/UXCoreModalParts/UXCoreModalHeader/UXCoreModalHeader.module.scss +++ b/src/uxcore/components/UXCoreModalParts/UXCoreModalHeader/UXCoreModalHeader.module.scss @@ -269,17 +269,23 @@ margin-top: 6px; } + // Theme toggle + language + close live as one centered row pinned + // to the modal's top-right corner (mirrors desktop). The language + // switcher button carries chunky paddings at this breakpoint, so the + // row is locked to 24px height and centered instead of flex-end — + // otherwise the sun icon and "EN" drift apart vertically. .LangAndCloseBtn { + position: absolute; + top: 14px; + right: 20px; + height: 24px; + align-items: center; justify-content: flex-end; + gap: 14px; .closeBtn { - position: absolute; - } - - .closeBtn { - position: absolute; - right: 20px; - top: 16px; + position: static; + padding: 0; } } } diff --git a/src/uxcore/components/UserProfile/UserProfile.module.scss b/src/uxcore/components/UserProfile/UserProfile.module.scss index 87791469..a2d5f3c0 100644 --- a/src/uxcore/components/UserProfile/UserProfile.module.scss +++ b/src/uxcore/components/UserProfile/UserProfile.module.scss @@ -161,6 +161,8 @@ @media (max-width: 901px) { .userProfile { width: 100%; + // width:100% + side padding overflows the wrapper without border-box. + box-sizing: border-box; background-size: cover; background-position: center; background-repeat: no-repeat; @@ -187,8 +189,6 @@ @media (max-width: 800px) { .userProfile { - max-width: unset; - .wrapper { .userInfo { justify-content: center; @@ -222,7 +222,8 @@ } .btnWrapperMobile { - width: 97vw; + // 97vw ignored the side margins and ran flush into the right screen edge. + width: auto; margin: 0 12px; } } diff --git a/src/uxcore/components/UserProfile/UserProfile.tsx b/src/uxcore/components/UserProfile/UserProfile.tsx index 70c32011..091c6c4c 100644 --- a/src/uxcore/components/UserProfile/UserProfile.tsx +++ b/src/uxcore/components/UserProfile/UserProfile.tsx @@ -137,7 +137,7 @@ const UserProfile: FC = ({ {`${ levelTitle ? levelTitle : guestLevel.attributes.lavelName } (${lvl} ${userLevel || 0}) ${ - !!userLevel ? '' : guestLevel?.attributes?.description + !!userLevel ? '' : guestLevel?.attributes?.description || '' } `} {!loggedIn ? ( diff --git a/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx b/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx index a1d58dda..9f2e82fd 100644 --- a/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx +++ b/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx @@ -2,7 +2,6 @@ import { getMyInfo } from '@uxcore/api/strapi'; import { userInfoUpdate } from '@uxcore/api/uxcat/settings'; import { getUserInfo } from '@uxcore/api/uxcat/users-me'; import MoonIcon from '@uxcore/assets/icons/MoonIcon'; -import PodcastIcon from '@uxcore/assets/icons/PodcastIcon'; import SunIcon from '@uxcore/assets/icons/SunIcon'; import { GlobalContext } from '@uxcore/components/Context/GlobalContext'; import LanguageSwitcher from '@uxcore/components/LanguageSwitcher'; @@ -21,9 +20,7 @@ import { FC, useContext, useEffect, useMemo, useState } from 'react'; import styles from './MobileHeader.module.scss'; type MobileHeaderProps = { - setHeaderPodcastOpen?: (updater: (prev: boolean) => boolean) => void; setUpdatedSettingsInfo?: (data: UserTypes) => void; - isPodcastOpen?: boolean; changeUserUrl?: boolean; instantSave?: boolean; isUserProfile?: boolean; @@ -35,8 +32,6 @@ type MobileHeaderProps = { blockLanguageSwitcher?: boolean; }; const MobileHeader: FC = ({ - setHeaderPodcastOpen, - isPodcastOpen, changeUserUrl, setSelectedTitle, isUserProfile, @@ -89,9 +84,6 @@ const MobileHeader: FC = ({ }; const title = changedTitle ? userInfo?.title : userInfo?.title; - const openPodcast = () => { - setHeaderPodcastOpen(prev => !prev); - }; const handleOpenSettings = () => { setOpenSettings(true); @@ -168,16 +160,6 @@ const MobileHeader: FC = ({
- {router.pathname === '/uxcore' && locale !== 'hy' && ( -
- -
- )}
@@ -281,9 +285,7 @@ const MobileView: FC = ({

UX CORE

uxcore.io

{description}

- - {explanationLink.title} - +