add reviewer suggestions

This commit is contained in:
Oliver Eggert
2025-10-01 14:09:38 -07:00
parent b7b90741cb
commit b1c5f3646e
2 changed files with 171 additions and 168 deletions

View File

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

View File

@@ -1,19 +1,19 @@
import * as React from 'react'; import * as React from 'react'
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom'
// @ts-ignore // @ts-ignore
import dynamicReact from '@markdoc/markdoc/dist/react'; import dynamicReact from '@markdoc/markdoc/dist/react'
import { Link } from '@redocly/theme/components/Link/Link'; import { Link } from '@redocly/theme/components/Link/Link'
import { useThemeHooks } from '@redocly/theme/core/hooks'; import { useThemeHooks } from '@redocly/theme/core/hooks'
import { idify } from '../helpers'; import { idify } from '../helpers'
import { Button } from '@redocly/theme/components/Button/Button'; import { Button } from '@redocly/theme/components/Button/Button'
export {default as XRPLoader} from '../components/XRPLoader'; export {default as XRPLoader} from '../components/XRPLoader'
export { XRPLCard, CardGrid } from '../components/XRPLCard'; export { XRPLCard, CardGrid } from '../components/XRPLCard'
export * from '../components/Amendments'; export { AmendmentsTable, AmendmentDisclaimer, Badge } from '../components/Amendments'
export function IndexPageItems() { export function IndexPageItems() {
const { usePageSharedData } = useThemeHooks(); const { usePageSharedData } = useThemeHooks()
const data = usePageSharedData('index-page-items') as any[]; const data = usePageSharedData('index-page-items') as any[]
return ( return (
<div className="children-display"> <div className="children-display">
<ul> <ul>
@@ -23,17 +23,21 @@ export function IndexPageItems() {
{ {
item.status === "not_enabled" ? (<NotEnabled />) : "" item.status === "not_enabled" ? (<NotEnabled />) : ""
} }
<p className='class="blurb child-blurb'>{item.seo?.description}</p> <p className="blurb child-blurb">{item.seo?.description}</p>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
); )
} }
export function InteractiveBlock(props: { children: React.ReactNode; label: string; steps: string[] }) { export function InteractiveBlock(props: {
const stepId = idify(props.label); children: React.ReactNode
const { pathname } = useLocation(); label: string
steps: string[]
}) {
const stepId = idify(props.label)
const { pathname } = useLocation()
return ( return (
// add key={pathname} to ensure old step state gets rerendered on page navigation // add key={pathname} to ensure old step state gets rerendered on page navigation
@@ -47,29 +51,28 @@ export function InteractiveBlock(props: { children: React.ReactNode; label: stri
data-stepid={stepId} data-stepid={stepId}
> >
{props.steps?.map((step, idx) => { {props.steps?.map((step, idx) => {
const iterStepId = idify(step).toLowerCase(); const iterStepId = idify(step).toLowerCase()
let className = `breadcrumb-item bc-${iterStepId}`; let className = `breadcrumb-item bc-${iterStepId}`
if (idx > 0) className += ' disabled'; if (idx > 0) className += ' disabled'
if (iterStepId === stepId) className += ' current'; if (iterStepId === stepId) className += ' current'
return ( return (
<li className={className} key={iterStepId}> <li className={className} key={iterStepId}>
<a href={`#interactive-${iterStepId}`}>{step}</a> <a href={`#interactive-${iterStepId}`}>{step}</a>
</li> </li>
); )
})} })}
</ul> </ul>
</div> </div>
<div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div> <div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div>
</div> </div>
</div> </div>
); )
} }
export function RepoLink(props: { export function RepoLink(props: {
children: React.ReactNode; children: React.ReactNode
path: string; path: string
github_fork: string; github_fork: string
github_branch: string github_branch: string
}) { }) {
const treeblob = props.path.indexOf(".") >= 0 ? "blob/" : "tree/" const treeblob = props.path.indexOf(".") >= 0 ? "blob/" : "tree/"
@@ -82,7 +85,7 @@ export function RepoLink(props: {
} }
export function CodePageName(props: { export function CodePageName(props: {
name: string; name: string
}) { }) {
return ( return (
<code>{props.name}</code> <code>{props.name}</code>
@@ -97,7 +100,7 @@ export function TryIt(props: {
}) { }) {
const { useTranslate } = useThemeHooks() const { useTranslate } = useThemeHooks()
const { translate } = useTranslate() const { translate } = useTranslate()
let use_server = ""; let use_server = ""
if (props.server == "s1") { if (props.server == "s1") {
use_server = "?server=wss%3A%2F%2Fs1.ripple.com%2F" use_server = "?server=wss%3A%2F%2Fs1.ripple.com%2F"
} else if (props.server == "s2") { } else if (props.server == "s2") {
@@ -125,7 +128,7 @@ export function TxExample(props: {
}) { }) {
const { useTranslate } = useThemeHooks() const { useTranslate } = useThemeHooks()
const { translate } = useTranslate() const { translate } = useTranslate()
let use_server = ""; let use_server = ""
if (props.server == "s1") { if (props.server == "s1") {
use_server = "&server=wss%3A%2F%2Fs1.ripple.com%2F" use_server = "&server=wss%3A%2F%2Fs1.ripple.com%2F"
} else if (props.server == "s2") { } else if (props.server == "s2") {
@@ -146,8 +149,8 @@ export function TxExample(props: {
} }
export function NotEnabled() { export function NotEnabled() {
const { useTranslate } = useThemeHooks(); const { useTranslate } = useThemeHooks()
const { translate } = useTranslate(); const { translate } = useTranslate()
return ( return (
<span className="status not_enabled" title={translate("This feature is not currently enabled on the production XRP Ledger.")}><i className="fa fa-flask"></i></span> <span className="status not_enabled" title={translate("This feature is not currently enabled on the production XRP Ledger.")}><i className="fa fa-flask"></i></span>
) )