Skip to content

Commit faa02b9

Browse files
authored
feat: protocol activity panel, project tab changes, improved charts, minor UI fixes. (#4691)
1 parent 36de2f5 commit faa02b9

29 files changed

Lines changed: 1042 additions & 449 deletions

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
},
5656
"dependencies": {
5757
"@apollo/client": "^3.10.8",
58-
"@getpara/ethers-v5-integration": "2.0.0-alpha.59",
59-
"@getpara/evm-wallet-connectors": "2.0.0-alpha.59",
60-
"@getpara/react-sdk-lite": "2.0.0-alpha.59",
58+
"@getpara/ethers-v5-integration": "2.0.0-alpha.64",
59+
"@getpara/evm-wallet-connectors": "2.0.0-alpha.64",
60+
"@getpara/react-sdk-lite": "2.0.0-alpha.64",
6161
"@headlessui/react": "^1.7.18",
6262
"@heroicons/react": "^2.1.4",
6363
"@jbx-protocol/contracts-v1": "2.0.0",
@@ -136,7 +136,7 @@
136136
"react-quill": "^2.0.0",
137137
"react-redux": "^8",
138138
"react-stop-propagation": "^0.2.0",
139-
"recharts": "^2.12.7",
139+
"recharts": "^3.3.0",
140140
"rehype-sanitize": "^5.0.1",
141141
"revnet-sdk": "^1.0.1-beta",
142142
"sass": "^1.77.6",

src/components/Navbar/SiteNavigation.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ThemePicker from './components/ThemePicker'
1414
import { TransactionsList } from './components/TransactionList/TransactionsList'
1515
import { ChangeNetworksButton } from './components/Wallet/ChangeNetworksButton'
1616
import { WalletButton } from './components/Wallet/WalletButton'
17+
import ProtocolActivityToggle from 'components/ProtocolActivity/ProtocolActivityToggle'
1718

1819
export function SiteNavigation() {
1920
const [hasMounted, setHasMounted] = useState(false)
@@ -73,6 +74,7 @@ const DesktopSiteNavigation = () => {
7374
<NavLanguageSelector className="md:order-2" />
7475
<ThemePicker className="md:order-3" />
7576
<QuickProjectSearchButton className="md:order-1" />
77+
<ProtocolActivityToggle className="hidden md:flex md:order-4" />
7678
<TransactionsList listClassName="absolute top-full mt-4 right-0 md:-right-6 md:w-[320px] w-full" />
7779
</div>
7880
</div>
@@ -155,6 +157,7 @@ const MobileSiteNavigation = () => {
155157
<NavLanguageSelector className="md:order-2" />
156158
<ThemePicker className="md:order-3" />
157159
<QuickProjectSearchButton className="md:order-1" />
160+
<ProtocolActivityToggle className="hidden md:flex md:order-4" />
158161
<TransactionsList listClassName="absolute top-full mt-4 right-0 md:-right-6 md:w-[320px] w-full" />
159162
</div>
160163
</div>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { CheckIcon } from '@heroicons/react/24/outline'
2+
import { NETWORKS } from 'constants/networks'
3+
import { JBChainId } from 'juice-sdk-core'
4+
import { ChainLogo } from 'packages/v4v5/components/ChainLogo'
5+
import React from 'react'
6+
import { twMerge } from 'tailwind-merge'
7+
8+
export const ChainFilterButton: React.FC<{
9+
chainId: number
10+
selected: boolean
11+
onChange: (selected: boolean) => void
12+
}> = ({ chainId, selected, onChange }) => {
13+
const chain = NETWORKS[chainId]
14+
15+
return (
16+
<button
17+
className={twMerge(
18+
'relative flex items-center justify-center rounded-lg border p-2 transition-colors',
19+
selected
20+
? 'border-bluebs-500 bg-bluebs-25 dark:border-bluebs-700 dark:bg-bluebs-900'
21+
: 'border-grey-200 hover:border-grey-300 hover:bg-grey-50 dark:border-grey-800 dark:hover:bg-slate-800',
22+
)}
23+
onClick={() => onChange(!selected)}
24+
title={chain.label}
25+
>
26+
<span className="h-6 w-6">
27+
<ChainLogo chainId={chainId as JBChainId} />
28+
</span>
29+
{selected && (
30+
<div className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full border border-bluebs-500 bg-bluebs-500 dark:border-bluebs-600 dark:bg-bluebs-600">
31+
<CheckIcon className="h-3 w-3 text-white" />
32+
</div>
33+
)}
34+
</button>
35+
)
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, {
2+
createContext,
3+
PropsWithChildren,
4+
useContext,
5+
useMemo,
6+
useState,
7+
} from 'react'
8+
9+
interface ProtocolActivityContextType {
10+
isOpen: boolean
11+
open: () => void
12+
close: () => void
13+
toggle: () => void
14+
}
15+
16+
const ProtocolActivityContext = createContext<
17+
ProtocolActivityContextType | undefined
18+
>(undefined)
19+
20+
export const useProtocolActivity = () => {
21+
const context = useContext(ProtocolActivityContext)
22+
if (!context) {
23+
throw new Error(
24+
'useProtocolActivity must be used within ProtocolActivityProvider',
25+
)
26+
}
27+
return context
28+
}
29+
30+
export const ProtocolActivityProvider: React.FC<PropsWithChildren> = ({
31+
children,
32+
}) => {
33+
const [isOpen, setIsOpen] = useState(true)
34+
35+
const value = useMemo(
36+
() => ({
37+
isOpen,
38+
open: () => setIsOpen(true),
39+
close: () => setIsOpen(false),
40+
toggle: () => setIsOpen(prev => !prev),
41+
}),
42+
[isOpen],
43+
)
44+
45+
return (
46+
<ProtocolActivityContext.Provider value={value}>
47+
{children}
48+
</ProtocolActivityContext.Provider>
49+
)
50+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { Button } from 'antd'
2+
import Loading from 'components/Loading'
3+
import { NETWORKS, TESTNET_IDS, MAINNET_IDS } from 'constants/networks'
4+
import { useActivityEventsQuery } from 'generated/v4v5/graphql'
5+
import { testnetBendystrawClient, mainnetBendystrawClient } from 'lib/apollo/bendystrawClient'
6+
import { translateEventDataToPresenter } from 'packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/V4V5ActivityList'
7+
import {
8+
AnyEvent,
9+
transformEventData,
10+
} from 'packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData'
11+
import React, { useState, useMemo } from 'react'
12+
import { ActivityEvent } from 'packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/activityEventElems/ActivityElement'
13+
import { twMerge } from 'tailwind-merge'
14+
import { ChainFilterButton } from './ChainFilterButtons'
15+
import Link from 'next/link'
16+
import { v4v5ProjectRoute } from 'packages/v4v5/utils/routes'
17+
18+
const PAGE_SIZE = 20
19+
const POLL_INTERVAL = 30000 // 30 seconds
20+
21+
type NetworkType = 'testnet' | 'mainnet'
22+
23+
// Default to testnet if NEXT_PUBLIC_TESTNET is true, otherwise mainnet
24+
const defaultNetwork: NetworkType = process.env.NEXT_PUBLIC_TESTNET === 'true' ? 'testnet' : 'mainnet'
25+
26+
export function ProtocolActivityList() {
27+
const [network, setNetwork] = useState<NetworkType>(defaultNetwork)
28+
const [selectedChainIds, setSelectedChainIds] = useState<Set<number>>(new Set())
29+
const [endCursor, setEndCursor] = useState<string | null>(null)
30+
31+
// Select client based on network toggle
32+
const client = network === 'testnet' ? testnetBendystrawClient : mainnetBendystrawClient
33+
34+
// Get available chains based on current network
35+
const availableChainIds = useMemo(() => {
36+
return network === 'testnet' ? Array.from(TESTNET_IDS) : Array.from(MAINNET_IDS)
37+
}, [network])
38+
39+
// Reset cursor and chains when network changes
40+
React.useEffect(() => {
41+
setEndCursor(null)
42+
setSelectedChainIds(new Set())
43+
}, [network])
44+
45+
// Reset cursor when chain selection changes
46+
React.useEffect(() => {
47+
setEndCursor(null)
48+
}, [selectedChainIds])
49+
50+
// Toggle chain selection
51+
const toggleChain = (chainId: number) => {
52+
setSelectedChainIds(prev => {
53+
const newSet = new Set(prev)
54+
if (newSet.has(chainId)) {
55+
newSet.delete(chainId)
56+
} else {
57+
newSet.add(chainId)
58+
}
59+
return newSet
60+
})
61+
}
62+
63+
// Build query filter for selected chains
64+
const queryFilter = useMemo(() => {
65+
if (selectedChainIds.size === 0) {
66+
return {} // No filter, show all chains
67+
}
68+
return { chainId_in: Array.from(selectedChainIds) }
69+
}, [selectedChainIds])
70+
71+
// Query protocol activity with optional chain filter
72+
const { data: activityEvents, loading, error } = useActivityEventsQuery({
73+
client,
74+
pollInterval: POLL_INTERVAL, // Poll every 30 seconds
75+
variables: {
76+
where: queryFilter,
77+
orderBy: 'timestamp',
78+
orderDirection: 'desc',
79+
after: endCursor,
80+
limit: PAGE_SIZE,
81+
},
82+
})
83+
84+
const projectEvents = React.useMemo(
85+
() =>
86+
activityEvents?.activityEvents.items
87+
.map(transformEventData)
88+
.filter((event): event is AnyEvent => !!event)
89+
.map(e => translateEventDataToPresenter(e, undefined)) ?? [],
90+
[activityEvents?.activityEvents.items],
91+
)
92+
93+
return (
94+
<div className="flex h-full flex-col">
95+
<div className="px-6 pt-6">
96+
<h2 className="mb-4 font-heading text-2xl font-medium">
97+
Protocol Activity
98+
</h2>
99+
{/* Network Toggle */}
100+
<div className="mb-4 flex gap-2">
101+
<button
102+
onClick={() => setNetwork('testnet')}
103+
className={twMerge(
104+
'rounded-md px-4 py-2 text-sm font-medium transition-colors',
105+
network === 'testnet'
106+
? 'bg-bluebs-500 text-white'
107+
: 'bg-smoke-100 text-grey-600 hover:bg-smoke-200 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600',
108+
)}
109+
>
110+
Testnet
111+
</button>
112+
<button
113+
onClick={() => setNetwork('mainnet')}
114+
className={twMerge(
115+
'rounded-md px-4 py-2 text-sm font-medium transition-colors',
116+
network === 'mainnet'
117+
? 'bg-bluebs-500 text-white'
118+
: 'bg-smoke-100 text-grey-600 hover:bg-smoke-200 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600',
119+
)}
120+
>
121+
Mainnet
122+
</button>
123+
</div>
124+
{/* Chain Filter Checkboxes */}
125+
<div className="mb-6 flex flex-wrap gap-2">
126+
{availableChainIds.map(chainId => (
127+
<ChainFilterButton
128+
key={chainId}
129+
chainId={chainId}
130+
selected={selectedChainIds.has(chainId)}
131+
onChange={() => toggleChain(chainId)}
132+
/>
133+
))}
134+
</div>
135+
</div>
136+
<div className="flex-1 overflow-y-auto px-6">
137+
<div className="flex flex-col gap-3">
138+
{loading && <Loading />}
139+
{error && (
140+
<div className="text-red-500 text-sm">
141+
Error loading activity: {error.message}
142+
</div>
143+
)}
144+
{loading || (projectEvents && projectEvents.length > 0) ? (
145+
<>
146+
{projectEvents?.map(event => {
147+
const projectLink = event.event.projectId && event.event.chainId
148+
? v4v5ProjectRoute({
149+
projectId: event.event.projectId,
150+
chainId: event.event.chainId,
151+
version: 5, // Default to v5 for all bendystraw projects
152+
})
153+
: null
154+
155+
return (
156+
<div
157+
className="border-smoke-200 pb-5 dark:border-grey-600 [&:not(:last-child)]:mb-5 [&:not(:last-child)]:border-b"
158+
key={event.event.id}
159+
>
160+
{projectLink ? (
161+
<Link
162+
href={projectLink}
163+
className="block cursor-pointer transition-colors hover:bg-smoke-50 dark:hover:bg-slate-800 -mx-3 px-3 py-2 rounded-lg"
164+
>
165+
<ActivityEvent
166+
event={event.event}
167+
header={event.header}
168+
subject={event.subject}
169+
extra={event.extra}
170+
/>
171+
</Link>
172+
) : (
173+
<ActivityEvent
174+
event={event.event}
175+
header={event.header}
176+
subject={event.subject}
177+
extra={event.extra}
178+
/>
179+
)}
180+
</div>
181+
)
182+
})}
183+
{activityEvents?.activityEvents.pageInfo.hasNextPage && (
184+
<Button
185+
onClick={() =>
186+
setEndCursor(activityEvents.activityEvents.pageInfo.endCursor)
187+
}
188+
className="mb-6"
189+
>
190+
Load more
191+
</Button>
192+
)}
193+
</>
194+
) : (
195+
!loading && <span className="text-zinc-500 text-sm">No activity yet.</span>
196+
)}
197+
</div>
198+
</div>
199+
</div>
200+
)
201+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { XMarkIcon } from '@heroicons/react/24/outline'
2+
import { twMerge } from 'tailwind-merge'
3+
import { ProtocolActivityList } from './ProtocolActivityList'
4+
import { useProtocolActivity } from './ProtocolActivityContext'
5+
6+
export function ProtocolActivityPanel() {
7+
const { isOpen, close } = useProtocolActivity()
8+
9+
return (
10+
<div
11+
className={twMerge(
12+
'sticky top-0 self-start bg-white transition-all duration-300 dark:bg-slate-900',
13+
'border-l-2 border-smoke-300 shadow-2xl dark:border-slate-700',
14+
'h-screen flex flex-col',
15+
isOpen ? 'w-[460px]' : 'w-0',
16+
)}
17+
>
18+
{isOpen && (
19+
<>
20+
{/* Close button */}
21+
<button
22+
onClick={close}
23+
className="absolute top-6 right-6 z-10 text-grey-400 hover:text-grey-600 dark:text-slate-200 dark:hover:text-slate-100"
24+
aria-label="Close protocol activity panel"
25+
>
26+
<XMarkIcon className="h-6 w-6" />
27+
</button>
28+
29+
{/* Content */}
30+
<div className="flex-1 overflow-y-auto">
31+
<ProtocolActivityList />
32+
</div>
33+
</>
34+
)}
35+
</div>
36+
)
37+
}

0 commit comments

Comments
 (0)