Files
web/src/app/admin/clients.tsx

274 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import {ReactNode, Suspense, use, useState} from 'react'
import Image from 'next/image'
import Link from 'next/link'
import {ChangePasswordDialog} from '@/components/composites/dialogs/change-password-dialog'
import {RealnameAuthDialog} from '@/components/composites/dialogs/realname-auth-dialog'
import UserCenter from '@/components/composites/user-center'
import {Button} from '@/components/ui/button'
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'
import {Archive, ArchiveRestore, Eye, HardDriveUpload, IdCard, LockKeyhole, Package, PanelLeftCloseIcon, PanelLeftOpenIcon, ShoppingCart, UserRound, UserRoundPen, Wallet} from 'lucide-react'
import {merge} from '@/lib/utils'
import logoAvatar from '@/assets/logo-avatar.svg'
import logoText from '@/assets/logo-text.svg'
import {useLayoutStore} from '@/components/stores/layout'
import {useProfileStore} from '@/components/stores/profile'
export function Shell(props: {
children: ReactNode
}) {
const navbar = useLayoutStore(store => store.navbar)
return (
<div
data-expand={navbar}
className={merge(
`transition-[grid-template-columns] duration-300 ease-in-out`,
`w-full h-full grid`,
`grid-rows-[64px_1fr]`,
`data-[expand=true]:grid-cols-[200px_1fr]`,
`data-[expand=false]:grid-cols-[0px_1fr]`,
`md:data-[expand=false]:grid-cols-[64px_1fr]`,
)}
>
{props.children}
</div>
)
}
export function Mask() {
const navbar = useLayoutStore(store => store.navbar)
const setNevBar = useLayoutStore(store => store.setNavbar)
return (
<div
data-expand={navbar}
className={merge(
`lg:hidden`,
`transition-opacity duration-300 ease-in-out`,
`col-start-1 row-start-1 col-span-2 row-span-2 bg-black/50 z-10`,
`data-[expand=true]:opacity-100 data-[expand=false]:opacity-0`,
`data-[expand=true]:pointer-events-auto data-[expand=false]:pointer-events-none`,
)}
onClick={() => setNevBar(false)}
>
</div>
)
}
export function Content(props: {children: ReactNode}) {
const navbar = useLayoutStore(store => store.navbar)
return (
<div
data-expand={navbar}
className={merge(
`transition-[margin] duration-300 ease-in-out`,
`absolute inset-0 overflow-hidden`,
`mt-16`,
`md:ml-16`,
`lg:data-[expand=true]:ml-[200px]`,
)}>
{props.children}
<Suspense>
<ContentResolved/>
</Suspense>
</div>
)
}
function ContentResolved() {
const profile = use(useProfileStore(store => store.profile))
if (profile)
return (
<>
<RealnameAuthDialog
triggerClassName="hidden"
defaultOpen={!profile.id_token}
/>
<ChangePasswordDialog
triggerClassName="hidden"
defaultOpen={!profile.has_password}
/>
</>
)
}
export function Header() {
const navbar = useLayoutStore(store => store.navbar)
const toggleNavbar = useLayoutStore(store => store.toggleNavbar)
return (
<header className={merge(
`flex-none h-16 overflow-hidden`,
`flex items-stretch`,
)}>
{/* left */}
<div className="flex-auto flex items-center gap-2">
<Button
theme="ghost"
className="w-9 h-9 ml-4 md:ml-0"
onClick={toggleNavbar}>
{navbar
? <PanelLeftCloseIcon/>
: <PanelLeftOpenIcon/>
}
</Button>
<span className="max-md:hidden">
</span>
</div>
{/* right */}
<div className="flex-none flex items-center justify-end pr-4 max-md:hidden gap-2">
<Link
href="/"
className={merge(
`flex-none h-16 flex items-center justify-center`,
)}>
</Link>
<Suspense>
<HeaderUserCenter/>
</Suspense>
</div>
</header>
)
}
function HeaderUserCenter() {
const profile = use(useProfileStore(store => store.profile))
if (profile) return <UserCenter profile={profile}/>
}
export function Navbar() {
const navbar = useLayoutStore(store => store.navbar)
return (
<nav
data-expand={navbar}
className={merge(
`transition-[flex-basis] duration-300 ease-in-out`,
`h-full flex flex-col overflow-hidden group`,
`data-[expand=true]:basis-52 data-[expand=false]:basis-16`,
)}>
{/* logo */}
<Link
href="/"
className={merge(
`flex-none h-16 flex items-center justify-center`,
)}>
<Image src={logoAvatar} alt="logo" className="w-10 h-10 object-contain"/>
<Image
src={logoText}
alt="logo"
className={merge(
`h-10 translate-1 object-cover object-left`,
`transition-[opacity,width] duration-[200ms,300ms] ease-in-out`,
`group-data-[expand=true]:delay-[100ms,0ms]`,
`group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
`group-data-[expand=true]:w-[85px] group-data-[expand=false]:w-0`,
)}/>
</Link>
{/* routes */}
<section className={merge(
`transition-[padding] duration-300 ease-in-out`,
`flex-auto overflow-x-hidden overflow-y-auto pb-4`,
`group-data-[expand=true]:px-4 group-data-[expand=false]:px-3`,
)}>
<TooltipProvider>
<NavItem href="/admin" icon={<UserRound size={20}/>} label="账户总览" expand={navbar}/>
<NavTitle label="快速开始"/>
<NavItem href="/admin/identify" icon={<IdCard size={20}/>} label="实名认证" expand={navbar}/>
<NavItem href="/admin/whitelist" icon={<LockKeyhole size={20}/>} label="白名单" expand={navbar}/>
<NavItem href="/admin/purchase" icon={<ShoppingCart size={20}/>} label="购买套餐" expand={navbar}/>
<NavItem href="/admin/extract" icon={<HardDriveUpload size={20}/>} label="提取 IP" expand={navbar}/>
<NavTitle label="个人中心"/>
<NavItem href="/admin/profile" icon={<UserRoundPen size={20}/>} label="基本信息" expand={navbar}/>
<NavItem href="/admin/bills" icon={<Wallet size={20}/>} label="我的账单" expand={navbar}/>
<NavTitle label="资源管理"/>
<NavItem href="/admin/resources" icon={<Package size={20}/>} label="我的套餐" expand={navbar}/>
<NavItem href="/admin/channels" icon={<Eye size={20}/>} label="IP 管理" expand={navbar}/>
<NavItem href="/admin/record" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
{/* <NavTitle label="数据统计"/>
<NavItem href="/admin" icon={<ArchiveRestore size={20}/>} label="使用记录" expand={navbar}/> */}
</TooltipProvider>
</section>
</nav>
)
}
function NavTitle(props: {
label: string
}) {
return (
<p className={merge(
`transition-[height] duration-300 ease-in-out`,
`text-sm text-gray-500 whitespace-nowrap flex items-center relative`,
`group-data-[expand=true]:h-9`,
`group-data-[expand=false]:h-4`,
)}>
<span className={merge(
`transition-opacity duration-200 ease-in-out absolute mx-4`,
`group-data-[expand=true]:delay-100 group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
)}>
{props.label}
</span>
<span className={merge(
`transition-opacity duration-200 ease-in-out absolute w-full border-b block`,
`group-data-[expand=false]:delay-100 group-data-[expand=false]:opacity-100 group-data-[expand=true]:opacity-0`,
)}>
</span>
</p>
)
}
function NavItem(props: {
href: string
icon?: ReactNode
label: string
expand?: boolean
}) {
const [open, setOpen] = useState(false)
const handleOpenChange = (open: boolean) => {
if (!props.expand) {
setOpen(open)
}
}
const setNavbar = useLayoutStore(store => store.setNavbar)
const closeNavBarIfMobile = () => {
if (window.innerWidth < 768) {
setNavbar(false)
}
}
return (
<Tooltip open={open} onOpenChange={handleOpenChange}>
<TooltipTrigger asChild>
<Link
className={merge(
`transition-[padding,background] duration-300 ease-in-out`,
`flex items-center rounded-md gap-2 whitespace-nowrap`,
`hover:bg-gray-100`,
`group-data-[expand=true]:px-4`,
)}
href={props.href}
onClick={closeNavBarIfMobile}
>
<span className="flex-none w-10 h-10 flex items-center justify-center">{props.icon}</span>
<span className={merge(
`flex-auto`,
`transition-[width,opacity] duration-300 ease-in-out`,
`group-data-[expand=true]:w-auto group-data-[expand=true]:opacity-100`,
`group-data-[expand=false]:w-0 group-data-[expand=false]:opacity-0`,
)}>
{props.label}
</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={16}>
<p>{props.label}</p>
</TooltipContent>
</Tooltip>
)
}