diff --git a/website/src/components/AIChatbot.tsx b/website/src/components/AIChatbot.tsx new file mode 100644 index 00000000..dd3a4192 --- /dev/null +++ b/website/src/components/AIChatbot.tsx @@ -0,0 +1,674 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { + Bot, + Send, + User, + X, + ThumbsUp, + ThumbsDown, + History, + Plus, + ChevronLeft, + ExternalLink, + Trash2, + MessageSquare, +} from 'lucide-react'; +import { Button } from '@site/src/components/ui/button'; +import { Input } from '@site/src/components/ui/input'; +import { findFAQ, SUGGESTION_CHIPS } from '@site/src/data/faqs'; +import { cn, truncate } from '@site/src/lib/utils'; + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; + sources?: Source[]; +} + +interface Source { + title: string; + url: string; + score?: number; +} + +interface Thread { + id: string; + title: string; + messages: Message[]; + createdAt: number; + updatedAt: number; +} + +const STORAGE_KEY = 'xdc-rag-threads-v1'; + +function generateId() { + return `${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`; +} + +function makeWelcomeMessage(): Message { + return { + id: generateId(), + role: 'assistant', + content: + "Hi! I'm the XDC Docs assistant. Ask me anything about XDC Network, smart contracts, subnets, nodes, or wallets.", + }; +} + +function getSuggestedPrompts(path = '') { + if (path.startsWith('/smartcontract')) { + return [ + 'How do I deploy a smart contract?', + 'Verify a contract on XDC', + 'Create an XRC20 token', + 'Gas optimization tips', + 'Hardhat setup guide', + 'Foundry vs Remix', + ]; + } + if (path.startsWith('/subnet')) { + return [ + 'How do I deploy a subnet?', + 'Subnet faucet setup', + 'What is SubSwap?', + 'Subnet checkpoints explained', + 'XDC Zero overview', + 'Subnet explorer setup', + ]; + } + if (path.startsWith('/xdcchain')) { + return [ + 'Run a masternode', + 'Masternode requirements', + 'Docker full node setup', + 'Validator handbook', + 'Slashing rules', + 'One-click installer', + ]; + } + if (path.startsWith('/api')) { + return ['Mainnet RPC endpoints', 'Apothem testnet RPC', 'WebSocket API guide', 'JSON-RPC methods']; + } + if (path.startsWith('/enterprise')) { + return ['ISO 20022 integration', 'RWA tokenization', 'Private subnets', 'Trade finance use cases']; + } + if (path.startsWith('/security')) { + return ['Security best practices', 'Set up a multisig wallet', 'Key management guide', 'Report a vulnerability']; + } + if (path.startsWith('/learn')) { + return ['XDC architecture', 'What is XDPoS?', 'Gas fees explained', 'Blockchain basics']; + } + if (path.startsWith('/whitepaper')) { + return ['XDC whitepaper summary', 'XDPoS technical details']; + } + return SUGGESTION_CHIPS; +} + +/** Minimal markdown → JSX: bold, inline code, lists, line breaks */ +function RenderMarkdown({ content }: { content: string }) { + const lines = content.split('\n'); + return ( + <> + {lines.map((line, li) => { + const isOl = /^\d+\.\s/.test(line); + const isUl = /^[-*]\s/.test(line); + const text = isOl ? line.replace(/^\d+\.\s/, '') : isUl ? line.replace(/^[-*]\s/, '') : line; + const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g); + const rendered = parts.map((part, i) => { + if (part.startsWith('**') && part.endsWith('**')) { + return {part.slice(2, -2)}; + } + if (part.startsWith('`') && part.endsWith('`')) { + return ( + + {part.slice(1, -1)} + + ); + } + return part; + }); + if (isOl) return
  • {rendered}
  • ; + if (isUl) return
  • {rendered}
  • ; + if (line === '') return
    ; + return {rendered}; + })} + + ); +} + +export function AIChatbot({ + open, + onClose, + currentPath, +}: { + open: boolean; + onClose: () => void; + currentPath?: string; +}) { + const { siteConfig } = useDocusaurusContext(); + const apiUrl = (siteConfig.customFields?.ragApiUrl as string) || 'http://localhost:3001'; + + const [threads, setThreads] = useState([]); + const [activeThreadId, setActiveThreadId] = useState(null); + const [view, setView] = useState<'chat' | 'history'>('chat'); + const [input, setInput] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const [feedback, setFeedback] = useState<{ [key: string]: { rating?: 'up' | 'down'; comment?: string; submitted?: boolean } }>({}); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + // Load threads from localStorage on mount. + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw) as Thread[]; + setThreads(parsed); + setActiveThreadId(parsed[0]?.id ?? null); + } + } catch { + // Ignore corrupt storage. + } + }, []); + + // Create an initial thread if none exists. + useEffect(() => { + if (threads.length === 0) { + const thread: Thread = { + id: generateId(), + title: 'New conversation', + messages: [makeWelcomeMessage()], + createdAt: Date.now(), + updatedAt: Date.now(), + }; + setThreads([thread]); + setActiveThreadId(thread.id); + } + }, [threads.length]); + + // Persist threads. + useEffect(() => { + if (threads.length > 0) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(threads)); + } catch { + // localStorage may be full/disabled. + } + } + }, [threads]); + + // Focus / body scroll lock. + useEffect(() => { + if (open) { + document.body.style.overflow = 'hidden'; + setTimeout(() => inputRef.current?.focus(), 100); + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [open]); + + // Close on Escape. + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keydown', handler); + return () => document.removeEventListener('keydown', handler); + }, [open, onClose]); + + const activeThread = useMemo( + () => threads.find((t) => t.id === activeThreadId), + [threads, activeThreadId] + ); + + // Auto-scroll. + useEffect(() => { + scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); + }, [activeThread?.messages, isTyping, view]); + + const suggestedPrompts = useMemo(() => getSuggestedPrompts(currentPath), [currentPath]); + + const updateThreadMessages = (threadId: string, updater: (msgs: Message[]) => Message[]) => { + setThreads((prev) => + prev.map((t) => + t.id === threadId + ? { + ...t, + messages: updater(t.messages), + updatedAt: Date.now(), + title: getThreadTitle(updater(t.messages), t.title), + } + : t + ) + ); + }; + + function getThreadTitle(messages: Message[], fallback: string) { + const firstUser = messages.find((m) => m.role === 'user'); + if (firstUser) return truncate(firstUser.content, 32); + return fallback; + } + + const handleSend = useCallback( + async (textOverride?: string) => { + const text = (textOverride ?? input).trim(); + if (!text || !activeThread || isTyping) return; + + const userMsg: Message = { id: generateId(), role: 'user', content: text }; + updateThreadMessages(activeThread.id, (prev) => [...prev, userMsg]); + setInput(''); + setIsTyping(true); + + try { + const payload = activeThread.messages + .concat(userMsg) + .map((m) => ({ role: m.role, content: m.content })); + const res = await fetch(`${apiUrl}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: payload, topK: 5 }), + }); + if (!res.ok) throw new Error(`Server returned ${res.status}`); + const data = await res.json(); + const assistantMsg: Message = { + id: generateId(), + role: 'assistant', + content: data.answer || 'No response generated.', + sources: data.sources || [], + }; + updateThreadMessages(activeThread.id, (prev) => [...prev, assistantMsg]); + } catch (err) { + const fallbackMsg: Message = { + id: generateId(), + role: 'assistant', + content: "I couldn't reach the AI backend. Make sure the RAG API is running, or try rephrasing your question.", + }; + updateThreadMessages(activeThread.id, (prev) => [...prev, fallbackMsg]); + } finally { + setIsTyping(false); + } + }, + [input, activeThread, isTyping, apiUrl] + ); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const startNewThread = () => { + const thread: Thread = { + id: generateId(), + title: 'New conversation', + messages: [makeWelcomeMessage()], + createdAt: Date.now(), + updatedAt: Date.now(), + }; + setThreads((prev) => [thread, ...prev]); + setActiveThreadId(thread.id); + setView('chat'); + inputRef.current?.focus(); + }; + + const selectThread = (id: string) => { + setActiveThreadId(id); + setView('chat'); + inputRef.current?.focus(); + }; + + const clearAllHistory = () => { + if (confirm('Clear all chat history? This cannot be undone.')) { + localStorage.removeItem(STORAGE_KEY); + const thread: Thread = { + id: generateId(), + title: 'New conversation', + messages: [makeWelcomeMessage()], + createdAt: Date.now(), + updatedAt: Date.now(), + }; + setThreads([thread]); + setActiveThreadId(thread.id); + setView('chat'); + } + }; + + const submitFeedback = async (messageId: string, rating: 'up' | 'down') => { + const msg = activeThread?.messages.find((m) => m.id === messageId); + const comment = feedback[messageId]?.comment || ''; + setFeedback((prev) => ({ + ...prev, + [messageId]: { ...prev[messageId], rating, submitted: true }, + })); + try { + await fetch(`${apiUrl}/api/feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messageId, + rating, + comment, + question: activeThread?.messages.find((m) => m.role === 'user')?.content || '', + answer: msg?.content || '', + sources: msg?.sources || [], + }), + }); + } catch { + // Feedback is best-effort; keep UI responsive. + } + }; + + const showSuggestions = + activeThread && + activeThread.messages.length === 1 && + activeThread.messages[0].role === 'assistant' && + !isTyping; + + if (!open) return null; + + return ( +
    + {/* Backdrop */} + +); +} + +export default AIChatbot; diff --git a/website/src/components/AccessControlVisualizer.tsx b/website/src/components/AccessControlVisualizer.tsx new file mode 100644 index 00000000..f10b9cdf --- /dev/null +++ b/website/src/components/AccessControlVisualizer.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { Shield, AlertTriangle, Lock, UserCheck } from 'lucide-react'; + +const roles = [ + { + name: 'Owner', + icon: Shield, + color: 'bg-amber-500/10 text-amber-500', + permissions: ['Upgrade contract', 'Transfer ownership', 'Emergency pause'], + }, + { + name: 'Admin', + icon: UserCheck, + color: 'bg-blue-500/10 text-blue-500', + permissions: ['Manage validators', 'Update parameters', 'Grant roles'], + }, + { + name: 'Pauser', + icon: AlertTriangle, + color: 'bg-red-500/10 text-red-500', + permissions: ['Pause operations', 'Resume operations'], + }, + { + name: 'Minter', + icon: Lock, + color: 'bg-emerald-500/10 text-emerald-500', + permissions: ['Mint tokens', 'Burn tokens'], + }, +]; + +export function AccessControlVisualizer() { + const [activeRole, setActiveRole] = useState(roles[0]); + + return ( +
    +
    +
    + +

    Access Control Roles

    +
    +
    +
    +
    + {roles.map((role) => { + const Icon = role.icon; + return ( + + ); + })} +
    +
    +
    + {React.createElement(activeRole.icon, { + className: `h-5 w-5 ${activeRole.color.split(' ')[1]}`, + })} +

    {activeRole.name} Permissions

    +
    +
      + {activeRole.permissions.map((perm) => ( +
    • + + {perm} +
    • + ))} +
    +
    +
    +
    + ); +} + +export default AccessControlVisualizer; diff --git a/website/src/components/AddressValidator.tsx b/website/src/components/AddressValidator.tsx new file mode 100644 index 00000000..e4536331 --- /dev/null +++ b/website/src/components/AddressValidator.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { CheckCircle2, XCircle, Search } from 'lucide-react'; + +export function AddressValidator() { + const [address, setAddress] = useState('xdc8ba9bc4ce7a37d8a84ac7f8'); + const [isValid, setIsValid] = useState(false); + + const validate = (value: string) => { + const regex = /^xdc[0-9a-fA-F]{40}$/; + setIsValid(regex.test(value)); + setAddress(value); + }; + + return ( +
    +
    +
    + +

    Address Validator

    +
    +
    +
    +
    + validate(e.target.value)} + placeholder="Enter xdc... address" + className="w-full rounded-lg border border-input bg-background px-3 py-2.5 pr-10 text-sm font-mono outline-none focus:ring-2 focus:ring-ring" + /> +
    + {isValid ? ( + + ) : ( + + )} +
    +
    +
    + {isValid ? 'Valid XDC address format' : 'Invalid XDC address format'} +
    +

    XDC addresses start with "xdc" followed by 40 hexadecimal characters.

    +
    +
    + ); +} + +export default AddressValidator; diff --git a/website/src/components/AnimatedHero/index.tsx b/website/src/components/AnimatedHero/index.tsx new file mode 100644 index 00000000..a7ca34aa --- /dev/null +++ b/website/src/components/AnimatedHero/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styles from './styles.module.css'; + +export default function AnimatedHero() { + return ( +
    +
    +

    + XDC Network +
    + Documentation +

    +

    + Build the future of decentralized finance on the XDC Chain +

    + +
    +
    +
    + ); +} diff --git a/website/src/components/AnimatedHero/styles.module.css b/website/src/components/AnimatedHero/styles.module.css new file mode 100644 index 00000000..0d4f57fb --- /dev/null +++ b/website/src/components/AnimatedHero/styles.module.css @@ -0,0 +1,112 @@ +.hero { + position: relative; + min-height: 80vh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + padding: 2rem; +} + +.content { + position: relative; + z-index: 1; + text-align: center; + max-width: 800px; +} + +.title { + font-size: 3.5rem; + font-weight: 800; + line-height: 1.1; + margin-bottom: 1rem; +} + +.gradient { + background: linear-gradient(135deg, #2563eb, #06b6d4); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: gradient-shift 3s ease infinite; + background-size: 200% 200%; +} + +@keyframes gradient-shift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +.subtitle { + font-size: 1.25rem; + opacity: 0.8; + margin-bottom: 2rem; +} + +.cta { + display: flex; + gap: 1rem; + justify-content: center; +} + +.primaryButton { + padding: 0.875rem 2rem; + border-radius: 12px; + background: linear-gradient(135deg, #2563eb, #1d4ed8); + color: white; + font-weight: 600; + text-decoration: none; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + transform-style: preserve-3d; +} + +.primaryButton:hover { + transform: translateY(-2px) translateZ(10px); + box-shadow: 0 10px 40px rgba(37, 99, 235, 0.3); +} + +.secondaryButton { + padding: 0.875rem 2rem; + border-radius: 12px; + border: 1px solid var(--ifm-color-emphasis-300); + color: var(--ifm-font-color-base); + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + background: transparent; +} + +.secondaryButton:hover { + background: var(--ifm-color-emphasis-100); + transform: translateY(-2px); +} + +.grid { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + background: + repeating-linear-gradient( + 90deg, + var(--ifm-color-emphasis-200) 0px, + var(--ifm-color-emphasis-200) 1px, + transparent 1px, + transparent 100px + ), + repeating-linear-gradient( + 0deg, + var(--ifm-color-emphasis-200) 0px, + var(--ifm-color-emphasis-200) 1px, + transparent 1px, + transparent 100px + ); + transform: perspective(500px) rotateX(60deg); + transform-origin: center top; + opacity: 0.2; + animation: grid-move 20s linear infinite; +} + +@keyframes grid-move { + 0% { background-position: 0 0; } + 100% { background-position: 0 100px; } +} diff --git a/website/src/components/AnimatedSection.tsx b/website/src/components/AnimatedSection.tsx new file mode 100644 index 00000000..aae15a0a --- /dev/null +++ b/website/src/components/AnimatedSection.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { motion, useReducedMotion } from 'framer-motion'; +import { cn } from '@site/src/lib/utils'; + +interface AnimatedSectionProps { + children: React.ReactNode; + className?: string; + delay?: number; +} + +export function AnimatedSection({ children, className, delay = 0 }: AnimatedSectionProps) { + const shouldReduceMotion = useReducedMotion(); + + return ( + + {children} + + ); +} + +interface StaggerContainerProps { + children: React.ReactNode; + className?: string; + staggerDelay?: number; +} + +export function StaggerContainer({ children, className, staggerDelay = 0.08 }: StaggerContainerProps) { + const shouldReduceMotion = useReducedMotion(); + + return ( + + {React.Children.map(children, (child) => + React.isValidElement(child) ? ( + + {child} + + ) : ( + child + ) + )} + + ); +} + +interface FadeInProps { + children: React.ReactNode; + className?: string; + delay?: number; + direction?: 'up' | 'down' | 'left' | 'right'; +} + +export function FadeIn({ children, className, delay = 0, direction = 'up' }: FadeInProps) { + const shouldReduceMotion = useReducedMotion(); + + const directionMap = { + up: { y: 24 }, + down: { y: -24 }, + left: { x: 24 }, + right: { x: -24 }, + }; + + return ( + + {children} + + ); +} diff --git a/website/src/components/AntiGravityField.tsx b/website/src/components/AntiGravityField.tsx new file mode 100644 index 00000000..427265cd --- /dev/null +++ b/website/src/components/AntiGravityField.tsx @@ -0,0 +1,302 @@ +import React, { useRef, useMemo, useEffect, useState } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import * as THREE from 'three'; + +/** + * AntiGravity-style interactive particle mesh. + * Thousands of particles floating in 3D space, connected by proximity lines, + * with mouse repulsion and gentle ambient drift. + * Inspired by premium fintech landing pages (antigravity, stripe, etc). + */ + +const PARTICLE_COUNT_DESKTOP = 2200; +const PARTICLE_COUNT_MOBILE = 900; +const CONNECTION_DISTANCE = 2.8; +const MAX_CONNECTIONS = 3; +const MOUSE_RADIUS = 4.5; +const MOUSE_FORCE = 0.08; + +interface ParticleFieldProps { + reducedMotion: boolean; + count: number; +} + +function ParticleField({ reducedMotion, count }: ParticleFieldProps) { + const pointsRef = useRef(null); + const linesRef = useRef(null); + const mouseRef = useRef(new THREE.Vector3(0, 0, 100)); + const raycasterRef = useRef(new THREE.Raycaster()); + const planeRef = useRef(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0)); + const { viewport, pointer, size } = useThree(); + + const { positions, velocities, originalPositions, colors, sizes } = useMemo(() => { + const positions = new Float32Array(count * 3); + const velocities = new Float32Array(count * 3); + const originalPositions = new Float32Array(count * 3); + const colors = new Float32Array(count * 3); + const sizes = new Float32Array(count); + + const width = viewport.width * 1.4; + const height = viewport.height * 1.4; + const depth = 12; + + for (let i = 0; i < count; i++) { + const i3 = i * 3; + const x = (Math.random() - 0.5) * width; + const y = (Math.random() - 0.5) * height; + const z = (Math.random() - 0.5) * depth; + + positions[i3] = x; + positions[i3 + 1] = y; + positions[i3 + 2] = z; + + originalPositions[i3] = x; + originalPositions[i3 + 1] = y; + originalPositions[i3 + 2] = z; + + velocities[i3] = (Math.random() - 0.5) * 0.004; + velocities[i3 + 1] = (Math.random() - 0.5) * 0.004; + velocities[i3 + 2] = (Math.random() - 0.5) * 0.002; + + // Mix of XDC blue, cyan, and subtle white + const colorChoice = Math.random(); + if (colorChoice > 0.92) { + colors[i3] = 0.6; colors[i3 + 1] = 0.85; colors[i3 + 2] = 1.0; + } else if (colorChoice > 0.75) { + colors[i3] = 0.12; colors[i3 + 1] = 0.56; colors[i3 + 2] = 1.0; + } else if (colorChoice > 0.55) { + colors[i3] = 0.2; colors[i3 + 1] = 0.75; colors[i3 + 2] = 0.9; + } else { + colors[i3] = 0.85; colors[i3 + 1] = 0.9; colors[i3 + 2] = 1.0; + } + + sizes[i] = 1.5 + Math.random() * 2.5; + } + + return { positions, velocities, originalPositions, colors, sizes }; + }, [viewport.width, viewport.height]); + + const pointGeometry = useMemo(() => { + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); + return geometry; + }, [positions, colors, sizes]); + + const lineGeometry = useMemo(() => { + const geometry = new THREE.BufferGeometry(); + const linePositions = new Float32Array(count * MAX_CONNECTIONS * 6); + const lineColors = new Float32Array(count * MAX_CONNECTIONS * 6); + geometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(lineColors, 3)); + geometry.setDrawRange(0, 0); + return geometry; + }, []); + + const pointMaterial = useMemo(() => { + return new THREE.ShaderMaterial({ + uniforms: { + uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }, + }, + vertexShader: ` + attribute float size; + attribute vec3 color; + varying vec3 vColor; + uniform float uPixelRatio; + void main() { + vColor = color; + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + gl_PointSize = size * uPixelRatio * (300.0 / -mvPosition.z); + gl_Position = projectionMatrix * mvPosition; + } + `, + fragmentShader: ` + varying vec3 vColor; + void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + float alpha = 1.0 - smoothstep(0.0, 0.5, dist); + gl_FragColor = vec4(vColor, alpha * 0.9); + } + `, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + vertexColors: true, + }); + }, []); + + const lineMaterial = useMemo(() => { + return new THREE.LineBasicMaterial({ + vertexColors: true, + transparent: true, + opacity: 0.15, + blending: THREE.AdditiveBlending, + depthWrite: false, + }); + }, []); + + useEffect(() => { + const handleResize = () => { + pointMaterial.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [pointMaterial]); + + useFrame((state) => { + if (!pointsRef.current || !linesRef.current) return; + + const posAttr = pointsRef.current.geometry.attributes.position as THREE.BufferAttribute; + const positions = posAttr.array as Float32Array; + const linePosAttr = linesRef.current.geometry.attributes.position as THREE.BufferAttribute; + const lineColAttr = linesRef.current.geometry.attributes.color as THREE.BufferAttribute; + const linePositions = linePosAttr.array as Float32Array; + const lineColors = lineColAttr.array as Float32Array; + + // Update mouse position in world space + raycasterRef.current.setFromCamera(pointer, state.camera); + const target = new THREE.Vector3(); + raycasterRef.current.ray.intersectPlane(planeRef.current, target); + if (target) { + mouseRef.current.lerp(target, 0.12); + } + + const time = state.clock.elapsedTime; + const mx = mouseRef.current.x; + const my = mouseRef.current.y; + + let lineIndex = 0; + const tempLineIndices: number[] = []; + + for (let i = 0; i < count; i++) { + const i3 = i * 3; + let px = positions[i3]; + let py = positions[i3 + 1]; + let pz = positions[i3 + 2]; + + if (!reducedMotion) { + // Ambient drift + px += velocities[i3] + Math.sin(time * 0.3 + i * 0.1) * 0.002; + py += velocities[i3 + 1] + Math.cos(time * 0.25 + i * 0.1) * 0.002; + pz += velocities[i3 + 2] + Math.sin(time * 0.15 + i * 0.05) * 0.001; + + // Soft return to origin + const ox = originalPositions[i3]; + const oy = originalPositions[i3 + 1]; + const oz = originalPositions[i3 + 2]; + px += (ox - px) * 0.003; + py += (oy - py) * 0.003; + pz += (oz - pz) * 0.003; + + // Mouse repulsion (anti-gravity field) + const dx = px - mx; + const dy = py - my; + const dz = pz; + const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist < MOUSE_RADIUS && dist > 0.01) { + const force = (1 - dist / MOUSE_RADIUS) * MOUSE_FORCE; + px += (dx / dist) * force; + py += (dy / dist) * force; + pz += (dz / dist) * force * 0.5; + } + } + + positions[i3] = px; + positions[i3 + 1] = py; + positions[i3 + 2] = pz; + + // Build proximity lines (limit connections per particle) + let connections = 0; + for (let j = i + 1; j < count && connections < MAX_CONNECTIONS; j++) { + const j3 = j * 3; + const dx = px - positions[j3]; + const dy = py - positions[j3 + 1]; + const dz = pz - positions[j3 + 2]; + const distSq = dx * dx + dy * dy + dz * dz; + + if (distSq < CONNECTION_DISTANCE * CONNECTION_DISTANCE) { + const dist = Math.sqrt(distSq); + const alpha = 1 - dist / CONNECTION_DISTANCE; + const li = lineIndex * 6; + + linePositions[li] = px; + linePositions[li + 1] = py; + linePositions[li + 2] = pz; + linePositions[li + 3] = positions[j3]; + linePositions[li + 4] = positions[j3 + 1]; + linePositions[li + 5] = positions[j3 + 2]; + + const r = 0.12 * alpha; + const g = 0.56 * alpha; + const b = 1.0 * alpha; + lineColors[li] = r; lineColors[li + 1] = g; lineColors[li + 2] = b; + lineColors[li + 3] = r; lineColors[li + 4] = g; lineColors[li + 5] = b; + + lineIndex++; + connections++; + } + } + } + + posAttr.needsUpdate = true; + linePosAttr.needsUpdate = true; + lineColAttr.needsUpdate = true; + linesRef.current.geometry.setDrawRange(0, lineIndex * 2); + }); + + return ( + <> + + + + ); +} + +export function AntiGravityField({ className }: { className?: string }) { + const [reducedMotion, setReducedMotion] = useState(false); + const [mounted, setMounted] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + setMounted(true); + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + setReducedMotion(mq.matches); + const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches); + mq.addEventListener('change', handler); + + const mobileMq = window.matchMedia('(max-width: 768px)'); + setIsMobile(mobileMq.matches); + const mobileHandler = (e: MediaQueryListEvent) => setIsMobile(e.matches); + mobileMq.addEventListener('change', mobileHandler); + + return () => { + mq.removeEventListener('change', handler); + mobileMq.removeEventListener('change', mobileHandler); + }; + }, []); + + if (!mounted) return null; + + const count = isMobile ? PARTICLE_COUNT_MOBILE : PARTICLE_COUNT_DESKTOP; + + return ( +
    + + + +
    + ); +} + +export default AntiGravityField; diff --git a/website/src/components/ArchitectureLayers.tsx b/website/src/components/ArchitectureLayers.tsx new file mode 100644 index 00000000..f1253dc6 --- /dev/null +++ b/website/src/components/ArchitectureLayers.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import { Layers, Server, Database, Globe } from 'lucide-react'; + +const layers = [ + { + name: 'Application Layer', + icon: Globe, + description: 'DApps, wallets, exchanges, and enterprise applications interact with the network.', + color: 'bg-purple-500/10 text-purple-500', + }, + { + name: 'Smart Contract Layer', + icon: Database, + description: 'Solidity smart contracts execute business logic and token standards like XRC20/721/1155.', + color: 'bg-blue-500/10 text-blue-500', + }, + { + name: 'Consensus Layer', + icon: Server, + description: 'XDPoS 2.0 ensures fast, secure, and energy-efficient block validation.', + color: 'bg-emerald-500/10 text-emerald-500', + }, + { + name: 'Network Layer', + icon: Layers, + description: 'Peer-to-peer communication propagates transactions and blocks globally.', + color: 'bg-amber-500/10 text-amber-500', + }, +]; + +export function ArchitectureLayers() { + const [activeLayer, setActiveLayer] = useState(0); + + return ( +
    +
    +
    + +

    XDC Architecture Layers

    +
    +
    +
    +
    + {layers.map((layer, index) => { + const Icon = layer.icon; + return ( + + ); + })} +
    +
    +
    + {React.createElement(layers[activeLayer].icon, { + className: `h-6 w-6 ${layers[activeLayer].color.split(' ')[1]}`, + })} +
    +

    {layers[activeLayer].name}

    +

    {layers[activeLayer].description}

    +
    +
    +
    + ); +} + +export default ArchitectureLayers; diff --git a/website/src/components/BlockVisualizer.tsx b/website/src/components/BlockVisualizer.tsx new file mode 100644 index 00000000..b7eb0afc --- /dev/null +++ b/website/src/components/BlockVisualizer.tsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect, useRef } from 'react'; + +interface Block { + number: number; + txs: number; + size: number; + time: string; +} + +export function BlockVisualizer() { + const [blocks, setBlocks] = useState([]); + const intervalRef = useRef | null>(null); + + useEffect(() => { + const base = 76543210; + const initial: Block[] = Array.from({ length: 5 }, (_, i) => ({ + number: base - (4 - i), + txs: Math.floor(Math.random() * 80) + 20, + size: Math.floor(Math.random() * 30000) + 10000, + time: `${4 - i}s ago`, + })); + setBlocks(initial); + + intervalRef.current = setInterval(() => { + setBlocks((prev) => { + const next = [...prev]; + next.shift(); + const last = next[next.length - 1].number; + next.push({ + number: last + 1, + txs: Math.floor(Math.random() * 80) + 20, + size: Math.floor(Math.random() * 30000) + 10000, + time: 'just now', + }); + return next.map((b, i) => ({ ...b, time: i === next.length - 1 ? 'just now' : `${(next.length - 1 - i) * 2}s ago` })); + }); + }, 2000); + + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, []); + + return ( +
    +
    +
    +
    +
    +

    Live Blocks

    +
    + ~2s block time +
    +
    +
    + {blocks.map((block, index) => ( +
    +
    +
    + #{block.number.toString().slice(-4)} +
    +
    +
    Block {block.number.toLocaleString()}
    +
    {block.txs} txs · {(block.size / 1024).toFixed(1)} KB
    +
    +
    +
    {block.time}
    +
    + ))} +
    +
    + ); +} + +export default BlockVisualizer; diff --git a/website/src/components/BridgeVisualizer.tsx b/website/src/components/BridgeVisualizer.tsx new file mode 100644 index 00000000..4cca2226 --- /dev/null +++ b/website/src/components/BridgeVisualizer.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { Network, ArrowRightLeft, Shield, Lock } from 'lucide-react'; + +const steps = [ + { icon: Lock, title: 'Lock Assets', description: 'Tokens are locked in a smart contract on the source chain.' }, + { icon: Network, title: 'Verify Proof', description: 'Validators confirm the lock transaction on the source chain.' }, + { icon: ArrowRightLeft, title: 'Mint Wrapped Assets', description: 'Equivalent wrapped tokens are minted on XDC.' }, + { icon: Shield, title: 'Use on XDC', description: 'Assets can now be used within the XDC ecosystem.' }, +]; + +export function BridgeVisualizer() { + const [activeStep, setActiveStep] = useState(0); + + return ( +
    +
    +
    + +

    Cross-Chain Bridge Flow

    +
    +
    +
    +
    + {steps.map((step, index) => { + const Icon = step.icon; + return ( + + ); + })} +
    + +
    +
    +
    + +
    +
    + {React.createElement(steps[activeStep].icon, { className: 'h-5 w-5 text-primary' })} +

    {steps[activeStep].title}

    +
    +

    {steps[activeStep].description}

    +
    +
    +
    + ); +} + +export default BridgeVisualizer;