封装 Header 和 Navbar 组件,调整用户界面
This commit is contained in:
17
README.md
17
README.md
@@ -1,14 +1,19 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
引入 redis,客户端密钥使用 redis 保存
|
|
||||||
|
|
||||||
使用 pure js 的包代替 canvas,加快编译速度
|
使用 pure js 的包代替 canvas,加快编译速度
|
||||||
|
|
||||||
提取后刷新提取页套餐可用余量
|
提取后刷新提取页套餐可用余量
|
||||||
|
|
||||||
保存客户端信息时用 jwt 序列化
|
白名单复用表格组件
|
||||||
|
|
||||||
---
|
首次登录弹窗:需要设置初始密码,展示 实名->购买->提取 的流程介绍
|
||||||
|
|
||||||
|
提取页表单性能优化,树组件性能优化
|
||||||
|
|
||||||
|
后台页面:
|
||||||
|
- [ ] 总览
|
||||||
|
- [ ] 个人中心
|
||||||
|
- [ ] IP 管理
|
||||||
|
- [ ] 提取记录
|
||||||
|
- [ ] 使用记录
|
||||||
|
|
||||||
页面数据:
|
|
||||||
- [ ] dashboard
|
|
||||||
|
|||||||
BIN
src/app/admin/_assets/logo-mini.webp
Normal file
BIN
src/app/admin/_assets/logo-mini.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
42
src/app/admin/_client/header.tsx
Normal file
42
src/app/admin/_client/header.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
'use client'
|
||||||
|
import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import Profile from './profile'
|
||||||
|
import {useLayoutStore} from '@/components/providers/StoreProvider'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
|
export type HeaderProps = {}
|
||||||
|
|
||||||
|
export default function Header(props: HeaderProps) {
|
||||||
|
|
||||||
|
const navbar = useLayoutStore(store => store.navbar)
|
||||||
|
const toggleNavbar = useLayoutStore(store => store.toggleNavbar)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={merge(
|
||||||
|
`flex-none h-16`,
|
||||||
|
`flex items-stretch`,
|
||||||
|
)}>
|
||||||
|
{/* left */}
|
||||||
|
<div className={`flex-auto flex items-center gap-2`}>
|
||||||
|
<Button
|
||||||
|
theme="ghost"
|
||||||
|
className="w-9 h-9"
|
||||||
|
onClick={toggleNavbar}>
|
||||||
|
{navbar
|
||||||
|
? <PanelLeftCloseIcon/>
|
||||||
|
: <PanelLeftOpenIcon/>
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
<span>
|
||||||
|
欢迎来到,蓝狐代理
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* right */}
|
||||||
|
<div className={`flex-none flex items-center justify-end pr-4`}>
|
||||||
|
<Profile/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
108
src/app/admin/_client/navbar.tsx
Normal file
108
src/app/admin/_client/navbar.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'use client'
|
||||||
|
import {ReactNode} from 'react'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
import {useLayoutStore} from '@/components/providers/StoreProvider'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import logo from '@/assets/logo.webp'
|
||||||
|
import logoMini from '../_assets/logo-mini.webp'
|
||||||
|
|
||||||
|
export type NavbarProps = {}
|
||||||
|
|
||||||
|
export default function Navbar(props: NavbarProps) {
|
||||||
|
|
||||||
|
const navbar = useLayoutStore(store => store.navbar)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav data-expand={navbar} className={merge(
|
||||||
|
`transition-[flex-basis] duration-200 ease-in-out`,
|
||||||
|
`flex flex-col overflow-hidden group`,
|
||||||
|
`flex-none ${navbar ? `expand basis-52` : `noexpand basis-16`}`,
|
||||||
|
)}>
|
||||||
|
{/* logo */}
|
||||||
|
<Logo mini={!navbar}/>
|
||||||
|
|
||||||
|
{/* routes */}
|
||||||
|
<section className={merge(
|
||||||
|
`transition-[padding] duration-200 ease-in-out`,
|
||||||
|
`flex-auto overflow-auto ${navbar ? `px-4` : `px-3`}`,
|
||||||
|
)}>
|
||||||
|
<NavItem href={'/admin'} icon={`🏠`} label={`账户总览`} expand={navbar}/>
|
||||||
|
<NavTitle label={`个人信息`}/>
|
||||||
|
<NavItem href={`/admin/profile`} icon={`📝`} label={`个人中心`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin/identify`} icon={`🆔`} label={`实名认证`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin/whitelist`} icon={`🔒`} label={`白名单`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin/bills`} icon={`💰`} label={`我的账单`} expand={navbar}/>
|
||||||
|
<NavTitle label={`套餐管理`}/>
|
||||||
|
<NavItem href={`/admin/purchase`} icon={`🛒`} label={`购买套餐`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin/resources`} icon={`📦`} label={`套餐管理`} expand={navbar}/>
|
||||||
|
<NavTitle label={`IP 管理`}/>
|
||||||
|
<NavItem href={`/admin/extract`} icon={`📤`} label={`提取 IP`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin`} icon={`👁️`} label={`IP 管理`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin`} icon={`📜`} label={`提取记录`} expand={navbar}/>
|
||||||
|
<NavItem href={`/admin`} icon={`🗂️`} label={`使用记录`} expand={navbar}/>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Logo(props: {
|
||||||
|
mini?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`flex-none h-[64px] flex items-center justify-center`}>
|
||||||
|
<Link href={'/'} className="block">
|
||||||
|
{props.mini
|
||||||
|
? <Image src={logoMini} alt={`logo`} className="h-9 object-contain"/>
|
||||||
|
: <Image src={logo} alt={`logo`} className="h-9 object-contain"/>
|
||||||
|
}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavTitle(props: {
|
||||||
|
label: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<p className={merge(
|
||||||
|
`transition-[height] duration-200 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-150 ease-in-out absolute mx-4`,
|
||||||
|
`group-data-[expand=true]:delay-[50ms] group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
|
||||||
|
)}>{props.label}</span>
|
||||||
|
<div className={merge(
|
||||||
|
`transition-[opacity] duration-150 ease-in-out absolute w-full border-b`,
|
||||||
|
`group-data-[expand=false]:delay-[50ms] group-data-[expand=false]:opacity-100 group-data-[expand=true]:opacity-0`,
|
||||||
|
)}></div>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavItem(props: {
|
||||||
|
href: string
|
||||||
|
icon?: ReactNode
|
||||||
|
label: string
|
||||||
|
expand: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link className={merge(
|
||||||
|
`transition-[padding] duration-200 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}>
|
||||||
|
<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-200 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page mode={`blank`}>
|
<Page>
|
||||||
<div className={`flex-3/4 flex flex-col bg-white rounded-lg overflow-hidden gap-16`}>
|
<div className={`flex-3/4 flex flex-col bg-white rounded-lg overflow-hidden gap-16`}>
|
||||||
|
|
||||||
{/* banner */}
|
{/* banner */}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import {ReactNode} from 'react'
|
import {ReactNode} from 'react'
|
||||||
import Image from 'next/image'
|
|
||||||
import logo from '@/assets/logo.webp'
|
|
||||||
import Profile from '@/app/admin/_server/profile'
|
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import Link from 'next/link'
|
|
||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
import {getProfile} from '@/actions/auth/auth'
|
import {getProfile} from '@/actions/auth/auth'
|
||||||
|
import Header from './_client/header'
|
||||||
|
import Navbar from '@/app/admin/_client/navbar'
|
||||||
|
|
||||||
export type DashboardLayoutProps = {
|
export type DashboardLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -13,88 +11,31 @@ export type DashboardLayoutProps = {
|
|||||||
|
|
||||||
export default async function DashboardLayout(props: DashboardLayoutProps) {
|
export default async function DashboardLayout(props: DashboardLayoutProps) {
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// profile
|
||||||
|
// ======================
|
||||||
|
|
||||||
const user = await getProfile()
|
const user = await getProfile()
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect(`/login?redirect=${encodeURIComponent('/admin')}`)
|
return redirect(`/login?redirect=${encodeURIComponent('/admin')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// render
|
||||||
|
// ======================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen flex flex-col overflow-hidden min-w-7xl overflow-y-hidden relative`}>
|
<div className={merge(
|
||||||
{/* background */}
|
`h-screen bg-card overflow-hidden min-w-7xl overflow-y-hidden`,
|
||||||
<div className={`-z-10 absolute inset-0 overflow-hidden`}>
|
`flex items-stretch`,
|
||||||
<div className={`absolute w-screen h-screen bg-gray-50`}></div>
|
)}>
|
||||||
<div className={`absolute w-[2000px] h-[2000px] -left-[1000px] -top-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
|
||||||
<div className={`absolute w-[2000px] h-[2000px] -right-[1000px] -top-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
|
||||||
<div className={`absolute w-[2000px] h-[2000px] left-[calc(50%-1000px)] -bottom-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* content */}
|
<Navbar/>
|
||||||
<header className={`flex-none basis-20 flex items-stretch`}>
|
|
||||||
{/* logo */}
|
|
||||||
<div className={`flex-none basis-60 flex items-center justify-center`}>
|
|
||||||
<Link href={'/'}><Image src={logo} alt={`logo`} height={40}/></Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* title */}
|
|
||||||
<div className={`flex-auto overflow-hidden flex items-center pl-4`}>
|
|
||||||
欢迎来到,蓝狐代理
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* profile */}
|
|
||||||
<div className={`flex-none basis-80 flex items-center justify-end pr-4`}>
|
|
||||||
<Profile/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className={`flex-auto overflow-hidden flex items-stretch gap-4`}>
|
|
||||||
<nav className={merge(
|
|
||||||
`flex-none basis-60 rounded-tr-xl bg-white p-4`,
|
|
||||||
`flex flex-col overflow-auto`,
|
|
||||||
)}>
|
|
||||||
<NavItem href={'/admin'} icon={`🏠`} label={`账户总览`}/>
|
|
||||||
<NavTitle label={`个人信息`}/>
|
|
||||||
<NavItem href={`/admin/profile`} icon={`📝`} label={`个人中心`}/>
|
|
||||||
<NavItem href={`/admin/identify`} icon={`🆔`} label={`实名认证`}/>
|
|
||||||
<NavItem href={`/admin/whitelist`} icon={`🔒`} label={`白名单`}/>
|
|
||||||
<NavItem href={`/admin/bills`} icon={`💰`} label={`我的账单`}/>
|
|
||||||
<NavTitle label={`套餐管理`}/>
|
|
||||||
<NavItem href={`/admin/purchase`} icon={`🛒`} label={`购买套餐`}/>
|
|
||||||
<NavItem href={`/admin/resources`} icon={`📦`} label={`我的套餐`}/>
|
|
||||||
<NavTitle label={`IP 管理`}/>
|
|
||||||
<NavItem href={`/admin/extract`} icon={`📤`} label={`IP提取`}/>
|
|
||||||
<NavItem href={`/admin`} icon={`📜`} label={`IP提取记录`}/>
|
|
||||||
<NavItem href={`/admin`} icon={`👁️`} label={`查看提取IP`}/>
|
|
||||||
<NavItem href={`/admin`} icon={`🗂️`} label={`查看使用IP`}/>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
<div className={`flex-auto flex flex-col items-stretch`}>
|
||||||
|
<Header/>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavTitle(props: {
|
|
||||||
label: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<p className={`px-4 py-2 text-sm text-gray-500`}>
|
|
||||||
{props.label}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavItem(props: {
|
|
||||||
href: string
|
|
||||||
icon?: ReactNode
|
|
||||||
label: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Link className={merge(
|
|
||||||
`px-4 py-2 flex items-center rounded-md`,
|
|
||||||
`hover:bg-gray-100`,
|
|
||||||
)} href={props.href}>
|
|
||||||
<span className={`w-6 h-6 flex items-center justify-center`}>{props.icon}</span>
|
|
||||||
<span className={`ml-2`}>{props.label}</span>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type PurchasePageProps = {}
|
|||||||
|
|
||||||
export default async function PurchasePage(props: PurchasePageProps) {
|
export default async function PurchasePage(props: PurchasePageProps) {
|
||||||
return (
|
return (
|
||||||
<Page mode={`blank`} className={`flex-col`}>
|
<Page className={`flex-col`}>
|
||||||
<Purchase/>
|
<Purchase/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
--fail: oklch(0.65 0.16 25);
|
--fail: oklch(0.65 0.16 25);
|
||||||
--fail-text: oklch(1 0 0);
|
--fail-text: oklch(1 0 0);
|
||||||
|
|
||||||
--card: oklch(0.985 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-text: oklch(0.25 0 0);
|
--card-text: oklch(0.25 0 0);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
|
|||||||
@@ -8,14 +8,25 @@ export type PageProps = {
|
|||||||
export default function Page(props: ComponentProps<'main'> & PageProps) {
|
export default function Page(props: ComponentProps<'main'> & PageProps) {
|
||||||
return (
|
return (
|
||||||
<main {...props} className={merge(
|
<main {...props} className={merge(
|
||||||
`flex-auto flex gap-4`,
|
`flex-auto rounded-tl-xl overflow-hidden relative`,
|
||||||
{
|
|
||||||
full: `bg-white rounded-tl-xl p-4 flex-col overflow-auto`,
|
|
||||||
blank: `items-stretch pb-4 pr-4 overflow-hidden`,
|
|
||||||
}[props.mode ?? 'full'],
|
|
||||||
props.className,
|
|
||||||
)}>
|
)}>
|
||||||
{props.children}
|
|
||||||
|
{/* background */}
|
||||||
|
<div className={`absolute inset-0 overflow-hidden`}>
|
||||||
|
<div className={`absolute w-screen h-screen bg-gray-50`}></div>
|
||||||
|
<div className={`absolute w-[2000px] h-[2000px] -left-[1000px] -top-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
||||||
|
<div className={`absolute w-[2000px] h-[2000px] -right-[1000px] -top-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
||||||
|
<div className={`absolute w-[2000px] h-[2000px] left-[calc(50%-1000px)] -bottom-[1000px] bg-radial from-blue-50 from-10% to-transparent to-50%`}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* content */}
|
||||||
|
<div className={merge(
|
||||||
|
`relative w-full h-full`,
|
||||||
|
`flex flex-col gap-4 p-4 overflow-auto`,
|
||||||
|
props.className,
|
||||||
|
)}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {createContext, ReactNode, useContext, useRef} from 'react'
|
import {createContext, ReactNode, useContext, useRef} from 'react'
|
||||||
import {createProfileStore, ProfileStore} from '@/stores/profile-store'
|
|
||||||
import {StoreApi} from 'zustand/vanilla'
|
import {StoreApi} from 'zustand/vanilla'
|
||||||
import {useStore} from 'zustand/react'
|
import {useStore} from 'zustand/react'
|
||||||
|
import {createProfileStore, ProfileStore} from '@/stores/profile'
|
||||||
|
import {createLayoutStore, LayoutStore} from '@/stores/layout'
|
||||||
|
|
||||||
|
|
||||||
export type StoreContextType = {
|
export type StoreContextType = {
|
||||||
profile: StoreApi<ProfileStore>
|
profile: StoreApi<ProfileStore>
|
||||||
|
layout: StoreApi<LayoutStore>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoreContext = createContext<StoreContextType | null>(null)
|
export const StoreContext = createContext<StoreContextType | null>(null)
|
||||||
@@ -18,15 +20,23 @@ export type ProfileProviderProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StoreProvider(props: ProfileProviderProps) {
|
export default function StoreProvider(props: ProfileProviderProps) {
|
||||||
|
|
||||||
const profile = useRef<StoreApi<ProfileStore>>(null)
|
const profile = useRef<StoreApi<ProfileStore>>(null)
|
||||||
if (!profile.current) {
|
if (!profile.current) {
|
||||||
console.log('create profile store')
|
console.log('create profile store')
|
||||||
profile.current = createProfileStore(props.user)
|
profile.current = createProfileStore(props.user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layout = useRef<StoreApi<LayoutStore>>(null)
|
||||||
|
if (!layout.current) {
|
||||||
|
console.log('create layout store')
|
||||||
|
layout.current = createLayoutStore()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoreContext.Provider value={{
|
<StoreContext.Provider value={{
|
||||||
profile: profile.current,
|
profile: profile.current,
|
||||||
|
layout: layout.current,
|
||||||
}}>
|
}}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</StoreContext.Provider>
|
</StoreContext.Provider>
|
||||||
@@ -41,3 +51,11 @@ export function useProfileStore<T>(selector: (store: ProfileStore) => T) {
|
|||||||
}
|
}
|
||||||
return useStore(ctx.profile, selector)
|
return useStore(ctx.profile, selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useLayoutStore<T>(selector: (store: LayoutStore) => T) {
|
||||||
|
const ctx = useContext(StoreContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useLayoutStore must be used within a StoreProvider')
|
||||||
|
}
|
||||||
|
return useStore(ctx.layout, selector)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const buttonVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ButtonProps = React.ComponentProps<'button'> & {
|
type ButtonProps = React.ComponentProps<'button'> & {
|
||||||
theme?: 'default' | 'outline' | 'gradient' | 'error' | 'accent'
|
theme?: 'default' | 'outline' | 'gradient' | 'error' | 'accent' | 'ghost'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button(rawProps: ButtonProps) {
|
function Button(rawProps: ButtonProps) {
|
||||||
@@ -55,6 +55,7 @@ function Button(rawProps: ButtonProps) {
|
|||||||
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
||||||
error: 'bg-fail text-white hover:bg-fail/90',
|
error: 'bg-fail text-white hover:bg-fail/90',
|
||||||
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||||
|
ghost: "text-foreground hover:bg-muted",
|
||||||
}[theme ?? 'default'],
|
}[theme ?? 'default'],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
24
src/stores/layout.ts
Normal file
24
src/stores/layout.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {createStore} from 'zustand/vanilla'
|
||||||
|
|
||||||
|
export type LayoutStore = LayoutState & LayoutActions
|
||||||
|
|
||||||
|
export type LayoutState = {
|
||||||
|
navbar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LayoutActions = {
|
||||||
|
toggleNavbar: () => void
|
||||||
|
setNavbar: (navbar: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLayoutStore = () => {
|
||||||
|
return createStore<LayoutStore>()(setState => ({
|
||||||
|
navbar: true,
|
||||||
|
toggleNavbar: () => setState(state => {
|
||||||
|
return {navbar: !state.navbar}
|
||||||
|
}),
|
||||||
|
setNavbar: (navbar) => setState(_ => {
|
||||||
|
return {navbar}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user