Skip to content
457 changes: 319 additions & 138 deletions frontend/app/account/[address]/AmountChart.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/app/account/[address]/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const Navbar: React.FC<NavbarProps> = ({
]

return (
<nav className="bg-theme-secondary border-b border-theme text-theme flex justify-between items-center p-3 lg:p-4 transition-colors duration-300">
<nav className="fixed top-0 left-0 right-0 z-40 bg-theme-secondary border-b border-theme text-theme flex justify-between items-center p-3 lg:p-4 transition-colors duration-300">
<div className="flex items-center gap-3">
{/* Mobile menu button */}
{isMobile && (
Expand Down
255 changes: 155 additions & 100 deletions frontend/app/account/[address]/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Image from 'next/image'
import SidebarProfile from './Profile'
import { NavItem } from './navigation'
import Link from 'next/link'
import { X } from 'lucide-react'
import { X, Pin, PinOff } from 'lucide-react'
import { useSpherreAccount } from '../../context/account-context'
import { sliceWalletAddress } from '@/components/utils'

Expand All @@ -15,54 +15,79 @@ interface SidebarProps {
navItems: NavItem[]
selectedPage: string
isMobile: boolean
isUltraWide: boolean
sidebarExpanded: boolean
setSidebarExpanded: (expanded: boolean) => void
desktopSidebarExpanded: boolean
setDesktopSidebarExpanded: (expanded: boolean) => void
}

const Sidebar = ({
accountName,
navItems,
selectedPage,
isMobile,
isUltraWide,
sidebarExpanded,
setSidebarExpanded,
setDesktopSidebarExpanded,
}: SidebarProps) => {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

// State to track sidebar expansion
const [expanded, setExpanded] = useState(false) // Start with false for SSR

// State to track if sidebar is pinned (fixed) or hover mode
const [isPinned, setIsPinned] = useState(false)

const { accountAddress } = useSpherreAccount()

// Load saved preference after mount
// Load saved preferences after mount
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const saved = localStorage.getItem('sidebarExpanded')
if (saved !== null) {
setExpanded(JSON.parse(saved))
if (typeof window !== 'undefined') {
const savedExpanded = localStorage.getItem('sidebarExpanded')
if (savedExpanded !== null) {
setExpanded(JSON.parse(savedExpanded))
}

const savedPinned = localStorage.getItem('sidebarPinned')
if (savedPinned !== null) {
setIsPinned(JSON.parse(savedPinned))
}
}
}, [mounted])
}, [])

// Store expanded state in localStorage when it changes
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
if (typeof window !== 'undefined') {
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded))
}
}, [expanded, mounted])
}, [expanded])

// Store pinned state in localStorage when it changes
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('sidebarPinned', JSON.stringify(isPinned))
}
}, [isPinned])

// Update parent state when sidebar expansion changes (hover or pin)
useEffect(() => {
if (!isMobile && !isUltraWide) {
const shouldExpand = isPinned || expanded
setDesktopSidebarExpanded(shouldExpand)
}
}, [isPinned, expanded, isMobile, isUltraWide, setDesktopSidebarExpanded])

// References for staggered animations
const itemsRef = useRef<(HTMLLIElement | null)[]>([])

// Reset expanded state when clicking outside
// Reset expanded state when clicking outside (not on ultra-wide or when pinned)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const sidebar = document.getElementById('sidebar')
if (sidebar && !sidebar.contains(event.target as Node)) {
if (isMobile) {
setSidebarExpanded(false)
} else {
} else if (!isUltraWide && !isPinned) {
setExpanded(false)
}
}
Expand All @@ -72,7 +97,7 @@ const Sidebar = ({
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isMobile, setSidebarExpanded])
}, [isMobile, isUltraWide, isPinned, setSidebarExpanded])

// Set animation delays for staggered menu reveal
useEffect(() => {
Expand All @@ -83,13 +108,13 @@ const Sidebar = ({
})
}, [expanded])

// Handle keyboard navigation
// Handle keyboard navigation (not on ultra-wide or when pinned)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (isMobile) {
setSidebarExpanded(false)
} else {
} else if (!isUltraWide && !isPinned) {
setExpanded(false)
}
}
Expand All @@ -99,7 +124,7 @@ const Sidebar = ({
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [isMobile, setSidebarExpanded])
}, [isMobile, isUltraWide, isPinned, setSidebarExpanded])

// Tooltip component for collapsed state
const Tooltip = ({
Expand All @@ -115,16 +140,22 @@ const Sidebar = ({
</div>
)

const isExpanded = isMobile ? sidebarExpanded : expanded

if (!mounted) return null
// Determine if sidebar should be expanded:
// - Ultra-wide: always expanded
// - Mobile: use sidebarExpanded state
// - Desktop: if pinned, always expanded; otherwise use hover-based expanded state
const isExpanded = isUltraWide
? true
: isMobile
? sidebarExpanded
: isPinned || expanded

return (
<>
{/* Mobile overlay */}
{isMobile && isExpanded && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden"
className="fixed top-16 left-0 right-0 bottom-0 bg-black bg-opacity-50 z-25 lg:hidden"
onClick={() => setSidebarExpanded(false)}
/>
)}
Expand All @@ -133,15 +164,19 @@ const Sidebar = ({
id="sidebar"
className={`${
isMobile
? `fixed top-0 left-0 h-screen w-64 transform transition-transform duration-300 z-30 ${
? `fixed top-16 left-0 h-[calc(100vh-4rem)] w-64 transform transition-transform duration-300 z-30 ${
isExpanded ? 'translate-x-0' : '-translate-x-full'
}`
: `flex-shrink-0 transition-all duration-300 ${isExpanded ? 'w-64' : 'w-16'}`
} sidebar-bg text-theme border-r border-theme sidebar-transition`}
onMouseEnter={() => !isMobile && setExpanded(true)}
onMouseLeave={() => !isMobile && setExpanded(false)}
: `fixed top-16 left-0 h-[calc(100vh-4rem)] flex-shrink-0 transition-all duration-300 z-20 ${isExpanded ? 'w-64' : 'w-16'}`
} sidebar-bg text-theme border-r border-theme-border sidebar-transition overflow-x-hidden`}
onMouseEnter={() =>
!isMobile && !isUltraWide && !isPinned && setExpanded(true)
}
onMouseLeave={() =>
!isMobile && !isUltraWide && !isPinned && setExpanded(false)
}
>
<div className="p-4 h-full flex flex-col">
<div className="h-full flex flex-col">
{/* Mobile close button */}
{isMobile && (
<div className="flex justify-end mb-4">
Expand All @@ -154,90 +189,53 @@ const Sidebar = ({
</div>
)}

{/* Logo */}
{/* Logo - Simple and Always Visible */}
<div
className={`flex items-center sidebar-transition ${
mounted && isExpanded ? 'gap-4 mb-14' : 'justify-center mb-14'
className={`flex items-center justify-center flex-shrink-0 ${
isExpanded ? 'py-6 px-4' : 'py-4 px-2'
}`}
>
<Image
src={logo}
alt="logo"
width={mounted && isExpanded ? 24 : 40}
height={mounted && isExpanded ? 24 : 40}
className="sidebar-transition"
alt="Spherre Logo"
width={40}
height={40}
priority
className={`transition-all duration-300 ${
isExpanded ? 'w-10 h-10' : 'w-8 h-8'
}`}
/>
<div
className={`overflow-hidden transition-all ${mounted && isExpanded ? 'w-auto opacity-100' : 'w-0 opacity-0'}`}
>
<h2 className="text-[24px] font-semibold whitespace-nowrap text-theme">
Spherre
</h2>
</div>
</div>

{/* Menu Items */}
<ul className="flex flex-col gap-5 text-[16px] flex-1 overflow-y-auto">
{navItems.map((item, index) => (
<li
key={item.name}
ref={(el) => {
itemsRef.current[index] = el
}}
className="staggered-item menu-item-animation"
>
{mounted && isExpanded ? (
<Link
href={item?.route ?? `/account/${accountAddress}/`}
className={`flex items-center p-3 rounded-lg sidebar-transition sidebar-menu-item ${
selectedPage === item.name
? 'active'
: 'text-theme-secondary hover:text-theme'
}`}
onClick={() => isMobile && setSidebarExpanded(false)}
>
<div className="relative flex items-center justify-center w-6 h-6 mr-3">
<Image
src={item.icon}
alt={item.name}
width={24}
height={24}
className="sidebar-transition"
/>
{item.notification && (
<span className="absolute -top-1 -right-1 text-xs bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
{item.notification}
</span>
)}
</div>
<span className="truncate">{item.name}</span>
{item.comingSoon && (
<span className="text-[10px] text-green-400 border-[0.5px] bg-green-400/10 border-green-400/40 px-2 py-[0.5px] rounded-xl ml-auto flex-shrink-0">
Coming soon
</span>
)}
</Link>
) : (
<Tooltip content={item.name}>
{/* Menu Items - Fill remaining space between logo and profile */}
<div className="flex-1 min-h-0 flex flex-col">
<ul
className={`flex flex-col ${isExpanded ? 'gap-5' : 'gap-6 mt-3'} text-[16px] flex-1 overflow-y-auto overflow-x-hidden`}
>
{navItems.map((item, index) => (
<li
key={item.name}
ref={(el) => {
itemsRef.current[index] = el
}}
className="staggered-item menu-item-animation"
>
{isExpanded ? (
<Link
href={item?.route ?? `/${accountAddress}/`}
className={`flex items-center justify-center p-3 rounded-lg sidebar-transition sidebar-menu-item ${
href={item?.route ?? `/account/${accountAddress}/`}
className={`flex items-center p-3 rounded-lg mx-2 sidebar-transition sidebar-menu-item ${
selectedPage === item.name
? 'active'
: 'text-theme-secondary hover:text-theme'
}`}
style={{
width: '40px',
height: '40px',
}}
onClick={() => isMobile && setSidebarExpanded(false)}
>
<div className="relative flex items-center justify-center">
<div className="relative flex items-center justify-center w-6 h-6 mr-3">
<Image
src={item.icon}
alt={item.name}
width={30}
height={30}
width={24}
height={24}
className="sidebar-transition"
/>
{item.notification && (
Expand All @@ -246,16 +244,73 @@ const Sidebar = ({
</span>
)}
</div>
<span className="truncate">{item.name}</span>
{item.comingSoon && (
<span className="text-[10px] text-green-400 border-[0.5px] bg-green-400/10 border-green-400/40 px-2 py-[0.5px] rounded-xl ml-auto flex-shrink-0">
Coming soon
</span>
)}
</Link>
</Tooltip>
)}
</li>
))}
</ul>
) : (
<Tooltip content={item.name}>
<Link
href={item?.route ?? `/${accountAddress}/`}
className={`flex items-center justify-center mx-auto rounded-lg sidebar-transition sidebar-menu-item ${
selectedPage === item.name
? 'active'
: 'text-theme-secondary hover:text-theme'
}`}
style={{
width: '32px',
height: '32px',
}}
onClick={() => isMobile && setSidebarExpanded(false)}
>
<div className="relative flex items-center justify-center">
<Image
src={item.icon}
alt={item.name}
width={20}
height={20}
className="sidebar-transition"
/>
{item.notification && (
<span className="absolute -top-1 -right-1 text-xs bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
{item.notification}
</span>
)}
</div>
</Link>
</Tooltip>
)}
</li>
))}
</ul>
</div>

{/* Pin/Unpin Toggle Button - Only visible on desktop (not mobile or ultra-wide) */}
{!isMobile && !isUltraWide && (
<div
className={`mb-40 ${isExpanded ? 'px-3' : 'flex justify-center'}`}
>
<button
onClick={() => setIsPinned(!isPinned)}
className={`p-2 rounded-lg transition-colors duration-200 hover:bg-theme-tertiary ${
isPinned
? 'bg-primary/10 text-primary'
: 'text-theme-secondary'
}`}
title={isPinned ? 'Unpin sidebar' : 'Pin sidebar'}
aria-label={isPinned ? 'Unpin sidebar' : 'Pin sidebar'}
>
{isPinned ? <Pin size={18} /> : <PinOff size={18} />}
</button>
</div>
)}

{/* Profile Section with smooth transition */}
<div
className={`profile-section mt-auto ${isExpanded ? 'h-auto opacity-100' : 'h-0 opacity-0'}`}
className={`profile-section flex-shrink-0 ${isExpanded ? 'h-auto opacity-100' : 'h-0 opacity-0'}`}
>
{isExpanded && (
<SidebarProfile
Expand Down
Loading