mirror of
https://github.com/vernu/textbee.git
synced 2026-07-31 01:17:52 -04:00
None of the three password fields declared autoComplete, so password managers could not tell them apart and would fill the saved password into the wrong box. They now declare current-password and new-password. The delete-account dialog, the most destructive action in the app, had a label whose htmlFor pointed at an id the textarea never had, and an email confirmation field with no label at all. Both are now wired, and the controls moved out of DialogDescription, which Radix targets with aria-describedby: opening the dialog previously read the labels and placeholders back as one long description string. Validation errors are now tied to their inputs via aria-describedby and announced with role=alert instead of appearing silently. confirmPassword required 4 characters while newPassword required 8, so a five-character confirmation reported "Passwords must match" rather than the actual length problem. Five icon-only buttons had no accessible name: copy API key, copy device ID, the API key menu, and both promo code copy buttons. Four of the five nav landmarks were unlabelled, leaving them indistinguishable in a screen reader's landmark list. Added a skip-to-content link, since keyboard users otherwise tab the whole sidebar on every page. The delete-account label guard was confirmed to fail against the previous markup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import Link from 'next/link'
|
|
import Image from 'next/image'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
|
|
import { Menu, LogOut, LayoutDashboard } from 'lucide-react'
|
|
import { signOut } from 'next-auth/react'
|
|
import { Routes } from '@/config/routes'
|
|
import { Session } from 'next-auth'
|
|
|
|
// Deliberately minimal: identity and brand only. Navigation lives in the
|
|
// sidebar (desktop) and the bottom tab bar (mobile), search in the command
|
|
// palette, and the theme control in the sidebar footer.
|
|
export default function AppHeader({ session }: { session: Session }) {
|
|
const router = useRouter()
|
|
|
|
const handleLogout = () => {
|
|
signOut()
|
|
router.push(Routes.login)
|
|
}
|
|
|
|
const isAuthenticated = useMemo(() => session?.user, [session?.user])
|
|
|
|
const AuthenticatedMenu = () => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant='ghost' className='relative h-8 gap-2 px-1.5'>
|
|
<Avatar className='h-8 w-8'>
|
|
<AvatarImage src={session.user?.avatar} alt={session.user?.name} />
|
|
<AvatarFallback>{session.user?.name?.charAt(0)}</AvatarFallback>
|
|
</Avatar>
|
|
<span className='hidden text-sm font-medium md:block'>
|
|
{session.user?.name?.split(' ')[0]}
|
|
</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className='w-56' align='end' forceMount>
|
|
<DropdownMenuItem className='flex flex-col items-start'>
|
|
<div className='font-medium'>{session.user?.name}</div>
|
|
<div className='text-xs text-muted-foreground'>
|
|
{session.user?.email}
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem asChild>
|
|
<Link href={Routes.dashboard} className='flex w-full items-center'>
|
|
<LayoutDashboard className='mr-2 h-4 w-4' />
|
|
<span>Dashboard</span>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={handleLogout} className='text-red-600'>
|
|
<LogOut className='mr-2 h-4 w-4' />
|
|
<span>Log out</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
|
|
// Only unauthenticated visitors need a sheet: signed-in users already have
|
|
// the bottom tab bar for navigation and the avatar menu for identity.
|
|
const SignedOutMobileMenu = () => (
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button variant='ghost' className='md:hidden' size='icon'>
|
|
<Menu className='h-5 w-5' />
|
|
<span className='sr-only'>Toggle menu</span>
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side='right' className='w-[300px] sm:w-[400px]'>
|
|
<nav aria-label='Account' className='flex flex-col gap-4'>
|
|
<Button asChild variant='ghost' className='justify-start'>
|
|
<Link href={Routes.login}>Log in</Link>
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
className='rounded-full bg-primary text-white hover:bg-primary/90'
|
|
>
|
|
<Link href={Routes.register}>Get started</Link>
|
|
</Button>
|
|
</nav>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
|
|
return (
|
|
<header className='sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
|
|
<div className='flex h-14 items-center gap-2 px-4'>
|
|
<Link className='flex items-center space-x-2' href={Routes.landingPage}>
|
|
<Image
|
|
src='/images/logo.png'
|
|
alt='textbee Logo'
|
|
width={24}
|
|
height={24}
|
|
className='h-6 w-6 rounded-full bg-white'
|
|
/>
|
|
<span className='font-bold'>
|
|
text<span className='text-primary'>bee</span>
|
|
<span className='align-center text-xs text-muted-foreground'>
|
|
.dev
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
|
|
<div className='flex flex-1 items-center justify-end gap-2'>
|
|
{isAuthenticated ? (
|
|
<AuthenticatedMenu />
|
|
) : (
|
|
<>
|
|
<div className='hidden md:flex md:items-center md:gap-2'>
|
|
<Button asChild variant='ghost'>
|
|
<Link href={Routes.login}>Log in</Link>
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
className='rounded-full bg-primary text-white hover:bg-primary/90'
|
|
>
|
|
<Link href={Routes.register}>Get started</Link>
|
|
</Button>
|
|
</div>
|
|
<SignedOutMobileMenu />
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|