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
2 changes: 1 addition & 1 deletion src/components/ECharts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface ILineAndBarChartProps {
}
chartOptions?: {
[key: string]: {
[key: string]: Value | Array<Value> | ((params: any) => string)
[key: string]: Value | Array<Value> | ((params: any) => string | number)
}
}
height?: string
Expand Down
40 changes: 40 additions & 0 deletions src/containers/TokenPnl/ComparisonPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { formattedNum } from '~/utils'
import { formatPercent } from './format'
import type { ComparisonEntry } from './types'

export const ComparisonPanel = ({ entries, activeId }: { entries: ComparisonEntry[]; activeId: string }) => {
if (!entries.length) return null
return (
<div className="rounded-md border border-(--cards-border) bg-(--cards-bg) p-4">
<div className="mb-3 flex items-center justify-between gap-2">
<h3 className="text-base font-semibold">Range Comparison</h3>
<span className="text-xs text-(--text-secondary)">BTC · ETH · SOL</span>
</div>
<div className="grid gap-3 sm:grid-cols-3">
{entries.map((entry) => {
const isPositive = entry.percentChange >= 0
return (
<div
key={entry.id}
className={`flex flex-col gap-1 rounded-md border border-(--cards-border) p-3 transition-colors duration-200 ${
entry.id === activeId ? 'bg-(--bg-surface)' : 'bg-(--cards-bg)'
}`}
>
<div className="flex items-center gap-2">
{entry.image ? (
<img src={entry.image} alt={entry.name} width={20} height={20} className="rounded-full" />
) : null}
<span className="text-sm font-medium tracking-wide uppercase">{entry.symbol}</span>
</div>
<span className={`text-xl font-semibold ${isPositive ? 'text-emerald-500' : 'text-red-500'}`}>
{formatPercent(entry.percentChange)}
</span>
<span className="text-xs text-(--text-secondary)">{`${isPositive ? '+' : ''}$${formattedNum(entry.absoluteChange)}`}</span>
<span className="text-xs text-(--text-secondary)">{`$${formattedNum(entry.startPrice)} → $${formattedNum(entry.endPrice)}`}</span>
</div>
)
})}
</div>
</div>
)
}
46 changes: 46 additions & 0 deletions src/containers/TokenPnl/DailyPnLGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Tooltip } from '~/components/Tooltip'
import { formatDateLabel, formatPercent } from './format'
import type { TimelinePoint } from './types'

export const DailyPnLGrid = ({ timeline }: { timeline: TimelinePoint[] }) => {
if (!timeline.length) return null
const days = timeline.slice(1)
if (!days.length) return null

const displayDays = days.slice(-90)

return (
<div className="overflow-hidden rounded-md border border-(--cards-border) bg-(--cards-bg) p-4">
<div className="mb-3 flex items-center justify-between gap-2">
<h3 className="text-base font-semibold">Daily Change Grid</h3>
<span className="text-xs text-(--text-secondary)">{displayDays.length} days shown</span>
</div>
<div className="no-scrollbar flex max-w-full flex-wrap gap-1.5 pb-1">
{displayDays.map((day) => {
const isPositive = day.percentChange > 0
const isZero = day.percentChange === 0
const alpha = Math.min(0.6, Math.max(0.22, Math.abs(day.percentChange) / 9))
const backgroundColor = isZero
? 'rgba(148, 163, 184, 0.22)'
: isPositive
? `rgba(16, 185, 129, ${alpha})` // emerald-500 with controlled alpha
: `rgba(239, 68, 68, ${alpha})` // red-500 with controlled alpha
return (
<Tooltip
key={day.timestamp}
placement="top"
content={`${formatPercent(day.percentChange)} • ${formatDateLabel(day.timestamp)}`}
>
<div
className="flex h-8 w-8 shrink-0 items-end justify-center rounded-md border border-white/5 transition-transform duration-200 hover:scale-[0.99]"
style={{ background: backgroundColor }}
>
<span className="sr-only">{`${formatPercent(day.percentChange)} on ${formatDateLabel(day.timestamp)}`}</span>
</div>
</Tooltip>
)
})}
</div>
</div>
)
}
29 changes: 29 additions & 0 deletions src/containers/TokenPnl/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const DateInput = ({
label,
value,
onChange,
min,
max,
invalid
}: {
label: string
value: string
onChange: (value: string) => void
min?: string
max?: string
invalid?: boolean
}) => {
return (
<label className="flex flex-col gap-1.5 text-sm">
<span className="font-light text-(--text-secondary)">{label}</span>
<input
type="date"
value={value}
onChange={(event) => onChange(event.target.value)}
min={min}
max={max}
className={`rounded-md border bg-(--bg-input) px-3 py-2.5 text-base text-black outline-0 transition-colors duration-200 focus:border-white/30 focus:ring-0 dark:text-white ${invalid ? 'border-red-500' : 'border-(--form-control-border)'}`}
/>
</label>
)
}
24 changes: 24 additions & 0 deletions src/containers/TokenPnl/StatsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const StatsCard = ({
label,
value,
subtle,
variant = 'default'
}: {
label: string
value: string
subtle?: string
variant?: 'default' | 'highlight'
}) => {
const base = 'flex flex-col rounded-md border p-3 transition-colors duration-200'
const containerClass =
variant === 'highlight'
? `${base} border-(--cards-border) bg-gradient-to-b from-white/5 to-transparent backdrop-blur-sm`
: `${base} border-(--cards-border) bg-(--cards-bg)`
return (
<div className={containerClass}>
<span className="text-xs font-light tracking-wide text-(--text-secondary) uppercase">{label}</span>
<span className={variant === 'highlight' ? 'text-xl font-bold' : 'text-lg font-medium'}>{value}</span>
{subtle ? <span className="text-xs text-(--text-secondary)">{subtle}</span> : null}
</div>
)
}
14 changes: 14 additions & 0 deletions src/containers/TokenPnl/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const formatPercent = (value: number) => {
if (!Number.isFinite(value)) return '0%'
const formatted = value.toFixed(2)
const prefix = value > 0 ? '+' : value < 0 ? '' : ''
return `${prefix}${formatted}%`
}

export const formatDateLabel = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
timeZone: 'UTC'
})
}
Loading