This commit is contained in:
Raphael Elita 2025-08-21 23:53:01 +02:00
parent f3c97db5f8
commit d22ef62afb
2 changed files with 83 additions and 31 deletions

View File

@ -293,6 +293,35 @@ const RightBrand = styled.div`
} }
`; `;
function usePreloadImages(urls: string[]) {
useEffect(() => {
if (!urls?.length) return;
const unique = Array.from(new Set(urls.filter(Boolean)));
unique.forEach((url) => {
if (!url) return;
// Проверяем, есть ли уже preload в <head>
const exists = document.querySelector<HTMLLinkElement>(
`link[rel="preload"][as="image"][href="${url}"]`
);
if (!exists) {
const link = document.createElement("link");
link.rel = "preload";
link.as = "image";
link.href = url;
document.head.appendChild(link);
}
// Дополнительно сразу грузим в memory cache
const img = new Image();
img.src = url;
});
}, [urls]);
}
/* ===================== Component ===================== */ /* ===================== Component ===================== */
export default function KeyBusinessSegments() { export default function KeyBusinessSegments() {
@ -304,20 +333,19 @@ export default function KeyBusinessSegments() {
const segments = useMemo(() => mapSegments(data), [data]); const segments = useMemo(() => mapSegments(data), [data]);
// --- предзагрузка иконок --- // --- предзагрузка иконок ---
useEffect(() => { // собираем все картинки для предзагрузки
if (!segments?.length) return; const preloadUrls = useMemo(() => {
const urls = segments.flatMap((seg) => if (!segments?.length) return [];
seg.info.flatMap((block) => return segments.flatMap((seg) =>
block.items seg.info.flatMap((block) => [
.map((item) => item.infoLogo) block.brandLogo, // ← бренд
.filter((u): u is string => Boolean(u)) ...block.items.map((item) => item.infoLogo), // ← иконки
) ])
); ).filter((u): u is string => Boolean(u));
urls.forEach((url) => {
const img = new Image();
img.src = url;
});
}, [segments]); }, [segments]);
// предзагрузка всех логотипов
usePreloadImages(preloadUrls);
// --------------------------- // ---------------------------
@ -328,11 +356,42 @@ export default function KeyBusinessSegments() {
const denom = Math.max(1, N - 1); const denom = Math.max(1, N - 1);
const defaultCenterIndex = useMemo(() => Math.floor(N / 2), [N]); const defaultCenterIndex = useMemo(() => Math.floor(N / 2), [N]);
const [activeIndex, setActiveIndex] = useState<number>(defaultCenterIndex); const [activeIndex, _setActiveIndex] = useState<number>(defaultCenterIndex);
const [hoverIndex, setHoverIndex] = useState<number | null>(null); const [hoverIndex, setHoverIndex] = useState<number | null>(null);
const [isOverflow, setIsOverflow] = useState(false); const [isOverflow, setIsOverflow] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const lockTimer = useRef<number | null>(null);
// обёртка для смены индекса
const setActiveIndex = (idx: number) => {
if (idx === activeIndex) return;
_setActiveIndex(idx);
setIsLocked(true);
if (lockTimer.current) clearTimeout(lockTimer.current);
lockTimer.current = window.setTimeout(() => {
setIsLocked(false);
}, 250); // чуть больше transition
};
useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
let raf = 0;
const onScroll = () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
if (isLocked) return; // 🔒 во время анимации не пересчитываем
setActiveIndex(indexFromScroll(el.scrollLeft));
});
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, [denom, isLocked]);
/* ===== reserve-height measuring ===== */ /* ===== reserve-height measuring ===== */
const gridWrapRef = useRef<HTMLDivElement>(null); const gridWrapRef = useRef<HTMLDivElement>(null);
const [gridMinHeight, setGridMinHeight] = useState(0); const [gridMinHeight, setGridMinHeight] = useState(0);
@ -393,20 +452,6 @@ export default function KeyBusinessSegments() {
return () => window.removeEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize);
}, [defaultCenterIndex, N]); }, [defaultCenterIndex, N]);
useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
let raf = 0;
const onScroll = () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
setActiveIndex(indexFromScroll(el.scrollLeft));
});
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, [denom]);
const realActiveIndex = hoverIndex ?? activeIndex; const realActiveIndex = hoverIndex ?? activeIndex;
const activeSegment = segments[Math.min(Math.max(realActiveIndex, 0), N - 1)]; const activeSegment = segments[Math.min(Math.max(realActiveIndex, 0), N - 1)];
@ -471,9 +516,16 @@ export default function KeyBusinessSegments() {
{block.items.map((item) => ( {block.items.map((item) => (
<BulletItem key={item.id} $span={item.span}> <BulletItem key={item.id} $span={item.span}>
<ItemRow> <ItemRow>
<CheckDot <img
src={item.infoLogo}
alt=""
style={{ style={{
backgroundImage: item.infoLogo ? `url(${item.infoLogo})` : undefined, width: 32,
height: 32,
borderRadius: 4,
objectFit: "contain",
background: "#16a34a",
padding: 4,
}} }}
/> />
<Text18_400_dom <Text18_400_dom

View File

@ -405,7 +405,7 @@ const Row = styled.div<{ $gap: number; $hidden?: boolean }>`
display: flex; display: flex;
gap: ${(p) => p.$gap}px; gap: ${(p) => p.$gap}px;
visibility: ${(p) => (p.$hidden ? "hidden" : "visible")}; visibility: ${(p) => (p.$hidden ? "hidden" : "visible")};
padding: 0px 10px; margin-left:10px;
`; `;
const Frame = styled.div` const Frame = styled.div`