Skip to content
Merged
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
2 changes: 1 addition & 1 deletion _config/goAPI/serveChain.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func ServeChain(c *gin.Context) {
fileName := c.Param("filename")

if !ContainsSubString([]string{"logo.svg", "logo-32.png", "logo-128.png"}, fileName) {
resolveNotFound(c)
resolveFallback(c, `/_config/nodeAPI/public/not-found.png`)
return
}

Expand Down
39 changes: 8 additions & 31 deletions _config/goAPI/serveToken.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,22 @@ import (
"github.com/gin-gonic/gin"
)

func resolveNotFound(c *gin.Context) {
// resolveFallback answers a request for a non-canonical asset. `?fallback=true` redirects to the
// bundled placeholder; `?fallback=<http(s) url>` redirects the browser to that URL — we never fetch
// it (no SSRF) and it renders on its own origin, http(s) only so the Location cannot carry a
// javascript: or data: scheme. Anything else is a plain 404.
func resolveFallback(c *gin.Context, placeholder string) {
fallback := c.Query("fallback")
if fallback == "true" {
c.Redirect(http.StatusTemporaryRedirect, RedirectBaseURI()+`/_config/nodeAPI/public/not-found.png`)
c.Redirect(http.StatusTemporaryRedirect, RedirectBaseURI()+placeholder)
c.Abort()
return
}

// A caller may point fallback at their own image URL. Redirect the browser to it instead of
// fetching it server-side: the daemon makes no arbitrary outbound request (no SSRF), and the
// image renders on its own origin, never ours. http(s) only, so the Location cannot carry a
// javascript: or data: scheme.
if strings.HasPrefix(fallback, "https://") || strings.HasPrefix(fallback, "http://") {
c.Redirect(http.StatusTemporaryRedirect, fallback)
c.Abort()
return
}

c.String(http.StatusNotFound, "Not found")
}

func resolveGasToken(c *gin.Context) {
fallback := c.Query("fallback")
if fallback == "true" {
c.Redirect(http.StatusTemporaryRedirect, RedirectBaseURI()+`/_config/nodeAPI/public/gas-token.png`)
c.Abort()
return
}

// A caller may point fallback at their own image URL. Redirect the browser to it instead of
// fetching it server-side: the daemon makes no arbitrary outbound request (no SSRF), and the
// image renders on its own origin, never ours. http(s) only, so the Location cannot carry a
// javascript: or data: scheme.
if strings.HasPrefix(fallback, "https://") || strings.HasPrefix(fallback, "http://") {
c.Redirect(http.StatusTemporaryRedirect, fallback)
c.Abort()
return
}

c.String(http.StatusNotFound, "Not found")
}

Expand All @@ -61,10 +38,10 @@ func ServeToken(c *gin.Context) {

if !ContainsSubString([]string{"logo.svg", "logo-32.png", "logo-128.png"}, fileName) {
if tokenAddress == "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" {
resolveGasToken(c)
resolveFallback(c, `/_config/nodeAPI/public/gas-token.png`)
return
}
resolveNotFound(c)
resolveFallback(c, `/_config/nodeAPI/public/not-found.png`)
return
}

Expand Down
5 changes: 2 additions & 3 deletions _config/ui/app/(app)/[chain]/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {CodeSnippets} from '@components/CodeSnippets';
import {InfoField} from '@components/InfoField';
import {TokenInfoFields} from '@components/TokenInfoFields';
import {LOGO_FORMATS, findChainBySlug} from '@utils/constants';
import {findChainBySlug, LOGO_FORMATS} from '@utils/constants';
import {tokenGithubURI, tokenLogoURI, tokenPageURI, truncateAddress} from '@utils/helpers';
import {findToken} from '@utils/tokens.server';
import type {Metadata} from 'next';
import Image from 'next/image';
import Link from 'next/link';
import {notFound} from 'next/navigation';

import type {Metadata} from 'next';
import type {ReactElement} from 'react';

type TTokenPageProps = {
Expand Down
3 changes: 1 addition & 2 deletions _config/ui/app/(app)/[chain]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {TokenList} from '@components/TokenList';
import {DEFAULT_CHAIN, findChainBySlug} from '@utils/constants';
import type {Metadata} from 'next';
import {redirect} from 'next/navigation';
import {type ReactElement, Suspense} from 'react';

import type {Metadata} from 'next';

type TChainPageProps = {
params: Promise<{chain?: string}>;
};
Expand Down
22 changes: 6 additions & 16 deletions _config/ui/app/_components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import SearchIcon from '@icons/search.svg';
import * as Dialog from '@radix-ui/react-dialog';
import {CHAINS} from '@utils/constants';
import {tokenLogoURI, tokenPageURI, truncateAddress, withSearch} from '@utils/helpers';
import type {TSearchEntry} from '@utils/types';
import Image from 'next/image';
import {useRouter} from 'next/navigation';
import {useCallback, useEffect, useRef, useState} from 'react';

import type {TSearchEntry} from '@utils/types';
import type {ReactElement, KeyboardEvent as ReactKeyboardEvent} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';

const CHAIN_BY_ID = new Map(CHAINS.map(chain => [chain.id, chain]));

Expand Down Expand Up @@ -50,7 +49,6 @@ function ResultRow({
ref={ref}
type={'button'}
id={optionID}
// biome-ignore lint/a11y/useSemanticElements: WAI-ARIA combobox options live outside a <select>; a native <option> cannot render this row.
role={'option'}
aria-selected={isActive}
tabIndex={-1}
Expand Down Expand Up @@ -171,14 +169,12 @@ export function CommandPalette({open, onOpenChange}: TCommandPaletteProps): Reac
const showEmpty = trimmed.length > 0 && results.length === 0 && !hasError;

return (
<Dialog.Root
open={open}
onOpenChange={onOpenChange}>
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className={'fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]'} />
<Dialog.Content
className={cn(
'-translate-x-1/2 fixed top-[12vh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-xl',
'fixed top-[12vh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-xl -translate-x-1/2',
'overflow-hidden rounded-lg border border-subtle bg-white shadow-2xl'
)}>
<Dialog.Title className={'sr-only'}>{'Search tokens'}</Dialog.Title>
Expand Down Expand Up @@ -207,10 +203,8 @@ export function CommandPalette({open, onOpenChange}: TCommandPaletteProps): Reac
/>
</div>

{/* biome-ignore lint/a11y/useFocusableInteractive: focus stays on the combobox input by design; the listbox itself must not be a tab stop. */}
<div
id={'palette-results'}
// biome-ignore lint/a11y/useSemanticElements: ARIA combobox pattern — the listbox is driven via aria-activedescendant, which a native <select> cannot do.
role={'listbox'}
aria-label={'Search results'}
className={'scrollbar-none max-h-[56vh] overflow-y-auto'}>
Expand All @@ -225,16 +219,12 @@ export function CommandPalette({open, onOpenChange}: TCommandPaletteProps): Reac
/>
))}
{showEmpty && (
<p
aria-live={'polite'}
className={'px-4 py-8 text-center font-mono text-sm text-subtle'}>
<p aria-live={'polite'} className={'px-4 py-8 text-center font-mono text-sm text-subtle'}>
{`No token matches "${trimmed}"`}
</p>
)}
{hasError && (
<p
aria-live={'assertive'}
className={'px-4 py-8 text-center font-mono text-error text-sm'}>
<p aria-live={'assertive'} className={'px-4 py-8 text-center font-mono text-error text-sm'}>
{'Could not load the search index. Close and reopen to retry.'}
</p>
)}
Expand Down
9 changes: 4 additions & 5 deletions _config/ui/app/_components/GridView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
'use client';

import {cn} from '@components/lib/utils';
import {NewBadge} from '@components/NewBadge';
import {Spinner} from '@components/Spinner';
import {TokenLogo} from '@components/TokenLogo';
import {cn} from '@components/lib/utils';
import {useChain} from '@contexts/WithChain';
import {useIntersectionObserver} from '@hooks/useIntersectionObserver';
import {isNewToken, truncateAddress} from '@utils/helpers';
import type {TToken} from '@utils/types';
import type {ReactElement, ReactNode, RefObject} from 'react';
import {memo, useCallback, useMemo, useRef} from 'react';
import {useResizeObserver} from 'usehooks-ts';

import type {TToken} from '@utils/types';
import type {ReactElement, ReactNode} from 'react';

type TProps = {
tokens: TToken[];
onClick: (address: string) => void;
Expand Down Expand Up @@ -93,7 +92,7 @@ function TokenGrid({tokens, onClick}: TProps): ReactNode {

export const GridView = ({tokens, onClick, hasNextPage, onLoadMore}: TProps): ReactElement => {
const gridRef = useRef<HTMLDivElement>(null);
const {width = 0} = useResizeObserver({ref: gridRef, box: 'border-box'});
const {width = 0} = useResizeObserver({ref: gridRef as RefObject<HTMLDivElement>, box: 'border-box'});
const tokensPerLine = Math.floor(width / 200) || 1;
const missingElements = (tokensPerLine - (tokens.length % tokensPerLine)) % tokensPerLine;
const newElements = useMemo(() => {
Expand Down
3 changes: 1 addition & 2 deletions _config/ui/app/_components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {CommandPalette} from '@components/CommandPalette';
import {MenuButtons} from '@components/Header/MenuButtons';
import {NavBar} from '@components/Header/Navbar';
import {SearchBar} from '@components/Header/SearchBar';
import {useState} from 'react';

import type {ReactElement} from 'react';
import {useState} from 'react';

export function Header(): ReactElement {
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
Expand Down
3 changes: 1 addition & 2 deletions _config/ui/app/_components/Header/MenuButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {useChain} from '@contexts/WithChain';
import Grid from '@icons/grid.svg';
import Link from 'next/link';
import {usePathname} from 'next/navigation';
import {useState} from 'react';

import type {ReactElement} from 'react';
import {useState} from 'react';

export function MenuButtons(): ReactElement {
const [isOpen, setIsOpen] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion _config/ui/app/_components/Header/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {ChainLogo} from '@components/ChainLogo';
import {cn} from '@components/lib/utils';
import {SelectContent, SelectItem, SelectTrigger, SelectValue} from '@components/ui/select';
import {useChain} from '@contexts/WithChain';
import {useSettings} from '@contexts/WithSettings';
import Grid from '@icons/grid.svg';
import List from '@icons/list.svg';
import {Select} from '@radix-ui/react-select';
import {CHAINS} from '@utils/constants';
import {withSearch} from '@utils/helpers';
import {useSettings} from 'app/_contexts/WithSettings';
import {useRouter, useSearchParams} from 'next/navigation';

import type {ReactElement} from 'react';
Expand Down
18 changes: 11 additions & 7 deletions _config/ui/app/_components/Header/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import Cross from '@icons/cross.svg';
import SearchIcon from '@icons/search.svg';
import {withSearch} from '@utils/helpers';
import {useRouter, useSearchParams} from 'next/navigation';
import type {ReactElement} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';
import {useDebounceValue} from 'usehooks-ts';

import type {ReactElement} from 'react';

export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactElement {
const router = useRouter();
const searchParams = useSearchParams();
const {chain} = useChain();
const isDrawerOpen = useDrawerOpen();
// Read via a ref (not a dependency) below: otherwise the drawer closing re-runs the push effect
// while the debounced value is still the token's search, re-applying it right after router.back()
// cleared it — the filter flash.
const isDrawerOpenRef = useRef(isDrawerOpen);
isDrawerOpenRef.current = isDrawerOpen;
const inputRef = useRef<HTMLInputElement>(null);

const currentSearch = searchParams.get('search') || '';
Expand All @@ -32,7 +36,7 @@ export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactEl
// user clicked a token card would push the bare chain URL on top of the drawer's URL,
// desyncing the @drawer slot into a drawer that router.back() can never close.
useEffect(() => {
if (isDrawerOpen) {
if (isDrawerOpenRef.current) {
return;
}
const params = new URLSearchParams(window.location.search);
Expand All @@ -45,7 +49,7 @@ export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactEl
params.delete('search');
}
router.push(withSearch(`/${chain.slug}`, params.toString()), {scroll: false});
}, [debouncedValue, router, chain.slug, isDrawerOpen]);
}, [debouncedValue, router, chain.slug]);

// Mirror an externally-set search (e.g. the command palette) into the field, unless the user is typing here.
useEffect(() => {
Expand All @@ -61,7 +65,7 @@ export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactEl
return (
<div className={'flex flex-1 justify-center max-md:w-full'}>
<div className={'relative max-md:w-full'}>
<div className={'-translate-y-1/2 absolute top-1/2 left-4'}>
<div className={'absolute top-1/2 left-4 -translate-y-1/2'}>
<SearchIcon
className={cn(
'size-4 transition-colors duration-75',
Expand Down Expand Up @@ -93,7 +97,7 @@ export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactEl
aria-hidden={!inputValue}
tabIndex={inputValue ? 0 : -1}
className={cn(
'-translate-y-1/2 absolute top-1/2 right-3 text-white md:right-[80px]',
'absolute top-1/2 right-3 -translate-y-1/2 text-white md:right-[80px]',
'transition-opacity',
inputValue ? 'opacity-100' : 'pointer-events-none opacity-0'
)}>
Expand All @@ -104,7 +108,7 @@ export function SearchBar({onOpenPalette}: {onOpenPalette: () => void}): ReactEl
onClick={onOpenPalette}
aria-label={'Search all chains'}
className={
'-translate-y-1/2 absolute top-1/2 right-3 hidden items-center gap-1 rounded-[2px] border border-disabled px-2 py-1 font-mono font-semibold text-white text-xs uppercase transition-colors hover:bg-primary-light md:flex'
'absolute top-1/2 right-3 hidden -translate-y-1/2 items-center gap-1 rounded-[2px] border border-disabled px-2 py-1 font-mono font-semibold text-white text-xs uppercase transition-colors hover:bg-primary-light md:flex'
}>
<span>{'ctrl'}</span>
<span>{'k'}</span>
Expand Down
5 changes: 2 additions & 3 deletions _config/ui/app/_components/HeroPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {BrandMark} from '@components/BrandMark';
import {cn} from '@components/lib/utils';
import {SiteFooter} from '@components/SiteFooter';
import {Stat} from '@components/Stat';
import {cn} from '@components/lib/utils';
import {Fragment} from 'react';

import type {ReactElement, ReactNode} from 'react';
import {Fragment} from 'react';

const dotGridClassName = cn(
'pointer-events-none absolute inset-0',
Expand Down
2 changes: 1 addition & 1 deletion _config/ui/app/_components/InfoField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function HttpLink({value, href}: {value: string; href?: string}): ReactEl
{value}
<LinkOutIcon
className={
'group-hover:-translate-y-0.5 ml-2 inline-block size-3 text-primary transition-transform group-hover:translate-x-0.5'
'ml-2 inline-block size-3 text-primary transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5'
}
/>
</Link>
Expand Down
5 changes: 2 additions & 3 deletions _config/ui/app/_components/ListView.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
'use client';

import {cn} from '@components/lib/utils';
import {NewBadge} from '@components/NewBadge';
import {Spinner} from '@components/Spinner';
import {TokenLogo} from '@components/TokenLogo';
import {cn} from '@components/lib/utils';
import {useChain} from '@contexts/WithChain';
import {useIntersectionObserver} from '@hooks/useIntersectionObserver';
import DownloadIcon from '@icons/download.svg';
import {copyToClipboard} from '@utils/clipboard';
import {LOGO_FORMATS} from '@utils/constants';
import {downloadLogo} from '@utils/download';
import {isNewToken, tokenLogoURI} from '@utils/helpers';
import {useCallback} from 'react';

import type {TToken} from '@utils/types';
import type {ReactElement, ReactNode} from 'react';
import {useCallback} from 'react';

type TProps = {
tokens: TToken[];
Expand Down
Loading
Loading