diff --git a/@theme/components/Amendments.tsx b/@theme/components/Amendments.tsx index 2cac0deb09..b7a3fc914d 100644 --- a/@theme/components/Amendments.tsx +++ b/@theme/components/Amendments.tsx @@ -1,125 +1,125 @@ -import * as React from 'react'; -import { Link } from '@redocly/theme/components/Link/Link'; -import { useThemeHooks } from '@redocly/theme/core/hooks'; +import * as React from 'react' +import { Link } from '@redocly/theme/components/Link/Link' +import { useThemeHooks } from '@redocly/theme/core/hooks' type Amendment = { - name: string; - rippled_version: string; - tx_hash?: string; - consensus?: string; - date?: string; - id: string; - eta?: string; -}; + name: string + rippled_version: string + tx_hash?: string + consensus?: string + date?: string + id: string + eta?: string +} type AmendmentsResponse = { - amendments: Amendment[]; -}; + amendments: Amendment[] +} type AmendmentsCachePayload = { - timestamp: number; - amendments: Amendment[]; + timestamp: number + amendments: Amendment[] } // API data caching -const amendmentsEndpoint = 'https://vhs.prod.ripplex.io/v1/network/amendments/vote/main/'; -const amendmentsCacheKey = 'xrpl.amendments.mainnet.cache'; -const amendmentsTTL = 12 * 60 * 60 * 1000; // 12 hours in milliseconds +const amendmentsEndpoint = 'https://vhs.prod.ripplex.io/v1/network/amendments/vote/main/' +const amendmentsCacheKey = 'xrpl.amendments.mainnet.cache' +const amendmentsTTL = 15 * 60 * 1000 // 15 minutes in milliseconds function readAmendmentsCache(): Amendment[] | null { - if (typeof window === 'undefined') return null; + if (typeof window === 'undefined') return null try { - const raw = window.localStorage.getItem(amendmentsCacheKey); - if (!raw) return null; - const parsed: AmendmentsCachePayload = JSON.parse(raw); - if (!parsed || !Array.isArray(parsed.amendments)) return null; - const fresh = Date.now() - parsed.timestamp < amendmentsTTL; - return fresh ? parsed.amendments : null; + const raw = window.localStorage.getItem(amendmentsCacheKey) + if (!raw) return null + const parsed: AmendmentsCachePayload = JSON.parse(raw) + if (!parsed || !Array.isArray(parsed.amendments)) return null + const fresh = Date.now() - parsed.timestamp < amendmentsTTL + return fresh ? parsed.amendments : null } catch { - return null; + return null } } function writeAmendmentsCache(amendments: Amendment[]) { - if (typeof window === 'undefined') return; + if (typeof window === 'undefined') return try { - const payload: AmendmentsCachePayload = { timestamp: Date.now(), amendments }; - window.localStorage.setItem(amendmentsCacheKey, JSON.stringify(payload)); + const payload: AmendmentsCachePayload = { timestamp: Date.now(), amendments } + window.localStorage.setItem(amendmentsCacheKey, JSON.stringify(payload)) } catch { // Ignore quota or serialization errors } } -// Sort amendments table +// Sort amendments table by status, then chronologically, then alphabetically function sortAmendments(list: Amendment[]): Amendment[] { const getStatusPriority = (amendment: Amendment): number => { - if (amendment.eta) return 0; // Expected - if (amendment.consensus) return 1; // Open for Voting - if (amendment.tx_hash) return 2; // Enabled - return 3; // Fallback - }; + if (amendment.consensus) return 0 // Open for Voting + if (amendment.eta) return 1 // Expected + if (amendment.tx_hash) return 2 // Enabled + return 3 // Fallback + } const getChronoKey = (amendment: Amendment): number => { - const raw = amendment.date || amendment.eta; - if (!raw) return 0; - const t = Date.parse(raw); - return isNaN(t) ? 0 : t; - }; + const raw = amendment.date || amendment.eta + if (!raw) return 0 + const t = Date.parse(raw) + return isNaN(t) ? 0 : t + } return [...list].sort((a, b) => { // 1. Status - const statusDiff = getStatusPriority(a) - getStatusPriority(b); - if (statusDiff !== 0) return statusDiff; + const statusDiff = getStatusPriority(a) - getStatusPriority(b) + if (statusDiff !== 0) return statusDiff // 2. Chronological (most recent first) - const chronoA = getChronoKey(a); - const chronoB = getChronoKey(b); - if (chronoA !== chronoB) return chronoB - chronoA; + const chronoA = getChronoKey(a) + const chronoB = getChronoKey(b) + if (chronoA !== chronoB) return chronoB - chronoA // 3. Alphabetical - return a.name.localeCompare(b.name); - }); + return a.name.localeCompare(b.name) + }) } // Generate amendments table with live mainnet data export function AmendmentsTable() { - const [amendments, setAmendments] = React.useState([]); - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); - const { useTranslate } = useThemeHooks(); - const { translate } = useTranslate(); + const [amendments, setAmendments] = React.useState([]) + const [loading, setLoading] = React.useState(true) + const [error, setError] = React.useState(null) + const { useTranslate } = useThemeHooks() + const { translate } = useTranslate() React.useEffect(() => { const fetchAmendments = async () => { try { - setLoading(true); + setLoading(true) // 1. Try cache first - const cached = readAmendmentsCache(); + const cached = readAmendmentsCache() if (cached) { - setAmendments(sortAmendments(cached)); - return; // Use current cache (fresh) + setAmendments(sortAmendments(cached)) + return // Use current cache (fresh) } // 2. Fetch new data if cache is stale - const response = await fetch(amendmentsEndpoint); + const response = await fetch(amendmentsEndpoint) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`) } - const data: AmendmentsResponse = await response.json(); - writeAmendmentsCache(data.amendments); + const data: AmendmentsResponse = await response.json() + writeAmendmentsCache(data.amendments) - setAmendments(sortAmendments(data.amendments)); + setAmendments(sortAmendments(data.amendments)) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch amendments'); + setError(err instanceof Error ? err.message : 'Failed to fetch amendments') } finally { - setLoading(false); + setLoading(false) } - }; + } - fetchAmendments(); - }, []); + fetchAmendments() + }, []) // Fancy schmancy loading icon if (loading) { @@ -130,7 +130,7 @@ export function AmendmentsTable() {
{translate("amendment.loading", "Loading amendments...")}
- ); + ) } if (error) { @@ -138,7 +138,7 @@ export function AmendmentsTable() {
{translate("amendment.error", "Error loading amendments:")}: {error}
- ); + ) } // Render amendment table @@ -167,115 +167,115 @@ export function AmendmentsTable() { - ); + ) } function AmendmentBadge(props: { amendment: Amendment }) { - const [status, setStatus] = React.useState('Loading...'); - const [href, setHref] = React.useState(undefined); - const [color, setColor] = React.useState('blue'); - const { useTranslate } = useThemeHooks(); - const { translate } = useTranslate(); + const [status, setStatus] = React.useState('Loading...') + const [href, setHref] = React.useState(undefined) + const [color, setColor] = React.useState('blue') + const { useTranslate } = useThemeHooks() + const { translate } = useTranslate() - const enabledLabel = translate("amendment.status.enabled", "Enabled"); - const votingLabel = translate("amendment.status.openForVoting", "Open for Voting"); - const etaLabel = translate("amendment.status.eta", "Expected"); + const enabledLabel = translate("amendment.status.enabled", "Enabled") + const votingLabel = translate("amendment.status.openForVoting", "Open for Voting") + const etaLabel = translate("amendment.status.eta", "Expected") React.useEffect(() => { - const amendment = props.amendment; + const amendment = props.amendment // Check if amendment is enabled (has tx_hash) if (amendment.tx_hash) { - const enabledDate = new Date(amendment.date).toISOString().split('T')[0]; - setStatus(`${enabledLabel}: ${enabledDate}`); - setColor('green'); - setHref(`https://livenet.xrpl.org/transactions/${amendment.tx_hash}`); + const enabledDate = new Date(amendment.date).toISOString().split('T')[0] + setStatus(`${enabledLabel}: ${enabledDate}`) + setColor('green') + setHref(`https://livenet.xrpl.org/transactions/${amendment.tx_hash}`) } // Check if expected activation is provided (has eta field) else if (amendment.eta) { - let etaDate = new Date(amendment.eta).toISOString().split('T')[0]; - setStatus(`${etaLabel}: ${etaDate}`); - setColor('blue'); - setHref(undefined); + let etaDate = new Date(amendment.eta).toISOString().split('T')[0] + setStatus(`${etaLabel}: ${etaDate}`) + setColor('blue') + setHref(undefined) } // Check if amendment is currently being voted on (has consensus field) else if (amendment.consensus) { - setStatus(`${votingLabel}: ${amendment.consensus}`); - setColor('80d0e0'); - setHref(undefined); // No link for voting amendments + setStatus(`${votingLabel}: ${amendment.consensus}`) + setColor('80d0e0') + setHref(undefined) // No link for voting amendments } - }, [props.amendment, enabledLabel, etaLabel, votingLabel]); + }, [props.amendment, enabledLabel, etaLabel, votingLabel]) // Split the status at the colon to create two-color badge - const parts = status.split(':'); - const label = shieldsIoEscape(parts[0]); - const message = shieldsIoEscape(parts.slice(1).join(':')); + const parts = status.split(':') + const label = shieldsIoEscape(parts[0]) + const message = shieldsIoEscape(parts.slice(1).join(':')) - const badgeUrl = `https://img.shields.io/badge/${label}-${message}-${color}`; + const badgeUrl = `https://img.shields.io/badge/${label}-${message}-${color}` if (href) { return ( {status} - ); + ) } - return {status}; + return {status} } export function AmendmentDisclaimer(props: { name: string, compact: boolean }) { - const [amendmentStatus, setStatus] = React.useState(null); - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); - const { useTranslate } = useThemeHooks(); - const { translate } = useTranslate(); + const [amendmentStatus, setStatus] = React.useState(null) + const [loading, setLoading] = React.useState(true) + const [error, setError] = React.useState(null) + const { useTranslate } = useThemeHooks() + const { translate } = useTranslate() const link = () => {props.name}{ props.compact ? "" : " amendment"} React.useEffect(() => { const loadAmendment = async () => { try { - setLoading(true); + setLoading(true) // 1. Try cache first - const cached = readAmendmentsCache(); + const cached = readAmendmentsCache() if (cached) { - const found = cached.find(a => a.name === props.name); + const found = cached.find(a => a.name === props.name) if (found) { - setStatus(found); - return; // amendment successfully found in cache + setStatus(found) + return // amendment successfully found in cache } } // 2. New API request for stale/missing cache. // Also catches edge case of new amendment appearing // on mainnet within cache TTL window. - const response = await fetch(amendmentsEndpoint); + const response = await fetch(amendmentsEndpoint) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`) } - const data: AmendmentsResponse = await response.json(); - writeAmendmentsCache(data.amendments); + const data: AmendmentsResponse = await response.json() + writeAmendmentsCache(data.amendments) - const found = data.amendments.find(a => a.name === props.name); + const found = data.amendments.find(a => a.name === props.name) if (!found) { - throw new Error(`Couldn't find ${props.name} amendment in status table.`); + throw new Error(`Couldn't find ${props.name} amendment in status table.`) } - setStatus(found); + setStatus(found) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch amendments'); + setError(err instanceof Error ? err.message : 'Failed to fetch amendments') } finally { - setLoading(false); + setLoading(false) } }; - loadAmendment(); - }, [props.name]); + loadAmendment() + }, [props.name]) if (loading) { return ( @@ -342,9 +342,9 @@ function shieldsIoEscape(s: string) { } export function Badge(props: { - children: React.ReactNode; - color: string; - href: string; + children: React.ReactNode + color: string + href: string }) { const DEFAULT_COLORS = { "open for voting": "80d0e0", @@ -372,7 +372,7 @@ export function Badge(props: { if (typeof child == "string") { childstrings += child } - }); + }) const parts = childstrings.split(":") const left : string = shieldsIoEscape(parts[0]) diff --git a/@theme/markdoc/components.tsx b/@theme/markdoc/components.tsx index c9fcf93085..366d8982b2 100644 --- a/@theme/markdoc/components.tsx +++ b/@theme/markdoc/components.tsx @@ -1,39 +1,43 @@ -import * as React from 'react'; -import { useLocation } from 'react-router-dom'; +import * as React from 'react' +import { useLocation } from 'react-router-dom' // @ts-ignore -import dynamicReact from '@markdoc/markdoc/dist/react'; -import { Link } from '@redocly/theme/components/Link/Link'; -import { useThemeHooks } from '@redocly/theme/core/hooks'; -import { idify } from '../helpers'; -import { Button } from '@redocly/theme/components/Button/Button'; +import dynamicReact from '@markdoc/markdoc/dist/react' +import { Link } from '@redocly/theme/components/Link/Link' +import { useThemeHooks } from '@redocly/theme/core/hooks' +import { idify } from '../helpers' +import { Button } from '@redocly/theme/components/Button/Button' -export {default as XRPLoader} from '../components/XRPLoader'; -export { XRPLCard, CardGrid } from '../components/XRPLCard'; -export * from '../components/Amendments'; +export {default as XRPLoader} from '../components/XRPLoader' +export { XRPLCard, CardGrid } from '../components/XRPLCard' +export { AmendmentsTable, AmendmentDisclaimer, Badge } from '../components/Amendments' export function IndexPageItems() { - const { usePageSharedData } = useThemeHooks(); - const data = usePageSharedData('index-page-items') as any[]; - return ( -
-
    - {data?.map((item: any) => ( -
  • - {item.title} - { - item.status === "not_enabled" ? () : "" - } -

    {item.seo?.description}

    -
  • - ))} -
-
- ); + const { usePageSharedData } = useThemeHooks() + const data = usePageSharedData('index-page-items') as any[] + return ( +
+
    + {data?.map((item: any) => ( +
  • + {item.title} + { + item.status === "not_enabled" ? () : "" + } +

    {item.seo?.description}

    +
  • + ))} +
+
+ ) } -export function InteractiveBlock(props: { children: React.ReactNode; label: string; steps: string[] }) { - const stepId = idify(props.label); - const { pathname } = useLocation(); +export function InteractiveBlock(props: { + children: React.ReactNode + label: string + steps: string[] +}) { + const stepId = idify(props.label) + const { pathname } = useLocation() return ( // add key={pathname} to ensure old step state gets rerendered on page navigation @@ -47,30 +51,29 @@ export function InteractiveBlock(props: { children: React.ReactNode; label: stri data-stepid={stepId} > {props.steps?.map((step, idx) => { - const iterStepId = idify(step).toLowerCase(); - let className = `breadcrumb-item bc-${iterStepId}`; - if (idx > 0) className += ' disabled'; - if (iterStepId === stepId) className += ' current'; + const iterStepId = idify(step).toLowerCase() + let className = `breadcrumb-item bc-${iterStepId}` + if (idx > 0) className += ' disabled' + if (iterStepId === stepId) className += ' current' return (
  • {step}
  • - ); + ) })} - - +
    {dynamicReact(props.children, React, {})}
    - ); + ) } export function RepoLink(props: { - children: React.ReactNode; - path: string; - github_fork: string; - github_branch: string + children: React.ReactNode + path: string + github_fork: string + github_branch: string }) { const treeblob = props.path.indexOf(".") >= 0 ? "blob/" : "tree/" const sep = props.github_fork[-1] == "/" ? "" : "/" @@ -82,7 +85,7 @@ export function RepoLink(props: { } export function CodePageName(props: { - name: string; + name: string }) { return ( {props.name} @@ -97,7 +100,7 @@ export function TryIt(props: { }) { const { useTranslate } = useThemeHooks() const { translate } = useTranslate() - let use_server = ""; + let use_server = "" if (props.server == "s1") { use_server = "?server=wss%3A%2F%2Fs1.ripple.com%2F" } else if (props.server == "s2") { @@ -125,7 +128,7 @@ export function TxExample(props: { }) { const { useTranslate } = useThemeHooks() const { translate } = useTranslate() - let use_server = ""; + let use_server = "" if (props.server == "s1") { use_server = "&server=wss%3A%2F%2Fs1.ripple.com%2F" } else if (props.server == "s2") { @@ -146,8 +149,8 @@ export function TxExample(props: { } export function NotEnabled() { - const { useTranslate } = useThemeHooks(); - const { translate } = useTranslate(); + const { useTranslate } = useThemeHooks() + const { translate } = useTranslate() return ( )