5 changed files with 82 additions and 72 deletions
@ -1,66 +0,0 @@ |
|||||||
import { cn } from '@/lib/utils' |
|
||||||
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' |
|
||||||
import { useTranslation } from 'react-i18next' |
|
||||||
|
|
||||||
type TabDefinition = { |
|
||||||
value: string |
|
||||||
label: string |
|
||||||
onClick?: () => void |
|
||||||
} |
|
||||||
|
|
||||||
export default function TabSwitcher({ |
|
||||||
tabs, |
|
||||||
value, |
|
||||||
className, |
|
||||||
onTabChange, |
|
||||||
threshold = 800 |
|
||||||
}: { |
|
||||||
tabs: TabDefinition[] |
|
||||||
value: string |
|
||||||
className?: string |
|
||||||
onTabChange?: (tab: string) => void |
|
||||||
threshold?: number |
|
||||||
}) { |
|
||||||
const { t } = useTranslation() |
|
||||||
const { deepBrowsing, lastScrollTop } = useDeepBrowsing() |
|
||||||
const activeIndex = tabs.findIndex((tab) => tab.value === value) |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
className={cn( |
|
||||||
'sticky top-12 bg-background z-30 w-full transition-transform', |
|
||||||
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : '', |
|
||||||
className |
|
||||||
)} |
|
||||||
> |
|
||||||
<div className="flex"> |
|
||||||
{tabs.map((tab) => ( |
|
||||||
<div |
|
||||||
key={tab.value} |
|
||||||
className={cn( |
|
||||||
`flex-1 text-center py-2 font-semibold clickable cursor-pointer rounded-lg`, |
|
||||||
value === tab.value ? '' : 'text-muted-foreground' |
|
||||||
)} |
|
||||||
onClick={() => { |
|
||||||
tab.onClick?.() |
|
||||||
onTabChange?.(tab.value) |
|
||||||
}} |
|
||||||
> |
|
||||||
{t(tab.label)} |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
<div className="relative"> |
|
||||||
<div |
|
||||||
className="absolute bottom-0 px-4 transition-all duration-500" |
|
||||||
style={{ |
|
||||||
width: `${100 / tabs.length}%`, |
|
||||||
left: `${activeIndex >= 0 ? activeIndex * (100 / tabs.length) : 0}%` |
|
||||||
}} |
|
||||||
> |
|
||||||
<div className="w-full h-1 bg-primary rounded-full" /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -0,0 +1,76 @@ |
|||||||
|
import { cn } from '@/lib/utils' |
||||||
|
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' |
||||||
|
import { useEffect, useRef, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
|
||||||
|
type TabDefinition = { |
||||||
|
value: string |
||||||
|
label: string |
||||||
|
} |
||||||
|
|
||||||
|
export default function Tabs({ |
||||||
|
tabs, |
||||||
|
value, |
||||||
|
onTabChange, |
||||||
|
threshold = 800 |
||||||
|
}: { |
||||||
|
tabs: TabDefinition[] |
||||||
|
value: string |
||||||
|
onTabChange?: (tab: string) => void |
||||||
|
threshold?: number |
||||||
|
}) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { deepBrowsing, lastScrollTop } = useDeepBrowsing() |
||||||
|
const activeIndex = tabs.findIndex((tab) => tab.value === value) |
||||||
|
const tabRefs = useRef<(HTMLDivElement | null)[]>([]) |
||||||
|
const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0 }) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const handleResize = () => { |
||||||
|
if (activeIndex >= 0 && tabRefs.current[activeIndex]) { |
||||||
|
const activeTab = tabRefs.current[activeIndex] |
||||||
|
const { offsetWidth, offsetLeft } = activeTab |
||||||
|
const padding = 32 // 16px padding on each side
|
||||||
|
setIndicatorStyle({ |
||||||
|
width: offsetWidth - padding, |
||||||
|
left: offsetLeft + padding / 2 |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
window.addEventListener('resize', handleResize) |
||||||
|
handleResize() // Initial call to set the indicator style
|
||||||
|
return () => { |
||||||
|
window.removeEventListener('resize', handleResize) |
||||||
|
} |
||||||
|
}, [activeIndex]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
'sticky flex justify-around top-12 p-1 bg-background z-30 w-full transition-transform', |
||||||
|
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : '' |
||||||
|
)} |
||||||
|
> |
||||||
|
{tabs.map((tab, index) => ( |
||||||
|
<div |
||||||
|
key={tab.value} |
||||||
|
ref={(el) => (tabRefs.current[index] = el)} |
||||||
|
className={cn( |
||||||
|
`text-center w-full py-2 font-semibold clickable cursor-pointer rounded-lg`, |
||||||
|
value === tab.value ? '' : 'text-muted-foreground' |
||||||
|
)} |
||||||
|
onClick={() => onTabChange?.(tab.value)} |
||||||
|
> |
||||||
|
{t(tab.label)} |
||||||
|
</div> |
||||||
|
))} |
||||||
|
<div |
||||||
|
className="absolute bottom-0 h-1 bg-primary rounded-full transition-all duration-500" |
||||||
|
style={{ |
||||||
|
width: `${indicatorStyle.width}px`, |
||||||
|
left: `${indicatorStyle.left}px` |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue