Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions staking-dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,12 @@ VITE_SAFE_API_KEY=xxxxxxxxxx

NODE_ENV='development'

# Validator Dashboard URL
VITE_VALIDATOR_DASHBOARD_URL=https://dashtec.xyz
# Validator Dashboard URL
VITE_VALIDATOR_DASHBOARD_URL=https://dashtec.xyz

# Network Switcher Configuration
# VITE_CURRENT_NETWORK: mainnet | testnet (defaults to mainnet)
VITE_CURRENT_NETWORK=mainnet
# URLs for switching between networks
VITE_MAINNET_URL=https://stake.aztec.network
VITE_TESTNET_URL=https://testnet.stake.aztec.network
26 changes: 18 additions & 8 deletions staking-dashboard/src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react"
import { Link } from "react-router-dom"
import { Icon } from "@/components/Icon"
import { CustomConnectButton } from "../CustomConnectButton"
import { NetworkSwitcher } from "../NetworkSwitcher"

/**
* Main navigation bar component
Expand All @@ -27,13 +28,16 @@ export const Navbar = () => {
<nav className="fixed top-0 w-full bg-ink/80 backdrop-blur-md border-b border-parchment/10 z-[60]">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
<Link to="/" className="flex items-center min-w-[44px] min-h-[44px] justify-center p-2">
<img
src="https://cdn.prod.website-files.com/6847005bc403085c1aa846e0/6847514dc37a9e8cfe8a66b8_aztec-logo.svg"
alt="Aztec"
className="h-4 w-auto xs:h-5 sm:h-6 md:h-7 lg:h-8 transition-all duration-200 hover:scale-105"
/>
</Link>
<div className="flex items-center gap-3 sm:gap-4">
<Link to="/" className="flex items-center min-w-[44px] min-h-[44px] justify-center p-2">
<img
src="https://cdn.prod.website-files.com/6847005bc403085c1aa846e0/6847514dc37a9e8cfe8a66b8_aztec-logo.svg"
alt="Aztec"
className="h-4 w-auto xs:h-5 sm:h-6 md:h-7 lg:h-8 transition-all duration-200 hover:scale-105"
/>
</Link>
<NetworkSwitcher />
</div>

<div className="hidden md:flex items-center space-x-6 lg:space-x-8">
{menuItems.map((item) =>
Expand Down Expand Up @@ -100,7 +104,13 @@ export const Navbar = () => {
</a>
)
)}
<div className="pt-4 border-t border-parchment/10">
<div className="pt-4 border-t border-parchment/10 space-y-4">
<div className="flex items-center justify-between">
<span className="font-oracle-standard text-sm uppercase tracking-wider text-parchment/60">
Network
</span>
<NetworkSwitcher />
</div>
<CustomConnectButton fullWidth size="lg" />
</div>
</div>
Expand Down
159 changes: 159 additions & 0 deletions staking-dashboard/src/components/NetworkSwitcher/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useState, useRef, useEffect } from "react"
import { Icon } from "@/components/Icon"
import { cn } from "@/lib/utils"

type Network = "mainnet" | "testnet"

interface NetworkConfig {
name: string
label: string
url: string
}

const NETWORK_CONFIGS: Record<Network, NetworkConfig> = {
mainnet: {
name: "mainnet",
label: "Mainnet",
url: import.meta.env.VITE_MAINNET_URL || "https://stake.aztec.network",
},
testnet: {
name: "testnet",
label: "Testnet",
url: import.meta.env.VITE_TESTNET_URL || "https://testnet.stake.aztec.network",
},
}

/**
* Get the current network from environment variable
* Defaults to mainnet if not specified
*/
const getCurrentNetwork = (): Network => {
const envNetwork = import.meta.env.VITE_CURRENT_NETWORK
if (envNetwork === "testnet" || envNetwork === "mainnet") {
return envNetwork
}
return "mainnet"
}

/**
* Network switcher dropdown component
* Allows users to switch between mainnet and testnet versions of the dashboard
*/
export const NetworkSwitcher = () => {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const currentNetwork = getCurrentNetwork()
const currentConfig = NETWORK_CONFIGS[currentNetwork]

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}

document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])

// Close dropdown on escape key
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsOpen(false)
}
}

document.addEventListener("keydown", handleEscape)
return () => document.removeEventListener("keydown", handleEscape)
}, [])

const handleNetworkSwitch = (network: Network) => {
if (network === currentNetwork) {
setIsOpen(false)
return
}

const targetUrl = NETWORK_CONFIGS[network].url
window.location.href = targetUrl
}

const otherNetworks = Object.entries(NETWORK_CONFIGS).filter(
([key]) => key !== currentNetwork
) as [Network, NetworkConfig][]

return (
<div ref={dropdownRef} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5",
"font-oracle-standard text-sm uppercase tracking-wider",
"text-parchment/80 hover:text-chartreuse transition-colors",
"border border-parchment/20 hover:border-chartreuse/50",
"bg-parchment/5 hover:bg-chartreuse/5",
"focus:outline-none focus:border-chartreuse"
)}
aria-expanded={isOpen}
aria-haspopup="listbox"
>
<span className="font-medium">{currentConfig.label}</span>
<Icon
name="chevronDown"
size="sm"
className={cn(
"transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>

{isOpen && (
<div
className={cn(
"absolute top-full left-0 mt-1 min-w-full z-50",
"bg-oxblood border border-parchment/20",
"shadow-xl",
"animate-in fade-in-0 zoom-in-95 duration-150"
)}
role="listbox"
aria-label="Select network"
>
{/* Current network (shown as selected) */}
<button
onClick={() => setIsOpen(false)}
className={cn(
"w-full flex items-center justify-between gap-2 px-3 py-2",
"font-oracle-triple-book text-sm text-chartreuse",
"bg-parchment/10 cursor-default"
)}
role="option"
aria-selected="true"
>
<span>{currentConfig.label}</span>
<Icon name="check" size="sm" className="text-chartreuse" />
</button>

{/* Other networks */}
{otherNetworks.map(([network, config]) => (
<button
key={network}
onClick={() => handleNetworkSwitch(network)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2",
"font-oracle-triple-book text-sm text-parchment",
"hover:bg-parchment/10 hover:text-chartreuse",
"transition-colors"
)}
role="option"
aria-selected="false"
>
<span>{config.label}</span>
</button>
))}
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NetworkSwitcher } from "./NetworkSwitcher"
30 changes: 30 additions & 0 deletions staking-dashboard/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_ATP_FACTORY_ADDRESS: string
readonly VITE_ATP_FACTORY_AUCTION_ADDRESS: string
readonly VITE_ATP_REGISTRY_ADDRESS: string
readonly VITE_ATP_REGISTRY_AUCTION_ADDRESS: string
readonly VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS: string
readonly VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS: string
readonly VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS: string
readonly VITE_STAKING_REGISTRY_ADDRESS: string
readonly VITE_ROLLUP_ADDRESS: string
readonly VITE_GENESIS_SEQUENCER_SALE_ADDRESS: string
readonly VITE_GOVERNANCE_ADDRESS: string
readonly VITE_GSE_ADDRESS: string
readonly VITE_CHAIN_ID: string
readonly VITE_RPC_URL: string
readonly VITE_WALLETCONNECT_PROJECT_ID: string
readonly VITE_EXPLORER_URL: string
readonly VITE_API_HOST: string
readonly VITE_SAFE_API_KEY: string
readonly VITE_VALIDATOR_DASHBOARD_URL: string
// Network switcher
readonly VITE_CURRENT_NETWORK?: "mainnet" | "testnet"
readonly VITE_MAINNET_URL?: string
readonly VITE_TESTNET_URL?: string
}

interface ImportMeta {
readonly env: ImportMetaEnv
}