feat: implement mobile-friendly card layout for tables, remove horizontal scroll
This commit is contained in:
@@ -91,41 +91,41 @@ export default function ConfiguracoesClient({
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
<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 */}
|
||||
<div className="relative border-b border-slate-100 bg-slate-50/40 p-10 lg:p-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<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 gap-4 md:flex-row md:items-end md:justify-between md:gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 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>
|
||||
<div className="flex items-center gap-2 mb-2 sm:mb-3">
|
||||
<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>
|
||||
<h2 className="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>
|
||||
<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-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 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 && (
|
||||
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
|
||||
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-6 sm:space-y-8 lg:space-y-10">
|
||||
{/* 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>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Identidade</h3>
|
||||
<p className="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.
|
||||
<h3 className="text-base sm:text-lg font-black text-slate-900 tracking-tight mb-1 sm:mb-2">Identidade</h3>
|
||||
<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.
|
||||
</p>
|
||||
</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">
|
||||
<Label htmlFor="name" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Nome da Organização</Label>
|
||||
<Input
|
||||
@@ -179,15 +179,15 @@ export default function ConfiguracoesClient({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Cores e Estilo</h3>
|
||||
<p className="text-xs text-slate-500 font-medium leading-relaxed">
|
||||
Escolha a cor primária que define a identidade do portal administrativo e público.
|
||||
<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-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
|
||||
Escolha a cor primária que define a identidade do portal.
|
||||
</p>
|
||||
</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">
|
||||
<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">
|
||||
@@ -232,19 +232,19 @@ export default function ConfiguracoesClient({
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
onClick={handleSave}
|
||||
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 }}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
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) => (
|
||||
<tr
|
||||
|
||||
@@ -90,41 +90,41 @@ export default function ProfileClient({
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
<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 */}
|
||||
<div className="relative border-b border-slate-100 bg-slate-50/40 p-10 lg:p-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<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 gap-4 md:flex-row md:items-end md:justify-between md:gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 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>
|
||||
<div className="flex items-center gap-2 mb-2 sm:mb-3">
|
||||
<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>
|
||||
<h2 className="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>
|
||||
<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-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 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 && (
|
||||
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
|
||||
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
|
||||
</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 */}
|
||||
<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>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Dados Básicos</h3>
|
||||
<p className="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.
|
||||
<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-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
|
||||
Essas informações são usadas para identificá-lo no sistema.
|
||||
</p>
|
||||
</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-2">
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<h3 className="text-lg font-black text-slate-900 tracking-tight mb-2">Segurança</h3>
|
||||
<p className="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.
|
||||
<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-[11px] sm:text-xs text-slate-500 font-medium leading-relaxed">
|
||||
Altere sua senha regularmente. A senha atual é necessária para validar a troca.
|
||||
</p>
|
||||
</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">
|
||||
<Label htmlFor="currentPassword" className="text-[9px] font-black uppercase tracking-widest text-slate-600 ml-1">Senha Atual</Label>
|
||||
<div className="relative group">
|
||||
@@ -211,19 +211,19 @@ export default function ProfileClient({
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
type="submit"
|
||||
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 }}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -215,19 +215,19 @@ export default function UsuariosClient({
|
||||
};
|
||||
|
||||
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} />
|
||||
|
||||
<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 */}
|
||||
<div className="relative bg-white p-8 lg:p-10">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<div className="relative bg-white p-4 sm:p-6 lg:p-8 xl:p-10">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between sm:gap-6">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<h2 className="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>
|
||||
<h2 className="text-xl sm:text-2xl font-black text-slate-900 tracking-tight mb-1">Usuários</h2>
|
||||
<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 className="flex items-center gap-3">
|
||||
@@ -237,7 +237,7 @@ export default function UsuariosClient({
|
||||
setShowNewUser(true);
|
||||
}}
|
||||
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]" />
|
||||
Novo Usuário
|
||||
@@ -246,7 +246,7 @@ export default function UsuariosClient({
|
||||
</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 && (
|
||||
<Alert variant={message.type === "success" ? "default" : "destructive"} className="mb-6 rounded-xl border-2">
|
||||
<AlertDescription className="font-bold text-xs">{message.text}</AlertDescription>
|
||||
@@ -257,7 +257,7 @@ export default function UsuariosClient({
|
||||
<StandardTable
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={(val) => { setSearchTerm(val); setCurrentPage(1); }}
|
||||
searchPlaceholder="Buscar usuários por nome ou e-mail..."
|
||||
searchPlaceholder="Buscar usuários..."
|
||||
totalItems={filteredUsers.length}
|
||||
showingCount={paginatedUsers.length}
|
||||
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>
|
||||
</>
|
||||
}
|
||||
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) => (
|
||||
<tr key={u.id} className="hover:bg-blue-50/40 transition-all group cursor-default">
|
||||
|
||||
@@ -25,6 +25,7 @@ interface StandardTableProps {
|
||||
// Table Content
|
||||
columns: React.ReactNode; // Os <th>
|
||||
children: React.ReactNode; // Os <tr>
|
||||
mobileCards?: React.ReactNode; // Mobile cards version
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
@@ -41,15 +42,16 @@ export function StandardTable({
|
||||
onPageChange,
|
||||
columns,
|
||||
children,
|
||||
mobileCards,
|
||||
isLoading = false,
|
||||
}: StandardTableProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl overflow-hidden shadow-none flex flex-col">
|
||||
{/* 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 && (
|
||||
<div className="relative w-full md:w-96 group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none">
|
||||
<div className="relative flex-1 sm:max-w-sm group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 sm:pl-3.5 flex items-center pointer-events-none">
|
||||
<Search
|
||||
className="text-slate-400 group-focus-within:text-red-500 transition-colors duration-300"
|
||||
size={16}
|
||||
@@ -60,12 +62,12 @@ export function StandardTable({
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchTerm}
|
||||
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 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">
|
||||
<span className="text-slate-900">{showingCount}</span>
|
||||
<span className="text-slate-300">/</span>
|
||||
@@ -75,8 +77,26 @@ export function StandardTable({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The Table Itself */}
|
||||
<div className="overflow-x-auto">
|
||||
{/* Mobile Cards View */}
|
||||
{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">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/30">
|
||||
@@ -108,28 +128,28 @@ export function StandardTable({
|
||||
|
||||
{/* Table Footer with Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="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">
|
||||
Página <span className="text-slate-900 font-bold">{currentPage}</span> de <span className="text-slate-900 font-bold">{totalPages}</span>
|
||||
<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-[10px] sm:text-xs font-medium text-slate-500">
|
||||
Pág. <span className="text-slate-900 font-bold">{currentPage}</span>/<span className="text-slate-900 font-bold">{totalPages}</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={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" />
|
||||
Anterior
|
||||
<ChevronLeft size={16} />
|
||||
<span className="hidden sm:inline ml-1">Anterior</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={currentPage === totalPages}
|
||||
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
|
||||
<ChevronRight size={16} className="ml-1" />
|
||||
<span className="hidden sm:inline mr-1">Próximo</span>
|
||||
<ChevronRight size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user