mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
feat: per-chain branding — logo, favicon, brand name customizable
- Add ChainBranding interface with logoSvg, faviconSvg, brandName per chain - Each chain (Lux, Zoo, Hanzo, SPC, Pars) gets distinct icon + brand name - TopBar renders chain-specific logo and brand name from registry - ChainSwitcher shows per-chain icons in dropdown items - Dynamic favicon via useChainFavicon hook (swaps SVG favicon on mount) - Page title uses chain branding via getNetworkTitle()
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
// Multi-tenant chain registry. Maps hostnames to chain and network configuration.
|
||||
// One Docker image serves all chains — the hostname determines the active chain.
|
||||
|
||||
export interface ChainBranding {
|
||||
|
||||
/** Display name in the header (e.g. "Lux Network", "Zoo Chain") */
|
||||
readonly brandName: string;
|
||||
|
||||
/** SVG path data for the 18×18 logo icon (viewBox "0 0 50 50") */
|
||||
readonly logoSvg: string;
|
||||
|
||||
/** SVG path data for the favicon circle icon (viewBox "0 0 512 512") */
|
||||
readonly faviconSvg: string;
|
||||
}
|
||||
|
||||
export interface ChainEntry {
|
||||
readonly name: string;
|
||||
readonly label: string;
|
||||
@@ -9,6 +21,7 @@ export interface ChainEntry {
|
||||
readonly hostnames: ReadonlyArray<string>;
|
||||
readonly explorerUrl: string;
|
||||
readonly apiUrl: string;
|
||||
readonly branding: ChainBranding;
|
||||
}
|
||||
|
||||
export interface NetworkEntry {
|
||||
@@ -19,6 +32,43 @@ export interface NetworkEntry {
|
||||
readonly explorerUrl: string;
|
||||
}
|
||||
|
||||
// ── Per-chain branding definitions ──
|
||||
|
||||
/** Lux triangle — downward-pointing triangle */
|
||||
const LUX_BRANDING: ChainBranding = {
|
||||
brandName: 'Lux Network',
|
||||
logoSvg: 'M25 46.65 50 3.35H0z',
|
||||
faviconSvg: '<circle cx="256" cy="256" r="256"/><path fill="#fff" d="m256 410 179-308H77z"/>',
|
||||
};
|
||||
|
||||
/** Zoo — hexagon (beehive / ecosystem) */
|
||||
const ZOO_BRANDING: ChainBranding = {
|
||||
brandName: 'Zoo Chain',
|
||||
logoSvg: 'M25 3 46.65 14.5v21L25 47 3.35 35.5v-21z',
|
||||
faviconSvg: '<circle cx="256" cy="256" r="256"/><path fill="#fff" d="M256 82l150 87v174l-150 87-150-87V169z"/>',
|
||||
};
|
||||
|
||||
/** Hanzo — four-pointed star */
|
||||
const HANZO_BRANDING: ChainBranding = {
|
||||
brandName: 'Hanzo AI',
|
||||
logoSvg: 'M25 3 31 19h16l-13 10 5 16-14-10-14 10 5-16L3 19h16z',
|
||||
faviconSvg: '<circle cx="256" cy="256" r="256"/><path fill="#fff" d="M256 62l50 144h152l-123 90 47 144-126-91-126 91 47-144L54 206h152z"/>',
|
||||
};
|
||||
|
||||
/** SPC — diamond / gem */
|
||||
const SPC_BRANDING: ChainBranding = {
|
||||
brandName: 'SPC Chain',
|
||||
logoSvg: 'M25 3 47 25 25 47 3 25z',
|
||||
faviconSvg: '<circle cx="256" cy="256" r="256"/><path fill="#fff" d="M256 62l194 194-194 194L62 256z"/>',
|
||||
};
|
||||
|
||||
/** Pars — octagram / eight-pointed star */
|
||||
const PARS_BRANDING: ChainBranding = {
|
||||
brandName: 'Pars Network',
|
||||
logoSvg: 'M25 3l6 15h16l-13 9 5 16-14-10-14 10 5-16-13-9h16z',
|
||||
faviconSvg: '<circle cx="256" cy="256" r="256"/><path fill="#fff" d="M256 72l48 128h136l-110 80 42 130-116-84-116 84 42-130-110-80h136z"/>',
|
||||
};
|
||||
|
||||
export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
{
|
||||
name: 'C-Chain',
|
||||
@@ -28,6 +78,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore.lux.network', 'localhost' ],
|
||||
explorerUrl: 'https://explore.lux.network',
|
||||
apiUrl: 'https://api-explore.lux.network',
|
||||
branding: LUX_BRANDING,
|
||||
},
|
||||
{
|
||||
name: 'Zoo',
|
||||
@@ -37,6 +88,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore-zoo.lux.network' ],
|
||||
explorerUrl: 'https://explore-zoo.lux.network',
|
||||
apiUrl: 'https://api-explore-zoo.lux.network',
|
||||
branding: ZOO_BRANDING,
|
||||
},
|
||||
{
|
||||
name: 'Hanzo',
|
||||
@@ -46,6 +98,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore-hanzo.lux.network' ],
|
||||
explorerUrl: 'https://explore-hanzo.lux.network',
|
||||
apiUrl: 'https://api-explore-hanzo.lux.network',
|
||||
branding: HANZO_BRANDING,
|
||||
},
|
||||
{
|
||||
name: 'SPC',
|
||||
@@ -55,6 +108,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore-spc.lux.network' ],
|
||||
explorerUrl: 'https://explore-spc.lux.network',
|
||||
apiUrl: 'https://api-explore-spc.lux.network',
|
||||
branding: SPC_BRANDING,
|
||||
},
|
||||
{
|
||||
name: 'Pars',
|
||||
@@ -64,6 +118,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore-pars.lux.network' ],
|
||||
explorerUrl: 'https://explore-pars.lux.network',
|
||||
apiUrl: 'https://api-explore-pars.lux.network',
|
||||
branding: PARS_BRANDING,
|
||||
},
|
||||
// Testnet chains
|
||||
{
|
||||
@@ -74,6 +129,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore.lux-test.network' ],
|
||||
explorerUrl: 'https://explore.lux-test.network',
|
||||
apiUrl: 'https://api-explore.lux-test.network',
|
||||
branding: LUX_BRANDING,
|
||||
},
|
||||
// Devnet chains
|
||||
{
|
||||
@@ -84,6 +140,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
|
||||
hostnames: [ 'explore.lux-dev.network' ],
|
||||
explorerUrl: 'https://explore.lux-dev.network',
|
||||
apiUrl: 'https://api-explore.lux-dev.network',
|
||||
branding: LUX_BRANDING,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getCurrentChain } from 'configs/app/chainRegistry';
|
||||
|
||||
/**
|
||||
* Dynamically sets the favicon SVG based on the current chain's branding.
|
||||
* Generates an inline data: URI SVG favicon so each chain gets its own icon.
|
||||
*/
|
||||
export default function useChainFavicon(): void {
|
||||
React.useEffect(() => {
|
||||
const chain = getCurrentChain();
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">${ chain.branding.faviconSvg }</svg>`;
|
||||
const dataUri = `data:image/svg+xml,${ encodeURIComponent(svg) }`;
|
||||
|
||||
// Update the SVG favicon link (browsers prefer SVG when available)
|
||||
const svgLink = document.querySelector<HTMLLinkElement>('link[rel="icon"][type="image/svg+xml"]');
|
||||
if (svgLink) {
|
||||
svgLink.href = dataUri;
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import config from 'configs/app';
|
||||
import { getCurrentChain } from 'configs/app/chainRegistry';
|
||||
|
||||
// TODO delete when page descriptions is refactored
|
||||
export default function getNetworkTitle() {
|
||||
return config.chain.name + (config.chain.shortName ? ` (${ config.chain.shortName })` : '') + ' Explorer';
|
||||
export default function getNetworkTitle(): string {
|
||||
// Prefer chain registry brand name (hostname-aware) over env var
|
||||
const chain = getCurrentChain();
|
||||
const name = chain.branding.brandName || config.chain.name;
|
||||
return name + (config.chain.shortName ? ` (${ config.chain.shortName })` : '') + ' Explorer';
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
|
||||
import { SettingsContextProvider } from 'lib/contexts/settings';
|
||||
import { initGrowthBook } from 'lib/growthbook/init';
|
||||
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
|
||||
import useChainFavicon from 'lib/hooks/useChainFavicon';
|
||||
import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/rollbar';
|
||||
import { SocketProvider } from 'lib/socket/context';
|
||||
import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
|
||||
@@ -61,6 +62,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
|
||||
const growthBook = initGrowthBook(pageProps.uuid);
|
||||
useLoadFeatures(growthBook);
|
||||
useChainFavicon();
|
||||
|
||||
const queryClient = useQueryClientConfig();
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ const ChainSwitcher = () => {
|
||||
onClick={ handleToggle }
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="14" height="14" style={{ flexShrink: 0 }}>
|
||||
<path d={ current.branding.logoSvg } fill="currentColor"/>
|
||||
</svg>
|
||||
{ current.name }
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="12" height="12">
|
||||
<path
|
||||
@@ -84,13 +87,18 @@ const ChainSwitcher = () => {
|
||||
transition="background 0.15s"
|
||||
textDecoration="none"
|
||||
>
|
||||
<Flex direction="column">
|
||||
<Text fontSize="sm" fontWeight={ isCurrent ? 600 : 400 } color="text.primary">
|
||||
{ chain.name }
|
||||
</Text>
|
||||
<Text fontSize="xs" color="text.secondary">
|
||||
{ chain.label }
|
||||
</Text>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="16" height="16" style={{ flexShrink: 0 }}>
|
||||
<path d={ chain.branding.logoSvg } fill="currentColor"/>
|
||||
</svg>
|
||||
<Flex direction="column">
|
||||
<Text fontSize="sm" fontWeight={ isCurrent ? 600 : 400 } color="text.primary">
|
||||
{ chain.branding.brandName }
|
||||
</Text>
|
||||
<Text fontSize="xs" color="text.secondary">
|
||||
{ chain.label }
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box
|
||||
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' }}
|
||||
|
||||
@@ -91,13 +91,13 @@ const TopBar = () => {
|
||||
gap="6px"
|
||||
flexShrink={ 0 }
|
||||
mr={ 3 }
|
||||
aria-label="Lux Network home"
|
||||
aria-label={ `${ chain.branding.brandName } home` }
|
||||
textDecoration="none"
|
||||
_hover={{ textDecoration: 'none', opacity: 0.8 }}
|
||||
transition="opacity 0.15s"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="18" height="18">
|
||||
<polygon points="25,46.65 50,3.35 0,3.35" fill="currentColor"/>
|
||||
<path d={ chain.branding.logoSvg } fill="currentColor"/>
|
||||
</svg>
|
||||
<chakra.span
|
||||
fontWeight={ 700 }
|
||||
@@ -106,7 +106,7 @@ const TopBar = () => {
|
||||
whiteSpace="nowrap"
|
||||
color="text.primary"
|
||||
>
|
||||
{ chain.name === 'C-Chain' ? 'Lux Network' : chain.name }
|
||||
{ chain.branding.brandName }
|
||||
</chakra.span>
|
||||
</chakra.a>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user