fix: sidebar width/centering + kanban card badge & table details
- Sidebar: expanded 256->200px, collapsed 80->64px; collapsed state now renders icon-only (label removed from flow) so icons are centered, not pushed left by an invisible text span - Kanban cards: add StatusBadge + email line to match design reference - Lead table: add "Mostrando N lead" footer and center/right-align Stato/Tag/Azioni headers per reference Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,30 +34,34 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
const isActive = (href: string, exact?: boolean) =>
|
const isActive = (href: string, exact?: boolean) =>
|
||||||
exact ? pathname === href : pathname === href || pathname.startsWith(href + "/");
|
exact ? pathname === href : pathname === href || pathname.startsWith(href + "/");
|
||||||
|
|
||||||
// Text fades out immediately on collapse, but only fades back in once the
|
// Shared row layout: full width + gap + label when expanded, icon centered
|
||||||
// width transition is underway on expand (avoids reflow mid-animation).
|
// (no gap, no padding) when collapsed so nothing pushes the icon off-center.
|
||||||
const textClass = cn(
|
const rowClass = collapsed ? "justify-center px-0" : "gap-3 px-3";
|
||||||
"whitespace-nowrap transition-opacity duration-200 delay-150",
|
|
||||||
collapsed && "opacity-0 pointer-events-none delay-0"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 min-h-screen bg-[#1A463C] flex flex-col overflow-hidden",
|
"shrink-0 min-h-screen bg-[#1A463C] flex flex-col overflow-hidden",
|
||||||
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
||||||
collapsed ? "w-20" : "w-64"
|
collapsed ? "w-16" : "w-[200px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="px-5 py-5 border-b border-white/10">
|
<div
|
||||||
<span className={cn("font-bold text-white tracking-tight text-sm", textClass)}>
|
className={cn(
|
||||||
iamcavalli
|
"h-[57px] flex items-center border-b border-white/10",
|
||||||
</span>
|
collapsed ? "justify-center px-0" : "px-5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!collapsed && (
|
||||||
|
<span className="font-bold text-white tracking-tight text-sm whitespace-nowrap">
|
||||||
|
iamcavalli
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav links */}
|
{/* Nav links */}
|
||||||
<nav className="flex-1 px-3 py-4 flex flex-col gap-0.5">
|
<nav className="flex-1 px-2 py-4 flex flex-col gap-0.5">
|
||||||
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => {
|
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => {
|
||||||
const active = isActive(href, exact);
|
const active = isActive(href, exact);
|
||||||
return (
|
return (
|
||||||
@@ -66,32 +70,41 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
href={href}
|
href={href}
|
||||||
title={collapsed ? label : undefined}
|
title={collapsed ? label : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors",
|
"flex items-center py-2 rounded-md text-sm transition-colors",
|
||||||
|
rowClass,
|
||||||
active
|
active
|
||||||
? "bg-white/15 text-white font-medium"
|
? "bg-white/15 text-white font-medium"
|
||||||
: "text-white/65 hover:text-white hover:bg-white/10"
|
: "text-white/65 hover:text-white hover:bg-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon size={16} strokeWidth={1.8} className="shrink-0" />
|
<Icon size={16} strokeWidth={1.8} className="shrink-0" />
|
||||||
<span className={textClass}>{label}</span>
|
{!collapsed && <span className="whitespace-nowrap">{label}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Theme + Logout */}
|
{/* Theme + Logout */}
|
||||||
<div className="px-3 py-4 border-t border-white/10 flex flex-col gap-2">
|
<div className="px-2 py-4 border-t border-white/10 flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between px-3">
|
<div
|
||||||
<span className={cn("text-xs text-white/50", textClass)}>Tema</span>
|
className={cn(
|
||||||
|
"flex items-center py-1",
|
||||||
|
collapsed ? "justify-center px-0" : "justify-between px-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!collapsed && <span className="text-xs text-white/50">Tema</span>}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut({ callbackUrl: "/admin/login" })}
|
onClick={() => signOut({ callbackUrl: "/admin/login" })}
|
||||||
title={collapsed ? "Esci" : undefined}
|
title={collapsed ? "Esci" : undefined}
|
||||||
className="flex items-center gap-3 px-3 py-2 w-full rounded-md text-sm text-white/65 hover:text-white hover:bg-white/10 transition-colors"
|
className={cn(
|
||||||
|
"flex items-center py-2 w-full rounded-md text-sm text-white/65 hover:text-white hover:bg-white/10 transition-colors",
|
||||||
|
rowClass
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<LogOut size={16} strokeWidth={1.8} className="shrink-0" />
|
<LogOut size={16} strokeWidth={1.8} className="shrink-0" />
|
||||||
<span className={textClass}>Esci</span>
|
{!collapsed && <span className="whitespace-nowrap">Esci</span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
renameLeadTag,
|
renameLeadTag,
|
||||||
} from "@/app/admin/pipeline/actions";
|
} from "@/app/admin/pipeline/actions";
|
||||||
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const COLUMN_HEADERS = [
|
const COLUMN_HEADERS = [
|
||||||
"Nome",
|
"Nome",
|
||||||
@@ -26,6 +27,11 @@ const COLUMN_HEADERS = [
|
|||||||
"Azioni",
|
"Azioni",
|
||||||
];
|
];
|
||||||
const COL_COUNT = COLUMN_HEADERS.length;
|
const COL_COUNT = COLUMN_HEADERS.length;
|
||||||
|
const HEADER_ALIGN: Record<string, string> = {
|
||||||
|
Stato: "text-center",
|
||||||
|
Tag: "text-center",
|
||||||
|
Azioni: "text-right",
|
||||||
|
};
|
||||||
|
|
||||||
function StatusCell({
|
function StatusCell({
|
||||||
lead,
|
lead,
|
||||||
@@ -196,7 +202,7 @@ export function LeadTable({
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border bg-muted/50 text-muted-foreground text-[11px] font-semibold uppercase tracking-wider">
|
<tr className="border-b border-border bg-muted/50 text-muted-foreground text-[11px] font-semibold uppercase tracking-wider">
|
||||||
{COLUMN_HEADERS.map((header) => (
|
{COLUMN_HEADERS.map((header) => (
|
||||||
<th key={header} className="py-4 px-6">
|
<th key={header} className={cn("py-4 px-6", HEADER_ALIGN[header])}>
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -213,6 +219,10 @@ export function LeadTable({
|
|||||||
{leads.length === 0 && (
|
{leads.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">Nessun lead trovato</div>
|
<div className="text-center py-8 text-muted-foreground">Nessun lead trovato</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-border px-6 py-4 text-xs text-muted-foreground">
|
||||||
|
Mostrando {leads.length} lead
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { updateLeadField } from "@/app/admin/pipeline/actions";
|
import { updateLeadField } from "@/app/admin/pipeline/actions";
|
||||||
import type { LeadWithTags } from "@/lib/admin-queries";
|
import type { LeadWithTags } from "@/lib/admin-queries";
|
||||||
|
import { StatusBadge } from "@/components/ui/StatusBadge";
|
||||||
|
|
||||||
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
|
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ function DroppableColumn({
|
|||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
{leads.map((lead) => (
|
{leads.map((lead) => (
|
||||||
<DraggableLeadCard key={lead.id} lead={lead} isActive={activeId === lead.id} />
|
<DraggableLeadCard key={lead.id} lead={lead} status={id} isActive={activeId === lead.id} />
|
||||||
))}
|
))}
|
||||||
{leads.length === 0 && (
|
{leads.length === 0 && (
|
||||||
<p className="text-xs text-muted-foreground italic text-center py-10 select-none">
|
<p className="text-xs text-muted-foreground italic text-center py-10 select-none">
|
||||||
@@ -77,7 +78,15 @@ function DroppableColumn({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraggableLeadCard({ lead, isActive }: { lead: LeadWithTags; isActive: boolean }) {
|
function DraggableLeadCard({
|
||||||
|
lead,
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
lead: LeadWithTags;
|
||||||
|
status: LeadStage;
|
||||||
|
isActive: boolean;
|
||||||
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: lead.id });
|
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: lead.id });
|
||||||
|
|
||||||
const style = transform
|
const style = transform
|
||||||
@@ -94,9 +103,15 @@ function DraggableLeadCard({ lead, isActive }: { lead: LeadWithTags; isActive: b
|
|||||||
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
|
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium text-foreground">{lead.name}</p>
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<p className="text-sm font-medium text-foreground">{lead.name}</p>
|
||||||
|
<StatusBadge status={status} className="rounded px-1.5 py-0.5 text-[9px]" />
|
||||||
|
</div>
|
||||||
|
{lead.email && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{lead.email}</p>
|
||||||
|
)}
|
||||||
{lead.company && (
|
{lead.company && (
|
||||||
<p className="text-xs text-muted-foreground">{lead.company}</p>
|
<p className="text-xs text-muted-foreground mt-1">{lead.company}</p>
|
||||||
)}
|
)}
|
||||||
{lead.next_action && (
|
{lead.next_action && (
|
||||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{lead.next_action}</p>
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{lead.next_action}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user