26 changed files with 175 additions and 118 deletions
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
import NoteList from '@/components/NoteList' |
||||
import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' |
||||
import { |
||||
getRelayUrlFromRelayReviewEvent, |
||||
getStarsFromRelayReviewEvent |
||||
} from '@/lib/event-metadata' |
||||
import { Event } from 'nostr-tools' |
||||
import { useCallback } from 'react' |
||||
|
||||
export default function ExploreRelayReviews() { |
||||
const extraShouldHideEvent = useCallback((evt: Event) => { |
||||
if (evt.kind !== ExtendedKind.RELAY_REVIEW) return false |
||||
if (!getRelayUrlFromRelayReviewEvent(evt)) return true |
||||
return !getStarsFromRelayReviewEvent(evt) |
||||
}, []) |
||||
|
||||
return ( |
||||
<div className="min-w-0 pt-1"> |
||||
<NoteList |
||||
showKinds={[ExtendedKind.RELAY_REVIEW]} |
||||
subRequests={[{ urls: [...FAST_READ_RELAY_URLS], filter: {} }]} |
||||
showKind1OPs={false} |
||||
showKind1Replies={false} |
||||
showKind1111={false} |
||||
extraShouldHideEvent={extraShouldHideEvent} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
@ -1,82 +0,0 @@
@@ -1,82 +0,0 @@
|
||||
import { cn } from '@/lib/utils' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { useRef, useEffect, useState } from 'react' |
||||
|
||||
export type TTabValue = 'replies' | 'quotes' |
||||
const TABS = [ |
||||
{ value: 'replies', label: 'Replies' }, |
||||
{ value: 'quotes', label: 'Quotes' } |
||||
] as { value: TTabValue; label: string }[] |
||||
|
||||
export function Tabs({ |
||||
selectedTab, |
||||
onTabChange, |
||||
hideQuotesForDiscussion = false |
||||
}: { |
||||
selectedTab: TTabValue |
||||
onTabChange: (tab: TTabValue) => void |
||||
/** Hide the quotes tab on discussion threads */ |
||||
hideQuotesForDiscussion?: boolean |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const tabRefs = useRef<(HTMLDivElement | null)[]>([]) |
||||
const containerRef = useRef<HTMLDivElement | null>(null) |
||||
const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0, top: 0 }) |
||||
|
||||
const visibleTabs = hideQuotesForDiscussion ? TABS.filter((tab) => tab.value !== 'quotes') : TABS |
||||
|
||||
useEffect(() => { |
||||
setTimeout(() => { |
||||
const activeIndex = visibleTabs.findIndex((tab) => tab.value === selectedTab) |
||||
if (activeIndex >= 0 && tabRefs.current[activeIndex] && containerRef.current) { |
||||
const activeTab = tabRefs.current[activeIndex] |
||||
const container = containerRef.current |
||||
const { offsetWidth, offsetLeft, offsetHeight } = activeTab |
||||
|
||||
// Get the container's top position relative to the viewport
|
||||
const containerTop = container.getBoundingClientRect().top |
||||
const tabTop = activeTab.getBoundingClientRect().top |
||||
|
||||
// Calculate the indicator's top position relative to the container
|
||||
// Position it at the bottom of the active tab's row
|
||||
const relativeTop = tabTop - containerTop + offsetHeight |
||||
// Responsive padding: smaller on mobile, larger on desktop
|
||||
const padding = window.innerWidth < 640 ? 16 : window.innerWidth < 768 ? 32 : 48 |
||||
|
||||
setIndicatorStyle({ |
||||
width: offsetWidth - padding, |
||||
left: offsetLeft + padding / 2, |
||||
top: relativeTop - 4 // 4px for the indicator height (1px) + spacing
|
||||
}) |
||||
} |
||||
}, 20) // ensure tabs are rendered before calculating
|
||||
}, [selectedTab, visibleTabs]) |
||||
|
||||
return ( |
||||
<div className="w-full min-w-0"> |
||||
<div ref={containerRef} className="flex relative gap-1 overflow-x-auto scrollbar-hide"> |
||||
{visibleTabs.map((tab, index) => ( |
||||
<div |
||||
key={tab.value} |
||||
ref={(el) => (tabRefs.current[index] = el)} |
||||
className={cn( |
||||
`text-center py-2 px-2 sm:px-4 md:px-6 font-semibold whitespace-nowrap clickable cursor-pointer rounded-lg text-xs sm:text-sm md:text-base shrink-0`, |
||||
selectedTab === tab.value ? '' : 'text-muted-foreground' |
||||
)} |
||||
onClick={() => onTabChange(tab.value)} |
||||
> |
||||
{t(tab.label)} |
||||
</div> |
||||
))} |
||||
<div |
||||
className="absolute h-1 bg-primary rounded-full transition-all duration-500" |
||||
style={{ |
||||
width: `${indicatorStyle.width}px`, |
||||
left: `${indicatorStyle.left}px`, |
||||
top: `${indicatorStyle.top}px` |
||||
}} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue