feat: implement mobile-friendly card layout for tables, remove horizontal scroll

This commit is contained in:
Erik Silva
2026-01-21 01:01:05 -03:00
parent ede4bbfabf
commit c6c385ec70
5 changed files with 249 additions and 75 deletions

View File

@@ -91,41 +91,41 @@ export default function ConfiguracoesClient({
}; };
return ( return (
<div className="min-h-screen bg-white flex"> <div className="min-h-screen bg-white flex flex-col lg:flex-row">
<Sidebar user={user} organization={organization} /> <Sidebar user={user} organization={organization} />
<main className="flex-1 overflow-y-auto flex flex-col"> <main className="flex-1 overflow-y-auto flex flex-col pt-16 lg:pt-0">
{/* Header Section - Integrated */} {/* Header Section - Integrated */}
<div className="relative border-b border-slate-100 bg-slate-50/40 p-10 lg:p-12"> <div className="relative border-b border-slate-100 bg-slate-50/40 p-4 sm:p-6 lg:p-10 xl:p-12">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6"> <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between md:gap-6">
<div> <div>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-2 sm:mb-3">
<span className="px-2.5 py-0.5 bg-white border border-slate-200 rounded-full text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Corporate Branding</span> <span className="px-2 py-0.5 bg-white border border-slate-200 rounded-full text-[9px] sm:text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Corporate Branding</span>
</div> </div>
<h2 className="text-3xl font-black text-slate-900 tracking-tight mb-1">Configurações</h2> <h2 className="text-xl sm:text-2xl lg:text-3xl font-black text-slate-900 tracking-tight mb-1">Configurações</h2>
<p className="text-base text-slate-500 font-medium">Personalize a identidade visual do seu portal de transparência.</p> <p className="text-xs sm:text-sm lg:text-base text-slate-500 font-medium hidden sm:block">Personalize a identidade visual do seu portal.</p>
</div> </div>
</div> </div>
</div> </div>
<div className="p-10 lg:p-12 w-full"> <div className="p-4 sm:p-6 lg:p-10 xl:p-12 w-full">
{message.text && ( {message.text && (
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2"> <Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription> <AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
</Alert> </Alert>
)} )}
<div className="space-y-10"> <div className="space-y-6 sm:space-y-8 lg:space-y-10">
{/* Identity Section */} {/* Identity Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-10">
<div> <div>
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Identidade</h3> <h3 className="text-base sm:text-lg font-black text-slate-900 tracking-tight mb-1 sm:mb-2">Identidade</h3>
<p className="text-xs text-slate-500 font-medium leading-relaxed"> <p className="text-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
Defina o nome oficial da organização e a marca que será exibida para o cidadão. Defina o nome oficial da organização e a marca que será exibida.
</p> </p>
</div> </div>
<div className="lg:col-span-2 bg-slate-50 p-8 rounded-[32px] space-y-6"> <div className="lg:col-span-2 bg-slate-50 p-4 sm:p-6 lg:p-8 rounded-2xl sm:rounded-[32px] space-y-4 sm:space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="name" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Nome da Organização</Label> <Label htmlFor="name" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Nome da Organização</Label>
<Input <Input
@@ -179,15 +179,15 @@ export default function ConfiguracoesClient({
</div> </div>
{/* Colors Section */} {/* Colors Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-10">
<div> <div>
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Cores e Estilo</h3> <h3 className="text-base sm:text-lg font-black text-slate-900 tracking-tight mb-1 sm:mb-2">Cores e Estilo</h3>
<p className="text-xs text-slate-500 font-medium leading-relaxed"> <p className="text-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
Escolha a cor primária que define a identidade do portal administrativo e público. Escolha a cor primária que define a identidade do portal.
</p> </p>
</div> </div>
<div className="lg:col-span-2 bg-slate-50 p-8 rounded-[32px] space-y-8"> <div className="lg:col-span-2 bg-slate-50 p-4 sm:p-6 lg:p-8 rounded-2xl sm:rounded-[32px] space-y-6 sm:space-y-8">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Cor de Destaque</Label> <Label className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Cor de Destaque</Label>
<div className="flex flex-col md:flex-row items-center gap-6"> <div className="flex flex-col md:flex-row items-center gap-6">
@@ -232,19 +232,19 @@ export default function ConfiguracoesClient({
</div> </div>
{/* Actions */} {/* Actions */}
<div className="pt-8 border-t-2 border-slate-50 flex justify-end"> <div className="pt-6 sm:pt-8 border-t-2 border-slate-50 flex flex-col sm:flex-row sm:justify-end gap-3">
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={isSaving} disabled={isSaving}
className="h-11 px-10 rounded-xl font-black text-xs uppercase tracking-widest transition-all active:scale-95 shadow-none text-white" className="h-11 px-6 sm:px-10 rounded-xl font-black text-xs uppercase tracking-widest transition-all active:scale-95 shadow-none text-white w-full sm:w-auto"
style={{ backgroundColor: primaryColor }} style={{ backgroundColor: primaryColor }}
> >
{isSaving ? ( {isSaving ? (
<Loader2 className="mr-3 h-4 w-4 animate-spin text-white" /> <Loader2 className="mr-2 sm:mr-3 h-4 w-4 animate-spin text-white" />
) : ( ) : (
<Save size={18} className="mr-2.5 stroke-[3]" /> <Save size={18} className="mr-2 sm:mr-2.5 stroke-[3]" />
)} )}
{isSaving ? "Gravando..." : "Salvar Configurações"} {isSaving ? "Gravando..." : "Salvar"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -743,6 +743,104 @@ export default function DocumentsClient({
<th className="pr-6 py-3 w-24 text-right text-[11px] font-bold text-slate-400 uppercase tracking-widest">Ações</th> <th className="pr-6 py-3 w-24 text-right text-[11px] font-bold text-slate-400 uppercase tracking-widest">Ações</th>
</> </>
} }
mobileCards={
<>
{paginatedItems.map((item: any) => (
<div
key={item.id}
className="bg-white border border-slate-100 rounded-xl p-4 space-y-3"
onClick={() => handleRowClick(item)}
>
<div className="flex items-start gap-3">
<div onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={selectedIds.includes(item.id)}
onCheckedChange={() => toggleSelect(item.id)}
className="rounded border-slate-300 data-[state=checked]:bg-red-500 data-[state=checked]:border-red-500 mt-1"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
{item.isFolder ? (
item.imageUrl ? (
<div className="w-10 h-10 rounded-lg overflow-hidden shrink-0 border border-slate-200">
<img src={item.imageUrl} alt={item.name} className="w-full h-full object-cover" />
</div>
) : (
<div className="w-10 h-10 flex items-center justify-center shrink-0 rounded-lg" style={{ backgroundColor: item.color + '20', color: item.color }}>
<FolderOpen size={20} fill="currentColor" fillOpacity={0.3} strokeWidth={2.5} />
</div>
)
) : (
<div className="w-10 h-10 flex items-center justify-center text-slate-400 shrink-0 bg-slate-50 rounded-lg">
<FileText size={20} strokeWidth={2} />
</div>
)}
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-slate-800 truncate uppercase tracking-tight">
{item.isFolder ? item.name : item.title}
</p>
<div className="flex items-center gap-2 text-[10px] text-slate-400 font-medium">
<span>{item.isFolder ? (!currentFolder ? "PROJETO" : "PASTA") : (item.fileType.split("/")[1]?.toUpperCase() || "DOC")}</span>
{!item.isFolder && (
<>
<span className="w-1 h-1 rounded-full bg-slate-200" />
<span>{formatFileSize(item.fileSize)}</span>
</>
)}
{item.isFolder && (
<>
<span className="w-1 h-1 rounded-full bg-slate-200" />
<span>{item._count?.documents || 0} itens</span>
</>
)}
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-50" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleTogglePublish(item)}
className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-tight transition-all ${item.isPublished ? "bg-green-100 text-green-600" : "bg-slate-100 text-slate-400"}`}
>
{item.isPublished ? "Público" : "Privado"}
</button>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 px-2 rounded-lg text-slate-400 hover:text-slate-900"
onClick={() => handleShare(item)}
>
<Share2 size={14} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 rounded-lg text-slate-400 hover:text-red-500"
onClick={() => {
setDocToDelete(item.id);
setShowDeleteDialog(true);
}}
>
<Trash2 size={14} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 rounded-lg text-slate-400"
onClick={() => handleRowClick(item)}
>
<ChevronRight size={16} />
</Button>
</div>
</div>
</div>
))}
</>
}
> >
{paginatedItems.map((item: any) => ( {paginatedItems.map((item: any) => (
<tr <tr

View File

@@ -90,41 +90,41 @@ export default function ProfileClient({
}; };
return ( return (
<div className="min-h-screen bg-white flex"> <div className="min-h-screen bg-white flex flex-col lg:flex-row">
<Sidebar user={user} organization={organization} /> <Sidebar user={user} organization={organization} />
<main className="flex-1 overflow-y-auto flex flex-col"> <main className="flex-1 overflow-y-auto flex flex-col pt-16 lg:pt-0">
{/* Header Section - Integrated */} {/* Header Section - Integrated */}
<div className="relative border-b border-slate-100 bg-slate-50/40 p-10 lg:p-12"> <div className="relative border-b border-slate-100 bg-slate-50/40 p-4 sm:p-6 lg:p-10 xl:p-12">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6"> <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between md:gap-6">
<div> <div>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-2 sm:mb-3">
<span className="px-2.5 py-0.5 bg-white border border-slate-200 rounded-full text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Account Settings</span> <span className="px-2 py-0.5 bg-white border border-slate-200 rounded-full text-[9px] sm:text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Account Settings</span>
</div> </div>
<h2 className="text-3xl font-black text-slate-900 tracking-tight mb-1">Meu Perfil</h2> <h2 className="text-xl sm:text-2xl lg:text-3xl font-black text-slate-900 tracking-tight mb-1">Meu Perfil</h2>
<p className="text-base text-slate-500 font-medium">Gerencie suas informações pessoais e credenciais de acesso.</p> <p className="text-xs sm:text-sm lg:text-base text-slate-500 font-medium hidden sm:block">Gerencie suas informações pessoais e credenciais de acesso.</p>
</div> </div>
</div> </div>
</div> </div>
<div className="p-10 lg:p-12 w-full"> <div className="p-4 sm:p-6 lg:p-10 xl:p-12 w-full">
{message.text && ( {message.text && (
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2"> <Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription> <AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
</Alert> </Alert>
)} )}
<form onSubmit={handleSave} className="space-y-10"> <form onSubmit={handleSave} className="space-y-6 sm:space-y-8 lg:space-y-10">
{/* Basic Info Section */} {/* Basic Info Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-10">
<div> <div>
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Dados Básicos</h3> <h3 className="text-base sm:text-lg font-black text-slate-900 tracking-tight mb-1 sm:mb-2">Dados Básicos</h3>
<p className="text-xs text-slate-500 font-medium leading-relaxed"> <p className="text-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
Essas informações são usadas para identificar você no sistema e em registros de atividade. Essas informações são usadas para identificá-lo no sistema.
</p> </p>
</div> </div>
<div className="lg:col-span-2 bg-slate-50 p-8 rounded-[32px] border-2 border-transparent"> <div className="lg:col-span-2 bg-slate-50 p-4 sm:p-6 lg:p-8 rounded-2xl sm:rounded-[32px] border-2 border-transparent">
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="name" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Nome Completo</Label> <Label htmlFor="name" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Nome Completo</Label>
@@ -159,15 +159,15 @@ export default function ProfileClient({
</div> </div>
{/* Security Section */} {/* Security Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-10">
<div> <div>
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Segurança</h3> <h3 className="text-base sm:text-lg font-black text-slate-900 tracking-tight mb-1 sm:mb-2">Segurança</h3>
<p className="text-xs text-slate-500 font-medium leading-relaxed"> <p className="text-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
Mantenha sua conta segura alterando sua senha regularmente. A senha atual é necessária para validar a troca. Altere sua senha regularmente. A senha atual é necessária para validar a troca.
</p> </p>
</div> </div>
<div className="lg:col-span-2 bg-slate-50 p-8 rounded-[32px] border-2 border-transparent space-y-6"> <div className="lg:col-span-2 bg-slate-50 p-4 sm:p-6 lg:p-8 rounded-2xl sm:rounded-[32px] border-2 border-transparent space-y-4 sm:space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="currentPassword" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Senha Atual</Label> <Label htmlFor="currentPassword" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Senha Atual</Label>
<div className="relative group"> <div className="relative group">
@@ -211,19 +211,19 @@ export default function ProfileClient({
</div> </div>
{/* Submit Bar */} {/* Submit Bar */}
<div className="pt-8 border-t-2 border-slate-50 flex justify-end"> <div className="pt-6 sm:pt-8 border-t-2 border-slate-50 flex flex-col sm:flex-row sm:justify-end gap-3">
<Button <Button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="h-11 px-8 rounded-xl font-black text-xs uppercase tracking-widest transition-all active:scale-95 shadow-none text-white" className="h-11 px-6 sm:px-8 rounded-xl font-black text-xs uppercase tracking-widest transition-all active:scale-95 shadow-none text-white w-full sm:w-auto"
style={{ backgroundColor: primaryColor }} style={{ backgroundColor: primaryColor }}
> >
{isSaving ? ( {isSaving ? (
<Loader2 className="mr-3 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 sm:mr-3 h-4 w-4 animate-spin" />
) : ( ) : (
<Save size={18} className="mr-2.5 stroke-[3]" /> <Save size={18} className="mr-2 sm:mr-2.5 stroke-[3]" />
)} )}
{isSaving ? "Processando..." : "Salvar Alterações"} {isSaving ? "Processando..." : "Salvar"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -215,19 +215,19 @@ export default function UsuariosClient({
}; };
return ( return (
<div className="min-h-screen bg-[#f9fafb] flex"> <div className="min-h-screen bg-[#f9fafb] flex flex-col lg:flex-row">
<Sidebar user={user} organization={organization} /> <Sidebar user={user} organization={organization} />
<main className="flex-1 overflow-y-auto flex flex-col"> <main className="flex-1 overflow-y-auto flex flex-col pt-16 lg:pt-0">
{/* Header Section - Integrated */} {/* Header Section - Integrated */}
<div className="relative bg-white p-8 lg:p-10"> <div className="relative bg-white p-4 sm:p-6 lg:p-8 xl:p-10">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6"> <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between sm:gap-6">
<div> <div>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="px-2 py-0.5 bg-white border border-slate-200 rounded-full text-[9px] font-bold uppercase tracking-[0.1em] text-slate-500">Access Control</span> <span className="px-2 py-0.5 bg-white border border-slate-200 rounded-full text-[9px] font-bold uppercase tracking-[0.1em] text-slate-500">Access Control</span>
</div> </div>
<h2 className="text-2xl font-black text-slate-900 tracking-tight mb-1">Usuários</h2> <h2 className="text-xl sm:text-2xl font-black text-slate-900 tracking-tight mb-1">Usuários</h2>
<p className="text-sm text-slate-500 font-medium">Gerencie quem tem permissão para editar no portal.</p> <p className="text-xs sm:text-sm text-slate-500 font-medium hidden sm:block">Gerencie quem tem permissão para editar no portal.</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -237,7 +237,7 @@ export default function UsuariosClient({
setShowNewUser(true); setShowNewUser(true);
}} }}
style={{ backgroundColor: primaryColor }} style={{ backgroundColor: primaryColor }}
className="h-10 px-6 rounded-lg font-bold text-xs shadow-none hover:opacity-90 active:scale-95 transition-all text-white" className="w-full sm:w-auto h-10 sm:h-10 px-4 sm:px-6 rounded-lg font-bold text-xs shadow-none hover:opacity-90 active:scale-95 transition-all text-white"
> >
<Plus size={18} className="mr-2 stroke-[3]" /> <Plus size={18} className="mr-2 stroke-[3]" />
Novo Usuário Novo Usuário
@@ -246,7 +246,7 @@ export default function UsuariosClient({
</div> </div>
</div> </div>
<div className="p-8 lg:p-10 w-full"> <div className="p-4 sm:p-6 lg:p-8 xl:p-10 w-full">
{message.text && ( {message.text && (
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2"> <Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription> <AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
@@ -257,7 +257,7 @@ export default function UsuariosClient({
<StandardTable <StandardTable
searchTerm={searchTerm} searchTerm={searchTerm}
onSearchChange={(val) => { setSearchTerm(val); setCurrentPage(1); }} onSearchChange={(val) => { setSearchTerm(val); setCurrentPage(1); }}
searchPlaceholder="Buscar usuários por nome ou e-mail..." searchPlaceholder="Buscar usuários..."
totalItems={filteredUsers.length} totalItems={filteredUsers.length}
showingCount={paginatedUsers.length} showingCount={paginatedUsers.length}
itemName="usuários" itemName="usuários"
@@ -273,6 +273,62 @@ export default function UsuariosClient({
<th className="pr-6 py-3.5 w-32 text-right text-[9px] font-bold text-slate-600 uppercase tracking-widest">Ações</th> <th className="pr-6 py-3.5 w-32 text-right text-[9px] font-bold text-slate-600 uppercase tracking-widest">Ações</th>
</> </>
} }
mobileCards={
<>
{paginatedUsers.map((u) => (
<div key={u.id} className="bg-white border border-slate-100 rounded-xl p-4 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-bold shrink-0"
style={{ backgroundColor: primaryColor }}
>
{u.name?.charAt(0) || u.email.charAt(0).toUpperCase()}
</div>
<div className="min-w-0">
<p className="font-bold text-slate-800 text-sm leading-tight truncate uppercase tracking-tight">
{u.name || "Sem nome"}
{u.id === user.id && (
<span className="ml-2 text-[9px] font-bold text-red-500 uppercase">Você</span>
)}
</p>
<p className="text-[11px] text-slate-400 truncate">{u.email}</p>
</div>
</div>
{getRoleBadge(u.role)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-50">
<span className="text-[10px] text-slate-400 font-medium">Desde {formatDate(u.createdAt)}</span>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 px-3 rounded-lg text-slate-500 hover:text-slate-900 hover:bg-slate-100 text-xs font-bold"
onClick={() => openEditModal(u)}
>
<Edit size={14} className="mr-1" />
Editar
</Button>
{u.id !== user.id && (
<Button
variant="ghost"
size="sm"
className="h-8 px-3 rounded-lg text-red-500 hover:text-red-600 hover:bg-red-50 text-xs font-bold"
onClick={() => {
setUserToDelete(u.id);
setShowDeleteDialog(true);
}}
>
<Trash2 size={14} />
</Button>
)}
</div>
</div>
</div>
))}
</>
}
> >
{paginatedUsers.map((u) => ( {paginatedUsers.map((u) => (
<tr key={u.id} className="hover:bg-blue-50/40 transition-all group cursor-default"> <tr key={u.id} className="hover:bg-blue-50/40 transition-all group cursor-default">

View File

@@ -25,6 +25,7 @@ interface StandardTableProps {
// Table Content // Table Content
columns: React.ReactNode; // Os <th> columns: React.ReactNode; // Os <th>
children: React.ReactNode; // Os <tr> children: React.ReactNode; // Os <tr>
mobileCards?: React.ReactNode; // Mobile cards version
isLoading?: boolean; isLoading?: boolean;
} }
@@ -41,15 +42,16 @@ export function StandardTable({
onPageChange, onPageChange,
columns, columns,
children, children,
mobileCards,
isLoading = false, isLoading = false,
}: StandardTableProps) { }: StandardTableProps) {
return ( return (
<div className="bg-white rounded-xl overflow-hidden shadow-none flex flex-col"> <div className="bg-white rounded-xl overflow-hidden shadow-none flex flex-col">
{/* Table Header with Search and Stats */} {/* Table Header with Search and Stats */}
<div className="bg-white px-2 py-4 flex flex-col md:flex-row justify-between items-center gap-4 border-b border-slate-100/60 transition-all"> <div className="bg-white px-3 sm:px-4 py-3 sm:py-4 flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3 sm:gap-4 border-b border-slate-100/60 transition-all">
{!hideSearch && ( {!hideSearch && (
<div className="relative w-full md:w-96 group"> <div className="relative flex-1 sm:max-w-sm group">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none"> <div className="absolute inset-y-0 left-0 pl-3 sm:pl-3.5 flex items-center pointer-events-none">
<Search <Search
className="text-slate-400 group-focus-within:text-red-500 transition-colors duration-300" className="text-slate-400 group-focus-within:text-red-500 transition-colors duration-300"
size={16} size={16}
@@ -60,12 +62,12 @@ export function StandardTable({
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
value={searchTerm} value={searchTerm}
onChange={(e) => onSearchChange?.(e.target.value)} onChange={(e) => onSearchChange?.(e.target.value)}
className="h-10 pl-11 pr-4 bg-slate-50/50 border-transparent rounded-lg text-sm font-medium text-slate-800 placeholder:text-slate-400 focus:bg-white focus:border-slate-200 focus:ring-0 transition-all duration-300 w-full" className="h-10 pl-10 sm:pl-11 pr-4 bg-slate-50/50 border-transparent rounded-lg text-sm font-medium text-slate-800 placeholder:text-slate-400 focus:bg-white focus:border-slate-200 focus:ring-0 transition-all duration-300 w-full"
/> />
</div> </div>
)} )}
<div className="flex items-center gap-2.5 px-3 py-1.5 bg-white border border-slate-100 rounded-lg"> <div className="flex items-center gap-2.5 px-3 py-1.5 bg-white border border-slate-100 rounded-lg self-start sm:self-auto">
<p className="text-[11px] font-bold text-slate-500 flex items-center gap-2"> <p className="text-[11px] font-bold text-slate-500 flex items-center gap-2">
<span className="text-slate-900">{showingCount}</span> <span className="text-slate-900">{showingCount}</span>
<span className="text-slate-300">/</span> <span className="text-slate-300">/</span>
@@ -75,8 +77,26 @@ export function StandardTable({
</div> </div>
</div> </div>
{/* The Table Itself */} {/* Mobile Cards View */}
<div className="overflow-x-auto"> {mobileCards && (
<div className="md:hidden p-3 sm:p-4 space-y-3">
{isLoading ? (
<div className="flex flex-col items-center gap-3 py-10">
<Loader2 className="animate-spin text-red-500" size={24} />
<p className="text-xs font-medium text-slate-400">Processando...</p>
</div>
) : showingCount === 0 ? (
<div className="text-center text-slate-400 font-medium text-xs py-10">
Nenhum {itemName} encontrado.
</div>
) : (
mobileCards
)}
</div>
)}
{/* Desktop Table View */}
<div className={`overflow-x-auto ${mobileCards ? 'hidden md:block' : ''}`}>
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr className="bg-slate-50/30"> <tr className="bg-slate-50/30">
@@ -108,28 +128,28 @@ export function StandardTable({
{/* Table Footer with Pagination */} {/* Table Footer with Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="p-4 bg-white flex items-center justify-between border-t border-slate-100/60 mt-auto"> <div className="p-3 sm:p-4 bg-white flex items-center justify-between border-t border-slate-100/60 mt-auto">
<span className="text-xs font-medium text-slate-500"> <span className="text-[10px] sm:text-xs font-medium text-slate-500">
Página <span className="text-slate-900 font-bold">{currentPage}</span> de <span className="text-slate-900 font-bold">{totalPages}</span> Pág. <span className="text-slate-900 font-bold">{currentPage}</span>/<span className="text-slate-900 font-bold">{totalPages}</span>
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
variant="ghost" variant="ghost"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => onPageChange(currentPage - 1)} onClick={() => onPageChange(currentPage - 1)}
className="h-9 px-4 rounded-lg font-bold text-xs text-slate-500 hover:text-red-500 hover:bg-red-50/50 transition-all disabled:opacity-30" className="h-8 sm:h-9 px-2 sm:px-4 rounded-lg font-bold text-xs text-slate-500 hover:text-red-500 hover:bg-red-50/50 transition-all disabled:opacity-30"
> >
<ChevronLeft size={16} className="mr-1" /> <ChevronLeft size={16} />
Anterior <span className="hidden sm:inline ml-1">Anterior</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={() => onPageChange(currentPage + 1)} onClick={() => onPageChange(currentPage + 1)}
className="h-9 px-4 rounded-lg font-bold text-xs text-slate-500 hover:text-red-500 hover:bg-red-50/50 transition-all disabled:opacity-30" className="h-8 sm:h-9 px-2 sm:px-4 rounded-lg font-bold text-xs text-slate-500 hover:text-red-500 hover:bg-red-50/50 transition-all disabled:opacity-30"
> >
Próximo <span className="hidden sm:inline mr-1">Próximo</span>
<ChevronRight size={16} className="ml-1" /> <ChevronRight size={16} />
</Button> </Button>
</div> </div>
</div> </div>