Skip to content

Commit d202fb6

Browse files
committed
New: EpochProgress component (src/components/ui/epochProgress.tsx)
Generic progress bar that accepts a period(now) callback, recomputing boundaries on every 1s tick so it auto-restarts when a period ends. Displays metadata pills, a thin fill bar, percentage, and human-readable remaining time. Deleted files - countdown.tsx + countdown.css — Circular countdown component replaced by EpochProgress - userDelegations.tsx — Removed (was already unused; its import was also removed from delegateLocal.tsx) fsp-stats.tsx - Added two EpochProgress bars at the top: reward epoch progress (FireBrick) and current round progress (green) - StatsChart now takes a height prop directly instead of relying on a wrapper <div style={{ height }}>, fixing the recharts -1 width/height console error - Chart formatY changed from Formatter.number(100 * v) to Formatter.percent(v) Validator statistics (Flare + Avalanche) - Both statistics.tsx files: replaced <Countdown> with <EpochProgress>, showing a validator expiration progress bar - Both types.ts files: countdown type changed from { endTime: Date } to { startTimeMs: number, endTimeMs: number } - Both data.tsx files: now provide startTimeMs/endTimeMs in ms; dates formatted with new Formatter.dateHuman instead of Formatter.date rewardClaims.tsx - Simplified chainToTransactionUrl and chainToAddressUrl for Flare — removed protocol-based FSP/Validator branching, always uses EVM explorer URLs - Removed unused Protocol import formatter.ts - Added dateHuman(unix) — formats as "Apr 1, 2026, 18:00" - Added duration(ms) — formats as "1d 5h 23m 12s" main.jsx - Removed countdown.css import
1 parent 3994079 commit d202fb6

16 files changed

Lines changed: 281 additions & 267 deletions

File tree

src/assets/css/countdown.css

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/components/pages/fsp-stats.tsx

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
import { useCallback } from "react"
12
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip } from "recharts"
23
import { Formatter } from "~/utils/misc/formatter"
34
import MeterBar from "~/components/ui/meterBar"
5+
import EpochProgress from "~/components/ui/epochProgress"
46
import { Chain } from "~/enums"
57
import { chainToSymbol } from "~/utils/misc/translations"
8+
import { FspEpoch, FTSO_CHAIN_CONFIG } from "~/utils/misc/flare"
69
import type { FspStatisticsDto } from "~/backendApi"
710

811

912
const chartMargin = { top: 20, right: 20, bottom: 5, left: 20 }
1013

11-
const StatsChart = ({ data, keys, formatY }: {
14+
const StatsChart = ({ data, keys, formatY, height = 200 }: {
1215
data: Record<string, number>[],
1316
keys: string[],
14-
formatY: (v: number) => string
17+
formatY: (v: number) => string,
18+
height?: number
1519
}) => (
16-
<ResponsiveContainer width="100%" height="100%">
20+
<ResponsiveContainer width="100%" height={height}>
1721
<LineChart data={data} margin={chartMargin}>
1822
<XAxis dataKey="x" tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 12 }} tickLine={false} axisLine={false} />
1923
<YAxis hide domain={['auto', 'auto']} />
@@ -43,6 +47,32 @@ const FspStatsComponent = ({ stats, chain }: { stats: FspStatisticsDto, chain: C
4347
const symbol = chainToSymbol(chain)
4448
const lastDelegationEpoch = Math.max(...stats.delegations.result.map(x => x.rewardEpoch))
4549

50+
const cfg = FTSO_CHAIN_CONFIG[chain as keyof typeof FTSO_CHAIN_CONFIG]
51+
52+
const epochPeriod = useCallback((now: number) => {
53+
const round = FspEpoch.timestampToRound(chain, now)
54+
const epoch = FspEpoch.roundToRewardEpoch(chain, round)
55+
const epochStart = epoch * cfg.rewardEpochDurationRounds
56+
return {
57+
startMs: FspEpoch.roundToTimestamp(chain, epochStart),
58+
endMs: FspEpoch.roundToTimestamp(chain, epochStart + cfg.rewardEpochDurationRounds),
59+
metadata: [
60+
{ label: "Reward Epoch", value: epoch },
61+
{ label: "Round", value: `${round - epochStart} / ${cfg.rewardEpochDurationRounds}` },
62+
],
63+
}
64+
}, [chain, cfg])
65+
66+
const roundPeriod = useCallback((now: number) => {
67+
const round = FspEpoch.timestampToRound(chain, now)
68+
const startMs = FspEpoch.roundToTimestamp(chain, round)
69+
return {
70+
startMs,
71+
endMs: startMs + cfg.roundDurationMs,
72+
metadata: [{ label: "Current Round", value: round }],
73+
}
74+
}, [chain, cfg])
75+
4676
const d1 = stats.submissions.result.map(({ rewardEpoch, primary, secondary }) => ({
4777
x: rewardEpoch, 'Primary Success Rate': primary, 'Secondary Success Rate': secondary
4878
}))
@@ -59,7 +89,14 @@ const FspStatsComponent = ({ stats, chain }: { stats: FspStatisticsDto, chain: C
5989

6090
const component = <>
6191
<div className='single-project-page-right wow fadeInUp delay-0-4s mt-30'>
62-
<h2>Provider Statistics</h2>
92+
<div className="row mb-30">
93+
<div className="col-lg-6">
94+
<EpochProgress period={epochPeriod} color="FireBrick" />
95+
</div>
96+
<div className="col-lg-6">
97+
<EpochProgress period={roundPeriod} color="#76B768" />
98+
</div>
99+
</div>
63100
<p>
64101
We value transparency, and stream a part of our monitoring to this site,
65102
so you can see live data of our provider's performance for the current <i>reward epoch</i> {last.rewardEpoch}.
@@ -86,9 +123,7 @@ const FspStatsComponent = ({ stats, chain }: { stats: FspStatisticsDto, chain: C
86123
success. Each <i>reward epoch</i> is a sequence of 3360 rounds (3.5 days), after which each provider's results are evaluated and the provider,
87124
along with its delegators, rewarded accordingly.
88125
</p>
89-
<div style={{ height: 250 }}>
90-
<StatsChart data={d1} keys={['Primary Success Rate', 'Secondary Success Rate']} formatY={v => Formatter.number(100 * v)} />
91-
</div>
126+
<StatsChart data={d1} keys={['Primary Success Rate', 'Secondary Success Rate']} formatY={v => Formatter.percent(v)} height={250} />
92127
</div>
93128

94129
<div className="row mt-40">
@@ -100,9 +135,7 @@ const FspStatsComponent = ({ stats, chain }: { stats: FspStatisticsDto, chain: C
100135
In the graphs below we also append the current state of our delegations as an approximation
101136
for the provider's weight in the following epoch {lastDelegationEpoch}.
102137
</p>
103-
<div style={{ height: 200 }}>
104-
<StatsChart data={d2} keys={[`${symbol} Delegated`]} formatY={v => Formatter.number(v)} />
105-
</div>
138+
<StatsChart data={d2} keys={[`${symbol} Delegated`]} formatY={v => Formatter.number(v)} />
106139
</div>
107140

108141
<div className="row mt-40">
@@ -112,9 +145,7 @@ const FspStatsComponent = ({ stats, chain }: { stats: FspStatisticsDto, chain: C
112145
provider's performance and delegation weight during the vote power block.
113146
Note that due to protocol's reward capping, high delegation volume can result in lower APY.
114147
</p>
115-
<div style={{ height: 200 }}>
116-
<StatsChart data={d4} keys={['APY']} formatY={v => Formatter.number(100 * v, 3)} />
117-
</div>
148+
<StatsChart data={d4} keys={['APY']} formatY={v => Formatter.percent(v)} />
118149
</div>
119150

120151
</div>

src/components/ui/countdown.tsx

Lines changed: 0 additions & 120 deletions
This file was deleted.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useEffect, useState } from "react"
2+
import { Formatter } from "~/utils/misc/formatter"
3+
4+
type MetadataItem = { label: string; value: string | number }
5+
6+
export type EpochPeriod = {
7+
startMs: number
8+
endMs: number
9+
metadata: MetadataItem[]
10+
}
11+
12+
type Props = {
13+
/** Compute the current period boundaries and metadata from a timestamp */
14+
period: (now: number) => EpochPeriod
15+
/** Optional accent color for the fill (default: white) */
16+
color?: string
17+
}
18+
19+
const EpochProgress = ({ period, color = "white" }: Props) => {
20+
const [now, setNow] = useState(Date.now)
21+
22+
useEffect(() => {
23+
const id = setInterval(() => setNow(Date.now()), 1000)
24+
return () => clearInterval(id)
25+
}, [])
26+
27+
const { startMs, endMs, metadata } = period(now)
28+
const total = endMs - startMs
29+
const elapsed = Math.min(Math.max(now - startMs, 0), total)
30+
const progress = total > 0 ? (elapsed / total) * 100 : 0
31+
const remaining = Math.max(endMs - now, 0)
32+
33+
return (
34+
<div style={styles.wrapper}>
35+
{metadata.length > 0 && (
36+
<div style={styles.metaRow}>
37+
{metadata.map((m) => (
38+
<span key={m.label} style={styles.pill}>
39+
<span style={styles.pillLabel}>{m.label}</span>
40+
<span style={styles.pillValue}>{m.value}</span>
41+
</span>
42+
))}
43+
</div>
44+
)}
45+
46+
<div style={styles.barOuter}>
47+
<div
48+
style={{
49+
...styles.barFill,
50+
width: `${progress}%`,
51+
background: color,
52+
}}
53+
/>
54+
</div>
55+
56+
<div style={styles.footer}>
57+
<span style={styles.footerText}>{progress.toFixed(1)}%</span>
58+
<span style={styles.footerText}>{Formatter.duration(remaining)} remaining</span>
59+
</div>
60+
</div>
61+
)
62+
}
63+
64+
const styles: Record<string, React.CSSProperties> = {
65+
wrapper: {
66+
width: "100%",
67+
},
68+
metaRow: {
69+
display: "flex",
70+
flexWrap: "wrap",
71+
gap: 8,
72+
marginBottom: 10.5,
73+
},
74+
pill: {
75+
display: "inline-flex",
76+
alignItems: "center",
77+
gap: 6,
78+
background: "rgba(255,255,255,0.06)",
79+
borderRadius: 6,
80+
padding: "4px 10px",
81+
fontSize: 13,
82+
},
83+
pillLabel: {
84+
color: "rgba(255,255,255,0.45)",
85+
textTransform: "uppercase" as const,
86+
letterSpacing: "0.04em",
87+
fontWeight: 500,
88+
},
89+
pillValue: {
90+
color: "rgba(255,255,255,0.9)",
91+
fontVariantNumeric: "tabular-nums",
92+
fontWeight: 600,
93+
},
94+
barOuter: {
95+
width: "100%",
96+
height: 6,
97+
borderRadius: 3,
98+
background: "rgba(255,255,255,0.08)",
99+
overflow: "hidden",
100+
},
101+
barFill: {
102+
height: "100%",
103+
borderRadius: 3,
104+
transition: "width 1s linear",
105+
},
106+
footer: {
107+
display: "flex",
108+
justifyContent: "space-between",
109+
marginTop: 6,
110+
fontSize: 12,
111+
color: "rgba(255,255,255,0.5)",
112+
fontVariantNumeric: "tabular-nums",
113+
},
114+
footerText: {},
115+
}
116+
117+
export default EpochProgress

0 commit comments

Comments
 (0)