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 { 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<Amendment[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [amendments, setAmendments] = React.useState<Amendment[]>([])
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState<string | null>(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() {
</div>
<div style={{ marginTop: '1rem' }}>{translate("amendment.loading", "Loading amendments...")}</div>
</div>
);
)
}
if (error) {
@@ -138,7 +138,7 @@ export function AmendmentsTable() {
<div className="alert alert-danger" role="alert">
<strong>{translate("amendment.error", "Error loading amendments:")}:</strong> {error}
</div>
);
)
}
// Render amendment table
@@ -167,115 +167,115 @@ export function AmendmentsTable() {
</tbody>
</table>
</div>
);
)
}
function AmendmentBadge(props: { amendment: Amendment }) {
const [status, setStatus] = React.useState<string>('Loading...');
const [href, setHref] = React.useState<string | undefined>(undefined);
const [color, setColor] = React.useState<string>('blue');
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [status, setStatus] = React.useState<string>('Loading...')
const [href, setHref] = React.useState<string | undefined>(undefined)
const [color, setColor] = React.useState<string>('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 (
<Link to={href} target="_blank">
<img src={badgeUrl} alt={status} className="shield" />
</Link>
);
)
}
return <img src={badgeUrl} alt={status} className="shield" />;
return <img src={badgeUrl} alt={status} className="shield" />
}
export function AmendmentDisclaimer(props: {
name: string,
compact: boolean
}) {
const [amendmentStatus, setStatus] = React.useState<Amendment | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [amendmentStatus, setStatus] = React.useState<Amendment | null>(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState<string | null>(null)
const { useTranslate } = useThemeHooks()
const { translate } = useTranslate()
const link = () => <Link to={`/resources/known-amendments#${props.name.toLowerCase()}`}>{props.name}{ props.compact ? "" : " amendment"}</Link>
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])

View File

@@ -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 (
<div className="children-display">
<ul>
{data?.map((item: any) => (
<li className="level-1" key={item.slug}>
<Link to={item.slug}>{item.title}</Link>
{
item.status === "not_enabled" ? (<NotEnabled />) : ""
}
<p className='class="blurb child-blurb'>{item.seo?.description}</p>
</li>
))}
</ul>
</div>
);
const { usePageSharedData } = useThemeHooks()
const data = usePageSharedData('index-page-items') as any[]
return (
<div className="children-display">
<ul>
{data?.map((item: any) => (
<li className="level-1" key={item.slug}>
<Link to={item.slug}>{item.title}</Link>
{
item.status === "not_enabled" ? (<NotEnabled />) : ""
}
<p className="blurb child-blurb">{item.seo?.description}</p>
</li>
))}
</ul>
</div>
)
}
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 (
<li className={className} key={iterStepId}>
<a href={`#interactive-${iterStepId}`}>{step}</a>
</li>
);
)
})}
</ul>
</div>
</div>
<div className="interactive-block-ui">{dynamicReact(props.children, React, {})}</div>
</div>
</div>
);
)
}
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 (
<code>{props.name}</code>
@@ -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 (
<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>
)