import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core"; import { Carousel } from "@mantine/carousel"; import { useState, useEffect, ReactNode, useRef, useCallback, useMemo, } from "react"; import { useRouter } from "@tanstack/react-router"; interface TabItem { label: string; content: ReactNode; } interface SwipeableTabsProps { tabs: TabItem[]; defaultTab?: number; onTabChange?: (index: number, tab: TabItem) => void; } function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, }: SwipeableTabsProps) { const router = useRouter(); const search = router.state.location.search as any; const [embla, setEmbla] = useState(null); const getActiveTabFromUrl = useCallback(() => { const urlTab = search?.tab; if (typeof urlTab === "string") { const tabIndex = tabs.findIndex( (tab) => tab.label.toLowerCase() === urlTab.toLowerCase() ); return tabIndex !== -1 ? tabIndex : defaultTab; } return defaultTab; }, [search?.tab, tabs, defaultTab]); const [activeTab, setActiveTab] = useState(getActiveTabFromUrl); const [rootRef, setRootRef] = useState(null); const controlsRefs = useRef>({}); const slideRefs = useRef>({}); const [carouselHeight, setCarouselHeight] = useState("auto"); const changeTab = useCallback( (index: number) => { if (index === activeTab || index < 0 || index >= tabs.length) return; setActiveTab(index); embla?.scrollTo(index); onTabChange?.(index, tabs[index]); const tabLabel = tabs[index].label.toLowerCase(); if (typeof window !== "undefined") { const url = new URL(window.location.href); url.searchParams.set("tab", tabLabel); window.history.replaceState(null, "", url.toString()); } }, [activeTab, tabs, embla, onTabChange] ); const handleEmblaSelect = useCallback(() => { if (!embla) return; const newIndex = embla.selectedScrollSnap(); changeTab(newIndex); }, [embla, changeTab]); useEffect(() => { if (!embla) return; embla.on("select", handleEmblaSelect); return () => embla.off("select", handleEmblaSelect); }, [embla, handleEmblaSelect]); useEffect(() => { const newActiveTab = getActiveTabFromUrl(); if (newActiveTab !== activeTab) { setActiveTab(newActiveTab); embla?.scrollTo(newActiveTab); } }, [search?.tab]); const updateHeight = useCallback(() => { const activeSlideRef = slideRefs.current[activeTab]; if (activeSlideRef) { const height = activeSlideRef.scrollHeight; setCarouselHeight(height); } }, [activeTab]); useEffect(() => { updateHeight(); }, [activeTab, updateHeight]); useEffect(() => { const timeoutId = setTimeout(updateHeight, 0); return () => clearTimeout(timeoutId); }, [updateHeight]); useEffect(() => { const activeSlideRef = slideRefs.current[activeTab]; if (!activeSlideRef) return; let timeoutId: any; const resizeObserver = new ResizeObserver(() => { clearTimeout(timeoutId); timeoutId = setTimeout(updateHeight, 16); }); resizeObserver.observe(activeSlideRef); return () => { resizeObserver.disconnect(); clearTimeout(timeoutId); }; }, [activeTab, updateHeight]); const setControlRef = useCallback( (index: number) => (node: HTMLSpanElement | null) => { controlsRefs.current[index] = node; }, [] ); const setSlideRef = useCallback( (index: number) => (node: HTMLDivElement | null) => { slideRefs.current[index] = node; }, [] ); return ( {tabs.map((tab, index) => ( changeTab(index)} style={{ flex: 1, padding: "var(--mantine-spacing-sm) var(--mantine-spacing-xs)", textAlign: "center", color: activeTab === index ? "var(--mantine-primary-color-filled)" : "var(--mantine-color-text)", fontWeight: activeTab === index ? 600 : 400, transition: "color 200ms ease, font-weight 200ms ease", backgroundColor: "transparent", border: "none", borderRadius: 0, }} > {tab.label} ))} {tabs.map((tab, index) => ( {tab.content} ))} ); } export default SwipeableTabs;