diff --git a/web/public/ref-step1-character.png b/web/public/ref-step1-character.png new file mode 100644 index 00000000..22cb5a40 Binary files /dev/null and b/web/public/ref-step1-character.png differ diff --git a/web/public/ref-step1-wallet.png b/web/public/ref-step1-wallet.png new file mode 100644 index 00000000..5ea01cda Binary files /dev/null and b/web/public/ref-step1-wallet.png differ diff --git a/web/public/ref-step2-character.png b/web/public/ref-step2-character.png new file mode 100644 index 00000000..a6820eb2 Binary files /dev/null and b/web/public/ref-step2-character.png differ diff --git a/web/public/ref-step2-wallet.png b/web/public/ref-step2-wallet.png new file mode 100644 index 00000000..5d53aa8a Binary files /dev/null and b/web/public/ref-step2-wallet.png differ diff --git a/web/public/ref-step3-character.png b/web/public/ref-step3-character.png new file mode 100644 index 00000000..0b460688 Binary files /dev/null and b/web/public/ref-step3-character.png differ diff --git a/web/public/ref-step3-wallet.png b/web/public/ref-step3-wallet.png new file mode 100644 index 00000000..48cb3ef0 Binary files /dev/null and b/web/public/ref-step3-wallet.png differ diff --git a/web/src/components/AppLayout.tsx b/web/src/components/AppLayout.tsx index 397fb34e..7870357d 100644 --- a/web/src/components/AppLayout.tsx +++ b/web/src/components/AppLayout.tsx @@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAuth } from '@/contexts/AuthContext'; +import LandingPageDesktop from './landing/LandingPageDesktop'; import HeroSection from './HeroSection'; import TokenomicsSection from './TokenomicsSection'; import PalletsGrid from './PalletsGrid'; @@ -82,6 +83,10 @@ const AppLayout: React.FC = () => { return ; } + if (!user) { + return ; + } + return (
{/* Navigation */} diff --git a/web/src/components/landing/LandingPageDesktop.tsx b/web/src/components/landing/LandingPageDesktop.tsx new file mode 100644 index 00000000..37a4f994 --- /dev/null +++ b/web/src/components/landing/LandingPageDesktop.tsx @@ -0,0 +1,1214 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { usePezkuwi } from '@/contexts/PezkuwiContext'; +import { PezkuwiWalletButton } from '@/components/PezkuwiWalletButton'; +import './landing.css'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +interface ChainStats { + latestBlock: number; + finalizedBlock: number; + blockHash: string; + peers: number; + validators: number; + nominators: number; + collators: number; + activeProposals: number; + totalVoters: number; + citizenCount: number; + tokensStakedPct: string; +} + +// ─── SVG Sprite (pallet icons) ─────────────────────────────────────────────── + +const Sprite: React.FC = () => ( + +); + +// ─── Pallet item ───────────────────────────────────────────────────────────── + +const PalletItem: React.FC<{ + icon: string; label: string; href?: string; + locked?: boolean; onClick?: () => void; +}> = ({ icon, label, href, locked, onClick }) => { + if (locked) { + return ( + + + + + + {label} + + ); + } + if (onClick) { + return ( + + ); + } + return ( + + + {label} + + ); +}; + +// ─── Main component ─────────────────────────────────────────────────────────── + +const LandingPageDesktop: React.FC = () => { + const navigate = useNavigate(); + const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady } = usePezkuwi(); + + const [hero, setHero] = useState<'v1'|'v2'|'v3'>('v1'); + const [tokTab, setTokTab] = useState<'hez'|'pez'>('hez'); + const [stats, setStats] = useState({ + latestBlock: 0, finalizedBlock: 0, blockHash: '', + peers: 0, validators: 0, nominators: 0, collators: 0, + activeProposals: 0, totalVoters: 0, citizenCount: 0, + tokensStakedPct: '—', + }); + + // Animate counter on scroll + const counterRefs = useRef>(new Map()); + const animated = useRef>(new Set()); + + const animateNum = useCallback((el: HTMLElement, target: number) => { + const dur = 1600; + const start = performance.now(); + const tick = (now: number) => { + const t = Math.min(1, (now - start) / dur); + const eased = 1 - Math.pow(1 - t, 3); + el.textContent = Math.round(target * eased).toLocaleString(); + if (t < 1) requestAnimationFrame(tick); + }; + requestAnimationFrame(tick); + }, []); + + // Scroll reveal + useEffect(() => { + const io = new IntersectionObserver((entries) => { + entries.forEach(e => { + if (e.isIntersecting) { + e.target.classList.add('lp-in'); + io.unobserve(e.target); + } + }); + }, { threshold: 0.1 }); + document.querySelectorAll('.lp-reveal').forEach(el => io.observe(el)); + return () => io.disconnect(); + }, []); + + // Counter animation on scroll + useEffect(() => { + const io = new IntersectionObserver((entries) => { + entries.forEach(e => { + const el = e.target as HTMLElement; + const key = el.dataset.counterKey ?? ''; + if (e.isIntersecting && !animated.current.has(key)) { + animated.current.add(key); + const target = parseInt(el.dataset.counter ?? '0', 10); + animateNum(el, target); + io.unobserve(el); + } + }); + }, { threshold: 0.5 }); + document.querySelectorAll('[data-counter]').forEach(el => io.observe(el)); + return () => io.disconnect(); + }, [animateNum, stats]); + + // Live block subscription + useEffect(() => { + if (!api || !isApiReady) return; + let unsub: (() => void) | null = null; + + api.rpc.chain.subscribeNewHeads((header) => { + const blockNum = header.number.toNumber(); + setStats(prev => ({ + ...prev, + latestBlock: blockNum, + blockHash: header.hash.toHex().slice(0, 10) + '…' + header.hash.toHex().slice(-8), + finalizedBlock: Math.max(prev.finalizedBlock, blockNum - 2), + })); + }).then(fn => { unsub = fn; }).catch(() => {}); + + api.rpc.system.peers().then(peers => { + setStats(prev => ({ ...prev, peers: peers.length })); + }).catch(() => {}); + + return () => { unsub?.(); }; + }, [api, isApiReady]); + + // Governance + validators + nominators from relay + useEffect(() => { + if (!api || !isApiReady) return; + (async () => { + try { + const [entries, votingKeys, sessionVals] = await Promise.all([ + api.query.referenda.referendumInfoFor.entries(), + api.query.convictionVoting.votingFor.keys(), + api.query.session.validators(), + ]); + const activeProposals = entries.filter(([, info]) => { + const d = info.toJSON(); + return d && typeof d === 'object' && 'ongoing' in d; + }).length; + const totalVoters = new Set(votingKeys.map(k => k.args[0].toString())).size; + const validators = sessionVals.length; + setStats(prev => ({ ...prev, activeProposals, totalVoters, validators })); + } catch {} + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nomCount = await (api.query.staking as any).counterForNominators?.(); + if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() })); + } catch {} + })(); + }, [api, isApiReady]); + + // Staking % from Asset Hub + useEffect(() => { + if (!assetHubApi || !isAssetHubReady) return; + (async () => { + try { + const ledgers = await assetHubApi.query.staking.ledger.entries(); + let total = BigInt(0); + for (const [, l] of ledgers) { + if (!l.isEmpty) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const d = (l as any).unwrap?.()?.toJSON() ?? (l as any).toJSON(); + total += BigInt(d?.active ?? d?.total ?? '0'); + } + } + const totalIssuance = await assetHubApi.query.balances.totalIssuance(); + const issuance = BigInt(totalIssuance.toString()); + if (issuance > 0n) { + const pct = (Number(total) / Number(issuance) * 100).toFixed(1); + setStats(prev => ({ ...prev, tokensStakedPct: pct + '%' })); + } + } catch {} + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const collCount = await (assetHubApi.query.collatorSelection as any)?.candidates?.(); + if (collCount != null) setStats(prev => ({ ...prev, collators: collCount.length })); + } catch {} + })(); + }, [assetHubApi, isAssetHubReady]); + + // Citizens from People Chain + useEffect(() => { + if (!peopleApi || !isPeopleReady) return; + (async () => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.(); + if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length })); + } catch {} + })(); + }, [peopleApi, isPeopleReady]); + + const shortHash = (h: string) => h || '0x——————…————'; + + // ─── Ticker content ─────────────────────────────────────────────────────── + const tickerItems = [ + { label: 'block', val: stats.latestBlock ? `#${stats.latestBlock.toLocaleString()}` : '—' }, + { label: 'validators', val: stats.validators || '—' }, + { label: 'nominators', val: stats.nominators ? stats.nominators.toLocaleString() : '—' }, + { label: 'citizens', val: stats.citizenCount ? stats.citizenCount.toLocaleString() : '—' }, + { label: 'staked', val: stats.tokensStakedPct }, + { label: 'proposals', val: stats.activeProposals || '—' }, + { label: 'peers', val: stats.peers || '—' }, + ]; + + return ( +
+ +