feat(portal): adiciona skeletons para melhor UX durante carregamento
All checks were successful
All checks were successful
- 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:
parent
aa9c09c30e
commit
811ad0641a
2 changed files with 81 additions and 28 deletions
|
|
@ -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,19 +116,29 @@ 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">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
{machineContextLoading ? (
|
||||||
<Avatar className="size-9 border border-slate-200">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
|
<Skeleton className="size-9 rounded-full" />
|
||||||
<AvatarFallback>{initials}</AvatarFallback>
|
<div className="flex flex-col gap-1.5">
|
||||||
</Avatar>
|
<Skeleton className="h-4 w-24" />
|
||||||
<div className="flex flex-col leading-tight">
|
<Skeleton className="h-3 w-32" />
|
||||||
<span className="font-semibold text-neutral-900">{displayName}</span>
|
</div>
|
||||||
<span className="text-xs text-neutral-500">{displayEmail || "Sem e-mail definido"}</span>
|
|
||||||
{personaValue ? (
|
|
||||||
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Avatar className="size-9 border border-slate-200">
|
||||||
|
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
|
||||||
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col leading-tight">
|
||||||
|
<span className="font-semibold text-neutral-900">{displayName}</span>
|
||||||
|
<span className="text-xs text-neutral-500">{displayEmail || "Sem e-mail definido"}</span>
|
||||||
|
{personaValue ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
|
||||||
|
) : null}
|
||||||
|
</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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
</div>
|
||||||
<CardTitle className="text-lg font-semibold text-neutral-900">Carregando chamados...</CardTitle>
|
<Card className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
<p className="text-sm text-neutral-600">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
Estamos buscando seus chamados mais recentes. Isso deve levar apenas alguns instantes.
|
<Skeleton className="h-10 rounded-lg" />
|
||||||
</p>
|
<Skeleton className="h-10 rounded-lg" />
|
||||||
|
<Skeleton className="h-10 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue