Add dashboard and RFV skeleton loaders
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 48s

This commit is contained in:
Cauê Faleiros
2026-06-26 11:41:39 -03:00
parent 3d25bc5179
commit 8c788b66ac
3 changed files with 199 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react';
import { ChevronLeft, ChevronRight, Download, Filter, Search, Users } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import { exportToCSV, fetchRfmAnalytics, getCachedRfmAnalytics } from '../dataService';
import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types';
@@ -157,6 +157,99 @@ const ScoreBadge = ({ value }: { value: number }) => (
</span>
);
const RfmSkeleton = () => (
<div className="space-y-6" aria-label="Carregando RFV">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[0, 1, 2].map(item => (
<div key={`rfv-kpi-skeleton-${item}`} className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="skeleton h-4 w-36" />
<div className="skeleton mt-3 h-8 w-24" />
<div className="skeleton mt-3 h-3 w-44" />
</div>
))}
</div>
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_340px] gap-6">
<section className="bg-dark-card p-5 rounded-2xl border border-dark-border shadow-sm">
<div className="flex items-start justify-between gap-4">
<div>
<div className="skeleton h-5 w-28" />
<div className="skeleton mt-2 h-3 w-72 max-w-full" />
</div>
<div className="skeleton h-3 w-44" />
</div>
<div className="mt-5 overflow-x-auto">
<div className="grid min-w-[720px] grid-cols-[104px_repeat(3,minmax(0,1fr))] gap-2">
{[0, 1, 2, 3].map(item => (
<div key={`rfv-matrix-head-skeleton-${item}`} className="rounded-xl border border-dark-border bg-dark-input/60 p-3">
<div className="skeleton mx-auto h-3 w-16" />
<div className="skeleton mx-auto mt-2 h-3 w-20" />
</div>
))}
{[0, 1, 2].map(row => (
<div key={`rfv-matrix-row-skeleton-${row}`} className="contents">
{[0, 1, 2, 3].map(column => (
<div key={`rfv-matrix-cell-skeleton-${row}-${column}`} className="min-h-28 rounded-xl border border-dark-border bg-dark-input/50 p-3">
<div className="skeleton h-2 w-8" />
<div className="skeleton mt-5 h-4 w-28" />
<div className="skeleton mt-2 h-3 w-36" />
<div className="mt-5 grid grid-cols-2 gap-3">
<div className="skeleton h-5" />
<div className="skeleton h-5" />
</div>
</div>
))}
</div>
))}
</div>
</div>
</section>
<section className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="skeleton h-5 w-28" />
<div className="mt-5 space-y-2">
{[0, 1, 2, 3, 4, 5, 6, 7, 8].map(item => (
<div key={`rfv-segment-skeleton-${item}`} className="rounded-xl border border-dark-border p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex flex-1 items-center gap-3">
<div className="skeleton h-2.5 w-2.5 rounded-full" />
<div className="skeleton h-4 w-32" />
</div>
<div className="skeleton h-4 w-8" />
</div>
<div className="skeleton mt-3 h-3 w-48" />
</div>
))}
</div>
</section>
</div>
<section className="bg-dark-card border border-dark-border rounded-2xl overflow-hidden shadow-sm">
<div className="flex flex-col gap-3 border-b border-dark-border p-4 sm:flex-row">
<div className="skeleton h-10 w-48" />
<div className="skeleton h-10 w-72" />
<div className="skeleton h-10 w-28 sm:ml-auto" />
</div>
<div className="p-6">
<div className="grid grid-cols-[1.5fr_1fr_120px_1fr_1fr_1fr_1fr] gap-5">
{[0, 1, 2, 3, 4, 5, 6].map(item => (
<div key={`rfv-table-head-skeleton-${item}`} className="skeleton h-3" />
))}
</div>
<div className="mt-6 space-y-5">
{[0, 1, 2, 3, 4, 5].map(row => (
<div key={`rfv-table-row-skeleton-${row}`} className="grid grid-cols-[1.5fr_1fr_120px_1fr_1fr_1fr_1fr] gap-5">
{[0, 1, 2, 3, 4, 5, 6].map(column => (
<div key={`rfv-table-cell-skeleton-${row}-${column}`} className="skeleton h-4" />
))}
</div>
))}
</div>
</div>
</section>
</div>
);
const Rfm = () => {
const { dateRange, setDateRange, refreshInterval, setRefreshInterval, themeMode } = useOutletContext<{
dateRange: DateRange;
@@ -342,6 +435,7 @@ const Rfm = () => {
exportToCSV(exportData, `rfv_${new Date().toISOString().split('T')[0]}.csv`);
};
const shouldShowSkeleton = isLoading && !analytics;
return (
<div className="space-y-6">
@@ -362,12 +456,10 @@ const Rfm = () => {
</div>
</div>
{isLoading && (
<div className="inline-flex items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-3 py-2 text-sm font-semibold text-dark-muted">
<Loader2 className="h-4 w-4 animate-spin text-brand-primary" />
Atualizando RFV
</div>
)}
{shouldShowSkeleton ? (
<RfmSkeleton />
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
@@ -697,6 +789,8 @@ const Rfm = () => {
</div>
</div>
</section>
</>
)}
</div>
);
};