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
99 changes: 99 additions & 0 deletions src/containers/Ledgers/LedgerCountdownBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useEffect, useState } from 'react'
import './css/ledgerCountdownBanner.scss'

const TARGET_LEDGER = 100_000_000
const SECONDS_PER_LEDGER = 3.9

interface CountdownTime {
ledgersRemaining: number
days: number
hours: number
minutes: number
seconds: number
}

const calculateCountdown = (currentLedger: number): CountdownTime => {
const ledgersRemaining = Math.max(0, TARGET_LEDGER - currentLedger)
const totalSeconds = ledgersRemaining * SECONDS_PER_LEDGER

const days = Math.floor(totalSeconds / (24 * 60 * 60))
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60))
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60)
const seconds = Math.floor(totalSeconds % 60)

return {
ledgersRemaining,
days,
hours,
minutes,
seconds,
}
}

interface LedgerCountdownBannerProps {
currentLedger?: number
}

export const LedgerCountdownBanner = ({
currentLedger,
}: LedgerCountdownBannerProps) => {
const [countdown, setCountdown] = useState<CountdownTime | null>(null)
const [isReached, setIsReached] = useState(false)

useEffect(() => {
if (currentLedger === undefined) return

const newCountdown = calculateCountdown(currentLedger)
setCountdown(newCountdown)
setIsReached(newCountdown.ledgersRemaining === 0)
}, [currentLedger])

if (!countdown) {
return null
}

if (isReached) {
return (
<div className="ledger-countdown-banner celebration">
<div className="banner-content">
<span className="celebration-icon">🎉</span>
<div className="banner-text">
<h2>XRPL has reached 100 million ledgers!</h2>
<p>A historic milestone for the XRP Ledger</p>
</div>
<span className="celebration-icon">🎉</span>
</div>
</div>
)
}

const formatNumber = (num: number) => num.toString().padStart(2, '0')

return (
<div className="ledger-countdown-banner">
<div className="banner-content">
<span className="celebration-icon">🎊</span>
<div className="banner-text">
<h2>Countdown to 100 Million Ledgers</h2>
<div className="countdown-info">
<div className="ledger-count">
<span className="label">Ledgers Remaining:</span>
<span className="value">
{countdown.ledgersRemaining.toLocaleString()}
</span>
</div>
<div className="time-estimate">
<span className="label">Estimated Time:</span>
<span className="value">
{countdown.days}d {formatNumber(countdown.hours)}h{' '}
{formatNumber(countdown.minutes)}m{' '}
{formatNumber(countdown.seconds)}s
</span>
</div>
</div>
</div>
<span className="celebration-icon">🎊</span>
</div>
</div>
)
}
144 changes: 144 additions & 0 deletions src/containers/Ledgers/css/ledgerCountdownBanner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
@use '../../shared/css/variables' as *;

.ledger-countdown-banner {
padding: 12px 20px;
border-radius: $border-radius;
margin: 0 20px 15px;
animation: slide-down 0.5s ease-out;
background: linear-gradient(135deg, $blue-purple-40 0%, $blue-purple-70 100%);
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
color: $white;

&.celebration {
animation: pulse 2s ease-in-out infinite;
background: linear-gradient(135deg, $magenta-40 0%, $magenta-70 100%);
}

.banner-content {
display: flex;
max-width: 1200px;
align-items: center;
justify-content: space-between;
margin: 0 auto;
gap: 12px;

@media (width <= 768px) {
flex-direction: column;
gap: 8px;
}
}

.celebration-icon {
flex-shrink: 0;
animation: bounce 1s ease-in-out infinite;
font-size: 24px;

&:nth-child(3) {
animation-delay: 0.2s;
}

@media (width <= 768px) {
font-size: 20px;
}
}

.banner-text {
flex: 1;
text-align: center;

h2 {
margin: 0 0 4px;
font-size: 16px;
@include semibold;

letter-spacing: 0.5px;

@media (width <= 768px) {
font-size: 14px;
}
}

p {
margin: 0;
font-size: 12px;
@include regular;

opacity: 0.95;
}
}

.countdown-info {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 4px;
gap: 20px;

@media (width <= 768px) {
gap: 12px;
}
}

.ledger-count,
.time-estimate {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;

.label {
font-size: 10px;
@include medium;

letter-spacing: 0.5px;
opacity: 0.85;
text-transform: uppercase;
}

.value {
font-family: 'Courier New', monospace;
font-size: 13px;
@include semibold;

letter-spacing: 0.5px;

@media (width <= 768px) {
font-size: 12px;
}
}
}
}

@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-20px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes bounce {
0%,
100% {
transform: translateY(0);
}

50% {
transform: translateY(-8px);
}
}

@keyframes pulse {
0%,
100% {
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
}

50% {
box-shadow: 0 4px 20px rgb(245 87 108 / 40%);
}
}
13 changes: 13 additions & 0 deletions src/containers/Ledgers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils'
import Streams from '../shared/components/Streams'
import { LedgerMetrics } from './LedgerMetrics'
import { Ledgers } from './Ledgers'
import { LedgerCountdownBanner } from './LedgerCountdownBanner'
import { Ledger, ValidatorResponse } from './types'
import { useAnalytics } from '../shared/analytics'
import NetworkContext from '../shared/NetworkContext'
Expand All @@ -26,6 +27,9 @@ export const LedgersPage = () => {
const [paused, setPaused] = useState(false)
const [metrics, setMetrics] = useState(undefined)
const [unlCount, setUnlCount] = useState<number | undefined>(undefined)
const [currentLedgerIndex, setCurrentLedgerIndex] = useState<
number | undefined
>(undefined)
const { isOnline } = useIsOnline()
const { t } = useTranslation()
const network = useContext(NetworkContext)
Expand Down Expand Up @@ -72,6 +76,14 @@ export const LedgersPage = () => {

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

// Extract current ledger index from the ledgers array
useEffect(() => {
if (ledgers.length > 0) {
const maxLedger = Math.max(...ledgers.map((l) => l.ledger_index || 0))
setCurrentLedgerIndex(maxLedger)
}
}, [ledgers])

return (
<div className="ledgers-page">
<Helmet title={t('ledgers')} />
Expand All @@ -83,6 +95,7 @@ export const LedgersPage = () => {
/>
)}
<SelectedValidatorProvider>
<LedgerCountdownBanner currentLedger={currentLedgerIndex} />
<LedgerMetrics data={metrics} onPause={() => pause()} paused={paused} />
<Ledgers
ledgers={ledgers}
Expand Down