mirror of
https://github.com/ershisan99/www.git
synced 2025-12-18 05:19:23 +00:00
add fumadocs
This commit is contained in:
7
src/app/(home)/layout.tsx
Normal file
7
src/app/(home)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { baseOptions } from '@/app/layout.config'
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <HomeLayout {...baseOptions}>{children}</HomeLayout>
|
||||
}
|
||||
@@ -20,110 +20,6 @@ import Link from 'next/link'
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<header className='sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
|
||||
<div className='container mx-auto flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0'>
|
||||
<div className='flex gap-6 md:gap-10'>
|
||||
<Link href='/' className='flex items-center space-x-2'>
|
||||
<img
|
||||
src={'/logo.png'}
|
||||
alt={'Balatro Multiplayer'}
|
||||
className={'size-8'}
|
||||
/>
|
||||
<span className='inline-block font-bold'>
|
||||
Balatro Multiplayer
|
||||
</span>
|
||||
</Link>
|
||||
<nav className='hidden gap-6 md:flex'>
|
||||
<Link
|
||||
href='/docs'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<BookOpen className='mr-1 h-4 w-4' />
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href='/leaderboards'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Trophy className='mr-1 h-4 w-4' />
|
||||
Leaderboards
|
||||
</Link>
|
||||
<Link
|
||||
href='/about'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Info className='mr-1 h-4 w-4' />
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href='/credits'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Award className='mr-1 h-4 w-4' />
|
||||
Credits
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className='md:hidden'>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
>
|
||||
<Menu className='h-6 w-6' />
|
||||
<span className='sr-only'>Toggle menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='left'>
|
||||
<Link href='/' className='flex items-center space-x-2'>
|
||||
<img
|
||||
src={'/logo.png'}
|
||||
alt={'Balatro Multiplayer'}
|
||||
className={'size-8'}
|
||||
/>
|
||||
<span className='inline-block font-bold'>
|
||||
Balatro Multiplayer
|
||||
</span>
|
||||
</Link>
|
||||
<div className='mt-6 flex flex-col space-y-3'>
|
||||
<Link
|
||||
href='/docs'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<BookOpen className='mr-2 h-4 w-4' />
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href='/leaderboards'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Trophy className='mr-2 h-4 w-4' />
|
||||
Leaderboards
|
||||
</Link>
|
||||
<Link
|
||||
href='/about'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Info className='mr-2 h-4 w-4' />
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href='/credits'
|
||||
className='flex items-center font-medium text-muted-foreground text-sm transition-colors hover:text-primary'
|
||||
>
|
||||
<Award className='mr-2 h-4 w-4' />
|
||||
Credits
|
||||
</Link>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
<div className='flex flex-1 items-center justify-end space-x-4'>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className='flex-1'>
|
||||
<section className='space-y-6 pt-6 pb-8 md:pt-10 md:pb-12 lg:py-32'>
|
||||
<div className='container mx-auto flex flex-col items-center gap-4 text-center'>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UserInfo } from '@/app/players/[id]/user'
|
||||
import { auth } from '@/server/auth'
|
||||
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
|
||||
import { HydrateClient, api } from '@/trpc/server'
|
||||
import { Suspense } from 'react'
|
||||
import { UserInfo } from './user'
|
||||
|
||||
export default async function PlayerPage({
|
||||
params,
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
Filter,
|
||||
IceCreamCone,
|
||||
InfoIcon,
|
||||
MinusCircle,
|
||||
ShieldHalf,
|
||||
Star,
|
||||
Trophy,
|
||||
170
src/app/_components/header.tsx
Normal file
170
src/app/_components/header.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { ThemeToggle } from 'fumadocs-ui/components/layout/theme-toggle'
|
||||
import type { HomeLayoutProps } from 'fumadocs-ui/layouts/home'
|
||||
import type { LinkItemType } from 'fumadocs-ui/layouts/links'
|
||||
import { replaceOrDefault } from 'fumadocs-ui/layouts/shared'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Fragment } from 'react'
|
||||
import { Menu, MenuContent, MenuLinkItem, MenuTrigger } from './home/menu'
|
||||
import {
|
||||
NavbarLink,
|
||||
NavbarMenu,
|
||||
NavbarMenuContent,
|
||||
NavbarMenuLink,
|
||||
NavbarMenuTrigger,
|
||||
} from './home/navbar'
|
||||
import { Navbar } from './home/navbar'
|
||||
import { LargeSearchToggle, SearchToggle } from './search-toggle'
|
||||
|
||||
export function Header({
|
||||
nav: { enableSearch = true, ...nav } = {},
|
||||
finalLinks,
|
||||
themeSwitch,
|
||||
}: HomeLayoutProps & {
|
||||
finalLinks: LinkItemType[]
|
||||
}) {
|
||||
const navItems = finalLinks.filter((item) =>
|
||||
['nav', 'all'].includes(item.on ?? 'all')
|
||||
)
|
||||
const menuItems = finalLinks.filter((item) =>
|
||||
['menu', 'all'].includes(item.on ?? 'all')
|
||||
)
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
className='inline-flex items-center gap-2.5 font-semibold'
|
||||
>
|
||||
{nav.title}
|
||||
</Link>
|
||||
{nav.children}
|
||||
<ul className='flex flex-row items-center gap-2 px-6 max-sm:hidden'>
|
||||
{navItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem key={i} item={item} className='text-sm' />
|
||||
))}
|
||||
</ul>
|
||||
<div className='flex flex-1 flex-row items-center justify-end gap-1.5'>
|
||||
{enableSearch ? (
|
||||
<>
|
||||
<SearchToggle className='lg:hidden' hideIfDisabled />
|
||||
<LargeSearchToggle
|
||||
className='w-full max-w-[240px] max-lg:hidden'
|
||||
hideIfDisabled
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
themeSwitch,
|
||||
<ThemeToggle className='max-lg:hidden' mode={themeSwitch?.mode} />
|
||||
)}
|
||||
</div>
|
||||
<ul className='flex flex-row items-center'>
|
||||
{navItems.filter(isSecondary).map((item, i) => (
|
||||
<NavbarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className='-me-1.5 max-lg:hidden'
|
||||
/>
|
||||
))}
|
||||
<Menu className='lg:hidden'>
|
||||
<MenuTrigger
|
||||
aria-label='Toggle Menu'
|
||||
className='group -me-2'
|
||||
enableHover={nav.enableHoverToOpen}
|
||||
>
|
||||
<ChevronDown className='size-3 transition-transform duration-300 group-data-[state=open]:rotate-180' />
|
||||
</MenuTrigger>
|
||||
<MenuContent className='sm:flex-row sm:items-center sm:justify-end'>
|
||||
{menuItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<MenuLinkItem key={i} item={item} className='sm:hidden' />
|
||||
))}
|
||||
<div className='-ms-1.5 flex flex-row items-center gap-1.5 max-sm:mt-2'>
|
||||
{menuItems.filter(isSecondary).map((item, i) => (
|
||||
<MenuLinkItem key={i} item={item} className='-me-1.5' />
|
||||
))}
|
||||
<div role='separator' className='flex-1' />
|
||||
|
||||
{replaceOrDefault(
|
||||
themeSwitch,
|
||||
<ThemeToggle mode={themeSwitch?.mode} />
|
||||
)}
|
||||
</div>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</ul>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
function NavbarLinkItem({
|
||||
item,
|
||||
...props
|
||||
}: {
|
||||
item: LinkItemType
|
||||
className?: string
|
||||
}) {
|
||||
if (item.type === 'custom') return <div {...props}>{item.children}</div>
|
||||
|
||||
if (item.type === 'menu') {
|
||||
const children = item.items.map((child, j) => {
|
||||
if (child.type === 'custom')
|
||||
return <Fragment key={j}>{child.children}</Fragment>
|
||||
|
||||
const {
|
||||
banner = child.icon ? (
|
||||
<div className='w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4'>
|
||||
{child.icon}
|
||||
</div>
|
||||
) : null,
|
||||
...rest
|
||||
} = child.menu ?? {}
|
||||
|
||||
return (
|
||||
<NavbarMenuLink key={j} href={child.url} {...rest}>
|
||||
{rest.children ?? (
|
||||
<>
|
||||
{banner}
|
||||
<p className='-mb-1 font-medium text-sm'>{child.text}</p>
|
||||
{child.description ? (
|
||||
<p className='text-[13px] text-fd-muted-foreground'>
|
||||
{child.description}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</NavbarMenuLink>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<NavbarMenu>
|
||||
<NavbarMenuTrigger {...props}>
|
||||
{item.url ? <Link href={item.url}>{item.text}</Link> : item.text}
|
||||
</NavbarMenuTrigger>
|
||||
<NavbarMenuContent>{children}</NavbarMenuContent>
|
||||
</NavbarMenu>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<NavbarLink
|
||||
{...props}
|
||||
item={item}
|
||||
variant={item.type}
|
||||
aria-label={item.type === 'icon' ? item.label : undefined}
|
||||
>
|
||||
{item.type === 'icon' ? item.icon : item.text}
|
||||
</NavbarLink>
|
||||
)
|
||||
}
|
||||
|
||||
function isSecondary(item: LinkItemType): boolean {
|
||||
return (
|
||||
('secondary' in item && item.secondary === true) || item.type === 'icon'
|
||||
)
|
||||
}
|
||||
126
src/app/_components/home/menu.tsx
Normal file
126
src/app/_components/home/menu.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
import {
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/components/ui/navigation-menu'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import Link from 'fumadocs-core/link'
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button'
|
||||
import { BaseLinkItem, type LinkItemType } from 'fumadocs-ui/layouts/links'
|
||||
import type { ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
const menuItemVariants = cva('', {
|
||||
variants: {
|
||||
variant: {
|
||||
main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
|
||||
icon: buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
button: buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5 [&_svg]:size-4',
|
||||
}),
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'main',
|
||||
},
|
||||
})
|
||||
|
||||
export function MenuLinkItem({
|
||||
item,
|
||||
...props
|
||||
}: {
|
||||
item: LinkItemType
|
||||
className?: string
|
||||
}) {
|
||||
if (item.type === 'custom')
|
||||
return <div className={cn('grid', props.className)}>{item.children}</div>
|
||||
|
||||
if (item.type === 'menu') {
|
||||
const header = (
|
||||
<>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('mb-4 flex flex-col', props.className)}>
|
||||
<p className='mb-1 text-fd-muted-foreground text-sm'>
|
||||
{item.url ? (
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href={item.url}>{header}</Link>
|
||||
</NavigationMenuLink>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
</p>
|
||||
{item.items.map((child, i) => (
|
||||
<MenuLinkItem key={i} item={child} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuLink asChild>
|
||||
<BaseLinkItem
|
||||
item={item}
|
||||
className={cn(
|
||||
menuItemVariants({ variant: item.type }),
|
||||
props.className
|
||||
)}
|
||||
aria-label={item.type === 'icon' ? item.label : undefined}
|
||||
>
|
||||
{item.icon}
|
||||
{item.type === 'icon' ? undefined : item.text}
|
||||
</BaseLinkItem>
|
||||
</NavigationMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
export const Menu = NavigationMenuItem
|
||||
|
||||
export function MenuTrigger({
|
||||
enableHover = false,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof NavigationMenuTrigger> & {
|
||||
/**
|
||||
* Enable hover to trigger
|
||||
*/
|
||||
enableHover?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuTrigger
|
||||
{...props}
|
||||
onPointerMove={enableHover ? undefined : (e) => e.preventDefault()}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</NavigationMenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export function MenuContent(
|
||||
props: ComponentPropsWithoutRef<typeof NavigationMenuContent>
|
||||
) {
|
||||
return (
|
||||
<NavigationMenuContent
|
||||
{...props}
|
||||
className={cn('flex flex-col p-4', props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</NavigationMenuContent>
|
||||
)
|
||||
}
|
||||
137
src/app/_components/home/navbar.tsx
Normal file
137
src/app/_components/home/navbar.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import type {
|
||||
NavigationMenuContentProps,
|
||||
NavigationMenuTriggerProps,
|
||||
} from '@radix-ui/react-navigation-menu'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import Link, { type LinkProps } from 'fumadocs-core/link'
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button'
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuViewport,
|
||||
} from 'fumadocs-ui/components/ui/navigation-menu'
|
||||
import { useNav } from 'fumadocs-ui/contexts/layout'
|
||||
import { BaseLinkItem } from 'fumadocs-ui/layouts/links'
|
||||
import { type ComponentProps, type HTMLAttributes, useState } from 'react'
|
||||
|
||||
const navItemVariants = cva(
|
||||
'inline-flex items-center gap-1 p-2 text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground data-[active=true]:text-fd-primary [&_svg]:size-4'
|
||||
)
|
||||
|
||||
export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
const [value, setValue] = useState('')
|
||||
const { isTransparent } = useNav()
|
||||
|
||||
return (
|
||||
<NavigationMenu value={value} onValueChange={setValue} asChild>
|
||||
<header
|
||||
id='nd-nav'
|
||||
{...props}
|
||||
className={cn(
|
||||
'-translate-x-1/2 fixed top-(--fd-banner-height) left-1/2 z-40 box-content w-full max-w-fd-container border-fd-foreground/10 border-b transition-colors lg:mt-2 lg:w-[calc(100%-1rem)] lg:rounded-2xl lg:border',
|
||||
value.length > 0 ? 'shadow-lg' : 'shadow-sm',
|
||||
(!isTransparent || value.length > 0) &&
|
||||
'bg-fd-background/80 backdrop-blur-lg',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<NavigationMenuList
|
||||
className='flex h-14 w-full flex-row items-center px-4 lg:h-12'
|
||||
asChild
|
||||
>
|
||||
<nav>{props.children}</nav>
|
||||
</NavigationMenuList>
|
||||
<NavigationMenuViewport />
|
||||
</header>
|
||||
</NavigationMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarMenu = NavigationMenuItem
|
||||
|
||||
export function NavbarMenuContent(props: NavigationMenuContentProps) {
|
||||
return (
|
||||
<NavigationMenuContent
|
||||
{...props}
|
||||
className={cn(
|
||||
'grid grid-cols-1 gap-3 px-4 pb-4 md:grid-cols-2 lg:grid-cols-3',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</NavigationMenuContent>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavbarMenuTrigger(props: NavigationMenuTriggerProps) {
|
||||
return (
|
||||
<NavigationMenuTrigger
|
||||
{...props}
|
||||
className={cn(navItemVariants(), 'rounded-md', props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</NavigationMenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavbarMenuLink(props: LinkProps) {
|
||||
return (
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
const linkVariants = cva('', {
|
||||
variants: {
|
||||
variant: {
|
||||
main: navItemVariants(),
|
||||
button: buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5 [&_svg]:size-4',
|
||||
}),
|
||||
icon: buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon',
|
||||
}),
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'main',
|
||||
},
|
||||
})
|
||||
|
||||
export function NavbarLink({
|
||||
item,
|
||||
variant,
|
||||
...props
|
||||
}: ComponentProps<typeof BaseLinkItem> & VariantProps<typeof linkVariants>) {
|
||||
return (
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild>
|
||||
<BaseLinkItem
|
||||
{...props}
|
||||
item={item}
|
||||
className={cn(linkVariants({ variant }), props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</BaseLinkItem>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
}
|
||||
74
src/app/_components/search-toggle.tsx
Normal file
74
src/app/_components/search-toggle.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
import { type ButtonProps, buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSearchContext } from 'fumadocs-ui/contexts/search'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import type { ButtonHTMLAttributes } from 'react'
|
||||
|
||||
export function SearchToggle({
|
||||
hideIfDisabled,
|
||||
size = 'icon',
|
||||
variant = 'ghost',
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
ButtonProps & {
|
||||
hideIfDisabled?: boolean
|
||||
}) {
|
||||
const { setOpenSearch, enabled } = useSearchContext()
|
||||
if (hideIfDisabled && !enabled) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
props.className
|
||||
)}
|
||||
data-search=''
|
||||
aria-label='Open Search'
|
||||
onClick={() => {
|
||||
setOpenSearch(true)
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function LargeSearchToggle({
|
||||
hideIfDisabled,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
hideIfDisabled?: boolean
|
||||
}) {
|
||||
const { enabled, hotKey, setOpenSearch } = useSearchContext()
|
||||
if (hideIfDisabled && !enabled) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
data-search-full=''
|
||||
{...props}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-full border bg-fd-secondary/50 p-1.5 text-fd-muted-foreground text-sm transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
props.className
|
||||
)}
|
||||
onClick={() => {
|
||||
setOpenSearch(true)
|
||||
}}
|
||||
>
|
||||
<SearchIcon className='ms-1 size-4' />
|
||||
Search
|
||||
<div className='ms-auto inline-flex gap-0.5'>
|
||||
{hotKey.map((k, i) => (
|
||||
<kbd key={i} className='rounded-md border bg-fd-background px-1.5'>
|
||||
{k.display}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
4
src/app/api/search/route.ts
Normal file
4
src/app/api/search/route.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createFromSource } from 'fumadocs-core/search/server'
|
||||
import { source } from '../../../../lib/source'
|
||||
|
||||
export const { GET } = createFromSource(source)
|
||||
14
src/app/docs-og/[...slug]/route.tsx
Normal file
14
src/app/docs-og/[...slug]/route.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { generateOGImage } from 'fumadocs-ui/og'
|
||||
import { metadataImage } from '../../../../lib/metadata'
|
||||
|
||||
export const GET = metadataImage.createAPI((page) => {
|
||||
return generateOGImage({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
site: 'My App',
|
||||
})
|
||||
})
|
||||
|
||||
export function generateStaticParams() {
|
||||
return metadataImage.generateParams()
|
||||
}
|
||||
47
src/app/docs/[[...slug]]/page.tsx
Normal file
47
src/app/docs/[[...slug]]/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx'
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { metadataImage } from '../../../../lib/metadata'
|
||||
import { source } from '../../../../lib/source'
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
if (!page) notFound()
|
||||
|
||||
const MDX = page.data.body
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX components={{ ...defaultMdxComponents }} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams()
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
if (!page) notFound()
|
||||
|
||||
return metadataImage.withImage(page.slugs, {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
})
|
||||
}
|
||||
13
src/app/docs/layout.tsx
Normal file
13
src/app/docs/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { baseOptions } from '@/app/layout.config'
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/notebook'
|
||||
import type { ReactNode } from 'react'
|
||||
import { source } from '../../../lib/source'
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
console.log(baseOptions)
|
||||
return (
|
||||
<DocsLayout {...baseOptions} tree={source.pageTree}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
)
|
||||
}
|
||||
41
src/app/layout.config.tsx
Normal file
41
src/app/layout.config.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
|
||||
import { Award, BookOpen, Info, Trophy } from 'lucide-react'
|
||||
import { Header } from './_components/header'
|
||||
|
||||
const links = [
|
||||
{
|
||||
text: 'Documentation',
|
||||
url: '/docs',
|
||||
icon: <BookOpen />,
|
||||
},
|
||||
{
|
||||
text: 'Leaderboards',
|
||||
url: '/leaderboards',
|
||||
icon: <Trophy />,
|
||||
},
|
||||
{
|
||||
text: 'About',
|
||||
url: '/about',
|
||||
icon: <Info />,
|
||||
},
|
||||
{
|
||||
text: 'Credits',
|
||||
url: '/credits',
|
||||
icon: <Award />,
|
||||
},
|
||||
]
|
||||
const nav = {
|
||||
title: (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<img src={'/logo.png'} alt={'Balatro Multiplayer'} className={'size-8'} />
|
||||
<span className='inline-block font-bold'>Balatro Multiplayer</span>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
links,
|
||||
nav: {
|
||||
...nav,
|
||||
component: <Header finalLinks={links} nav={nav} />,
|
||||
},
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
import '@/styles/globals.css'
|
||||
|
||||
import type {Metadata} from 'next'
|
||||
import {Geist} from 'next/font/google'
|
||||
|
||||
import {MainHeader} from '@/components/header'
|
||||
import {ThemeProvider} from '@/components/theme-provider'
|
||||
import {TRPCReactProvider} from '@/trpc/react'
|
||||
import {SessionProvider} from 'next-auth/react'
|
||||
import {NextIntlClientProvider} from 'next-intl'
|
||||
import {getLocale} from 'next-intl/server'
|
||||
import { TRPCReactProvider } from '@/trpc/react'
|
||||
import { RootProvider } from 'fumadocs-ui/provider'
|
||||
import type { Metadata } from 'next'
|
||||
import { SessionProvider } from 'next-auth/react'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { getLocale } from 'next-intl/server'
|
||||
import PlausibleProvider from 'next-plausible'
|
||||
import { Geist } from 'next/font/google'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Balatro Multiplayer',
|
||||
description: 'Unofficial (for now) stats for the Balatro Multiplayer Mod',
|
||||
icons: [{rel: 'icon', url: '/favicon.ico'}],
|
||||
icons: [{ rel: 'icon', url: '/favicon.ico' }],
|
||||
}
|
||||
|
||||
const geist = Geist({
|
||||
@@ -23,8 +20,8 @@ const geist = Geist({
|
||||
})
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const locale = await getLocale()
|
||||
return (
|
||||
<html
|
||||
@@ -32,33 +29,25 @@ export default async function RootLayout({
|
||||
className={`${geist.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<title/>
|
||||
<PlausibleProvider
|
||||
domain='balatromp.com'
|
||||
customDomain={'https://plausible.balatromp.com'}
|
||||
trackOutboundLinks
|
||||
trackFileDownloads
|
||||
selfHosted
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<TRPCReactProvider>
|
||||
<NextIntlClientProvider>
|
||||
<SessionProvider>
|
||||
<ThemeProvider
|
||||
attribute='class'
|
||||
defaultTheme='system'
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
</NextIntlClientProvider>
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
<head>
|
||||
<title />
|
||||
<PlausibleProvider
|
||||
domain='balatromp.com'
|
||||
customDomain={'https://plausible.balatromp.com'}
|
||||
trackOutboundLinks
|
||||
trackFileDownloads
|
||||
selfHosted
|
||||
/>
|
||||
</head>
|
||||
<body className={'flex min-h-screen flex-col'}>
|
||||
<TRPCReactProvider>
|
||||
<NextIntlClientProvider>
|
||||
<SessionProvider>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</SessionProvider>
|
||||
</NextIntlClientProvider>
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user