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
5 changes: 5 additions & 0 deletions .changeset/mobile-left-nav-drawer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@audius/web": patch
---

Add left-slide navigation drawer on mobile web (tapped from top-right avatar or kebab) with account header and items for Profile, Notifications, Messages, Wallet, Fan Clubs, Rewards, Contests, Upload, and Settings — matching the native app's LeftNavDrawer.
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,7 @@ export const ProfileHeaderSkeleton = () => {
<Skeleton style={[styles.profilePicture, { top: insets.top + 48 }]} />
{/* Matches the real ProfileHeader structure so the skeleton→content
transition doesn't cause layout shift. */}
<Flex
column
pv='s'
ph='m'
backgroundColor='white'
style={{ gap: 9 }}
>
<Flex column pv='s' ph='m' backgroundColor='white' style={{ gap: 9 }}>
<Flex column pv='s' gap='s'>
<Flex row justifyContent='flex-end' gap='xs'>
<StaticSkeleton height={32} width={32} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ const StaticBottomButton = ({
justifyContent: 'center'
}}
>
<Icon
width={28}
height={28}
color={isActive ? 'active' : 'default'}
/>
<Icon width={28} height={28} color={isActive ? 'active' : 'default'} />
{badge}
</div>
)
Expand Down
74 changes: 74 additions & 0 deletions packages/web/src/components/nav/mobile/LeftNavDrawer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.drawer {
z-index: 22;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 85%;
max-width: 360px;
background: var(--harmony-white);
box-shadow:
0 8px 16px rgba(0, 0, 0, 0.08),
0 0 4px rgba(0, 0, 0, 0.04);
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
padding-left: env(safe-area-inset-left, 0px);
display: flex;
flex-direction: column;
justify-content: space-between;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
transform: translate3d(-100%, 0, 0);
transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform;
border-right: 1px solid var(--harmony-border-default);
}

.content {
display: flex;
flex-direction: column;
}

.drawerOpen {
transform: translate3d(0, 0, 0);
}

.background {
z-index: 21;
background-color: black;
position: fixed;
right: 0;
left: 0;
top: 0;
bottom: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.28s ease-in-out;
}

.backgroundOpen {
opacity: 0.5;
pointer-events: auto;
}

.navItem {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
background: none;
border: none;
text-align: left;
color: var(--harmony-text-default);
text-decoration: none;
cursor: pointer;
font-family: var(--harmony-font-family);
width: 100%;
transition: background-color 0.15s ease-in-out;
border-radius: 8px;
}

.navItem:hover,
.navItem:active {
background-color: var(--harmony-n-50);
}
260 changes: 260 additions & 0 deletions packages/web/src/components/nav/mobile/LeftNavDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { MouseEvent, ReactNode, useCallback, useContext } from 'react'

import { useCurrentAccountUser, useCurrentUserId } from '@audius/common/api'
import { formatCount, route } from '@audius/common/utils'
import {
Divider,
Flex,
IconAudiusLogoHorizontal,
IconComponent,
IconFanClub,
IconGift,
IconSettings,
IconUser,
IconWallet,
Text
} from '@audius/harmony'
import cn from 'classnames'
import { useDispatch } from 'react-redux'

import { RouterContext } from 'components/animated-switch/RouterContextProvider'
import { Avatar } from 'components/avatar/Avatar'
import UserBadges from 'components/user-badges/UserBadges'
import { usePortal } from 'hooks/usePortal'
import { push } from 'utils/navigation'

import styles from './LeftNavDrawer.module.css'

const {
CLUBS_EXPLORE_PAGE,
FOLLOWERS_USERS_ROUTE,
FOLLOWING_USERS_ROUTE,
REWARDS_PAGE,
SETTINGS_PAGE,
WALLET_PAGE,
profilePage
} = route

const messages = {
profile: 'My Profile',
audio: '$AUDIO',
artistCoins: 'Artist Coins',
rewards: 'Rewards',
settings: 'Settings',
followers: 'Followers',
following: 'Following'
}

type NavItemProps = {
icon: IconComponent
label: string
href: string
onNavigate: (href: string) => void
right?: ReactNode
}

const NavItem = ({
icon: Icon,
label,
href,
onNavigate,
right
}: NavItemProps) => {
const handleClick = useCallback(
(e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault()
onNavigate(href)
},
[href, onNavigate]
)

return (
<a href={href} onClick={handleClick} className={styles.navItem}>
<Flex alignItems='center' gap='l' flex={1}>
<Icon size='l' color='default' />
<Text variant='title' size='l' strength='weak'>
{label}
</Text>
</Flex>
{right}
</a>
)
}

type LeftNavDrawerProps = {
isOpen: boolean
onClose: () => void
}

export const LeftNavDrawer = ({ isOpen, onClose }: LeftNavDrawerProps) => {
const Portal = usePortal({})
const dispatch = useDispatch()
const { setStackReset } = useContext(RouterContext)

const { data: currentUserId } = useCurrentUserId()
const { data: accountUser } = useCurrentAccountUser({
select: (user) => ({
handle: user?.handle,
name: user?.name,
followerCount: user?.follower_count ?? 0,
followeeCount: user?.followee_count ?? 0
})
})

const {
handle,
name,
followerCount = 0,
followeeCount = 0
} = accountUser ?? {}

const handleNavigate = useCallback(
(href: string) => {
setStackReset(true)
setImmediate(() => dispatch(push(href)))
onClose()
},
[dispatch, setStackReset, onClose]
)

const goToProfile = useCallback(() => {
if (!handle) return
handleNavigate(profilePage(handle))
}, [handle, handleNavigate])

const goToFollowers = useCallback(
() => handleNavigate(FOLLOWERS_USERS_ROUTE),
[handleNavigate]
)

const goToFollowing = useCallback(
() => handleNavigate(FOLLOWING_USERS_ROUTE),
[handleNavigate]
)

return (
<Portal>
<div
className={cn(styles.background, { [styles.backgroundOpen]: isOpen })}
onClick={onClose}
aria-hidden={!isOpen}
/>
<div
role='dialog'
aria-label='Main menu'
aria-hidden={!isOpen}
className={cn(styles.drawer, { [styles.drawerOpen]: isOpen })}
>
<div className={styles.content}>
{currentUserId ? (
<Flex
direction='column'
gap='m'
ph='xl'
pt='xl'
pb='l'
alignItems='flex-start'
>
<Flex
direction='column'
gap='s'
alignItems='flex-start'
onClick={goToProfile}
css={{ cursor: 'pointer' }}
>
<Avatar userId={currentUserId} h={96} w={96} disableLink />
<Flex direction='column' gap='2xs' alignItems='flex-start'>
<Flex alignItems='center' gap='xs'>
{name ? (
<Text variant='title' size='l' strength='strong' ellipses>
{name}
</Text>
) : null}
<UserBadges userId={currentUserId} size='s' inline />
</Flex>
{handle ? (
<Text variant='body' size='l' color='default' ellipses>
@{handle}
</Text>
) : null}
</Flex>
</Flex>
<Flex alignItems='center' gap='l'>
<Flex
alignItems='center'
gap='xs'
onClick={goToFollowers}
css={{ cursor: 'pointer' }}
>
<Text variant='label' size='l' color='default'>
{formatCount(followerCount)}
</Text>
<Text variant='body' size='m' color='subdued'>
{messages.followers}
</Text>
</Flex>
<Flex
alignItems='center'
gap='xs'
onClick={goToFollowing}
css={{ cursor: 'pointer' }}
>
<Text variant='label' size='l' color='default'>
{formatCount(followeeCount)}
</Text>
<Text variant='body' size='m' color='subdued'>
{messages.following}
</Text>
</Flex>
</Flex>
</Flex>
) : null}
<Divider />
<Flex direction='column' pv='s'>
{currentUserId && handle ? (
<NavItem
icon={IconUser}
label={messages.profile}
href={profilePage(handle)}
onNavigate={handleNavigate}
/>
) : null}
<NavItem
icon={IconWallet}
label={messages.audio}
href={WALLET_PAGE}
onNavigate={handleNavigate}
/>
<NavItem
icon={IconFanClub}
label={messages.artistCoins}
href={CLUBS_EXPLORE_PAGE}
onNavigate={handleNavigate}
/>
<NavItem
icon={IconGift}
label={messages.rewards}
href={REWARDS_PAGE}
onNavigate={handleNavigate}
/>
<NavItem
icon={IconSettings}
label={messages.settings}
href={SETTINGS_PAGE}
onNavigate={handleNavigate}
/>
</Flex>
</div>
<Flex
direction='column'
alignItems='flex-start'
justifyContent='center'
ph='xl'
pv='xl'
>
<IconAudiusLogoHorizontal color='subdued' sizeH='l' width='auto' />
</Flex>
</div>
</Portal>
)
}
Loading
Loading