mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Adds .hanzo/workflows/cicd.yml calling the canonical pipeline and drops the GitHub-hosted workflows it replaces. Lands here because this is where the repo is written — git.hanzo.ai is upstream and github.com is its push mirror, so the same change made on github.com never reaches the runner fleet.
241 lines
9.0 KiB
TypeScript
241 lines
9.0 KiB
TypeScript
import { animations } from '@hanzogui/config/v5-css';
|
|
import { createGui, GuiProvider } from '@hanzogui/core';
|
|
import { Toaster } from '@luxfi/ui/toaster';
|
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
import type { AppProps } from 'next/app';
|
|
import dynamic from 'next/dynamic';
|
|
import React from 'react';
|
|
|
|
// Initialize @hanzogui once at module scope. Runs on both server and client.
|
|
//
|
|
// Three things have to line up before any @hanzogui-derived @luxfi/ui
|
|
// component renders, or we get cascading throws:
|
|
//
|
|
// 1. createGui must run before getConfig() is called (used inside
|
|
// every styled() component's render path). We give it a non-empty
|
|
// themes object so getThemesDeduped doesn't trip Object.keys
|
|
// (undefined) — the original hydration crash.
|
|
// 2. tokens.color must hold default color tokens, otherwise
|
|
// getThemesDeduped's `tokens.color` spread produces an empty
|
|
// theme value, and downstream getThemedChildren has no theme
|
|
// colors to inject.
|
|
// 3. The render tree must be wrapped in <GuiProvider config>. Without
|
|
// it, useThemeState reads ThemeStateContext (default "") and
|
|
// throws "Missing theme." in production. createGui alone is not
|
|
// enough — it sets the config, but the theme-state Provider
|
|
// tree has to exist as well.
|
|
// 4. `animations` must be set. @hanzogui ships v5 WITHOUT a driver, and
|
|
// `@luxfi/ui`'s own config (src/lux.config.ts) has carried this key
|
|
// since the day it was written — this config predates it and did not.
|
|
// gui's Sheet reads `useAnimatedNumber` straight off the driver and
|
|
// calls `.setValue()` on the result, so with no driver the home
|
|
// page threw
|
|
// TypeError: Cannot read properties of undefined (reading 'setValue')
|
|
// out of SheetImplementationCustom. Every @luxfi/ui Drawer here — the
|
|
// burger menu, mobile search, the wallet and profile sheets — mounts
|
|
// ONLY below the mobile breakpoint, so desktop and tablet were clean
|
|
// and every phone visitor got "Oops! Something went wrong" instead of
|
|
// the explorer. Measured: crash at 390px, clean at 834px and 1440px.
|
|
//
|
|
// We materialize the config once, on both server and client (no `typeof
|
|
// window` gate). createGui is idempotent via getConfigMaybe() — if a
|
|
// downstream package somehow calls it again, the existing config is
|
|
// merged forward.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const guiConfig: any = createGui({
|
|
animations,
|
|
themes: {
|
|
light: { background: '#ffffff', color: '#000000' },
|
|
dark: { background: '#000000', color: '#ffffff' },
|
|
},
|
|
tokens: {
|
|
color: { background: '#ffffff', text: '#000000' },
|
|
space: { '0': 0, '1': 4, '2': 8, '3': 12, '4': 16, 'true': 16 },
|
|
size: { '0': 0, '1': 4, '2': 8, '3': 12, '4': 16, 'true': 16 },
|
|
radius: { '0': 0, '1': 4, '2': 8, 'true': 4 },
|
|
zIndex: { '0': 0, '1': 100, '2': 200, 'true': 100 },
|
|
},
|
|
settings: { autocompleteSpecificTokens: 'except-special' },
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any);
|
|
|
|
import type { NextPageWithLayout } from 'nextjs/types';
|
|
|
|
import type { Route } from 'nextjs-routes';
|
|
import PageMetadata from 'nextjs/PageMetadata';
|
|
|
|
import config from 'configs/app';
|
|
import getSocketUrl from 'lib/api/getSocketUrl';
|
|
import useQueryClientConfig from 'lib/api/useQueryClientConfig';
|
|
import { AppContextProvider } from 'lib/contexts/app';
|
|
import { FallbackProvider } from 'lib/contexts/fallback';
|
|
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
|
|
import { SettingsContextProvider } from 'lib/contexts/settings';
|
|
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 ThemeProvider } from 'toolkit/next/provider';
|
|
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
|
|
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
|
|
import HanzoInsights from 'ui/shared/HanzoInsights';
|
|
import Layout from 'ui/shared/layout/Layout';
|
|
import Web3Provider from 'ui/shared/web3/Web3Provider';
|
|
|
|
const RewardsContextProvider = dynamic(() => import('lib/contexts/rewards').then(module => module.RewardsContextProvider), { ssr: false });
|
|
const RewardsActivityTracker = dynamic(() => import('ui/rewards/RewardsActivityTracker'), { ssr: false });
|
|
|
|
import 'lib/setLocale';
|
|
// import 'focus-visible/dist/focus-visible';
|
|
import 'nextjs/global.css';
|
|
|
|
type AppPropsWithLayout = AppProps & {
|
|
Component: NextPageWithLayout;
|
|
};
|
|
|
|
const ERROR_SCREEN_STYLES = {
|
|
className: 'h-screen flex flex-col items-start justify-center w-fit max-w-[800px] mx-auto p-4 lg:p-0',
|
|
};
|
|
|
|
const CONSOLE_SCAM_WARNING = `\u26A0\uFE0FWARNING: Do not paste or execute any scripts here!
|
|
Anyone asking you to run code here might be trying to scam you and steal your data.
|
|
If you don't understand what this console is for, close it now and stay safe.`;
|
|
|
|
const CONSOLE_SCAM_WARNING_DELAY_MS = 500;
|
|
|
|
// Outer error boundary that catches crashes from providers or @hanzogui init.
|
|
// Uses zero external UI libraries so it always renders.
|
|
|
|
function handleReload() {
|
|
window.location.reload();
|
|
}
|
|
|
|
// eslint-disable-next-line react/require-optimization
|
|
class OuterErrorBoundary extends React.Component<
|
|
{ children: React.ReactNode },
|
|
{ error: Error | null }
|
|
> {
|
|
state: { error: Error | null } = { error: null };
|
|
|
|
static getDerivedStateFromError(error: Error) {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(error: Error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[OuterErrorBoundary]', error);
|
|
}
|
|
|
|
render() {
|
|
if (this.state.error) {
|
|
return (
|
|
<div style={{
|
|
maxWidth: 600,
|
|
margin: '80px auto',
|
|
padding: 24,
|
|
fontFamily: 'Geist, system-ui, sans-serif',
|
|
}}>
|
|
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 12 }}>
|
|
Something went wrong
|
|
</h1>
|
|
<p style={{ color: '#666', marginBottom: 16 }}>
|
|
An unexpected error occurred while loading the explorer.
|
|
</p>
|
|
<pre style={{
|
|
background: '#f5f5f5',
|
|
padding: 12,
|
|
borderRadius: 4,
|
|
fontSize: 13,
|
|
overflow: 'auto',
|
|
maxHeight: 200,
|
|
}}>
|
|
{ this.state.error.message }
|
|
</pre>
|
|
<button
|
|
onClick={ handleReload }
|
|
style={{
|
|
marginTop: 16,
|
|
padding: '8px 16px',
|
|
border: '1px solid #ccc',
|
|
borderRadius: 4,
|
|
cursor: 'pointer',
|
|
background: '#fff',
|
|
}}
|
|
>
|
|
Reload page
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function MyApp({ Component, pageProps, router }: AppPropsWithLayout) {
|
|
|
|
useChainFavicon();
|
|
|
|
const queryClient = useQueryClientConfig();
|
|
|
|
React.useEffect(() => {
|
|
// after the app is rendered/hydrated, show the console scam warning
|
|
const timeoutId = window.setTimeout(() => {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(CONSOLE_SCAM_WARNING);
|
|
}, CONSOLE_SCAM_WARNING_DELAY_MS);
|
|
|
|
return () => window.clearTimeout(timeoutId);
|
|
}, []);
|
|
|
|
const content = (() => {
|
|
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
|
|
|
|
return (
|
|
<>
|
|
{ getLayout(<Component { ...pageProps }/>) }
|
|
<Toaster/>
|
|
{ config.features.rewards.isEnabled && <RewardsActivityTracker/> }
|
|
</>
|
|
);
|
|
})();
|
|
|
|
const RewardsProvider = config.features.rewards.isEnabled ? RewardsContextProvider : FallbackProvider;
|
|
|
|
const socketUrl = !config.features.multichain.isEnabled ? getSocketUrl() : undefined;
|
|
|
|
return (
|
|
<OuterErrorBoundary>
|
|
<GuiProvider config={ guiConfig }>
|
|
<PageMetadata pathname={ router.pathname as Route['pathname'] } query={ pageProps.query } apiData={ pageProps.apiData }/>
|
|
<ThemeProvider>
|
|
<RollbarProvider config={ rollbarConfig }>
|
|
<AppErrorBoundary
|
|
{ ...ERROR_SCREEN_STYLES }
|
|
Container={ AppErrorGlobalContainer }
|
|
>
|
|
<QueryClientProvider client={ queryClient }>
|
|
<Web3Provider>
|
|
<AppContextProvider pageProps={ pageProps }>
|
|
<SocketProvider url={ socketUrl }>
|
|
<RewardsProvider>
|
|
<MarketplaceContextProvider>
|
|
<SettingsContextProvider>
|
|
{ content }
|
|
</SettingsContextProvider>
|
|
</MarketplaceContextProvider>
|
|
</RewardsProvider>
|
|
</SocketProvider>
|
|
<HanzoInsights/>
|
|
</AppContextProvider>
|
|
</Web3Provider>
|
|
</QueryClientProvider>
|
|
</AppErrorBoundary>
|
|
</RollbarProvider>
|
|
</ThemeProvider>
|
|
</GuiProvider>
|
|
</OuterErrorBoundary>
|
|
);
|
|
}
|
|
|
|
export default MyApp;
|