Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4b0d0d4
refactor: reduce activity panel size for better visual hierarchy
jlm0 Oct 22, 2025
69f5a29
feat: replace automated events with manual events in activity feeds
jlm0 Oct 22, 2025
95979c1
feat: add quick access button for NFT creation on NFT tab
jlm0 Oct 22, 2025
c0d3214
fix: use proper project-scoped routing for NFT settings
jlm0 Oct 22, 2025
d88879f
chore: update translation template
jlm0 Oct 22, 2025
5d3f1da
fix: show correct project logos in protocol activity feed
jlm0 Oct 23, 2025
9bd8205
fix: prevent undefined twitterCreator in seoProps serialization
jlm0 Oct 23, 2025
37932f4
feat: persist protocol activity panel state across page navigation
jlm0 Oct 23, 2025
564033b
feat: display correct currency symbols for USDC-based projects
jlm0 Oct 23, 2025
f87438c
fix: convert currency bigint to hex address for proper USDC detection
jlm0 Oct 23, 2025
7d7e32c
fix: use token field instead of currency for symbol lookup
jlm0 Oct 23, 2025
9bd4273
fix: use correct decimals for amount formatting in activity feeds
jlm0 Oct 23, 2025
524af2d
fix: display correct total raised for USDC-based projects
jlm0 Oct 23, 2025
06cfab6
chore: remove debugging data file
jlm0 Oct 23, 2025
5a223d4
Merge branch 'dev' into fix-balance-handling
jlm0 Oct 23, 2025
b0f2c5c
fix: display correct currency and volumes in analytics charts for USD…
jlm0 Oct 23, 2025
e0d8fc8
fix: display correct currency and amounts in Rulesets & Funds tab for…
jlm0 Oct 23, 2025
d63a085
fix: use wei format (18 decimals) for SDK balance hooks
jlm0 Oct 23, 2025
1457d2e
fix: adjust width of ProtocolActivityPanel and update currency format…
jlm0 Oct 23, 2025
cf17128
fix: make caller field optional in BaseEventInput type
jlm0 Oct 23, 2025
51ed126
refactor: use Juicebox ETH token address convention
jlm0 Oct 24, 2025
08bc142
refactor: use SDK USDC_ADDRESSES for all supported chains
jlm0 Oct 24, 2025
1d5e17a
fix: conditionally include twitterCreator in metadata object
jlm0 Oct 24, 2025
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
15 changes: 14 additions & 1 deletion src/components/ProtocolActivity/ProtocolActivityContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {
createContext,
PropsWithChildren,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
Expand All @@ -17,6 +18,8 @@ const ProtocolActivityContext = createContext<
ProtocolActivityContextType | undefined
>(undefined)

const STORAGE_KEY = 'protocolActivityPanelOpen'

export const useProtocolActivity = () => {
const context = useContext(ProtocolActivityContext)
if (!context) {
Expand All @@ -30,7 +33,17 @@ export const useProtocolActivity = () => {
export const ProtocolActivityProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const [isOpen, setIsOpen] = useState(true)
// Initialize state from localStorage, default to true if not set
const [isOpen, setIsOpen] = useState(() => {
if (typeof window === 'undefined') return true
const stored = localStorage.getItem(STORAGE_KEY)
return stored !== null ? stored === 'true' : true
})

// Persist state changes to localStorage
useEffect(() => {
localStorage.setItem(STORAGE_KEY, String(isOpen))
}, [isOpen])

const value = useMemo(
() => ({
Expand Down
5 changes: 5 additions & 0 deletions src/components/ProtocolActivity/ProtocolActivityElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface ProtocolActivityElementEvent {
projectId?: number
projectName?: string | null
projectHandle?: string | null
projectLogoUri?: string | null
projectToken?: string | null
projectCurrency?: string | null
projectDecimals?: number | null
}

export function ProtocolActivityElement({
Expand All @@ -39,6 +43,7 @@ export function ProtocolActivityElement({
className="h-12 w-12"
projectId={event.projectId}
name={displayName}
uri={event.projectLogoUri ?? undefined}
pv={'5'}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProtocolActivity/ProtocolActivityPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function ProtocolActivityPanel() {
'sticky top-0 self-start bg-white transition-all duration-300 dark:bg-slate-900',
'border-l-2 border-smoke-300 shadow-2xl dark:border-slate-700',
'h-screen flex flex-col',
isOpen ? 'w-[460px]' : 'w-0',
isOpen ? 'w-[400px]' : 'w-0',
)}
>
{isOpen && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
import { BigNumber } from '@ethersproject/bignumber'
import { AmountInCurrency } from 'components/currency/AmountInCurrency'
import { USDC_ADDRESSES } from 'juice-sdk-core'
import { ETH_TOKEN_ADDRESS } from 'constants/juiceboxTokens'
import { AnyEvent } from 'packages/v4v5/views/V4V5ProjectDashboard/V4V5ProjectTabs/V4V5ActivityPanel/utils/transformEventsData'
import { formatActivityAmount } from 'utils/format/formatActivityAmount'

// Build currency mapping from SDK constants
// Maps token addresses (lowercase) to their symbols
const CURRENCY_SYMBOLS: Record<string, string> = {
// Add ETH token address
[ETH_TOKEN_ADDRESS.toLowerCase()]: 'ETH',
// Add USDC addresses for all supported chains
...Object.values(USDC_ADDRESSES).reduce((acc, address) => {
acc[address.toLowerCase()] = 'USDC'
return acc
}, {} as Record<string, string>),
}

/**
* Get currency symbol from currency address (hex string)
*/
function getCurrencySymbol(currency?: string | null): string {
if (!currency) return 'ETH'
// Normalize to lowercase for lookup
const symbol = CURRENCY_SYMBOLS[currency.toLowerCase()]
return symbol || 'ETH'
}

/**
* Translate event data to protocol activity presenter with formatted amounts
*/
export function translateEventDataToProtocolPresenter(event: AnyEvent) {
// Use projectToken (the actual token address) for currency symbol lookup
const currencySymbol = getCurrencySymbol(event.projectToken)
// Use project decimals (e.g., 6 for USDC, 18 for ETH)
const decimals = event.projectDecimals ?? 18

switch (event.type) {
case 'payEvent':
return {
event,
header: 'Paid',
subject: `${formatActivityAmount(event.amount.value)} ETH`,
subject: `${formatActivityAmount(event.amount.value, decimals)} ${currencySymbol}`,
}
case 'addToBalanceEvent':
return {
event,
header: 'Added to balance',
subject: `${formatActivityAmount(event.amount.value)} ETH`,
subject: `${formatActivityAmount(event.amount.value, decimals)} ${currencySymbol}`,
}
case 'manualMintTokensEvent':
return {
Expand All @@ -30,7 +59,7 @@ export function translateEventDataToProtocolPresenter(event: AnyEvent) {
return {
event,
header: 'Cashed out',
subject: `${formatActivityAmount(event.reclaimAmount.value)} ETH`,
subject: `${formatActivityAmount(event.reclaimAmount.value, decimals)} ${currencySymbol}`,
}
case 'deployedERC20Event':
return {
Expand All @@ -48,7 +77,7 @@ export function translateEventDataToProtocolPresenter(event: AnyEvent) {
return {
event,
header: 'Send payouts',
subject: `${formatActivityAmount(event.amount.value)} ETH`,
subject: `${formatActivityAmount(event.amount.value, decimals)} ${currencySymbol}`,
}
case 'distributeReservedTokensEvent':
return {
Expand All @@ -66,13 +95,13 @@ export function translateEventDataToProtocolPresenter(event: AnyEvent) {
return {
event,
header: 'Send to payout split',
subject: `${formatActivityAmount(event.amount.value)} ETH`,
subject: `${formatActivityAmount(event.amount.value, decimals)} ${currencySymbol}`,
}
case 'useAllowanceEvent':
return {
event,
header: 'Used allowance',
subject: `${formatActivityAmount(event.amount.value)} ETH`,
subject: `${formatActivityAmount(event.amount.value, decimals)} ${currencySymbol}`,
}
case 'manualBurnEvent':
return {
Expand Down
40 changes: 38 additions & 2 deletions src/components/VolumeChart/components/TimelineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { t } from '@lingui/macro'
import CurrencySymbol from 'components/currency/CurrencySymbol'
import { ThemeContext } from 'contexts/Theme/ThemeContext'
import { useTrendingProjects } from 'hooks/useTrendingProjects'
import { USDC_ADDRESSES } from 'juice-sdk-core'
import tailwind from 'lib/tailwind'
import moment from 'moment'
import { CSSProperties, useContext, useMemo } from 'react'
Expand All @@ -25,24 +26,51 @@ import {
ProjectTimelineRange,
ProjectTimelineView,
} from '../types'
import { ETH_TOKEN_ADDRESS } from 'constants/juiceboxTokens'

const now = Date.now().valueOf()

// Build currency mapping from SDK constants
const CURRENCY_SYMBOLS: Record<string, string> = {
[ETH_TOKEN_ADDRESS.toLowerCase()]: 'ETH',
...Object.values(USDC_ADDRESSES).reduce((acc, address) => {
acc[address.toLowerCase()] = 'USDC'
return acc
}, {} as Record<string, string>),
}

/**
* Get currency symbol from currency address (hex string)
*/
function getCurrencySymbol(currency?: string | null): string {
if (!currency) return 'ETH'
// Normalize to lowercase for lookup
const symbol = CURRENCY_SYMBOLS[currency.toLowerCase()]
return symbol || 'ETH'
}

export default function TimelineChart({
points,
view,
range,
height,
projectToken,
projectDecimals,
}: {
points: ProjectTimelinePoint[] | undefined
view: ProjectTimelineView
range: ProjectTimelineRange
height: CSSProperties['height']
projectToken?: string
projectDecimals?: number
}) {
const { themeOption } = useContext(ThemeContext)

const defaultYDomain = useTimelineYDomain(points?.map(p => p[view]))

// Determine currency symbol based on project token
const currencySymbol = getCurrencySymbol(projectToken)

const { data: trendingProjects } = useTrendingProjects(1)
const highTrendingScore = trendingProjects?.length
? wadToFloat(trendingProjects[0].trendingScore)
Expand Down Expand Up @@ -166,7 +194,11 @@ export default function TimelineChart({
fill={color}
transform={`translate(${props.x + 4},${props.y + 4})`}
>
<CurrencySymbol currency="ETH" />
{currencySymbol === 'ETH' ? (
<CurrencySymbol currency="ETH" />
) : (
`${currencySymbol} `
)}
{formattedValue}
</text>
</g>
Expand Down Expand Up @@ -241,7 +273,11 @@ export default function TimelineChart({
</div>
{view !== 'trendingScore' && (
<div className="font-medium">
<CurrencySymbol currency="ETH" />
{currencySymbol === 'ETH' ? (
<CurrencySymbol currency="ETH" />
) : (
`${currencySymbol} `
)}
{amount.toFixed(amount > 10 ? 1 : amount > 1 ? 2 : 4)}
</div>
)}
Expand Down
17 changes: 16 additions & 1 deletion src/components/VolumeChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default function VolumeChart({
version,
lockedView,
hideViewSelector,
projectToken,
projectDecimals,
}: {
height: CSSProperties['height']
createdAt: number | undefined
Expand All @@ -27,6 +29,8 @@ export default function VolumeChart({
version?: number
lockedView?: ProjectTimelineView
hideViewSelector?: boolean
projectToken?: string
projectDecimals?: number
}) {
const [timelineView, setTimelineView] =
useState<ProjectTimelineView>(lockedView || 'volume')
Expand All @@ -42,7 +46,12 @@ export default function VolumeChart({

// V4/V5: Use new database-based hook (only for V4 projects)
const shouldUseV4Hook = pv === PV_V4
const { points: v4v5Points, loading: v4v5Loading } = useV4V5ProjectTimeline({
const {
points: v4v5Points,
loading: v4v5Loading,
projectToken: v4v5Token,
projectDecimals: v4v5Decimals,
} = useV4V5ProjectTimeline({
projectId: shouldUseV4Hook ? projectId : 0,
range,
version: version || 4,
Expand All @@ -51,6 +60,10 @@ export default function VolumeChart({
const points = shouldUseV4Hook ? v4v5Points : v1v2v3Points
const loading = shouldUseV4Hook ? v4v5Loading : legacyLoading

// Use token/decimals from hook if not provided via props
const token = projectToken ?? v4v5Token
const decimals = projectDecimals ?? v4v5Decimals

// Use locked view if provided, otherwise use state
const currentView = lockedView || timelineView

Expand All @@ -73,6 +86,8 @@ export default function VolumeChart({
view={currentView}
range={range}
height={height}
projectToken={token}
projectDecimals={decimals}
/>
{!points?.length && (
<div className="absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center">
Expand Down
4 changes: 4 additions & 0 deletions src/packages/v4v5/graphql/queries/activityEvents.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ query ActivityEvents(
project {
name
handle
logoUri
token
currency
decimals
}
payEvent {
id
Expand Down
3 changes: 3 additions & 0 deletions src/packages/v4v5/graphql/queries/project.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ query Project($chainId: Float!, $projectId: Float!, $version: Float!) {
suckerGroupId
createdAt
isRevnet
token
currency
decimals
permissionHolders {
items {
operator
Expand Down
26 changes: 21 additions & 5 deletions src/packages/v4v5/hooks/useV4V5ProjectTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import { useMemo } from 'react'
import { wadToFloat } from 'utils/format/formatNumber'
import { ProjectTimelinePoint, ProjectTimelineRange } from 'components/VolumeChart/types'

/**
* Convert volume/balance value from token decimals to float
* Volume in Bendystraw is stored in the token's native decimals (6 for USDC, 18 for ETH)
*/
function formatVolumeValue(value: string | bigint, decimals: number = 18): number {
const num = typeof value === 'string' ? BigInt(value) : value
const divisor = BigInt(10) ** BigInt(decimals)
// Convert to float with proper decimal places
return Number(num) / Number(divisor)
}

export function useV4V5ProjectTimeline({
projectId,
range,
Expand Down Expand Up @@ -89,21 +100,24 @@ export function useV4V5ProjectTimeline({
if (!timestamps || !project?.project) return
if (queryResult === undefined) return // Still loading

// Get project decimals for proper volume/balance conversion
const decimals = project.project.decimals ? Number(project.project.decimals) : 18

const previous = queryResult.previous.items.length
? queryResult.previous.items[0]
: undefined

const rangeItems = queryResult.range.items.map(item => ({
timestamp: item.timestamp,
volume: wadToFloat(item.volume),
balance: wadToFloat(item.balance),
trendingScore: wadToFloat(item.trendingScore),
volume: formatVolumeValue(item.volume, decimals),
balance: formatVolumeValue(item.balance, decimals),
trendingScore: wadToFloat(item.trendingScore), // Trending score is always in wad
}))

// Base values to use when no data exists
const baseValues = previous ? {
volume: wadToFloat(previous.volume),
balance: wadToFloat(previous.balance),
volume: formatVolumeValue(previous.volume, decimals),
balance: formatVolumeValue(previous.balance, decimals),
trendingScore: wadToFloat(previous.trendingScore),
} : {
volume: 0,
Expand Down Expand Up @@ -153,5 +167,7 @@ export function useV4V5ProjectTimeline({
return {
points,
loading: isLoadingProject || isLoadingTimeline,
projectToken: project?.project?.token ?? undefined,
projectDecimals: project?.project?.decimals ? Number(project.project.decimals) : undefined,
}
}
Loading
Loading