Skip to content

Commit b9e546b

Browse files
100 Million ledger celebration banner (#1258)
## High Level Overview of Change Adds a banner to the home page that has a countdown, and then celebrates the 100 Million ledger mark. <img width="1505" height="521" alt="Screenshot 2025-11-04 at 1 32 44 PM" src="https://github.com/user-attachments/assets/c5c0fbd3-11ef-4e27-b8ae-d27d0ec4d601" /> <img width="1501" height="258" alt="Screenshot 2025-11-04 at 1 33 00 PM" src="https://github.com/user-attachments/assets/e07f6757-5dc5-41cb-9060-f40c10fa0d53" /> <!-- Please include a summary/list of the changes. If too broad, please consider splitting into multiple PRs. --> ### Context of Change <!-- Please include the context of a change. If a bug fix, when was the bug introduced? What was the behavior? If a new feature, why was this architecture chosen? What were the alternatives? If a refactor, how is this better than the previous implementation? If there is a design document for this feature, please link it here. --> ### Type of Change <!-- Please check relevant options, delete irrelevant ones. --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (non-breaking change that only restructures code) - [ ] Tests (You added tests for code that already exists, or your new feature included in this PR) - [ ] Documentation Updates - [ ] Translation Updates - [ ] Release ### Codebase Modernization <!-- In an effort to modernize the codebase, you should convert the files that you work with to React Hooks and TypeScript, and update tests to use the React Testing Library instead of Enzyme. If this is not possible (e.g. it's too many changes, touching too many files, etc.) please explain why here. --> - [ ] Updated files to React Hooks - [ ] Updated files to TypeScript - [ ] Updated tests to React Testing Library ## Before / After <!-- If just refactoring / back-end changes, this can be just an in-English description of the change at a technical level. If a UI change, screenshots should be included. --> ## Test Plan <!-- Please describe the tests that you ran to verify your changes and provide instructions so that others can reproduce. --> <!-- ## Future Tasks For future tasks related to PR. -->
1 parent bb1d642 commit b9e546b

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useEffect, useState } from 'react'
2+
import './css/ledgerCountdownBanner.scss'
3+
4+
const TARGET_LEDGER = 100_000_000
5+
const SECONDS_PER_LEDGER = 3.9
6+
7+
interface CountdownTime {
8+
ledgersRemaining: number
9+
days: number
10+
hours: number
11+
minutes: number
12+
seconds: number
13+
}
14+
15+
const calculateCountdown = (currentLedger: number): CountdownTime => {
16+
const ledgersRemaining = Math.max(0, TARGET_LEDGER - currentLedger)
17+
const totalSeconds = ledgersRemaining * SECONDS_PER_LEDGER
18+
19+
const days = Math.floor(totalSeconds / (24 * 60 * 60))
20+
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60))
21+
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60)
22+
const seconds = Math.floor(totalSeconds % 60)
23+
24+
return {
25+
ledgersRemaining,
26+
days,
27+
hours,
28+
minutes,
29+
seconds,
30+
}
31+
}
32+
33+
interface LedgerCountdownBannerProps {
34+
currentLedger?: number
35+
}
36+
37+
export const LedgerCountdownBanner = ({
38+
currentLedger,
39+
}: LedgerCountdownBannerProps) => {
40+
const [countdown, setCountdown] = useState<CountdownTime | null>(null)
41+
const [isReached, setIsReached] = useState(false)
42+
43+
useEffect(() => {
44+
if (currentLedger === undefined) return
45+
46+
const newCountdown = calculateCountdown(currentLedger)
47+
setCountdown(newCountdown)
48+
setIsReached(newCountdown.ledgersRemaining === 0)
49+
}, [currentLedger])
50+
51+
if (!countdown) {
52+
return null
53+
}
54+
55+
if (isReached) {
56+
return (
57+
<div className="ledger-countdown-banner celebration">
58+
<div className="banner-content">
59+
<span className="celebration-icon">🎉</span>
60+
<div className="banner-text">
61+
<h2>XRPL has reached 100 million ledgers!</h2>
62+
<p>A historic milestone for the XRP Ledger</p>
63+
</div>
64+
<span className="celebration-icon">🎉</span>
65+
</div>
66+
</div>
67+
)
68+
}
69+
70+
const formatNumber = (num: number) => num.toString().padStart(2, '0')
71+
72+
return (
73+
<div className="ledger-countdown-banner">
74+
<div className="banner-content">
75+
<span className="celebration-icon">🎊</span>
76+
<div className="banner-text">
77+
<h2>Countdown to 100 Million Ledgers</h2>
78+
<div className="countdown-info">
79+
<div className="ledger-count">
80+
<span className="label">Ledgers Remaining:</span>
81+
<span className="value">
82+
{countdown.ledgersRemaining.toLocaleString()}
83+
</span>
84+
</div>
85+
<div className="time-estimate">
86+
<span className="label">Estimated Time:</span>
87+
<span className="value">
88+
{countdown.days}d {formatNumber(countdown.hours)}h{' '}
89+
{formatNumber(countdown.minutes)}m{' '}
90+
{formatNumber(countdown.seconds)}s
91+
</span>
92+
</div>
93+
</div>
94+
</div>
95+
<span className="celebration-icon">🎊</span>
96+
</div>
97+
</div>
98+
)
99+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
@use '../../shared/css/variables' as *;
2+
3+
.ledger-countdown-banner {
4+
padding: 12px 20px;
5+
border-radius: $border-radius;
6+
margin: 0 20px 15px;
7+
animation: slide-down 0.5s ease-out;
8+
background: linear-gradient(135deg, $blue-purple-40 0%, $blue-purple-70 100%);
9+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
10+
color: $white;
11+
12+
&.celebration {
13+
animation: pulse 2s ease-in-out infinite;
14+
background: linear-gradient(135deg, $magenta-40 0%, $magenta-70 100%);
15+
}
16+
17+
.banner-content {
18+
display: flex;
19+
max-width: 1200px;
20+
align-items: center;
21+
justify-content: space-between;
22+
margin: 0 auto;
23+
gap: 12px;
24+
25+
@media (width <= 768px) {
26+
flex-direction: column;
27+
gap: 8px;
28+
}
29+
}
30+
31+
.celebration-icon {
32+
flex-shrink: 0;
33+
animation: bounce 1s ease-in-out infinite;
34+
font-size: 24px;
35+
36+
&:nth-child(3) {
37+
animation-delay: 0.2s;
38+
}
39+
40+
@media (width <= 768px) {
41+
font-size: 20px;
42+
}
43+
}
44+
45+
.banner-text {
46+
flex: 1;
47+
text-align: center;
48+
49+
h2 {
50+
margin: 0 0 4px;
51+
font-size: 16px;
52+
@include semibold;
53+
54+
letter-spacing: 0.5px;
55+
56+
@media (width <= 768px) {
57+
font-size: 14px;
58+
}
59+
}
60+
61+
p {
62+
margin: 0;
63+
font-size: 12px;
64+
@include regular;
65+
66+
opacity: 0.95;
67+
}
68+
}
69+
70+
.countdown-info {
71+
display: flex;
72+
flex-wrap: wrap;
73+
justify-content: center;
74+
margin-top: 4px;
75+
gap: 20px;
76+
77+
@media (width <= 768px) {
78+
gap: 12px;
79+
}
80+
}
81+
82+
.ledger-count,
83+
.time-estimate {
84+
display: flex;
85+
flex-direction: column;
86+
align-items: center;
87+
gap: 2px;
88+
89+
.label {
90+
font-size: 10px;
91+
@include medium;
92+
93+
letter-spacing: 0.5px;
94+
opacity: 0.85;
95+
text-transform: uppercase;
96+
}
97+
98+
.value {
99+
font-family: 'Courier New', monospace;
100+
font-size: 13px;
101+
@include semibold;
102+
103+
letter-spacing: 0.5px;
104+
105+
@media (width <= 768px) {
106+
font-size: 12px;
107+
}
108+
}
109+
}
110+
}
111+
112+
@keyframes slide-down {
113+
from {
114+
opacity: 0;
115+
transform: translateY(-20px);
116+
}
117+
118+
to {
119+
opacity: 1;
120+
transform: translateY(0);
121+
}
122+
}
123+
124+
@keyframes bounce {
125+
0%,
126+
100% {
127+
transform: translateY(0);
128+
}
129+
130+
50% {
131+
transform: translateY(-8px);
132+
}
133+
}
134+
135+
@keyframes pulse {
136+
0%,
137+
100% {
138+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
139+
}
140+
141+
50% {
142+
box-shadow: 0 4px 20px rgb(245 87 108 / 40%);
143+
}
144+
}

src/containers/Ledgers/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils'
88
import Streams from '../shared/components/Streams'
99
import { LedgerMetrics } from './LedgerMetrics'
1010
import { Ledgers } from './Ledgers'
11+
import { LedgerCountdownBanner } from './LedgerCountdownBanner'
1112
import { Ledger, ValidatorResponse } from './types'
1213
import { useAnalytics } from '../shared/analytics'
1314
import NetworkContext from '../shared/NetworkContext'
@@ -26,6 +27,9 @@ export const LedgersPage = () => {
2627
const [paused, setPaused] = useState(false)
2728
const [metrics, setMetrics] = useState(undefined)
2829
const [unlCount, setUnlCount] = useState<number | undefined>(undefined)
30+
const [currentLedgerIndex, setCurrentLedgerIndex] = useState<
31+
number | undefined
32+
>(undefined)
2933
const { isOnline } = useIsOnline()
3034
const { t } = useTranslation()
3135
const network = useContext(NetworkContext)
@@ -72,6 +76,14 @@ export const LedgersPage = () => {
7276

7377
const pause = () => setPaused(!paused)
7478

79+
// Extract current ledger index from the ledgers array
80+
useEffect(() => {
81+
if (ledgers.length > 0) {
82+
const maxLedger = Math.max(...ledgers.map((l) => l.ledger_index || 0))
83+
setCurrentLedgerIndex(maxLedger)
84+
}
85+
}, [ledgers])
86+
7587
return (
7688
<div className="ledgers-page">
7789
<Helmet title={t('ledgers')} />
@@ -83,6 +95,7 @@ export const LedgersPage = () => {
8395
/>
8496
)}
8597
<SelectedValidatorProvider>
98+
<LedgerCountdownBanner currentLedger={currentLedgerIndex} />
8699
<LedgerMetrics data={metrics} onPause={() => pause()} paused={paused} />
87100
<Ledgers
88101
ledgers={ledgers}

0 commit comments

Comments
 (0)