diff --git a/public/icons/llama-logos/defi-llama-dark-theme.svg b/public/icons/llama-logos/defi-llama-dark-theme.svg new file mode 100644 index 000000000..a0a12114a --- /dev/null +++ b/public/icons/llama-logos/defi-llama-dark-theme.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/llama-logos/defi-llama-icon.svg b/public/icons/llama-logos/defi-llama-icon.svg new file mode 100644 index 000000000..0bb7ba7b5 --- /dev/null +++ b/public/icons/llama-logos/defi-llama-icon.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/llama-logos/defillama-light-theme.svg b/public/icons/llama-logos/defillama-light-theme.svg new file mode 100644 index 000000000..a52082fef --- /dev/null +++ b/public/icons/llama-logos/defillama-light-theme.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/v25.svg b/public/icons/v25.svg index 32b277ce3..d211cfe06 100644 --- a/public/icons/v25.svg +++ b/public/icons/v25.svg @@ -132,5 +132,8 @@ + + + diff --git a/public/pages.json b/public/pages.json index 2d01f31be..4e71a81f1 100644 --- a/public/pages.json +++ b/public/pages.json @@ -716,56 +716,53 @@ "icon": "pencil-ruler" } ], - "More": [ + "Resources": [ { "name": "Support", - "route": "/support" + "route": "/support", + "icon": "help-circle" + }, + { + "name": "Documentation", + "route": "https://api-docs.defillama.com", + "icon": "code" + }, + { + "name": "Contact us", + "route": "/about", + "icon": "chat" }, { "name": "Careers", - "route": "https://github.com/DefiLlama/careers/blob/master/README.md" + "route": "https://github.com/DefiLlama/careers/blob/master/README.md", + "icon": "users" }, { "name": "Report Incorrect Data", - "route": "/report-error" + "route": "/report-error", + "icon": "flag" }, { "name": "Press / Media", "route": "/press" }, - { - "name": "API Reference", - "route": "https://api-docs.defillama.com" - }, { "name": "List your project", "route": "https://docs.llama.fi/list-your-project/submit-a-project" - } - ], - "About Us": [ - { - "name": "About / Contact", - "route": "/about" }, { "name": "Twitter", - "route": "https://x.com/DefiLlama" + "route": "https://x.com/DefiLlama", + "icon": "twitter" }, { "name": "Discord", - "route": "https://discord.defillama.com" + "route": "https://discord.defillama.com", + "icon": "discord" }, { "name": "Donate", "route": "/donations" - }, - { - "name": "Terms of Service", - "route": "/terms" - }, - { - "name": "Privacy Policy", - "route": "/privacy-policy" } ], "Hidden": [ diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 33e6c8aa8..9c02772bc 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -66,6 +66,7 @@ type Name = | 'chat' | 'earth' | 'twitter' + | 'discord' | 'file-text' | 'flame' | 'house' diff --git a/src/components/Metrics.tsx b/src/components/Metrics.tsx index ff5701b0a..4ea97bc15 100644 --- a/src/components/Metrics.tsx +++ b/src/components/Metrics.tsx @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query' import { matchSorter } from 'match-sorter' import { Icon } from '~/components/Icon' import { BasicLink } from '~/components/Link' +import { PINNED_METRICS_KEY } from '~/components/Nav/utils' import { TOTAL_TRACKED_BY_METRIC_API } from '~/constants' import { subscribeToPinnedMetrics } from '~/contexts/LocalStorage' import defillamaPages from '~/public/pages.json' @@ -188,7 +189,7 @@ export const LinkToMetricOrToolPage = React.memo(function LinkToMetricOrToolPage }) { const pinnedMetrics = useSyncExternalStore( subscribeToPinnedMetrics, - () => localStorage.getItem('pinned-metrics') ?? '[]', + () => localStorage.getItem(PINNED_METRICS_KEY) ?? '[]', () => '[]' ) @@ -240,9 +241,9 @@ export const LinkToMetricOrToolPage = React.memo(function LinkToMetricOrToolPage render={ ) : ( - Sign In / Subscribe + {!isCollapsed && 'Sign In / Subscribe'} )} diff --git a/src/components/Nav/Desktop.tsx b/src/components/Nav/Desktop.tsx index dc2a7ef53..9139cddd2 100644 --- a/src/components/Nav/Desktop.tsx +++ b/src/components/Nav/Desktop.tsx @@ -1,176 +1,244 @@ import * as React from 'react' -import { useRouter } from 'next/router' import { Icon } from '~/components/Icon' import { BasicLink } from '~/components/Link' -import { Tooltip } from '../Tooltip' +import { Tooltip } from '~/components/Tooltip' +import { useSidebarState } from '~/contexts/SidebarContext' +import { NavCollapseProvider } from './NavCollapseContext' +import { NavItems, NavLink } from './NavGroup' +import { primaryNavigation, resourcesNavigation } from './navStructure' +import type { NavLink as NavLinkType } from './navStructure' import { ThemeSwitch } from './ThemeSwitch' -import { TNavLink, TNavLinks } from './types' +import { getPinnedNavStructure, unpinRoute } from './utils' const Account = React.lazy(() => import('./Account').then((mod) => ({ default: mod.Account }))) export const DesktopNav = React.memo(function DesktopNav({ - mainLinks, pinnedPages, userDashboards, - footerLinks + accountAttention }: { - mainLinks: TNavLinks - pinnedPages: TNavLink[] - userDashboards: TNavLink[] - footerLinks: TNavLinks + pinnedPages: string[] + userDashboards: NavLinkType[] + accountAttention?: boolean }) { - const { asPath } = useRouter() + // Convert pinned routes to grouped structure + const pinnedNavStructure = React.useMemo(() => getPinnedNavStructure(pinnedPages), [pinnedPages]) + + const { isCollapsed, isPinned, toggle, setCollapsed } = useSidebarState() + const hoverTimerRef = React.useRef | null>(null) + const leaveTimerRef = React.useRef | null>(null) + + // Handle mouse enter with delay + const handleMouseEnter = React.useCallback(() => { + // Clear any pending leave timer (user came back) + if (leaveTimerRef.current) { + clearTimeout(leaveTimerRef.current) + leaveTimerRef.current = null + } + + if (!isCollapsed || isPinned) return + + const timer = setTimeout(() => { + setCollapsed(false) + }, 200) // 200ms delay to prevent accidental triggers + + hoverTimerRef.current = timer + }, [isCollapsed, isPinned, setCollapsed]) + + // Handle mouse leave with delay + const handleMouseLeave = React.useCallback(() => { + // Clear any pending hover timer + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current) + hoverTimerRef.current = null + } + + // Don't auto-collapse if pinned + if (isPinned) return + + // Set leave timer with grace period + leaveTimerRef.current = setTimeout(() => { + setCollapsed(true) + }, 800) // 800ms grace period for users to recover + }, [isPinned, setCollapsed]) + + // Handle Escape key to collapse when temporarily expanded (not pinned) + React.useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !isCollapsed && !isPinned) { + setCollapsed(true) + } + } + + document.addEventListener('keydown', handleEscape) + return () => document.removeEventListener('keydown', handleEscape) + }, [isCollapsed, isPinned, setCollapsed]) + + // Determine if sidebar should appear expanded + const isExpanded = !isCollapsed + const showOverlay = !isCollapsed && !isPinned + const dynamicNavWidth = isExpanded ? 'w-[244px]' : 'w-[80px]' return ( - - ) -}) - -const LinkToPage = React.memo(function LinkToPage({ - route, - name, - icon, - attention, - asPath -}: { - route: string - name: string - icon?: string - attention?: boolean - asPath: string -}) { - const isActive = route === asPath.split('/?')[0].split('?')[0] + + - return ( - - {icon ? : null} - - {name} - {attention ? ( - - ) : null} - - +
+
+ }> + + +
+ +
+
+ + ) }) diff --git a/src/components/Nav/Mobile/Menu.tsx b/src/components/Nav/Mobile/Menu.tsx index 6e5f91237..720a068b2 100644 --- a/src/components/Nav/Mobile/Menu.tsx +++ b/src/components/Nav/Mobile/Menu.tsx @@ -1,37 +1,41 @@ import * as React from 'react' import { Suspense, useEffect, useRef, useState } from 'react' -import { useRouter } from 'next/router' import * as Ariakit from '@ariakit/react' import { Icon } from '~/components/Icon' import { BasicLink } from '~/components/Link' import { Account } from '../Account' -import { TNavLink, TNavLinks } from '../types' +import { NavItems, NavLink } from '../NavGroup' +import { NavCollapseProvider } from '../NavCollapseContext' +import { primaryNavigation, resourcesNavigation } from '../navStructure' +import type { NavLink as NavLinkType } from '../navStructure' +import { getPinnedNavStructure, unpinRoute } from '../utils' export const Menu = React.memo(function Menu({ - mainLinks, pinnedPages, userDashboards, - footerLinks + accountAttention }: { - mainLinks: TNavLinks - pinnedPages: TNavLink[] - userDashboards: TNavLink[] - footerLinks: TNavLinks + pinnedPages: string[] + userDashboards: NavLinkType[] + accountAttention?: boolean }) { const [show, setShow] = useState(false) const buttonEl = useRef(null) const navEl = useRef(null) - const { asPath } = useRouter() + + // Convert pinned routes to grouped structure + const pinnedNavStructure = React.useMemo(() => getPinnedNavStructure(pinnedPages), [pinnedPages]) useEffect(() => { - function handleClick(e) { + function handleClick(e: MouseEvent) { + const target = e.target as HTMLElement if ( !( buttonEl.current && navEl.current && - (buttonEl.current.contains(e.target) || - navEl.current.isSameNode(e.target) || - 'togglemenuoff' in e.target.dataset) + (buttonEl.current.contains(target) || + navEl.current.contains(target) || + target.dataset?.togglemenuoff) ) ) { setShow(false) @@ -55,60 +59,105 @@ export const Menu = React.memo(function Menu({ >