feat(portal): adiciona skeletons para melhor UX durante carregamento
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 5s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 4m16s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
Quality Checks / Lint, Test and Build (push) Successful in 4m48s

- Adiciona skeleton no header quando dados do usuario estao carregando
- Remove mensagem "Sem e-mail definido" durante loading
- Substitui spinner por skeleton cards na lista de tickets
- Cria componente PortalTicketCardSkeleton para estado de loading

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-17 09:52:56 -03:00
parent aa9c09c30e
commit 811ad0641a
2 changed files with 81 additions and 28 deletions

View file

@ -9,6 +9,7 @@ import { toast } from "sonner"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useAuth, signOut } from "@/lib/auth-client" import { useAuth, signOut } from "@/lib/auth-client"
@ -115,6 +116,15 @@ export function PortalShell({ children }: PortalShellProps) {
})} })}
</nav> </nav>
<div className="flex w-full flex-col items-start gap-3 sm:w-auto sm:flex-row sm:items-center"> <div className="flex w-full flex-col items-start gap-3 sm:w-auto sm:flex-row sm:items-center">
{machineContextLoading ? (
<div className="flex items-center gap-2 text-sm">
<Skeleton className="size-9 rounded-full" />
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
</div>
) : (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Avatar className="size-9 border border-slate-200"> <Avatar className="size-9 border border-slate-200">
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} /> <AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
@ -128,6 +138,7 @@ export function PortalShell({ children }: PortalShellProps) {
) : null} ) : null}
</div> </div>
</div> </div>
)}
{!isMachineSession ? ( {!isMachineSession ? (
<Button <Button
size="sm" size="sm"
@ -152,11 +163,6 @@ export function PortalShell({ children }: PortalShellProps) {
</p> </p>
</div> </div>
) : null} ) : null}
{!machineContextError && machineContextLoading ? (
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-neutral-600 shadow-sm">
Recuperando dados do colaborador vinculado...
</div>
) : null}
{children} {children}
</main> </main>
<footer className="border-t border-slate-200 bg-white/70"> <footer className="border-t border-slate-200 bg-white/70">

View file

@ -11,6 +11,7 @@ import { useAuth } from "@/lib/auth-client"
import Link from "next/link" import Link from "next/link"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Skeleton } from "@/components/ui/skeleton"
import { Spinner } from "@/components/ui/spinner" import { Spinner } from "@/components/ui/spinner"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { PortalTicketCard } from "@/components/portal/portal-ticket-card" import { PortalTicketCard } from "@/components/portal/portal-ticket-card"
@ -195,19 +196,26 @@ export function PortalTicketList() {
if (isLoading) { if (isLoading) {
return ( return (
<Card className="rounded-2xl border border-slate-200 bg-white shadow-sm"> <div className="space-y-4">
<CardContent className="flex h-56 flex-col items-center justify-center gap-3 px-5 text-center"> <div className="flex items-center justify-between">
<div className="inline-flex size-12 items-center justify-center rounded-full border border-slate-200 bg-slate-50"> <div>
<Spinner className="size-5 text-neutral-600" /> <h2 className="text-lg font-semibold text-neutral-900">Meus chamados</h2>
<p className="text-sm text-neutral-600">Acompanhe seus tickets e veja as últimas atualizações.</p>
</div> </div>
<div className="space-y-1">
<CardTitle className="text-lg font-semibold text-neutral-900">Carregando chamados...</CardTitle>
<p className="text-sm text-neutral-600">
Estamos buscando seus chamados mais recentes. Isso deve levar apenas alguns instantes.
</p>
</div> </div>
</CardContent> <Card className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div className="grid grid-cols-3 gap-4">
<Skeleton className="h-10 rounded-lg" />
<Skeleton className="h-10 rounded-lg" />
<Skeleton className="h-10 rounded-lg" />
</div>
</Card> </Card>
<div className="grid gap-4">
{Array.from({ length: 4 }).map((_, index) => (
<PortalTicketCardSkeleton key={index} />
))}
</div>
</div>
) )
} }
@ -393,6 +401,45 @@ export function PortalTicketList() {
) )
} }
/** Skeleton do card de ticket para estado de carregamento */
function PortalTicketCardSkeleton() {
return (
<Card className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<CardHeader className="flex flex-row items-start justify-between gap-3 px-5 pb-3 pt-5">
<div className="flex-1">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="mt-2 h-6 w-3/4" />
</div>
<div className="flex flex-col items-end gap-2">
<Skeleton className="h-9 w-24 rounded-full" />
<Skeleton className="h-6 w-16 rounded-full" />
</div>
</CardHeader>
<CardContent className="flex flex-wrap items-center justify-between gap-4 border-t border-slate-100 px-5 py-4">
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-8" />
<Skeleton className="h-4 w-20" />
</div>
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-12" />
<Skeleton className="h-4 w-24" />
</div>
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-4 w-28" />
</div>
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-4 w-32" />
</div>
</CardContent>
</Card>
)
}
/** /**
* Gera array de números de página para exibição. * Gera array de números de página para exibição.
* Mostra primeira, última e páginas próximas à atual com reticências. * Mostra primeira, última e páginas próximas à atual com reticências.