teqo/src/components/campaign/shell/CampaignBottomNav.tsx
Francisco Solla f0dff330bd feat(C102): sem hamburger/sheet de sidebar no mobile staff — navegação pela barra + Mais
No mobile (<768px) staff perde o botão de abrir sidebar na top bar
(CampaignMobileTopBar role-gated via useCampaignPageChromeRole +
isStaffCampaignRole; leader mantém) e o sheet de navegação deixa de
montar (CampaignSidebar early-return quando isMobile && staff).
Filtros salvos de Municípios (B18) migram para o drawer Mais
(MunicipalityNavSavedFilters variant 'overflow', mesma store,
apagar/desfazer/sucessão de foco; drawer ganha overflow-y-auto).

Correção de bug pré-existente (D4): o Toaster vivia dentro do wrapper
[contain:layout_paint] do painel da AI, que vira containing block para
position:fixed e stacking context próprio — toasts com ação (ex.
Desfazer) ficavam inalcançáveis sob os portais z-50 de drawer/sheet.
Movido para o root layout (campaign).

Testes: unit top bar (staff sem trigger / leader com trigger) + e2e
(staff sem trigger/dialog, leader com sheet, B18 no drawer completo).
2026-08-09 19:13:43 -03:00

181 lines
6.7 KiB
TypeScript

'use client'
import { LogOutIcon, UserIcon } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState, type FormEvent } from 'react'
import { logoutCampaign } from '@/app/(campaign)/campanha/actions/auth'
import { MunicipalityNavSavedFilters } from '@/components/campaign/shell/MunicipalityNavSavedFilters'
import {
getCampaignBottomNav,
getCampaignOverflowNav,
isCampaignNavActive,
} from '@/components/campaign/shell/nav'
import { Button } from '@/components/ui/button'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from '@/components/ui/Drawer'
import { Spinner } from '@/components/ui/Spinner'
import { clearLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
import { CAMPAIGN_PROFILE_HOME } from '@/lib/campaignPaths'
import { cn } from '@/lib/utils'
import { clearCampaignPwaCaches } from '@/utilities/campaignPwaClient'
import { type CampaignUserShellView } from '@/utilities/campaignUserProfile'
import { clearMunicipalitySavedFilters } from '@/utilities/municipality/municipalitySavedFilters'
import { clearRecentVisits } from '@/utilities/recentVisits'
export const CampaignBottomNav = ({ user }: { user: CampaignUserShellView }) => {
const pathname = usePathname()
const navItems = getCampaignBottomNav(user.role)
const overflowNav = getCampaignOverflowNav(user.role)
const [overflowOpen, setOverflowOpen] = useState(false)
const [isLoggingOut, setIsLoggingOut] = useState(false)
const handleLogout = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (isLoggingOut) return
setIsLoggingOut(true)
clearRecentVisits()
clearLastActedMunicipality()
clearMunicipalitySavedFilters()
await clearCampaignPwaCaches()
await logoutCampaign()
}
return (
<>
<nav
aria-label="Navegação principal"
className={cn(
'fixed inset-x-0 bottom-0 z-30 grid w-full',
'grid-cols-[repeat(5,minmax(0,1fr))]',
'border-t bg-background',
'pt-2.5 pb-[env(safe-area-inset-bottom)]',
'md:hidden print:hidden',
)}
>
{navItems.map((item) => {
const active = item.href === '' ? overflowOpen : isCampaignNavActive(pathname, item.href)
if (item.href === '') {
return (
<button
key="Mais"
type="button"
aria-haspopup="dialog"
aria-expanded={overflowOpen}
aria-label="Mais"
onClick={() => setOverflowOpen(true)}
className={cn(
'flex flex-1 flex-col items-center justify-center gap-1 text-[10px] whitespace-nowrap font-medium text-muted-foreground outline-none',
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
active && 'text-primary',
)}
>
<item.icon aria-hidden="true" className="size-5 shrink-0" />
<span>{item.title}</span>
</button>
)
}
return (
<Link
key={item.href}
href={item.href}
aria-current={active ? 'page' : undefined}
className={cn(
'flex flex-1 flex-col items-center justify-center gap-1 text-[10px] whitespace-nowrap font-medium text-muted-foreground outline-none',
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
active && 'text-primary',
)}
>
<item.icon aria-hidden="true" className="size-5 shrink-0" />
<span>{item.title}</span>
</Link>
)
})}
</nav>
<Drawer
open={overflowOpen}
onOpenChange={setOverflowOpen}
showSwipeHandle
swipeDirection="down"
modal={true}
>
<DrawerContent
id="CampaignBottomNavOverflow"
className="max-h-[85dvh] border-t border-border bg-background"
>
<DrawerHeader>
<DrawerTitle>Navegação</DrawerTitle>
<DrawerDescription>Destinos da campanha</DrawerDescription>
</DrawerHeader>
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-2">
<div className="flex flex-col gap-1">
{overflowNav.map((item) => {
const active = isCampaignNavActive(pathname, item.href)
return (
<Link
key={item.href}
href={item.href}
aria-current={active ? 'page' : undefined}
onClick={() => setOverflowOpen(false)}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium text-foreground outline-none',
'hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring',
active && 'bg-muted text-primary',
)}
>
<item.icon aria-hidden="true" className="size-5 shrink-0" />
<span>{item.title}</span>
</Link>
)
})}
</div>
{/* C102: the sheet is gone for staff on mobile — B18's saved filters
move into the overflow drawer, the new home of secondary nav. */}
<MunicipalityNavSavedFilters
variant="overflow"
onNavigate={() => setOverflowOpen(false)}
/>
</div>
<DrawerFooter>
<Link
href={CAMPAIGN_PROFILE_HOME}
onClick={() => setOverflowOpen(false)}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium text-foreground outline-none',
'hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring',
)}
>
<UserIcon aria-hidden="true" className="size-5 shrink-0" />
<span>Perfil</span>
</Link>
<form onSubmit={handleLogout} className="w-full">
<Button
type="submit"
variant="ghost"
disabled={isLoggingOut}
className="w-full justify-start gap-3 px-3 py-2.5 text-sm font-medium text-foreground hover:bg-muted"
>
{isLoggingOut ? (
<Spinner aria-hidden="true" className="size-5" />
) : (
<LogOutIcon aria-hidden="true" className="size-5" />
)}
<span>Sair</span>
</Button>
</form>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
)
}