Files
textbee/web/components/shared/app-header.tsx
isra el 3e4597294c refactor: turn on strict TypeScript and fix the 63 errors it found
My earlier estimate of 34 errors was wrong, and the reason is worth
recording: tsconfig carried "strictNullChecks": false as a duplicate key
AFTER the strict flag, and an explicit option beats the strict umbrella
even when --strict is passed on the CLI. So the measurement that produced
34 had silently excluded every null-safety error. The real number was 63.
Both duplicate keys are gone; strict: true now stands alone.

Most were implicit-any, but strict caught several genuine type lies:

- SendSmsPayload.deviceId and WebhookData._id were optional while both
  are interpolated into request paths, so an absent value would have hit
  /gateway/devices/undefined/send-sms or PATCH /webhooks/undefined.
- webhook-table's deviceName was typed string while buildDeviceLabel
  returns string | string[] and the Device cell already renders the
  array case. The type never described what the code produced.
- webhooks-section read `webhooks?.data?.length > 0`, comparing
  undefined against 0 while the query was still in flight.
- app-header declared a non-null Session while its own body guarded with
  session?.user throughout. Making the type honest surfaced four
  genuinely unguarded accesses.
- api-keys kept a local ApiKeyRow duplicating the shared ApiKey type, so
  the list callback annotated rows as one type while the hook returned
  the other. ApiKeyRow is now an alias and the two extra fields moved
  onto ApiKey.
- The notifications envelope typed its rows as unknown[], so the
  deliveries table's row type went entirely unchecked. Now a real
  WebhookNotification type.

The react-hook-form cluster (20 of the 63) was one root cause: zod's
.default() makes the input and output types differ, so z.infer (the
output) is not what the resolver takes. Fixed by typing the forms with
z.input and z.output separately, which changes nothing at runtime.

Also bumped target es5 to ES2017, which fixes the Set-iteration error
that made tsc --noEmit fail before any of this. Next compiles browser
output via SWC and its own browserslist, so bundle targeting is
unaffected. Added @types/papaparse and @types/react-syntax-highlighter,
and a typecheck script, since next build does not check test files.

No @ts-expect-error and no new any were used.

Verified: typecheck clean, build clean, 0 lint errors (21 warnings,
unchanged), 155 unit tests, 78 e2e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 04:19:35 +03:00

144 lines
5.1 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.
// Nullable: the root layout renders this for signed-out visitors too, and the
// body already guards with session?.user throughout.
export default function AppHeader({ session }: { session: Session | null }) {
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 ?? undefined}
alt={session?.user?.name ?? undefined}
/>
<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>
)
}