More cards
This commit is contained in:
+7
-7
@@ -63,7 +63,7 @@ export default function App() {
|
||||
try {
|
||||
const data = await request<TestnetSnapshot>('state');
|
||||
if (data.network !== NETWORK || data.algorithm !== 'RandomX' || typeof data.chainId !== 'string' ||
|
||||
!Array.isArray(data.blocks) || !Array.isArray(data.transfers) || !Array.isArray(data.pendingTransfers) || !data.nextArticle)
|
||||
!Array.isArray(data.blocks) || !Array.isArray(data.transfers) || !Array.isArray(data.pendingTransfers))
|
||||
throw new Error('Incompatible node. This interface requires the Wikideck RandomX testnet API.');
|
||||
if (version === refreshVersion.current) { setSnapshot(data); setRpcAvailable(true); setError(''); }
|
||||
return data;
|
||||
@@ -168,7 +168,7 @@ export default function App() {
|
||||
{error && <div className="error-banner" role="alert"><CircleHelp size={17} /><span>{error}</span><button onClick={retry}>Retry</button><button aria-label="Dismiss error" onClick={() => setError('')}><X size={16} /></button></div>}
|
||||
{view === 'Discover' && <>
|
||||
<section className="hero">
|
||||
<div className="hero-copy"><span className="eyebrow"><span /> KNOWLEDGE, COLLECTED ON ITS OWN CHAIN.</span><h1>Mine knowledge.<br />Collect the <span>extraordinary.</span></h1><p>Four curated article snapshots. A chain of new discoveries. Mine native NFTs with an opt-in RandomX miner, and keep the keys to your collection.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Explore native mining <ArrowUpRight size={17} /></button><button className="text-button" onClick={() => setDialog('guide')}>How it works <ArrowRight size={16} /></button></div><div className="hero-foot"><span><Check size={13} /> Independent testnet</span><span><Check size={13} /> No browser mining</span></div></div>
|
||||
<div className="hero-copy"><span className="eyebrow"><span /> KNOWLEDGE, COLLECTED ON ITS OWN CHAIN.</span><h1>Mine knowledge.<br />Collect the <span>extraordinary.</span></h1><p>Random Wikipedia articles, each with a picture. A chain of new discoveries. Mine native NFTs with an opt-in RandomX miner, and keep the keys to your collection.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Explore native mining <ArrowUpRight size={17} /></button><button className="text-button" onClick={() => setDialog('guide')}>How it works <ArrowRight size={16} /></button></div><div className="hero-foot"><span><Check size={13} /> Independent testnet</span><span><Check size={13} /> No browser mining</span></div></div>
|
||||
<div className="hero-visual"><div className="orbit orbit-one" /><div className="orbit orbit-two" /><span className="orbit-spark spark-one">✳</span><span className="orbit-spark spark-two">+</span><div className="floating-label label-top"><Sparkles size={12} /> Preview cards · not owned</div><div className="hero-card back-card"><Card card={samples[2]} onClick={() => setSelected(samples[2])} /></div><div className="hero-card front-card"><Card card={samples[0]} onClick={() => setSelected(samples[0])} /></div><div className="floating-label label-bottom"><Sparkles size={15} /> A world worth collecting <ArrowUpRight size={13} /></div></div>
|
||||
</section>
|
||||
<section className="metrics" aria-label="Canonical chain statistics">
|
||||
@@ -179,11 +179,11 @@ export default function App() {
|
||||
</section>
|
||||
<section className="mining-panel native-mining" ref={minerPanel}>
|
||||
<div className="mining-description"><div className="section-eyebrow"><span className="idle-dot" /> EXPLICIT OPT-IN · OUTSIDE YOUR BROWSER</div><h2>Native RandomX mining</h2><p>This page never runs mining code or launches a native program. Build the trusted native binary, download an encrypted wallet backup, then start the miner yourself in an interactive terminal.</p><div className="mining-meta"><span><Cpu size={13} /> One native worker</span><span><Shield size={13} /> Independent validating peers</span></div><div className="native-memory"><strong>Light · default</strong><span>256 MiB cache + scratchpad and overhead.</span><strong>Fast · explicit --fast</strong><span>~2 GiB dataset plus cache; budget over 2.3 GiB per process. Higher CPU, energy and memory use. RandomX is not a promise of ASIC-proof mining.</span></div></div>
|
||||
<div className="miner-controls"><div className="miner-display"><div><span>NEXT CONSENSUS ARTICLE</span><strong>{snapshot?.nextArticle.title ?? 'Waiting for testnet RPC…'}</strong></div></div><p className="muted">Fixed four-article snapshot catalog, not a live random Wikipedia feed.</p><ol className="native-steps"><li>Build <code>wikideck-randomx</code> using the repository’s native build instructions.</li><li>Save your encrypted backup and keep its password separate.</li><li>Replace <code><backup.json></code> with your local file path (quote paths with spaces), then run:</li></ol><pre className="native-command"><code>{nativeCommand}</code></pre><p className="miner-footnote">Node: {window.location.origin}. Same-origin RPC, including the development proxy. Use HTTPS for remote nodes. Password entry stays local; never put it in the command. Defaults to one worker and at most one block. Ctrl+C stops the native miner; closing this page does not.</p><button className="button dark full" onClick={openWallet}><Download size={16} /> Download wallet backup</button><button className="text-button" onClick={() => setDialog('guide')}>Read the testnet field guide <ArrowRight size={14} /></button></div>
|
||||
<div className="miner-controls"><div className="miner-display"><div><span>NEXT WIKIPEDIA ARTICLE</span><strong>{snapshot ? 'Chosen when the miner requests work' : 'Waiting for testnet RPC…'}</strong></div></div><p className="muted">Each mining template requests a random English Wikipedia article with a Wikimedia picture. Pages may repeat; every minted card carries its own statistics snapshot.</p><ol className="native-steps"><li>Build <code>wikideck-randomx</code> using the repository’s native build instructions.</li><li>Save your encrypted backup and keep its password separate.</li><li>Replace <code><backup.json></code> with your local file path (quote paths with spaces), then run:</li></ol><pre className="native-command"><code>{nativeCommand}</code></pre><p className="miner-footnote">Node: {window.location.origin}. Same-origin RPC, including the development proxy. Use HTTPS for remote nodes. Password entry stays local; never put it in the command. Defaults to one worker and at most one block. Ctrl+C stops the native miner; closing this page does not.</p><button className="button dark full" onClick={openWallet}><Download size={16} /> Download wallet backup</button><button className="text-button" onClick={() => setDialog('guide')}>Read the testnet field guide <ArrowRight size={14} /></button></div>
|
||||
</section>
|
||||
</>}
|
||||
{view !== 'Block explorer' ? <section className="discover-section">
|
||||
<div className="section-heading"><div><div className="section-eyebrow">{view === 'My collection' ? 'YOUR PERSONAL ENCYCLOPEDIA' : 'AN ENCYCLOPEDIA, REIMAGINED'}</div><h2>{view === 'My collection' ? 'Your cabinet of curiosities' : 'A world of possible discoveries'}<span className="count-badge">{visibleCards.length}</span></h2><p>{view === 'My collection' ? 'Confirmed native NFT ownership. Pending transfers stay here until mined.' : snapshot?.blocks.length ? 'Recent native NFTs on this node’s canonical testnet chain.' : 'Preview examples from the fixed catalog. These are not owned NFTs.'}</p></div>{view === 'Discover' && <button className="text-button" onClick={() => setView('My collection')}>My collection <ArrowRight size={16} /></button>}</div>
|
||||
<div className="section-heading"><div><div className="section-eyebrow">{view === 'My collection' ? 'YOUR PERSONAL ENCYCLOPEDIA' : 'AN ENCYCLOPEDIA, REIMAGINED'}</div><h2>{view === 'My collection' ? 'Your cabinet of curiosities' : 'A world of possible discoveries'}<span className="count-badge">{visibleCards.length}</span></h2><p>{view === 'My collection' ? 'Confirmed native NFT ownership. Pending transfers stay here until mined.' : snapshot?.blocks.length ? 'Recent native NFTs on this node’s canonical testnet chain.' : 'Sample previews only; mined NFTs use random Wikipedia pages with pictures. The same page may be minted more than once.'}</p></div>{view === 'Discover' && <button className="text-button" onClick={() => setView('My collection')}>My collection <ArrowRight size={16} /></button>}</div>
|
||||
<div className="filter-bar"><div className="category-tabs">{['All cards', 'Nature', 'Space', 'Science', 'People', 'History', 'Culture'].map(c => <button key={c} className={category === c ? 'selected' : ''} onClick={() => setCategory(c)}>{c}</button>)}</div><label className="search-input"><Search size={15} /><input aria-label="Search cards" placeholder="Find a discovery…" value={query} onChange={e => setQuery(e.target.value)} /></label></div>
|
||||
{filtered.length ? <div className="card-grid">{filtered.map(c => <div className="card-wrap" key={c.block?.tokenId ?? c.hash}><Card card={c} onClick={() => { setSelected(c); setTransferError(''); }} /><div className="card-list-caption"><span>{c.block ? `Block #${c.block.height}` : 'Example discovery'}</span><span className="caption-dot" />{c.block && snapshot ? short(testnetOwnerOf(snapshot, c.block.tokenId) ?? '') : 'Not owned'}</div>{c.block && snapshot?.pendingTransfers.some(t => t.cardHash === c.block!.tokenId) && <span className="pending-label">Transfer pending mining</span>}</div>)}</div> : <div className="empty-state"><Layers3 size={38} /><h3>{query || category !== 'All cards' ? 'No discoveries here. Yet.' : 'Your first discovery is waiting.'}</h3><p>{query || category !== 'All cards' ? 'Try another search or explore a different category.' : 'Use the native miner or receive a mined transfer on this chain. Restore a backup to access an existing wallet.'}</p><button className="button dark" onClick={goMine}><Pickaxe size={16} /> Explore native mining</button></div>}
|
||||
</section> : <section className="explorer-section"><div className="section-eyebrow">FOLLOW THE CANONICAL CHAIN</div><div className="section-heading"><div><h1>Block explorer<span className="count-badge">{snapshot?.blocks.length ?? 0}</span></h1><p>Native NFT mints, RandomX proofs, and mined ownership transfers.</p></div><a className="button outline" href="/api/chain" target="_blank" rel="noreferrer">View full chain JSON <ExternalLink size={15} /></a></div>
|
||||
@@ -193,15 +193,15 @@ export default function App() {
|
||||
<h2 className="transfer-title">Mined ownership transfers</h2>{snapshot?.transfers.length ? <div className="journal">{snapshot.transfers.map((t, i) => <div key={`${t.cardHash}-${t.sequence}`}><ArrowDownLeft size={18} /><a href={`/api/nfts/${encodeURIComponent(t.cardHash)}`} target="_blank" rel="noreferrer" title={t.cardHash}>{short(t.cardHash)}</a><span>{short(t.from)} → {short(t.to)}</span><span>Sequence {t.sequence} · mined #{snapshot.blocks.find(b => b.transactions.some(tx => tx.signature === t.signature))?.height ?? '—'}</span><Check size={14} aria-label={`Mined transfer ${i + 1}`} /></div>)}</div> : <p className="muted">No mined transfers. Ownership changes only when a transfer is included in a canonical block.</p>}
|
||||
<h2 className="transfer-title">Pending transfers <span className="count-badge">{snapshot?.pendingTransfers.length ?? 0}</span></h2><p className="muted">Mempool only · not confirmed ownership. Pending transactions may be dropped or invalidated by a reorganization.</p><div className="journal">{snapshot?.pendingTransfers.map(t => <div key={`${t.cardHash}-${t.sequence}`}><LoaderCircle size={16} /><span title={t.cardHash}>{short(t.cardHash)}</span><span>{short(t.from)} → {short(t.to)}</span><span className="pending-label">Awaiting mining</span></div>)}</div>
|
||||
</section>}
|
||||
<footer><a href="https://en.wikipedia.org" target="_blank" rel="noreferrer"><BookOpen size={16} /> Built on the shoulders of Wikipedia.</a><span>Knowledge, not financial promises.<span className="footer-star">✦</span></span><p>Independent experiment, not affiliated with Wikimedia. Fixed snapshot catalog; attribution and image licenses on each card. Native NFTs are not ERC-721 tokens, fiat, or rights to article content. No promised monetary value.</p></footer>
|
||||
<footer><a href="https://en.wikipedia.org" target="_blank" rel="noreferrer"><BookOpen size={16} /> Built on the shoulders of Wikipedia.</a><span>Knowledge, not financial promises.<span className="footer-star">✦</span></span><p>Independent experiment, not affiliated with Wikimedia. Mined article snapshots are selected from Wikipedia; attribution and image licenses appear on each card. Native NFTs are not ERC-721 tokens, fiat, or rights to article content. No promised monetary value.</p></footer>
|
||||
</main>
|
||||
</div>
|
||||
{toast && <div className="toast" role="status"><span><Check size={16} /></span>{toast}<button onClick={() => setToast('')} aria-label="Dismiss notification"><X size={14} /></button></div>}
|
||||
{(dialog || selected) && <div className="modal-backdrop" onClick={e => { if (e.target === e.currentTarget) close(); }}><section className={`modal ${dialog === 'wallet' ? 'wallet-modal' : selected && !dialog ? 'card-modal' : ''}`} role="dialog" aria-modal="true" aria-label={dialog === 'wallet' ? 'Local testnet wallet' : dialog === 'transfer' ? 'Transfer native NFT' : dialog === 'guide' ? 'How Wikideck works' : 'Card details'}><button className="modal-close" aria-label="Close dialog" onClick={close} disabled={transferBusy}><X size={20} /></button>
|
||||
{dialog === 'guide' ? <><span className="modal-icon"><Sparkles size={24} /></span><div className="section-eyebrow">THE TESTNET FIELD GUIDE</div><h2>A universe in every card.</h2><div className="guide-steps"><div><span>01</span><div><h3>Mine natively. Only by choice.</h3><p>No browser mining or automatic native execution. Build the trusted RandomX binary and run the miner yourself with an encrypted wallet backup and explicit --consent. One worker defaults to light mode (256 MiB cache plus overhead); --fast adds a ~2 GiB dataset. Mining uses CPU and energy, may warm your device, and is not guaranteed ASIC-proof. Ctrl+C stops it.</p></div></div><div><span>02</span><div><h3>A small catalog. A sovereign chain.</h3><p>Consensus uses four fixed article snapshots, not live random articles. The genesis and chain ID pin this catalog; expanding it currently requires a genesis reset and a new chain. Independent peers validate RandomX blocks, native NFT mints, and signed transactions, choosing the cumulative-work canonical chain. This testnet is unbootstrapped: there is no public mainnet, and an isolated node has no peer-backed network security.</p></div></div><div><span>03</span><div><h3>Keep your keys. Confirm your transfers.</h3><p>Your P-256 wallet is a local signing identity, not a server login. The new testnet wallet is separate from the original demo wallet. Download a password-encrypted backup and restore it locally to control the same NFTs. A submitted transfer is pending, not final ownership. Mints require two subsequent blocks at inclusion height; reorganizations can change confirmations and ownership.</p></div></div></div><div className="info-box"><strong>Native NFTs, not content ownership.</strong><p>Token IDs identify native assets on this chain, not ERC-721 tokens on another network. RandomX proof hashes determine game rarity and stats; block IDs identify blocks. An NFT does not grant copyright or other intellectual-property rights to Wikipedia text or images. Follow their licenses and attribution. No fiat, financial return, or monetary value is promised.</p></div><button className="button dark full" onClick={() => { close(); goMine(); }}>Explore native mining <ArrowRight size={16} /></button></>
|
||||
{dialog === 'guide' ? <><span className="modal-icon"><Sparkles size={24} /></span><div className="section-eyebrow">THE TESTNET FIELD GUIDE</div><h2>A universe in every card.</h2><div className="guide-steps"><div><span>01</span><div><h3>Mine natively. Only by choice.</h3><p>No browser mining or automatic native execution. Build the trusted RandomX binary and run the miner yourself with an encrypted wallet backup and explicit --consent. One worker defaults to light mode (256 MiB cache plus overhead); --fast adds a ~2 GiB dataset. Mining uses CPU and energy, may warm your device, and is not guaranteed ASIC-proof. Ctrl+C stops it.</p></div></div><div><span>02</span><div><h3>A page, a snapshot. A sovereign chain.</h3><p>Each mining template asks Wikipedia for a random English article with a picture. A page can appear on multiple NFTs; each block commits its own article and mint-time statistics snapshot. Validators verify the snapshot's structure and commitments without contacting Wikimedia during consensus validation. Independent peers validate RandomX work, native NFT mints, and signed transactions, choosing the cumulative-work canonical chain. This testnet is unbootstrapped: there is no public mainnet, and an isolated node has no peer-backed network security.</p></div></div><div><span>03</span><div><h3>Keep your keys. Confirm your transfers.</h3><p>Your P-256 wallet is a local signing identity, not a server login. The new testnet wallet is separate from the original demo wallet. Download a password-encrypted backup and restore it locally to control the same NFTs. A submitted transfer is pending, not final ownership. Mints require two subsequent blocks at inclusion height; reorganizations can change confirmations and ownership.</p></div></div></div><div className="info-box"><strong>Native NFTs, not content ownership.</strong><p>Token IDs identify native assets on this chain, not ERC-721 tokens on another network. RandomX proof hashes determine game rarity and stats; block IDs identify blocks. An NFT does not grant copyright or other intellectual-property rights to Wikipedia text or images. Follow their licenses and attribution. No fiat, financial return, or monetary value is promised.</p></div><button className="button dark full" onClick={() => { close(); goMine(); }}>Explore native mining <ArrowRight size={16} /></button></>
|
||||
: dialog === 'wallet' ? <><span className="modal-icon"><WalletIcon size={24} /></span><div className="section-eyebrow">YOUR LOCAL TESTNET IDENTITY</div><h2>A wallet for your wonders.</h2><p className="muted">No server login. This recoverable wallet controls native NFTs through local signatures. It is separate from the original demo wallet.</p><label className="field-label">YOUR RECEIVE ADDRESS</label><div className="address-box">{wallet?.address ?? 'Wallet unavailable. Enable local browser storage and retry.'}</div><button className="button dark full" onClick={copyAddress} disabled={!wallet}><Copy size={16} /> Copy wallet address</button><div className="wallet-recovery"><WalletPanel wallet={wallet} onRestore={updateWallet} onNotice={setToast} /></div><p className="wallet-count"><Layers3 size={17} /> {owned.length} confirmed native NFTs in your collection</p></>
|
||||
: dialog === 'transfer' ? <><span className="modal-icon"><ArrowUpRight size={24} /></span><div className="section-eyebrow">PASS ON THE CURIOSITY</div><h2>Send {selected?.article.title}.</h2><p className="muted">Use the recipient’s 64-character testnet wallet address on this same chain. Signing submits a pending transaction; it does not immediately transfer ownership.</p><label className="field-label">NFT TOKEN ID</label><div className="address-box">{selected?.block?.tokenId}</div><label className="field-label" htmlFor="recipient">RECIPIENT ADDRESS</label><input className="recipient-input" id="recipient" value={recipient} disabled={transferBusy} onChange={e => setRecipient(e.target.value)} placeholder="Paste a 64-character wallet address" autoComplete="off" maxLength={64} /><div className="info-box"><strong>Review before signing.</strong><p>Transfers take effect only when mined into the canonical chain. Once confirmed, there is no undo button or recovery for a wrong address. Mints require two subsequent blocks at inclusion height; only one pending transfer per NFT is allowed.</p></div>{(!rpcAvailable || transferDisabledReason) && <p className="transfer-error" role="alert">{!rpcAvailable ? 'RPC unavailable. Reconnect before signing.' : transferDisabledReason}</p>}{transferError && <p className="transfer-error" role="alert">{transferError}</p>}<button className="button dark full" disabled={transferBusy || !rpcAvailable || !!transferDisabledReason || !/^[a-fA-F0-9]{64}$/.test(recipient.trim()) || recipient.trim().toLowerCase() === wallet?.address} onClick={transfer}>{transferBusy ? <LoaderCircle className="spin" size={16} /> : <ArrowUpRight size={16} />}{transferBusy ? 'Signing & submitting…' : 'Sign & submit pending transfer'}</button></>
|
||||
: selected && <><div className="detail-card"><Card card={selected} /></div><div className="detail-copy"><div className="section-eyebrow">{selected.block ? `MINTED IN BLOCK #${selected.block.height}` : 'EXAMPLE CARD · NOT OWNED'}</div><h2>{selected.article.title}</h2><p>{selected.article.extract}</p><div className="detail-facts"><span>Page ID<strong>#{selected.article.pageId}</strong></span><span>Rarity<strong>{rarity(selected.hash)}</strong></span><span>Content<strong>{selected.article.source === 'live' ? `Pinned revision ${selected.article.revisionId}` : 'Fixed starter snapshot'}</strong></span></div>{selected.block && <><label className="field-label">NATIVE NFT TOKEN ID</label><div className="address-box">{selected.block.tokenId}</div><label className="field-label">BLOCK ID</label><div className="address-box">{selected.block.hash}</div><label className="field-label">RANDOMX PROOF HASH · GAME STATS</label><div className="address-box">{selected.hash}</div><p className="muted">{confirmations} canonical confirmations{!rpcAvailable ? ' · last known state' : ''}. {selectedBlock ? `Confirmed owner: ${selectedOwner}` : 'This mint is not on the loaded canonical chain.'}</p>{pending && <p className="pending-label">Transfer pending mining · confirmed owner unchanged.</p>}</>}<div className="detail-links"><a href={selected.article.url} target="_blank" rel="noreferrer">Read on Wikipedia <ExternalLink size={14} /></a><a href={imageCredit(selected.article)} target="_blank" rel="noreferrer">Image & license <ExternalLink size={14} /></a>{selected.block && <a href={`/api/nfts/${encodeURIComponent(selected.block.tokenId)}`} target="_blank" rel="noreferrer">NFT metadata <ExternalLink size={14} /></a>}</div><p className="attribution">{selected.article.source === 'live' ? 'Pinned text by Wikipedia contributors, CC BY-SA 4.0. Excerpt shortened for card display.' : 'Original fixed starter summary, linked to the real Wikipedia article; no live revision captured.'} Stats are game values, not encyclopedia facts. NFT ownership grants no rights to article text or images.</p>{selected.block && selectedOwner === wallet?.address && <>{transferDisabledReason && <p className="muted">{transferDisabledReason}</p>}<button className="button dark full" disabled={!rpcAvailable || !!transferDisabledReason} onClick={() => { setRecipient(''); setTransferError(''); setDialog('transfer'); }}>Transfer native NFT <ArrowUpRight size={16} /></button></>}</div></>}
|
||||
: selected && <><div className="detail-card"><Card card={selected} /></div><div className="detail-copy"><div className="section-eyebrow">{selected.block ? `MINTED IN BLOCK #${selected.block.height}` : 'EXAMPLE CARD · NOT OWNED'}</div><h2>{selected.article.title}</h2><p>{selected.article.extract}</p><div className="detail-facts"><span>Page ID<strong>#{selected.article.pageId}</strong></span><span>Rarity<strong>{rarity(selected.hash)}</strong></span><span>Content<strong>{selected.article.source === 'live' ? `Pinned revision ${selected.article.revisionId}` : 'Sample preview'}</strong></span></div>{selected.block && <><label className="field-label">NATIVE NFT TOKEN ID</label><div className="address-box">{selected.block.tokenId}</div><label className="field-label">BLOCK ID</label><div className="address-box">{selected.block.hash}</div><label className="field-label">RANDOMX PROOF HASH · GAME STATS</label><div className="address-box">{selected.hash}</div><p className="muted">{confirmations} canonical confirmations{!rpcAvailable ? ' · last known state' : ''}. {selectedBlock ? `Confirmed owner: ${selectedOwner}` : 'This mint is not on the loaded canonical chain.'}</p>{pending && <p className="pending-label">Transfer pending mining · confirmed owner unchanged.</p>}</>}<div className="detail-links"><a href={selected.article.url} target="_blank" rel="noreferrer">Read on Wikipedia <ExternalLink size={14} /></a><a href={imageCredit(selected.article)} target="_blank" rel="noreferrer">Image & license <ExternalLink size={14} /></a>{selected.block && <a href={`/api/nfts/${encodeURIComponent(selected.block.tokenId)}`} target="_blank" rel="noreferrer">NFT metadata <ExternalLink size={14} /></a>}</div><p className="attribution">{selected.article.source === 'live' ? 'Pinned text by Wikipedia contributors, CC BY-SA 4.0. Excerpt shortened for card display.' : 'Original fixed starter summary, linked to the real Wikipedia article; no live revision captured.'} Stats are game values, not encyclopedia facts. NFT ownership grants no rights to article text or images.</p>{selected.block && selectedOwner === wallet?.address && <>{transferDisabledReason && <p className="muted">{transferDisabledReason}</p>}<button className="button dark full" disabled={!rpcAvailable || !!transferDisabledReason} onClick={() => { setRecipient(''); setTransferError(''); setDialog('transfer'); }}>Transfer native NFT <ArrowUpRight size={16} /></button></>}</div></>}
|
||||
</section></div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import { rarity, stats, type Article } from '../shared/protocol.ts';
|
||||
import type { TestnetBlock } from '../shared/testnet.ts';
|
||||
import { STARTER_ARTICLES } from '../shared/catalog.ts';
|
||||
import { useEnrichment } from './useEnrichment.ts';
|
||||
import { CardInsights, EnrichmentScores } from './CardInsights.tsx';
|
||||
import { CardInsights, EnrichmentScores, MintStatsScores } from './CardInsights.tsx';
|
||||
|
||||
// hash is the RandomX proof hash for game stats; block.tokenId is NFT identity.
|
||||
export type CardData = { article: Article; hash: string; block?: TestnetBlock };
|
||||
@@ -30,8 +30,8 @@ export function Card({ card, onClick }: { card: CardData; onClick?: () => void }
|
||||
</div>
|
||||
<div className="card-content"><div className="card-title"><h3>{article.title}</h3><ArrowUpRight size={16} /></div><p>{article.extract}</p>
|
||||
<div className="card-stats" aria-label="Immutable proof-derived game stats"><span title="Power · proof-derived"><Swords size={12} />{s.power}</span><span title="Defense · proof-derived"><Shield size={12} />{s.defense}</span><span title="Knowledge · proof-derived"><BookOpen size={12} />{s.knowledge}</span></div>
|
||||
<div className="card-enrichment">{enrichment?<><div className="card-readership"><span>LIVE WIKIPEDIA</span><span>{enrichment.pageviews.status==='available'?`${new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1}).format(enrichment.pageviews.months.at(-1)!.views)} views / month`:'Views unavailable'}</span></div><EnrichmentScores data={enrichment}/></>:<span className="card-enrichment-status">{error?'Live metadata unavailable':'Loading article insights…'}</span>}</div>
|
||||
<div className="card-enrichment">{enrichment?<><div className="card-readership"><span>CURRENT WIKIPEDIA</span><span>{enrichment.pageviews.status==='available'?`${new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1}).format(enrichment.pageviews.months.at(-1)!.views)} views / month`:'Views unavailable'}</span></div><EnrichmentScores data={enrichment}/></>:<span className="card-enrichment-status">{error?'Current metadata unavailable':'Loading article insights…'}</span>}{card.block&&<MintStatsScores snapshot={card.block.statsSnapshot} compactView/>}</div>
|
||||
</div>
|
||||
<div className="card-bottom"><span>WIKIPEDIA · #{article.pageId}</span><span>{card.block ? `BLOCK ${card.block.height}` : 'PREVIEW'}</span></div>
|
||||
</button>{!onClick&&<CardInsights pageId={article.pageId}/>}</>;
|
||||
</button>{!onClick&&<CardInsights pageId={article.pageId} snapshot={card.block?.statsSnapshot}/>}</>;
|
||||
}
|
||||
|
||||
+31
-9
@@ -1,28 +1,49 @@
|
||||
import { BookOpen, Eye, Globe2, Tags, TrendingUp } from 'lucide-react';
|
||||
import { useEnrichment } from './useEnrichment.ts';
|
||||
import type { PageEnrichment } from '../shared/enrichment.ts';
|
||||
import type { MintStatsSnapshot, PageEnrichment } from '../shared/enrichment.ts';
|
||||
import './enrichment.css';
|
||||
|
||||
const compact=new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1});
|
||||
const full=new Intl.NumberFormat('en',{maximumFractionDigits:0});
|
||||
const month=(value:string)=>new Date(`${value}-01T00:00:00Z`).toLocaleDateString('en',{month:'short',year:'numeric',timeZone:'UTC'});
|
||||
const score=(value:number|null)=>value===null?'—':`${value}`;
|
||||
export function EnrichmentScores({data}: {data:PageEnrichment}){
|
||||
return <div className="enrichment-scores" aria-label="Live Wikipedia-derived scores out of 100">
|
||||
<span title="Attention: logarithmic score from the latest complete month's human page views"><Eye size={12}/><b>{score(data.stats.attention)}</b><small>Attention</small></span>
|
||||
<span title="Depth: logarithmic score from article bytes, not a quality judgment"><BookOpen size={12}/><b>{score(data.stats.depth)}</b><small>Depth</small></span>
|
||||
<span title="Reach: logarithmic score from interlanguage links"><Globe2 size={12}/><b>{score(data.stats.reach)}</b><small>Reach</small></span>
|
||||
function ScoreStrip({stats,label}: {stats:PageEnrichment['stats'];label:string}){
|
||||
return <div className="enrichment-scores" aria-label={`${label} Wikipedia-derived scores out of 100`}>
|
||||
<span title="Attention: logarithmic score from the latest complete month's human page views"><Eye size={12}/><b>{score(stats.attention)}</b><small>Attention</small></span>
|
||||
<span title="Depth: logarithmic score from article bytes, not a quality judgment"><BookOpen size={12}/><b>{score(stats.depth)}</b><small>Depth</small></span>
|
||||
<span title="Reach: logarithmic score from interlanguage links"><Globe2 size={12}/><b>{score(stats.reach)}</b><small>Reach</small></span>
|
||||
</div>;
|
||||
}
|
||||
export function CardInsights({pageId}:{pageId:number}){
|
||||
export function EnrichmentScores({data}: {data:PageEnrichment}){
|
||||
return <ScoreStrip stats={data.stats} label="Current"/>;
|
||||
}
|
||||
export function MintStatsScores({snapshot,compactView=false}: {snapshot:MintStatsSnapshot;compactView?:boolean}){
|
||||
const latest=snapshot.pageviews.months.at(-1);
|
||||
if(compactView)return <div className="mint-snapshot-compact"><span>AT MINT</span><ScoreStrip stats={snapshot.stats} label="At mint"/><small>{latest?`${compact.format(latest.views)} views · ${snapshot.pageBytes.toLocaleString()} bytes`:'Page views unavailable'} · {new Date(snapshot.capturedAt).toLocaleDateString()}</small></div>;
|
||||
return <section className="mint-snapshot" aria-label="Wikipedia statistics snapshot at mint">
|
||||
<div className="insights-heading"><h3>Statistics at mint</h3><span>{new Date(snapshot.capturedAt).toLocaleString()}</span></div>
|
||||
<ScoreStrip stats={snapshot.stats} label="At mint"/>
|
||||
<dl className="article-fields">
|
||||
<div><dt><Eye size={12}/> Monthly views</dt><dd>{latest?full.format(latest.views):'Unavailable'}<small>{month(snapshot.pageviews.latestMonth)} · complete month</small></dd></div>
|
||||
<div><dt>6-month views</dt><dd>{snapshot.pageviews.total===null?'Unavailable':full.format(snapshot.pageviews.total)}</dd></div>
|
||||
<div><dt><TrendingUp size={12}/> Monthly change</dt><dd>{snapshot.pageviews.trendPercent===null?'Unavailable':`${snapshot.pageviews.trendPercent>=0?'+':''}${snapshot.pageviews.trendPercent.toFixed(1)}%`}</dd></div>
|
||||
<div><dt>Daily average</dt><dd>{snapshot.pageviews.dailyAverage===null?'Unavailable':full.format(snapshot.pageviews.dailyAverage)}<small>latest complete month</small></dd></div>
|
||||
<div><dt>Article size</dt><dd>{full.format(snapshot.pageBytes)} bytes</dd></div>
|
||||
<div><dt>Languages</dt><dd>{snapshot.languagesTruncated?'≥ ':''}{full.format(snapshot.languageCount)}<small>other-language links</small></dd></div>
|
||||
<div><dt>Revision at capture</dt><dd><a href={`https://en.wikipedia.org/w/index.php?oldid=${snapshot.revisionId}`} target="_blank" rel="noreferrer">{snapshot.revisionId}</a></dd></div>
|
||||
</dl>
|
||||
{snapshot.pageviews.status==='available'?<div className="mint-months" aria-label="Six-month pageview snapshot">{snapshot.pageviews.months.map(item=><span key={item.month}>{month(item.month)}: {full.format(item.views)}</span>)}</div>:<p className="insights-unavailable">Page views were unavailable when this NFT was minted. Missing counts were not recorded as zero.</p>}
|
||||
</section>;
|
||||
}
|
||||
export function CardInsights({pageId,snapshot}:{pageId:number;snapshot?:MintStatsSnapshot}){
|
||||
const {data,error,retry}=useEnrichment(pageId);
|
||||
if(!data)return <section className="article-insights" aria-label="Wikipedia insights"><h3>Wikipedia insights</h3><p role="status">{error?'Live metadata is unavailable. The NFT and its original game stats are unchanged.':'Loading Wikipedia metadata and monthly readership…'}</p>{error&&<button className="text-button" onClick={retry}>Retry live metadata</button>}</section>;
|
||||
if(!data)return <section className="article-insights" aria-label="Wikipedia insights"><h3>Current Wikipedia statistics</h3><p role="status">{error?'Current metadata is unavailable. The mint-time statistics remain on the NFT.':'Loading current Wikipedia metadata and readership…'}</p>{error&&<button className="text-button" onClick={retry}>Retry live metadata</button>}{snapshot&&<MintStatsScores snapshot={snapshot}/>}</section>;
|
||||
const views=data.pageviews;
|
||||
const latest=views.months.at(-1);
|
||||
const maximum=Math.max(1,...views.months.map(m=>m.views));
|
||||
const trend=views.trendPercent;
|
||||
return <section className="article-insights" aria-label="Wikipedia insights">
|
||||
<div className="insights-heading"><h3>Wikipedia insights</h3><span>LIVE · OFF-CHAIN</span></div>
|
||||
<div className="insights-heading"><h3>Current Wikipedia statistics</h3><span>LIVE · OFF-CHAIN</span></div>
|
||||
{data.description&&<p>{data.description}</p>}
|
||||
<EnrichmentScores data={data}/>
|
||||
<dl className="article-fields">
|
||||
@@ -36,6 +57,7 @@ export function CardInsights({pageId}:{pageId:number}){
|
||||
<div><dt>Wikidata</dt><dd>{data.wikidataId?<a href={`https://www.wikidata.org/wiki/${data.wikidataId}`} target="_blank" rel="noreferrer">{data.wikidataId}</a>:'Not linked'}</dd></div>
|
||||
</dl>
|
||||
{views.status==='available'?<figure className="pageview-history"><figcaption>Monthly readership · six complete UTC months</figcaption><div className="pageview-bars">{views.months.map(item=><div key={item.month} title={`${month(item.month)}: ${full.format(item.views)} views`}><span>{compact.format(item.views)}</span><div className="pageview-track"><i style={{height:`${Math.max(1,item.views/maximum*100)}%`}}/></div><small>{month(item.month)}</small></div>)}</div><table className="visually-hidden"><caption>Monthly human page views</caption><tbody>{views.months.map(item=><tr key={item.month}><th>{month(item.month)}</th><td>{item.views}</td></tr>)}</tbody></table></figure>:<p className="insights-unavailable">Page views are unavailable or incomplete. Unknown counts are not treated as zero.</p>}
|
||||
{snapshot&&<MintStatsScores snapshot={snapshot}/>}
|
||||
<h4 className="insights-category-heading"><Tags size={13}/> Wikipedia categories{data.categoriesTruncated?' · first 50':''}</h4>
|
||||
<div className="article-categories">{data.categories.length?data.categories.map(category=><a key={category} href={`https://en.wikipedia.org/wiki/${encodeURIComponent(`Category:${category.replace(/ /g,'_')}`)}`} target="_blank" rel="noreferrer">{category}</a>):<span>No visible categories returned</span>}</div>
|
||||
<details className="insights-method"><summary>How are these scores calculated?</summary><p>Scores range from 0–100, capped and rounded: Attention = log₁₀(monthly views + 1) ÷ 6 × 100; Depth = log₁₀(article bytes + 1) ÷ 6 × 100; Reach = log₁₀(language links + 1) ÷ 3 × 100. Depth measures size, not accuracy or quality. Scores can change and do not affect proof-derived rarity, NFT ownership, or consensus.</p></details>
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ export function WalletPanel({ wallet, onRestore, onNotice }: Props) {
|
||||
|
||||
return <section aria-labelledby={`${id}-title`} aria-busy={busy}>
|
||||
<h2 id={`${id}-title`}>Wallet backup & recovery</h2>
|
||||
<p>This is a recoverable P-256 wallet for wikideck-randomx-testnet-2, separate from your old demo wallet.</p>
|
||||
<p>This is a recoverable P-256 wallet for wikideck-randomx-testnet-4, separate from your old demo wallet.</p>
|
||||
<p>The exportable private key is stored locally in this browser’s IndexedDB, not encrypted by this password.
|
||||
Scripts running on this origin or someone with access to your browser profile may access it.
|
||||
Clearing site data without a backup loses access.</p>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
.card-modal{grid-template-areas:"card detail" "insights insights"}.card-modal .detail-card{display:contents}.card-modal .detail-card>.wiki-card{grid-area:card;align-self:start}.card-modal .detail-copy{grid-area:detail;min-width:0}.card-modal .article-insights{grid-area:insights;margin-top:0}.card-modal .article-fields{grid-template-columns:repeat(4,minmax(0,1fr))}@media(max-width:560px){.card-modal .detail-card>.wiki-card{margin-bottom:24px}.card-modal .article-insights{margin:24px 0}.card-modal .article-fields{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.card-enrichment{border-top:1px solid var(--theme-border);margin-top:12px;padding-top:10px;min-height:68px}.card-readership{display:flex;justify-content:space-between;gap:6px;font-size:8px;color:var(--ctp-subtext0)}.card-readership>span:first-child{font-size:7px;letter-spacing:.7px;color:var(--ctp-teal)}.enrichment-scores{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}.enrichment-scores>span{display:grid;grid-template-columns:auto auto;justify-content:center;align-items:center;gap:3px 5px;font-size:11px;color:var(--ctp-lavender)}.enrichment-scores b{font-weight:600;color:var(--ctp-text)}.enrichment-scores small{grid-column:1/-1;text-align:center;font-size:8px;color:var(--ctp-subtext0)}.card-enrichment-status{color:var(--ctp-subtext0);font-size:9px}.hero-card .card-enrichment{display:none}.article-insights{margin-top:24px;padding:20px;background:var(--ctp-mantle);border:1px solid var(--ctp-surface1);border-radius:10px;text-align:left;pointer-events:auto}.article-insights h3{font-size:18px;letter-spacing:-.3px;font-weight:550;color:var(--ctp-text)}.article-insights p{font-size:11px;line-height:1.85;color:var(--ctp-subtext0);margin-top:12px}.insights-heading{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:9px}.insights-heading>span{font-size:8px;letter-spacing:1px;color:var(--ctp-teal)}.article-insights .enrichment-scores{padding:16px 0;border-bottom:1px solid var(--theme-border)}.article-insights .enrichment-scores b{font-size:21px}.article-insights .enrichment-scores small{font-size:10px}.article-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 14px;margin:20px 0}.article-fields dt{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--ctp-subtext0)}.article-fields dd{margin:6px 0 0;font-size:14px;color:var(--ctp-text);overflow-wrap:anywhere}.article-fields dd small{display:block;font-size:9px;color:var(--ctp-subtext0);margin-top:4px}.article-insights a{color:var(--ctp-blue);text-decoration:underline;text-underline-offset:3px}.pageview-history{margin:22px 0}.pageview-history figcaption{font-size:10px;color:var(--ctp-subtext1);margin-bottom:14px}.pageview-bars{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:7px}.pageview-bars>div{text-align:center;min-width:0}.pageview-bars span{font-size:8px;color:var(--ctp-subtext1)}.pageview-track{height:80px;display:flex;align-items:flex-end;margin-top:5px;background:var(--ctp-base);border-radius:4px;overflow:hidden}.pageview-track i{display:block;width:100%;background:var(--ctp-mauve);border-radius:3px 3px 0 0}.pageview-bars small{display:block;margin-top:7px;font-size:8px;color:var(--ctp-subtext0);line-height:1.6}.insights-category-heading{display:flex;align-items:center;gap:6px;color:var(--ctp-subtext1);font-size:11px;margin:20px 0 12px}.article-categories{display:flex;flex-wrap:wrap;gap:6px;max-height:170px;overflow:auto}.article-categories a,.article-categories>span{font-size:9px;line-height:1.6;border-radius:4px;background:var(--ctp-surface0);padding:4px 7px;text-decoration:none;color:var(--ctp-subtext1);overflow-wrap:anywhere}.article-categories a:hover{color:var(--ctp-mauve)}.insights-method{margin-top:20px;color:var(--ctp-lavender);font-size:10px;line-height:1.8}.insights-method summary{cursor:pointer}.article-insights p.insights-attribution{font-size:9px;overflow-wrap:anywhere}.visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.card-modal .detail-card{align-self:start}.card-modal .detail-copy{align-self:start}.card-modal .detail-card .wiki-card{pointer-events:none}@media(max-width:560px){.card-readership{flex-direction:column;font-size:8px;gap:4px}.enrichment-scores{gap:4px}.enrichment-scores>span{font-size:10px}.enrichment-scores small{font-size:7px}.article-insights{padding:16px}.card-modal .detail-card{max-width:none}.card-modal .detail-card>.wiki-card{max-width:230px;margin-inline:auto}}
|
||||
.mint-snapshot-compact{margin-top:8px;padding-top:7px;border-top:1px dashed var(--theme-border)}.mint-snapshot-compact>span{display:block;text-align:center;font-size:7px;letter-spacing:.7px;color:var(--ctp-peach)}.mint-snapshot-compact .enrichment-scores{margin-top:4px}.mint-snapshot-compact .enrichment-scores>span{font-size:9px}.mint-snapshot-compact .enrichment-scores small{font-size:7px}.mint-snapshot-compact>small{display:block;margin-top:4px;text-align:center;font-size:7px;color:var(--ctp-subtext0)}.mint-snapshot{margin:24px 0;padding:18px;border:1px dashed var(--ctp-surface2);border-radius:8px;background:var(--ctp-base)}.mint-snapshot .insights-heading h3{font-size:15px}.mint-snapshot .insights-heading>span{font-size:9px;color:var(--ctp-subtext0)}.mint-snapshot .enrichment-scores{padding-bottom:12px;border-bottom:1px solid var(--theme-border)}.mint-months{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-top:14px;font-size:9px;color:var(--ctp-subtext1)}.mint-months span{padding:6px;border-radius:4px;background:var(--ctp-mantle)}
|
||||
|
||||
Reference in New Issue
Block a user