diff --git a/website/src/components/KeypairGenerator.tsx b/website/src/components/KeypairGenerator.tsx new file mode 100644 index 00000000..aab8da9e --- /dev/null +++ b/website/src/components/KeypairGenerator.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Key, ArrowRight, Fingerprint } from 'lucide-react'; + +export function KeypairGenerator() { + const [privateKey, setPrivateKey] = useState('0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('')); + const [showPrivate, setShowPrivate] = useState(false); + + const generate = () => { + setPrivateKey('0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('')); + }; + + const publicKey = 'xdc' + privateKey.slice(2, 42); + + return ( +
+
+
+ +

Keypair Demo

+
+
+
+
+
+
+ Private Key +
+
{showPrivate ? privateKey : '•'.repeat(42)}
+
+ +
+
+ Public Address +
+
{publicKey}
+
+
+
+ + +
+

Demo only — never share your real private key.

+
+
+ ); +} + +export default KeypairGenerator; diff --git a/website/src/components/NetworkGlobe.tsx b/website/src/components/NetworkGlobe.tsx new file mode 100644 index 00000000..4d145021 --- /dev/null +++ b/website/src/components/NetworkGlobe.tsx @@ -0,0 +1,347 @@ +import React, { useRef, useMemo, Suspense, useCallback, useEffect } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import * as THREE from 'three'; + +// ─── Data: 108 simulated masternodes spread around the globe ───────────────── +const MASTERNODE_LOCATIONS = [ + // Asia-Pacific + { lat: 35.6762, lng: 139.6503, label: 'Tokyo', region: 'APAC' }, + { lat: 22.3193, lng: 114.1694, label: 'Hong Kong', region: 'APAC' }, + { lat: 1.3521, lng: 103.8198, label: 'Singapore', region: 'APAC' }, + { lat: 37.5665, lng: 126.9780, label: 'Seoul', region: 'APAC' }, + { lat: 31.2304, lng: 121.4737, label: 'Shanghai', region: 'APAC' }, + { lat: 19.0760, lng: 72.8777, label: 'Mumbai', region: 'APAC' }, + { lat: -33.8688, lng: 151.2093, label: 'Sydney', region: 'APAC' }, + { lat: 13.7563, lng: 100.5018, label: 'Bangkok', region: 'APAC' }, + // Europe + { lat: 51.5074, lng: -0.1278, label: 'London', region: 'EU' }, + { lat: 48.8566, lng: 2.3522, label: 'Paris', region: 'EU' }, + { lat: 52.5200, lng: 13.4050, label: 'Berlin', region: 'EU' }, + { lat: 41.9028, lng: 12.4964, label: 'Rome', region: 'EU' }, + { lat: 52.3676, lng: 4.9041, label: 'Amsterdam', region: 'EU' }, + { lat: 59.3293, lng: 18.0686, label: 'Stockholm', region: 'EU' }, + { lat: 55.7558, lng: 37.6173, label: 'Moscow', region: 'EU' }, + { lat: 48.2082, lng: 16.3738, label: 'Vienna', region: 'EU' }, + // Americas + { lat: 40.7128, lng: -74.0060, label: 'New York', region: 'NA' }, + { lat: 37.7749, lng: -122.4194, label: 'San Francisco', region: 'NA' }, + { lat: 41.8781, lng: -87.6298, label: 'Chicago', region: 'NA' }, + { lat: 45.5051, lng: -73.5550, label: 'Montreal', region: 'NA' }, + { lat: -23.5505, lng: -46.6333, label: 'São Paulo', region: 'SA' }, + { lat: -34.6037, lng: -58.3816, label: 'Buenos Aires', region: 'SA' }, + // Middle East & Africa + { lat: 25.2048, lng: 55.2708, label: 'Dubai', region: 'MEA' }, + { lat: 26.8206, lng: 30.8025, label: 'Cairo', region: 'MEA' }, + { lat: -26.2041, lng: 28.0473, label: 'Johannesburg', region: 'MEA' }, +]; + +// Fill up to 108 nodes with extras +const EXTRA_NODES = 108 - MASTERNODE_LOCATIONS.length; +const allNodes = [...MASTERNODE_LOCATIONS]; +for (let i = 0; i < EXTRA_NODES; i++) { + const lat = (Math.random() - 0.5) * 160; + const lng = (Math.random() - 0.5) * 360; + allNodes.push({ lat, lng, label: `Node-${i + 100}`, region: 'OTHER' }); +} + +function latLngToVec3(lat: number, lng: number, r: number): THREE.Vector3 { + const phi = (90 - lat) * (Math.PI / 180); + const theta = (lng + 180) * (Math.PI / 180); + return new THREE.Vector3( + -r * Math.sin(phi) * Math.cos(theta), + r * Math.cos(phi), + r * Math.sin(phi) * Math.sin(theta) + ); +} + +const GLOBE_R = 2.0; +const regionColors: { [key: string]: string } = { + APAC: '#38bdf8', + EU: '#818cf8', + NA: '#34d399', + SA: '#fb923c', + MEA: '#f472b6', + OTHER: '#60a5fa', +}; + +// ─── Arcs: animated great-circle connections between masternodes ───────────── +function AnimatedArcs({ time }: { time: number }) { + const arcsRef = useRef(null); + + const arcPairs = useMemo(() => { + const pairs: Array<{ from: (typeof allNodes)[0]; to: (typeof allNodes)[0] }> = []; + // Create 24 animated arcs between named nodes + const named = allNodes.filter(n => n.region !== 'OTHER').slice(0, 20); + for (let i = 0; i < 24; i++) { + const from = named[i % named.length]; + const to = named[(i + 5) % named.length]; + if (from !== to) pairs.push({ from, to }); + } + return pairs; + }, []); + + return ( + + {arcPairs.map((pair, i) => { + const progressRef = { current: (time * 0.3 + i * 0.15) % 1 }; + return ( + + ); + })} + + ); +} + +function ArcLine({ + from, + to, + progress, + color, +}: { + from: THREE.Vector3; + to: THREE.Vector3; + progress: React.MutableRefObject; + color: string; +}) { + const lineRef = useRef(null); + + const { geometry, fullPoints } = useMemo(() => { + const mid = from.clone().add(to).multiplyScalar(0.5); + const dist = from.distanceTo(to); + mid.normalize().multiplyScalar(GLOBE_R + dist * 0.4); + + const curve = new THREE.QuadraticBezierCurve3(from, mid, to); + const pts = curve.getPoints(60); + return { + geometry: new THREE.BufferGeometry().setFromPoints(pts), + fullPoints: pts, + }; + }, [from, to]); + + const geoRef = useRef(null); + + useEffect(() => { + geoRef.current = geometry; + return () => { + geoRef.current?.dispose(); + }; + }, [geometry]); + + useFrame(() => { + if (!lineRef.current || !geoRef.current) return; + const segLen = Math.floor(fullPoints.length * 0.2); + const start = Math.floor(progress.current * fullPoints.length); + const positions = new Float32Array(segLen * 3); + for (let j = 0; j < segLen; j++) { + const p = fullPoints[(start + j) % fullPoints.length]; + positions[j * 3] = p.x; + positions[j * 3 + 1] = p.y; + positions[j * 3 + 2] = p.z; + } + geoRef.current.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geoRef.current.attributes.position.needsUpdate = true; + }); + + const material = useMemo( + () => new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.55, linewidth: 1 }), + [color] + ); + + return ( + + ); +} + +// ─── Globe sphere + grid ────────────────────────────────────────────────────── +function GlobeSphere({ isDark }: { isDark: boolean }) { + return ( + <> + {/* Translucent core */} + + + + + {/* Wireframe grid */} + + + + + {/* Outer glow ring */} + + + + + + ); +} + +// ─── Masternode dots on surface ─────────────────────────────────────────────── +function MasternodePoints() { + const { positions, colors, sizes } = useMemo(() => { + const pos = new Float32Array(allNodes.length * 3); + const col = new Float32Array(allNodes.length * 3); + const sz = new Float32Array(allNodes.length); + + allNodes.forEach((node, i) => { + const v = latLngToVec3(node.lat, node.lng, GLOBE_R + 0.01); + pos[i * 3] = v.x; + pos[i * 3 + 1] = v.y; + pos[i * 3 + 2] = v.z; + + const c = new THREE.Color(regionColors[node.region] || '#1e90ff'); + col[i * 3] = c.r; + col[i * 3 + 1] = c.g; + col[i * 3 + 2] = c.b; + + sz[i] = node.region !== 'OTHER' ? 0.065 : 0.04; + }); + + return { positions: pos, colors: col, sizes: sz }; + }, []); + + const geoRef = useRef(null); + + return ( + + + + + + + + + ); +} + +// ─── Floating flake particles around the globe ──────────────────────────────── +function FloatingFlakes({ count = 180 }: { count?: number }) { + const { positions, sizes } = useMemo(() => { + const pos = new Float32Array(count * 3); + const sz = new Float32Array(count); + for (let i = 0; i < count; i++) { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + const r = GLOBE_R + 0.4 + Math.random() * 1.6; + pos[i * 3] = r * Math.sin(phi) * Math.cos(theta); + pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + pos[i * 3 + 2] = r * Math.cos(phi); + sz[i] = 0.02 + Math.random() * 0.04; + } + return { positions: pos, sizes: sz }; + }, [count]); + + const geoRef = useRef(null); + + useFrame((state) => { + if (!geoRef.current) return; + const positions = geoRef.current.attributes.position.array as Float32Array; + const time = state.clock.elapsedTime; + for (let i = 0; i < count; i++) { + const idx = i * 3; + positions[idx + 1] += Math.sin(time * 0.5 + i) * 0.001; + positions[idx] += Math.cos(time * 0.3 + i) * 0.0008; + } + geoRef.current.attributes.position.needsUpdate = true; + }); + + return ( + + + + + + + + ); +} + +// ─── Atmosphere halo ────────────────────────────────────────────────────────── +function Atmosphere() { + return ( + + + + + ); +} + +// ─── Main rotating globe scene ──────────────────────────────────────────────── +function GlobeScene({ isDark }: { isDark: boolean }) { + const groupRef = useRef(null); + const timeRef = useRef(0); + const { gl } = useThree(); + + // Touch/mouse tilt + const tiltRef = useRef({ x: 0, y: 0 }); + useEffect(() => { + const onMove = (e: MouseEvent) => { + tiltRef.current = { + x: ((e.clientY / window.innerHeight) - 0.5) * 0.3, + y: ((e.clientX / window.innerWidth) - 0.5) * 0.3, + }; + }; + window.addEventListener('mousemove', onMove, { passive: true }); + return () => window.removeEventListener('mousemove', onMove); + }, []); + + useFrame((state, delta) => { + if (!groupRef.current) return; + timeRef.current += delta; + const t = timeRef.current; + + // Slow auto-rotate Y + gentle tilt tracking + groupRef.current.rotation.y += delta * 0.06; + groupRef.current.rotation.x = THREE.MathUtils.lerp( + groupRef.current.rotation.x, + tiltRef.current.x, + 0.04 + ); + }); + + return ( + <> + + + + + + + + + + + + + + ); +} + +// ─── Exported wrapper ───────────────────────────────────────────────────────── +export function NetworkGlobeHero({ className }: { className?: string }) { + const isDark = typeof document !== 'undefined' + ? document.documentElement.getAttribute('data-theme') === 'dark' + : true; + + return ( +
+ + + + + +
+ ); +} + +export default NetworkGlobeHero; diff --git a/website/src/components/NetworkSelector/index.tsx b/website/src/components/NetworkSelector/index.tsx new file mode 100644 index 00000000..f615397c --- /dev/null +++ b/website/src/components/NetworkSelector/index.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import styles from './styles.module.css'; + +export default function NetworkSelector() { + const [network, setNetwork] = useState('mainnet'); + const networks = [ + { id: 'mainnet', name: 'XDC Mainnet', chainId: 50, color: '#10b981' }, + { id: 'apothem', name: 'Apothem Testnet', chainId: 51, color: '#f59e0b' }, + { id: 'devnet', name: 'Devnet', chainId: 551, color: '#6366f1' }, + ]; + + return ( +
+
+ {networks.map((net) => ( + + ))} +
+
+ ); +} diff --git a/website/src/components/NetworkSelector/styles.module.css b/website/src/components/NetworkSelector/styles.module.css new file mode 100644 index 00000000..19e92cc8 --- /dev/null +++ b/website/src/components/NetworkSelector/styles.module.css @@ -0,0 +1,53 @@ +.networkSelector { + perspective: 1000px; + margin: 1rem 0; +} + +.cardInner { + display: flex; + gap: 0.5rem; + transform-style: preserve-3d; +} + +.networkButton { + position: relative; + padding: 0.75rem 1rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 12px; + background: var(--ifm-card-background-color); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + transform-style: preserve-3d; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.networkButton:hover { + transform: translateY(-2px) translateZ(10px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.networkButton.active { + border-color: var(--net-color); + box-shadow: 0 0 20px var(--net-color), inset 0 0 20px rgba(255, 255, 255, 0.05); + transform: translateZ(20px); +} + +.indicator { + width: 8px; + height: 8px; + border-radius: 50%; + box-shadow: 0 0 8px currentColor; +} + +.name { + font-weight: 600; + font-size: 0.875rem; +} + +.chainId { + font-size: 0.75rem; + opacity: 0.7; + font-family: var(--ifm-font-family-monospace); +} diff --git a/website/src/components/NetworkStatusWidget.tsx b/website/src/components/NetworkStatusWidget.tsx new file mode 100644 index 00000000..fb696e87 --- /dev/null +++ b/website/src/components/NetworkStatusWidget.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Activity, CheckCircle2, AlertCircle, Clock, Globe, Server } from 'lucide-react'; + +interface NetworkStatus { + name: string; + status: 'operational' | 'degraded' | 'maintenance'; + height: number; + latency: number; + icon: React.ElementType; +} + +const initialNetworks: NetworkStatus[] = [ + { name: 'XDC Mainnet', status: 'operational', height: 76543210, latency: 45, icon: Globe }, + { name: 'Apothem Testnet', status: 'operational', height: 45234123, latency: 62, icon: Server }, + { name: 'Devnet', status: 'operational', height: 1234567, latency: 28, icon: Activity }, +]; + +export function NetworkStatusWidget() { + const [networks] = useState(initialNetworks); + + const statusConfig = { + operational: { color: 'text-emerald-500', bg: 'bg-emerald-500/10', label: 'Operational', icon: CheckCircle2 }, + degraded: { color: 'text-amber-500', bg: 'bg-amber-500/10', label: 'Degraded', icon: AlertCircle }, + maintenance: { color: 'text-blue-500', bg: 'bg-blue-500/10', label: 'Maintenance', icon: Clock }, + }; + + return ( +
+
+
+ +

Network Status

+
+
+
+ {networks.map((network) => { + const config = statusConfig[network.status]; + const StatusIcon = config.icon; + const NetworkIcon = network.icon; + return ( +
+
+
+ +
+
+
{network.name}
+
Block {network.height.toLocaleString()}
+
+
+
+
+
{network.latency}ms latency
+
+
+ + {config.label} +
+
+
+ ); + })} +
+
+ ); +} + +export default NetworkStatusWidget; diff --git a/website/src/components/NoiseOverlay.tsx b/website/src/components/NoiseOverlay.tsx new file mode 100644 index 00000000..e0211178 --- /dev/null +++ b/website/src/components/NoiseOverlay.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; + +/** + * Subtle film grain / noise overlay. + * Adds texture and a premium feel without hurting performance. + * Uses a tiny base64 SVG noise pattern. + */ + +const NOISE_PATTERN = + 'url("data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noise%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noise)%22/%3E%3C/svg%3E")'; + +export function NoiseOverlay() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + return ( +