Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions frontend/components/swap-transfer-tracker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { ArrowLeftRight, Send } from 'lucide-react'
import { ArrowLeftRight, Info, Pause, Play, Send } from 'lucide-react'
import { useState } from 'react'
import { LiveBadge } from '@/components/common/live-badge'
import { SectionHeader } from '@/components/ui/section-header'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
Expand All @@ -26,13 +27,32 @@ export default function SwapTransferTracker() {
isConnected: isTransferConnected,
cumulativeTransferred,
} = useTransferEvents()
const [isFollowingData, setIsFollowingData] = useState(true)

return (
<div className="w-full flex flex-col gap-4 sm:gap-6">
<SectionHeader
title="Swap & Transfer Tracker"
description="Live economic activity observed directly from execution events."
/>
>
<button
type="button"
onClick={() => setIsFollowingData(!isFollowingData)}
className={cn(
'flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm font-medium cursor-pointer transition-all duration-200 w-fit',
isFollowingData
? 'bg-zinc-800 border-zinc-700 text-white hover:bg-zinc-700'
: 'bg-zinc-900 border-zinc-700 text-zinc-400 hover:bg-zinc-800 hover:text-white',
)}
>
{isFollowingData ? (
<Pause className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)}
{isFollowingData ? 'Pause' : 'Resume'}
</button>
</SectionHeader>

<div className="flex flex-col dark-component-colors rounded-xl border overflow-hidden">
<Tabs defaultValue="transfers" className="w-full">
Expand All @@ -52,25 +72,31 @@ export default function SwapTransferTracker() {
<div className="mt-1 w-full h-px bg-zinc-800" />
</div>

<TabsContent
value="transfers"
className="mt-0 overflow-x-auto scrollbar-none"
>
<TabsContent value="transfers" className="mt-0">
<Transfers
transfers={allTransfers}
isLoading={!isTransferConnected}
cumulativeTransferred={cumulativeTransferred}
isFollowingData={isFollowingData}
/>
</TabsContent>

<TabsContent
value="swaps"
className="mt-0 overflow-x-auto scrollbar-none"
>
<Swaps data={allSwaps} isLoading={!isSwapConnected} />
<Swaps
data={allSwaps}
isLoading={!isSwapConnected}
isFollowingData={isFollowingData}
/>
</TabsContent>
</Tabs>
</div>
<div className="flex items-center gap-2 text-sm text-zinc-500">
<Info className="w-4 h-[1lh]" />
<span>Hover to pause</span>
</div>
</div>
)
}
9 changes: 2 additions & 7 deletions frontend/components/swap-transfer-tracker/swap-row.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { motion } from 'framer-motion'
import { ExternalLink } from '@/components/ui/external-link'
import { EXPLORER_URL } from '@/constants/common'
import { getSwapProviderConfig } from '@/constants/swap-provider-config'
Expand All @@ -19,11 +18,7 @@ export function SwapRow({ swap, gridClass }: SwapRowProps) {
const config = getSwapProviderConfig(swap.provider)

return (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.15 }}
<div
className={cn(
'py-3 items-center border-b border-zinc-800/50 hover:bg-tracker-row-hover transition-colors',
gridClass,
Expand Down Expand Up @@ -76,6 +71,6 @@ export function SwapRow({ swap, gridClass }: SwapRowProps) {
<span className="text-sm font-mono text-zinc-400 tabular-nums">
{formatTimeDisplay(swap.timestamp)}
</span>
</motion.div>
</div>
)
}
67 changes: 58 additions & 9 deletions frontend/components/swap-transfer-tracker/swaps.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,54 @@
'use client'

import { AnimatePresence } from 'framer-motion'
import type { ReactElement } from 'react'
import { useState } from 'react'
import { List, type RowComponentProps } from 'react-window'
import { useVirtualizedList } from '@/hooks/use-virtualized-list'
import { cn } from '@/lib/utils'
import type { SwapData } from '@/types/swap'
import { SwapRow } from './swap-row'

interface SwapsProps {
data: SwapData[]
isLoading: boolean
isFollowingData: boolean
}

const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4'
const ROW_HEIGHT = 45

interface SwapRowData {
dataRef: React.RefObject<SwapData[]>
gridClass: string
}

// Cell component defined outside to maintain stable reference
function SwapCell({
index,
style,
dataRef,
gridClass,
}: RowComponentProps<SwapRowData>): ReactElement {
const swap = dataRef.current?.[index]

return (
<div style={style}>
{swap && <SwapRow swap={swap} gridClass={gridClass} />}
</div>
)
}

export function Swaps({ data, isLoading, isFollowingData }: SwapsProps) {
const [isHovering, setIsHovering] = useState(false)
const isFollowing = isFollowingData && !isHovering

const { containerRef, listRef, containerHeight, displayedData, rowProps } =
useVirtualizedList({
data,
isFollowing,
gridClass: TABLE_GRID,
})

export function Swaps({ data, isLoading }: SwapsProps) {
return (
<div className="flex flex-col min-w-4xl lg:min-w-0">
<div
Expand All @@ -29,19 +65,32 @@ export function Swaps({ data, isLoading }: SwapsProps) {
<span>Time</span>
</div>

<div className="h-96 overflow-y-auto scrollbar-none">
{data.length === 0 ? (
<div
ref={containerRef}
className="h-96"
onPointerEnter={() => setIsHovering(true)}
onPointerLeave={() => setIsHovering(false)}
>
{displayedData.length === 0 ? (
<div className="flex items-center justify-center h-full">
<p className="text-sm text-zinc-400">
{isLoading ? 'Waiting for events...' : 'No swaps yet'}
</p>
</div>
) : (
<AnimatePresence mode="popLayout">
{data.map((swap) => (
<SwapRow key={swap.id} swap={swap} gridClass={TABLE_GRID} />
))}
</AnimatePresence>
<List
listRef={listRef}
rowComponent={SwapCell}
rowCount={displayedData.length}
rowHeight={ROW_HEIGHT}
defaultHeight={containerHeight}
rowProps={rowProps}
style={{
overflowX: 'hidden',
overflowY: isFollowing ? 'hidden' : 'auto',
}}
className="scrollbar-none"
/>
)}
</div>
</div>
Expand Down
9 changes: 2 additions & 7 deletions frontend/components/swap-transfer-tracker/transfer-row.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { motion } from 'framer-motion'
import { ExternalLink } from '@/components/ui/external-link'
import { EXPLORER_URL } from '@/constants/common'
import { formatTimeDisplay } from '@/lib/timestamp'
Expand All @@ -18,11 +17,7 @@ export function TransferRow({ transfer, gridClass }: TransferRowProps) {
const tokenSymbol = transfer.type === 'native' ? 'MON' : 'WMON'

return (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.15 }}
<div
className={cn(
'py-3 items-center border-b border-zinc-800/50 hover:bg-tracker-row-hover transition-colors',
gridClass,
Expand Down Expand Up @@ -61,6 +56,6 @@ export function TransferRow({ transfer, gridClass }: TransferRowProps) {
<span className="text-sm font-mono text-zinc-400 tabular-nums">
{formatTimeDisplay(transfer.timestamp)}
</span>
</motion.div>
</div>
)
}
70 changes: 58 additions & 12 deletions frontend/components/swap-transfer-tracker/transfers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client'

import { AnimatePresence } from 'framer-motion'
import type { ReactElement } from 'react'
import { useState } from 'react'
import { List, type RowComponentProps } from 'react-window'
import { useVirtualizedList } from '@/hooks/use-virtualized-list'
import { cn } from '@/lib/utils'
import type { TransferData } from '@/types/transfer'
import { CumulativeTransferCounter } from './cumulative-transfer-counter'
Expand All @@ -10,15 +13,49 @@ interface TransfersProps {
transfers: TransferData[]
isLoading: boolean
cumulativeTransferred: bigint
isFollowingData: boolean
}

const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4'
const ROW_HEIGHT = 45

interface TransferRowData {
dataRef: React.RefObject<TransferData[]>
gridClass: string
}

// Cell component defined outside to maintain stable reference
function TransferCell({
index,
style,
dataRef,
gridClass,
}: RowComponentProps<TransferRowData>): ReactElement {
const transfer = dataRef.current?.[index]

return (
<div style={style}>
{transfer && <TransferRow transfer={transfer} gridClass={gridClass} />}
</div>
)
}

export function Transfers({
transfers,
isLoading,
cumulativeTransferred,
isFollowingData,
}: TransfersProps) {
const [isHovering, setIsHovering] = useState(false)
const isFollowing = isFollowingData && !isHovering

const { containerRef, listRef, containerHeight, displayedData, rowProps } =
useVirtualizedList({
data: transfers,
isFollowing,
gridClass: TABLE_GRID,
})

return (
<div className="flex flex-col min-w-4xl lg:min-w-0">
<CumulativeTransferCounter
Expand All @@ -39,23 +76,32 @@ export function Transfers({
<span>Time</span>
</div>

<div className="h-96 overflow-y-auto scrollbar-none">
{transfers.length === 0 ? (
<div
ref={containerRef}
className="h-96"
onPointerEnter={() => setIsHovering(true)}
onPointerLeave={() => setIsHovering(false)}
>
{displayedData.length === 0 ? (
<div className="flex items-center justify-center h-full">
<p className="text-sm text-zinc-400">
{isLoading ? 'Waiting for events...' : 'No transfers yet'}
</p>
</div>
) : (
<AnimatePresence mode="popLayout">
{transfers.map((transfer) => (
<TransferRow
key={transfer.id}
transfer={transfer}
gridClass={TABLE_GRID}
/>
))}
</AnimatePresence>
<List
listRef={listRef}
rowComponent={TransferCell}
rowCount={displayedData.length}
rowHeight={ROW_HEIGHT}
defaultHeight={containerHeight}
rowProps={rowProps}
style={{
overflowX: 'hidden',
overflowY: isFollowing ? 'hidden' : 'auto',
}}
className="scrollbar-none"
/>
)}
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/components/ui/stat-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function StatCard({
iconBgClassName = 'bg-tracker-active/10',
}: StatCardProps) {
return (
<div className="dark-component-colors rounded-xl border p-5 w-full flex flex-col gap-4">
<div className="flex flex-row justify-between">
<div className="flex flex-col gap-1">
<div className="dark-component-colors rounded-xl border p-5 w-full flex flex-col gap-4 items-start">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col items-start gap-1">
<p className="text-sm sm:text-base text-[#8888a0]">{label}</p>
<p className="text-lg sm:text-xl font-medium text-[#8888a0]">
<span className="text-3xl sm:text-5xl text-white font-bold tabular-nums">
Expand Down
2 changes: 1 addition & 1 deletion frontend/hooks/use-transfer-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { parseTopicsString } from '@/lib/abi-decode'
import type { SerializableEventData } from '@/types/events'
import type { TransferData } from '@/types/transfer'

const MAX_TRANSFERS = 50
const MAX_TRANSFERS = Infinity

/**
* Parse native transfer event (TxnCallFrame with non-zero value)
Expand Down
Loading