()
| 344 | |
| 345 | // ─── Main Component ────────────────────────────────────────────────────────── |
| 346 | const Sidebar = () => { |
| 347 | // State |
| 348 | const [isDrawerOpen, setDrawerOpen] = useState(false); |
| 349 | const [isSponsorsOpen, setSponsorsOpen] = useState(false); |
| 350 | const [linePosition, setLinePosition] = useState(null); |
| 351 | const [isLineVisible, setIsLineVisible] = useState(false); |
| 352 | const [hoverLinePosition, setHoverLinePosition] = useState(null); |
| 353 | const [isHoverLineVisible, setIsHoverLineVisible] = useState(false); |
| 354 | const [pendingActivePath, setPendingActivePath] = useState(null); |
| 355 | |
| 356 | // Refs |
| 357 | const sidebarRef = useRef(null); |
| 358 | const sidebarContainerRef = useRef(null); |
| 359 | const itemRefs = useRef({}); |
| 360 | const hoverTimeoutRef = useRef(null); |
| 361 | const hoverDelayTimeoutRef = useRef(null); |
| 362 | |
| 363 | // Hooks |
| 364 | const location = useLocation(); |
| 365 | const navigate = useNavigate(); |
| 366 | const { toggleSearch } = useSearch(); |
| 367 | const { startTransition, isTransitioning } = useTransition(); |
| 368 | const savedSet = useFavoritesSync(); |
| 369 | const isScrolledToBottom = useScrolledToBottom(sidebarContainerRef); |
| 370 | |
| 371 | // Helpers |
| 372 | const findActiveElement = useCallback(() => { |
| 373 | const activePath = pendingActivePath || location.pathname; |
| 374 | for (const category of CATEGORIES) { |
| 375 | const activeItem = category.subcategories.find(sub => activePath === `/${slug(category.name)}/${slug(sub)}`); |
| 376 | if (activeItem) return itemRefs.current[`/${slug(category.name)}/${slug(activeItem)}`]; |
| 377 | } |
| 378 | return null; |
| 379 | }, [location.pathname, pendingActivePath]); |
| 380 | |
| 381 | const updateLinePosition = useCallback(el => { |
| 382 | if (!el || !sidebarRef.current?.offsetParent) return null; |
| 383 | const sidebarRect = sidebarRef.current.getBoundingClientRect(); |
| 384 | const elRect = el.getBoundingClientRect(); |
| 385 | return elRect.top - sidebarRect.top + elRect.height / 2; |
| 386 | }, []); |
| 387 | |
| 388 | const scrollActiveItemIntoView = useCallback(() => { |
| 389 | const activeEl = findActiveElement(); |
| 390 | if (!activeEl || !sidebarContainerRef.current) return; |
| 391 | |
| 392 | const containerRect = sidebarContainerRef.current.getBoundingClientRect(); |
| 393 | const elementRect = activeEl.getBoundingClientRect(); |
| 394 | |
| 395 | const isOutOfView = |
| 396 | elementRect.top < containerRect.top + SCROLL_OFFSET || elementRect.bottom > containerRect.bottom - SCROLL_OFFSET; |
| 397 | |
| 398 | if (isOutOfView) { |
| 399 | sidebarContainerRef.current.scrollTo({ |
| 400 | top: sidebarContainerRef.current.scrollTop + (elementRect.top - containerRect.top) - SCROLL_OFFSET, |
| 401 | behavior: 'smooth' |
| 402 | }); |
| 403 | } |
nothing calls this directly
no test coverage detected