auth: eliminar flash do login

- Simplifica AuthGuard para não redirecionar no cliente (gate feito no middleware)
- Adiciona skeleton de carregamento no AppShell enquanto
- Troca anchors por Next Link no sidebar para navegação client-side

Sem mudanças de schema/DB; apenas UX e roteamento no cliente.
This commit is contained in:
Esdras Renan 2025-10-14 09:52:39 -03:00
parent 32488d48ca
commit c88622d762
3 changed files with 61 additions and 43 deletions

View file

@ -1,27 +1,59 @@
"use client"
import { Suspense, type ReactNode } from "react"
import { AppSidebar } from "@/components/app-sidebar"
import { AuthGuard } from "@/components/auth/auth-guard"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { Skeleton } from "@/components/ui/skeleton"
import { useAuth } from "@/lib/auth-client"
interface AppShellProps {
header: ReactNode
children: ReactNode
}
export function AppShell({ header, children }: AppShellProps) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
export function AppShell({ header, children }: AppShellProps) {
const { isLoading } = useAuth()
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<Suspense fallback={null}>
<AuthGuard />
</Suspense>
{header}
<main className="flex flex-1 flex-col gap-8 bg-gradient-to-br from-background via-background to-primary/10 pb-12 pt-6">
{children}
</main>
</SidebarInset>
</SidebarProvider>
)
}
{isLoading ? (
<div className="px-4 pt-4 lg:px-6">
<div className="flex items-center justify-between gap-4">
<Skeleton className="h-7 w-48" />
<div className="hidden items-center gap-2 sm:flex">
<Skeleton className="h-9 w-28" />
<Skeleton className="h-9 w-28" />
</div>
</div>
<Skeleton className="mt-2 h-4 w-72" />
</div>
) : (
header
)}
<main className="flex flex-1 flex-col gap-8 bg-gradient-to-br from-background via-background to-primary/10 pb-12 pt-6">
{isLoading ? (
<div className="space-y-6">
<div className="px-4 lg:px-6">
<div className="grid gap-6 lg:grid-cols-2">
<Skeleton className="h-56 w-full rounded-xl" />
<Skeleton className="h-56 w-full rounded-xl" />
</div>
</div>
<div className="px-4 lg:px-6">
<Skeleton className="h-64 w-full rounded-xl" />
</div>
</div>
) : (
children
)}
</main>
</SidebarInset>
</SidebarProvider>
)
}

View file

@ -22,6 +22,7 @@ import {
ShieldCheck,
} from "lucide-react"
import { usePathname } from "next/navigation"
import Link from "next/link"
import { SearchForm } from "@/components/search-form"
import { VersionSwitcher } from "@/components/version-switcher"
@ -240,7 +241,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<React.Fragment key={item.title}>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={parentActive}>
<a href={item.url} className={cn("gap-2", "relative pr-7") }>
<Link href={item.url} className={cn("gap-2", "relative pr-7") }>
{item.icon ? <item.icon className="size-4" /> : null}
<span className="flex-1">{item.title}</span>
<span
@ -258,17 +259,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
>
<ChevronDown className="size-3" />
</span>
</a>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{isExpanded
? childItems.map((child) => (
<SidebarMenuItem key={`${item.title}-${child.title}`}>
<SidebarMenuButton asChild isActive={isActive(child)}>
<a href={child.url} className="gap-2 pl-7 text-sm">
<Link href={child.url} className="gap-2 pl-7 text-sm">
{child.icon ? <child.icon className="size-3.5 text-neutral-500" /> : null}
<span>{child.title}</span>
</a>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))
@ -280,10 +281,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={isActive(item)}>
<a href={item.url} className="gap-2">
<Link href={item.url} className="gap-2">
{item.icon ? <item.icon className="size-4" /> : null}
<span>{item.title}</span>
</a>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)

View file

@ -1,29 +1,14 @@
"use client"
import { useEffect } from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useAuth } from "@/lib/auth-client"
// Melhor abordagem: sem redirecionamentos no cliente.
// O middleware (middleware.ts) já gateia todas as rotas não públicas com base no cookie/sessão.
// Mantemos este componente como no-op para evitar qualquer flash de /login.
export function AuthGuard() {
const { session, isLoading } = useAuth()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
if (isLoading) return
if (session?.user) return
const search = searchParams?.toString()
const callbackUrl = pathname
? search && search.length > 0
? `${pathname}?${search}`
: pathname
: undefined
const nextUrl = callbackUrl ? `/login?callbackUrl=${encodeURIComponent(callbackUrl)}` : "/login"
router.replace(nextUrl)
}, [isLoading, session?.user, pathname, searchParams, router])
// Podemos, se quisermos, ler isLoading para futuramente exibir um skeleton.
// No momento, não fazemos nada aqui.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isLoading } = useAuth()
return null
}