feat: improve mobile experience

This commit is contained in:
codytseng 2025-01-02 21:57:14 +08:00
parent 8ec0d46d58
commit 3946e603b3
98 changed files with 2508 additions and 1058 deletions

View file

@ -0,0 +1,89 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useFetchProfile } from '@/hooks'
import { toProfile, toSettings } from '@/lib/link'
import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { LogIn } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import LoginDialog from '../LoginDialog'
import LogoutDialog from '../LogoutDialog'
import SidebarItem from './SidebarItem'
export default function AccountButton() {
const { pubkey } = useNostr()
if (pubkey) {
return <ProfileButton />
} else {
return <LoginButton />
}
}
function ProfileButton() {
const { t } = useTranslation()
const { account } = useNostr()
const pubkey = account?.pubkey
const { profile } = useFetchProfile(pubkey)
const { push } = useSecondaryPage()
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
if (!pubkey) return null
const defaultAvatar = generateImageByPubkey(pubkey)
const { username, avatar } = profile || { username: formatPubkey(pubkey), avatar: defaultAvatar }
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="clickable shadow-none p-2 xl:px-2 xl:py-2 w-12 h-12 xl:w-full xl:h-auto flex items-center bg-transparent text-foreground hover:text-accent-foreground rounded-lg justify-start gap-4 text-lg font-semibold"
>
<div className="flex gap-2 items-center flex-1 w-0">
<Avatar className="w-8 h-8">
<AvatarImage src={avatar} />
<AvatarFallback>
<img src={defaultAvatar} />
</AvatarFallback>
</Avatar>
<div className="truncate font-semibold text-lg">{username}</div>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>{t('Profile')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => push(toSettings())}>{t('Settings')}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLoginDialogOpen(true)}>
{t('Switch account')}
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setLogoutDialogOpen(true)}
>
{t('Logout')}
</DropdownMenuItem>
</DropdownMenuContent>
<LoginDialog open={loginDialogOpen} setOpen={setLoginDialogOpen} />
<LogoutDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen} />
</DropdownMenu>
)
}
function LoginButton() {
const { checkLogin } = useNostr()
return (
<SidebarItem onClick={() => checkLogin()} title="Login">
<LogIn strokeWidth={3} />
</SidebarItem>
)
}

View file

@ -0,0 +1,13 @@
import { usePrimaryPage } from '@/PageManager'
import { Home } from 'lucide-react'
import SidebarItem from './SidebarItem'
export default function HomeButton() {
const { navigate, current } = usePrimaryPage()
return (
<SidebarItem title="Home" onClick={() => navigate('home')} active={current === 'home'}>
<Home strokeWidth={3} />
</SidebarItem>
)
}

View file

@ -0,0 +1,17 @@
import { usePrimaryPage } from '@/PageManager'
import { Bell } from 'lucide-react'
import SidebarItem from './SidebarItem'
export default function NotificationsButton() {
const { navigate, current } = usePrimaryPage()
return (
<SidebarItem
title="Notifications"
onClick={() => navigate('notifications')}
active={current === 'notifications'}
>
<Bell strokeWidth={3} />
</SidebarItem>
)
}

View file

@ -0,0 +1,24 @@
import PostEditor from '@/components/PostEditor'
import { PencilLine } from 'lucide-react'
import { useState } from 'react'
import SidebarItem from './SidebarItem'
export default function PostButton() {
const [open, setOpen] = useState(false)
return (
<>
<SidebarItem
title="New post"
description="Post"
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
>
<PencilLine strokeWidth={3} />
</SidebarItem>
<PostEditor open={open} setOpen={setOpen} />
</>
)
}

View file

@ -0,0 +1,24 @@
import { Search } from 'lucide-react'
import { useState } from 'react'
import { SearchDialog } from '../SearchDialog'
import SidebarItem from './SidebarItem'
export default function SearchButton() {
const [open, setOpen] = useState(false)
return (
<>
<SidebarItem
title="Search"
description="Search"
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
>
<Search strokeWidth={3} />
</SidebarItem>
<SearchDialog open={open} setOpen={setOpen} />
</>
)
}

View file

@ -0,0 +1,31 @@
import { Button, ButtonProps } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
const SidebarItem = forwardRef<
HTMLButtonElement,
ButtonProps & { title: string; description?: string; active?: boolean }
>(({ children, title, description, className, active, ...props }, ref) => {
const { t } = useTranslation()
return (
<Button
className={cn(
'flex shadow-none items-center bg-transparent w-12 h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-4 rounded-lg xl:justify-start gap-4 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4',
active && 'text-primary disabled:opacity-100',
className
)}
disabled={active}
variant="ghost"
title={t(title)}
ref={ref}
{...props}
>
{children}
<div className="max-xl:hidden">{t(description ?? title)}</div>
</Button>
)
})
SidebarItem.displayName = 'SidebarItem'
export default SidebarItem

View file

@ -1,35 +1,25 @@
import Icon from '@/assets/Icon'
import Logo from '@/assets/Logo'
import { Button } from '@/components/ui/button'
import { Info } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import AboutInfoDialog from '../AboutInfoDialog'
import AccountButton from '../AccountButton'
import NotificationButton from '../NotificationButton'
import PostButton from '../PostButton'
import RelaySettingsButton from '../RelaySettingsButton'
import SearchButton from '../SearchButton'
import AccountButton from './AccountButton'
import HomeButton from './HomeButton'
import NotificationsButton from './NotificationButton'
import PostButton from './PostButton'
import SearchButton from './SearchButton'
export default function PrimaryPageSidebar() {
const { t } = useTranslation()
return (
<div className="w-52 h-full shrink-0 hidden xl:flex flex-col pb-8 pt-10 pl-4 justify-between relative">
<div className="absolute top-0 left-0 h-11 w-full" />
<div className="w-16 xl:w-52 hidden sm:flex flex-col pb-2 pt-4 px-2 justify-between h-full shrink-0">
<div className="space-y-2">
<div className="ml-4 mb-8 w-40">
<Logo />
<div className="px-2 mb-10 w-full">
<Icon className="xl:hidden" />
<Logo className="max-xl:hidden" />
</div>
<PostButton variant="sidebar" />
<RelaySettingsButton variant="sidebar" />
<NotificationButton variant="sidebar" />
<SearchButton variant="sidebar" />
<AboutInfoDialog>
<Button variant="sidebar" size="sidebar">
<Info />
{t('About')}
</Button>
</AboutInfoDialog>
<HomeButton />
<NotificationsButton />
<SearchButton />
<PostButton />
</div>
<AccountButton variant="sidebar" />
<AccountButton />
</div>
)
}