Skip to content

Commit 45e51a8

Browse files
authored
feat: redesign header
1 parent 2da8651 commit 45e51a8

File tree

9 files changed

+237
-55
lines changed

9 files changed

+237
-55
lines changed

block-explorer/app/layout.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// "use client";
22
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration';
3-
import Container from '@/components/container';
43
import QueryProvider from '@/components/layouts/query-provider';
5-
import MainSearch from '@/components/main-search';
64
import ProgressBarWrapper from '@/components/progress-bar-wrapper';
75
import SiteFooter from '@/components/site-footer';
86
import { SiteHeader } from '@/components/site-header';
@@ -65,9 +63,6 @@ export default async function RootLayout({ children }: RootLayoutProps) {
6563
<QueryProvider>
6664
<div className="relative flex min-h-screen flex-col justify-between">
6765
<SiteHeader />
68-
<Container className="lg:hidden">
69-
<MainSearch />
70-
</Container>
7166
{children}
7267
<SiteFooter />
7368
</div>
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
'use client';
22

3-
import { ChangeEvent, KeyboardEvent, useState } from 'react';
3+
import { ChangeEvent, KeyboardEvent as ReactKeyboardEvent, useRef, useState } from 'react';
44

55
import { performSearchMainSearch } from '@/lib/helpers';
6-
import { CornerDownLeft, Search } from 'lucide-react';
6+
import { useKeyDown } from '@/lib/hooks/useKeyDown.ts';
7+
import { RiSearchLine } from '@remixicon/react';
78

89
import { Input } from './ui/input';
910

1011
export default function MainSearch() {
12+
const inputRef = useRef<HTMLInputElement>(null);
13+
1114
const [value, setValue] = useState('');
1215

13-
const handlePress = async (e: KeyboardEvent) => {
16+
const handlePress = async (e: ReactKeyboardEvent) => {
1417
if (e?.key === 'Enter') {
1518
await performSearchMainSearch(value);
1619
setValue('');
@@ -21,17 +24,31 @@ export default function MainSearch() {
2124
setValue(e.target.value);
2225
};
2326

27+
const handleFocus = (event?: KeyboardEvent) => {
28+
if (event && document.activeElement !== document.body) return;
29+
event?.preventDefault();
30+
event?.stopPropagation();
31+
setTimeout(() => inputRef.current?.focus());
32+
};
33+
34+
useKeyDown('/', handleFocus);
35+
2436
return (
2537
<div className="relative">
26-
<Search className="absolute left-2.5 top-2.5 -z-10 size-5 text-muted-foreground" />
38+
<RiSearchLine className="absolute left-4 top-3 -z-10 size-4 text-muted-foreground" />
2739
<Input
28-
onChange={handleChangeValue}
29-
value={value}
40+
ref={inputRef}
41+
className="px-10"
3042
placeholder="Search by address / txn hash / block..."
43+
value={value}
44+
onChange={handleChangeValue}
3145
onKeyDown={handlePress}
32-
className="px-10"
3346
/>
34-
<CornerDownLeft className="absolute right-2.5 top-2.5 -z-10 m-auto size-5 text-muted-foreground" />
47+
<div className="absolute right-2.5 top-2.5 -z-10 m-auto flex size-5 items-center justify-center rounded-[4px] bg-[#F5F5F5] text-muted-foreground dark:bg-[#1C1C1C]">
48+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
49+
<path d="M8.06061 2.33337H9.33334L5.9394 11.6667H4.66667L8.06061 2.33337Z" fill="currentColor" />
50+
</svg>
51+
</div>
3552
</div>
3653
);
3754
}

block-explorer/components/navigation.tsx

Lines changed: 142 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,53 @@
22

33
import { Fragment, useState } from 'react';
44

5+
import { Card, CardContent } from '@/components/ui/card';
6+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible.tsx';
7+
import {
8+
DropdownMenu,
9+
DropdownMenuContent,
10+
DropdownMenuItem,
11+
DropdownMenuTrigger,
12+
} from '@/components/ui/dropdown-menu.tsx';
513
import { cn } from '@/lib/utils';
6-
import { MenuIcon, XIcon } from 'lucide-react';
14+
import { RiArrowDownSLine, RiCloseLargeLine, RiMenuLine } from '@remixicon/react';
715
import Link from 'next/link';
816
import { usePathname } from 'next/navigation';
917

10-
import { Card, CardContent, CardHeader } from './ui/card';
18+
type HeaderMenuLink = {
19+
title: string;
20+
href: string;
21+
target?: '_blank';
22+
};
23+
24+
type HeaderMenuGroupLink = {
25+
title: string;
26+
subitems: Omit<HeaderMenuLink, 'target'>[];
27+
};
1128

12-
const items = [
13-
{ title: 'Blocks', href: '/blocks' },
14-
{ title: 'Extrinsics', href: '/extrinsics' },
15-
{ title: 'Events', href: '/events' },
16-
{ title: 'EVM Transactions', href: '/evm-transactions' },
29+
const items: Array<HeaderMenuLink | HeaderMenuGroupLink> = [
30+
{
31+
title: 'Blockchain',
32+
subitems: [
33+
{ title: 'Blocks', href: '/blocks' },
34+
{ title: 'Extrinsics', href: '/extrinsics' },
35+
{ title: 'Events', href: '/events' },
36+
{ title: 'EVM Transactions', href: '/evm-transactions' },
37+
],
38+
},
39+
{
40+
title: 'Track',
41+
subitems: [
42+
{ title: 'Bridge', href: '/bridge' },
43+
{ title: 'DEX', href: '/dex' },
44+
{ title: 'Staking', href: '/staking' },
45+
],
46+
},
1747
{ title: 'Addresses', href: '/addresses' },
18-
{ title: 'Bridge', href: '/bridge' },
1948
{ title: 'Tokens', href: '/tokens' },
20-
{ title: 'DEX', href: '/dex' },
21-
{ title: 'Staking', href: '/staking' },
2249
{ title: 'Verified Contracts', href: '/verified-contracts' },
2350
{ title: 'Ecosystem', href: '/ecosystem' },
24-
{ title: 'API', href: 'https://build.rootscan.io', newTab: true },
51+
{ title: 'API Portal', href: 'https://build.rootscan.io', target: '_blank' },
2552
];
2653

2754
export function Navigation() {
@@ -30,19 +57,52 @@ export function Navigation() {
3057

3158
return (
3259
<Fragment>
33-
<div className="hidden items-center gap-4 px-4 lg:flex">
34-
{items.map((item, _) => (
35-
<Link href={item.href} key={_} target={item.newTab ? '_blank' : '_self'}>
36-
<div
60+
<div className="hidden items-center gap-0.5 px-4 lg:flex">
61+
{items.map((item, _) => {
62+
const { title } = item;
63+
const { href, target } = item as HeaderMenuLink;
64+
const { subitems } = item as HeaderMenuGroupLink;
65+
const isLink = !!href;
66+
67+
const headerMenuLink = (
68+
<Link
69+
href={isLink ? href : '#'}
70+
target={target ? '_blank' : '_self'}
3771
className={cn([
38-
'text-muted-foreground hover:text-primary flex items-center text-sm font-bold duration-150 ease-in',
39-
formattedPathname === item.href ? 'text-primary' : '',
72+
'group font-semibold gap-2 inline-flex items-center py-1.5 px-3 transition-colors text-[14px]/[20px] text-muted-foreground data-[state=open]:text-primary hover:text-primary focus:outline-0',
73+
!isLink && 'pr-2.5',
74+
formattedPathname === href ? 'text-primary' : '',
4075
])}
4176
>
42-
{item.title}
43-
</div>
44-
</Link>
45-
))}
77+
{title}
78+
{!isLink && <RiArrowDownSLine className="size-4 transition-all group-data-[state=open]:rotate-180" />}
79+
</Link>
80+
);
81+
82+
return (
83+
<Fragment key={_}>
84+
{isLink ? (
85+
headerMenuLink
86+
) : (
87+
<DropdownMenu>
88+
<DropdownMenuTrigger asChild>{headerMenuLink}</DropdownMenuTrigger>
89+
<DropdownMenuContent className="flex flex-col gap-1 rounded-[12px] bg-popover p-2">
90+
{subitems?.map((subitem, i) => (
91+
<DropdownMenuItem key={i} asChild>
92+
<Link
93+
href={subitem.href}
94+
className="cursor-pointer rounded-[4px] px-3 py-2 text-[14px]/[20px] font-semibold"
95+
>
96+
{subitem.title}
97+
</Link>
98+
</DropdownMenuItem>
99+
))}
100+
</DropdownMenuContent>
101+
</DropdownMenu>
102+
)}
103+
</Fragment>
104+
);
105+
})}
46106
</div>
47107
<MobileMenu />
48108
</Fragment>
@@ -52,25 +112,72 @@ export function Navigation() {
52112
export const MobileMenu = () => {
53113
const [open, setOpen] = useState<boolean>(false);
54114

115+
const pathname = usePathname();
116+
117+
const isHomePage = pathname === '/';
118+
55119
return (
56120
<Fragment>
57121
<div className="block lg:hidden">
58-
<div className="duration-300 animate-in animate-out fade-in fade-out" onClick={() => setOpen(!open)}>
59-
{open ? <XIcon /> : <MenuIcon />}
122+
<div className="p-1 duration-300 animate-in animate-out fade-in fade-out" onClick={() => setOpen(!open)}>
123+
{open ? <RiCloseLargeLine className="size-6" /> : <RiMenuLine className="size-6" />}
60124
</div>
61125
</div>
62-
<div className={cn([open ? 'absolute left-0 top-[64px] !m-0 w-full' : 'hidden'])}>
63-
<Card className="rounded-b-2xl rounded-t-none">
64-
<CardHeader className="pb-0" />
65-
<CardContent>
66-
<div className="flex flex-col gap-4">
67-
{items.map((item, _) => (
68-
<Link href={item.href} onClick={() => setOpen(false)} key={_} target={item.newTab ? '_blank' : '_self'}>
69-
<div className="flex items-center text-sm font-bold text-muted-foreground duration-150 ease-in hover:text-primary">
70-
{item.title}
71-
</div>
72-
</Link>
73-
))}
126+
<div
127+
className={cn(
128+
'absolute left-0 top-[64px] !m-0 w-full',
129+
!open && 'hidden',
130+
isHomePage ? 'top-[96px]' : 'top-[128px]',
131+
)}
132+
>
133+
<Card className="rounded-b-2xl rounded-t-none dark:bg-black">
134+
<CardContent className="p-2">
135+
<div className="flex flex-col gap-0.5">
136+
{items.map((item, _) => {
137+
const { title } = item;
138+
const { href } = item as HeaderMenuLink;
139+
const { subitems } = item as HeaderMenuGroupLink;
140+
const isLink = !!href;
141+
142+
const headerMenuLink = (
143+
<Link
144+
className="group flex items-center justify-between py-3.5 pl-2 pr-3.5 text-[14px]/[20px] font-semibold transition-all"
145+
href={isLink ? href : '#'}
146+
onClick={() => isLink && setOpen(false)}
147+
>
148+
{title}
149+
{!isLink && (
150+
<RiArrowDownSLine className="size-5 transition-all group-data-[state=open]:rotate-180" />
151+
)}
152+
</Link>
153+
);
154+
155+
return (
156+
<Fragment key={_}>
157+
{isLink ? (
158+
headerMenuLink
159+
) : (
160+
<Collapsible>
161+
<CollapsibleTrigger asChild>{headerMenuLink}</CollapsibleTrigger>
162+
<CollapsibleContent>
163+
<div className="flex flex-col gap-1 rounded-[12px] border p-2">
164+
{subitems?.map((subitem, i) => (
165+
<Link
166+
key={i}
167+
href={subitem.href}
168+
className="cursor-pointer rounded-[4px] px-3 py-2 text-[14px]/[20px] font-semibold"
169+
onClick={() => setOpen(false)}
170+
>
171+
{subitem.title}
172+
</Link>
173+
))}
174+
</div>
175+
</CollapsibleContent>
176+
</Collapsible>
177+
)}
178+
</Fragment>
179+
);
180+
})}
74181
</div>
75182
</CardContent>
76183
</Card>

block-explorer/components/site-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { Badge } from './ui/badge';
1111
export function SiteHeader() {
1212
return (
1313
<Fragment>
14-
<header className="z-40 flex w-full flex-col bg-white py-4 dark:bg-black/50">
14+
<header className="z-40 flex w-full flex-col bg-white dark:bg-black">
1515
<SiteTopBar />
16-
<div className="container flex items-center justify-between gap-4 space-x-4 pt-0 sm:space-x-0 lg:pt-4">
16+
<div className="container flex items-center justify-between gap-4 space-x-4 p-4 sm:space-x-0 lg:px-8">
1717
<Link href="/" className="flex items-center gap-2">
1818
<Image
1919
src="/site-logos/rootscan-logo.png"

block-explorer/components/site-top-bar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ export const SiteTopBar = () => {
1212
const isHomePage = pathname === '/';
1313

1414
return (
15-
<div className="hidden border-b pb-4 lg:block">
16-
<div className="container">
15+
<div className="border-b pb-[11px] pt-3">
16+
<div className="container px-4 lg:px-8">
1717
<div className="flex items-center justify-between gap-4">
18-
<div className="flex w-full select-none items-center gap-2 text-xs text-primary/80">
18+
<div className="hidden w-full select-none items-center gap-2 text-xs text-primary/80 lg:flex">
1919
<OnlyMainnet>
2020
<RootPrice />
2121
</OnlyMainnet>
22-
{isHomePage && <div className="flex-1" />}
22+
{isHomePage && <div className="hidden lg:flex-1" />}
2323
<RiGasStationLine className="size-4 text-muted-foreground" />{' '}
2424
<span className="text-muted-foreground">EVM Gas: </span>
2525
<span>7500 Gwei</span>
2626
</div>
2727
{!isHomePage && (
28-
<div className="hidden max-w-2xl grow lg:block">
28+
<div className="w-full grow lg:max-w-[656px]">
2929
<MainSearch />
3030
</div>
3131
)}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
2+
3+
const Collapsible = CollapsiblePrimitive.Root;
4+
5+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6+
7+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8+
9+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
3+
export const useKeyDown = (
4+
key: string,
5+
callback: (event: KeyboardEvent) => void
6+
) => {
7+
const callbackRef = React.useRef(callback)
8+
callbackRef.current = callback
9+
10+
React.useEffect(() => {
11+
const handleKeydown = (event: KeyboardEvent) => {
12+
if (event.key === key) {
13+
callbackRef.current(event)
14+
}
15+
}
16+
document.addEventListener('keydown', handleKeydown)
17+
return () => {
18+
document.removeEventListener('keydown', handleKeydown)
19+
}
20+
}, [])
21+
}

block-explorer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@icons-pack/react-simple-icons": "^9.7.0",
2323
"@radix-ui/react-accordion": "^1.2.3",
2424
"@radix-ui/react-avatar": "^1.1.3",
25+
"@radix-ui/react-collapsible": "^1.1.11",
2526
"@radix-ui/react-dropdown-menu": "^2.1.6",
2627
"@radix-ui/react-hover-card": "^1.1.6",
2728
"@radix-ui/react-navigation-menu": "^1.2.5",

0 commit comments

Comments
 (0)