Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
145
wacrm/src/components/layout/header.tsx
Normal file
145
wacrm/src/components/layout/header.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { LogOut, Menu, Settings as SettingsIcon, User } from "lucide-react";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ModeToggle } from "@/components/layout/mode-toggle";
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
"/dashboard": "Dashboard",
|
||||
"/inbox": "Inbox",
|
||||
"/notifications": "Notifications",
|
||||
"/contacts": "Contacts",
|
||||
"/pipelines": "Pipelines",
|
||||
"/broadcasts": "Broadcasts",
|
||||
"/automations": "Automations",
|
||||
"/settings": "Settings",
|
||||
};
|
||||
|
||||
function getPageTitle(pathname: string): string {
|
||||
if (pageTitles[pathname]) return pageTitles[pathname];
|
||||
const match = Object.entries(pageTitles).find(([path]) =>
|
||||
pathname.startsWith(path),
|
||||
);
|
||||
return match ? match[1] : "Dashboard";
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
/** Wired to the shell's drawer state. Used only on mobile — the
|
||||
* hamburger button is hidden on lg+. */
|
||||
onOpenSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function Header({ onOpenSidebar }: HeaderProps) {
|
||||
const pathname = usePathname();
|
||||
const { profile, signOut } = useAuth();
|
||||
const title = getPageTitle(pathname);
|
||||
|
||||
const initial =
|
||||
profile?.full_name?.charAt(0)?.toUpperCase() ??
|
||||
profile?.email?.charAt(0)?.toUpperCase() ??
|
||||
"U";
|
||||
|
||||
return (
|
||||
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border bg-background px-4 lg:px-6">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenSidebar}
|
||||
aria-label="Open menu"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground lg:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
<h1 className="truncate text-base font-semibold text-foreground sm:text-lg">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<ModeToggle />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-muted/70 focus:bg-muted/70 focus:outline-none data-popup-open:bg-muted/70 sm:gap-3 sm:pl-1 sm:pr-3"
|
||||
aria-label="Open account menu"
|
||||
>
|
||||
<Avatar className="size-8">
|
||||
{profile?.avatar_url ? (
|
||||
<AvatarImage
|
||||
src={profile.avatar_url}
|
||||
alt={profile.full_name ?? "Avatar"}
|
||||
/>
|
||||
) : null}
|
||||
<AvatarFallback className="bg-primary/10 text-sm font-medium text-primary">
|
||||
{initial}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden text-sm font-medium text-foreground sm:inline">
|
||||
{profile?.full_name ?? "User"}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={6}
|
||||
className="min-w-56 bg-popover text-popover-foreground ring-border"
|
||||
>
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{profile?.full_name ?? "User"}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{profile?.email ?? ""}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenuSeparator className="bg-border" />
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
href="/settings?tab=profile"
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<User className="size-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
href="/settings?tab=whatsapp"
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsIcon className="size-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-border" />
|
||||
<DropdownMenuItem
|
||||
onClick={signOut}
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<LogOut className="size-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
37
wacrm/src/components/layout/mode-toggle.tsx
Normal file
37
wacrm/src/components/layout/mode-toggle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
|
||||
import { useTheme } from "@/hooks/use-theme";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Light/dark mode toggle — a single icon button that flips the app
|
||||
* between the two modes. Sun shows in light mode (click → go dark),
|
||||
* moon shows in dark mode (click → go light); the label always names
|
||||
* the destination so screen-reader users hear what the click does.
|
||||
*
|
||||
* 40×40 hit target to match the header's other touch controls.
|
||||
*/
|
||||
export function ModeToggle({ className }: { className?: string }) {
|
||||
const { mode, toggleMode } = useTheme();
|
||||
const goingTo = mode === "dark" ? "light" : "dark";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMode}
|
||||
aria-label={`Switch to ${goingTo} mode`}
|
||||
title={`Switch to ${goingTo} mode`}
|
||||
className={cn(
|
||||
"flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{mode === "dark" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
398
wacrm/src/components/layout/sidebar.tsx
Normal file
398
wacrm/src/components/layout/sidebar.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTotalUnread } from "@/hooks/use-total-unread";
|
||||
import { useUnreadNotifications } from "@/hooks/use-unread-notifications";
|
||||
import {
|
||||
Bell,
|
||||
Bot,
|
||||
Crown,
|
||||
GitBranch,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
MessageSquare,
|
||||
Radio,
|
||||
Settings,
|
||||
Shield,
|
||||
User,
|
||||
UserCog,
|
||||
Users,
|
||||
UsersRound,
|
||||
Workflow,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import type { AccountRole } from "@/lib/auth/roles";
|
||||
|
||||
// Per-role chip metadata used in the sidebar's account strip + the
|
||||
// Members tab roster. Keeping this near both consumers in a single
|
||||
// place avoids drift between the two surfaces — when a designer
|
||||
// wants to recolour "agent" rows, this is the one diff.
|
||||
const ROLE_CHIP: Record<
|
||||
AccountRole,
|
||||
{ icon: typeof Crown; label: string; className: string }
|
||||
> = {
|
||||
owner: {
|
||||
icon: Crown,
|
||||
label: "Owner",
|
||||
// Amber: scarce, immutable, "the boss" — gets visual emphasis.
|
||||
className:
|
||||
"border-amber-500/40 bg-amber-500/10 text-amber-300",
|
||||
},
|
||||
admin: {
|
||||
icon: Shield,
|
||||
label: "Admin",
|
||||
// Primary-tinted: significant but not as scarce as owner.
|
||||
className:
|
||||
"border-primary/40 bg-primary/10 text-primary",
|
||||
},
|
||||
agent: {
|
||||
icon: UserCog,
|
||||
label: "Agent",
|
||||
// Neutral slate: the operational default.
|
||||
className:
|
||||
"border-border bg-muted text-foreground",
|
||||
},
|
||||
viewer: {
|
||||
icon: User,
|
||||
label: "Viewer",
|
||||
// Muted slate: read-only role; visually quieter than agent.
|
||||
className:
|
||||
"border-border bg-card text-muted-foreground",
|
||||
},
|
||||
};
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: typeof LayoutDashboard;
|
||||
/**
|
||||
* When true, the nav row renders a small "Beta" chip after the label.
|
||||
* Purely informational — doesn't affect routing or access.
|
||||
*/
|
||||
beta?: boolean;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ href: "/inbox", label: "Inbox", icon: MessageSquare },
|
||||
{ href: "/notifications", label: "Notifications", icon: Bell },
|
||||
{ href: "/contacts", label: "Contacts", icon: Users },
|
||||
{ href: "/pipelines", label: "Pipelines", icon: GitBranch },
|
||||
{ href: "/broadcasts", label: "Broadcasts", icon: Radio },
|
||||
{ href: "/automations", label: "Automations", icon: Zap },
|
||||
{ href: "/flows", label: "Flows", icon: Workflow, beta: true },
|
||||
{ href: "/agents", label: "AI Agents", icon: Bot },
|
||||
];
|
||||
|
||||
const bottomNavItems = [
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
/** Controlled on mobile by the Header's hamburger button. Ignored on lg+. */
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const { profile, profileLoading, account, accountRole, signOut } = useAuth();
|
||||
const totalUnread = useTotalUnread();
|
||||
const unreadNotifications = useUnreadNotifications();
|
||||
// Only surface the account-name strip when it actually carries
|
||||
// information. A solo user's personal account is named after them
|
||||
// (the 017 signup trigger seeds it from `full_name`), so showing it
|
||||
// here would just duplicate the user name in the footer below. Once
|
||||
// the account is renamed or the user joins a shared account, the
|
||||
// name diverges and the strip becomes meaningful — that's the signal
|
||||
// we gate on. Wait for the profile fetch to settle first, otherwise
|
||||
// the strip flashes in once the row resolves (a layout jump).
|
||||
const showAccountStrip =
|
||||
!profileLoading &&
|
||||
!!account?.name &&
|
||||
account.name !== profile?.full_name;
|
||||
|
||||
// Close the drawer when route changes — users opened it to navigate,
|
||||
// so once they pick a destination the drawer should get out of the way.
|
||||
useEffect(() => {
|
||||
onClose?.();
|
||||
// Only pathname drives this — onClose identity doesn't need to re-run it.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
// Lock body scroll and allow Escape to close while the drawer is open on
|
||||
// mobile. No-ops on desktop because the sidebar isn't positioned there.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose?.();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.body.style.overflow = prev;
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop — only exists on mobile and only when open. Clicking
|
||||
it closes the drawer. Hidden from lg+ since the sidebar is
|
||||
part of the main flex row there. */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"fixed inset-0 z-30 bg-background/70 backdrop-blur-sm transition-opacity lg:hidden",
|
||||
open
|
||||
? "pointer-events-auto opacity-100"
|
||||
: "pointer-events-none opacity-0",
|
||||
)}
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
// Mobile: fixed drawer that slides in from the left.
|
||||
"fixed inset-y-0 left-0 z-40 flex h-full w-64 flex-col border-r border-border bg-card",
|
||||
"transition-transform duration-200 ease-out will-change-transform",
|
||||
open ? "translate-x-0" : "-translate-x-full",
|
||||
// Desktop: static, always visible — reset all the mobile framing.
|
||||
"lg:static lg:z-0 lg:w-60 lg:translate-x-0 lg:transition-none",
|
||||
)}
|
||||
aria-label="Primary"
|
||||
>
|
||||
{/* Logo row. On mobile we put a close button here; on desktop the
|
||||
close button is hidden since the sidebar is always-visible. */}
|
||||
<div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
CRM Template for WhatsApp
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close menu"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main navigation */}
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
||||
<ul className="flex flex-col gap-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== "/dashboard" && pathname.startsWith(item.href));
|
||||
|
||||
const showUnreadDot =
|
||||
item.href === "/inbox" && totalUnread > 0 && !isActive;
|
||||
|
||||
// Unlike the inbox dot, the notifications count stays visible
|
||||
// even while the page is active — it reflects unread state
|
||||
// (cleared by marking notifications read), not "currently
|
||||
// viewing this section".
|
||||
const showNotificationBadge =
|
||||
item.href === "/notifications" && unreadNotifications > 0;
|
||||
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
// Taller on mobile so fingers can hit the row reliably (≥44px).
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
{item.beta && (
|
||||
<span
|
||||
aria-label="Beta feature"
|
||||
className="rounded-full border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-amber-300"
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
{showUnreadDot && (
|
||||
<span
|
||||
aria-label={`${totalUnread} unread conversation${totalUnread === 1 ? "" : "s"}`}
|
||||
className="relative flex h-2 w-2"
|
||||
>
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
)}
|
||||
{showNotificationBadge && (
|
||||
<span
|
||||
aria-label={`${unreadNotifications} unread notification${unreadNotifications === 1 ? "" : "s"}`}
|
||||
className="flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground"
|
||||
>
|
||||
{unreadNotifications > 9 ? "9+" : unreadNotifications}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="my-4 border-t border-border" />
|
||||
|
||||
<ul className="flex flex-col gap-1">
|
||||
{bottomNavItems.map((item) => {
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* User section */}
|
||||
<div className="shrink-0 border-t border-border p-3">
|
||||
{/* Account name display — surfaced only when the account
|
||||
name differs from the user's own name (see
|
||||
`showAccountStrip`). For a default solo account the two
|
||||
match, so we hide it to avoid duplicating the user name
|
||||
below; for renamed or shared accounts it tells the user
|
||||
which account they're acting in. */}
|
||||
{showAccountStrip && account?.name ? (
|
||||
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-muted-foreground">
|
||||
<UsersRound className="size-3.5 shrink-0" />
|
||||
{/* `title=` exposes the full name on hover when it
|
||||
gets truncated (long account names + narrow
|
||||
sidebars). Cheap a11y win. */}
|
||||
<span className="truncate" title={account.name}>
|
||||
{account.name}
|
||||
</span>
|
||||
{accountRole ? (
|
||||
// Always render the chip — owners used to be
|
||||
// invisible here, which made them indistinguishable
|
||||
// from admins at a glance. Now everyone sees their
|
||||
// role (with a colour cue) regardless of tier.
|
||||
(() => {
|
||||
const meta = ROLE_CHIP[accountRole];
|
||||
const Icon = meta.icon;
|
||||
return (
|
||||
<span
|
||||
className={`ml-auto inline-flex shrink-0 items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider ${meta.className}`}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{meta.label}
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-muted/60 focus:bg-muted/60 focus:outline-none data-popup-open:bg-muted/60">
|
||||
<Avatar className="size-8 shrink-0">
|
||||
{profile?.avatar_url ? (
|
||||
<AvatarImage
|
||||
src={profile.avatar_url}
|
||||
alt={profile.full_name ?? "Avatar"}
|
||||
/>
|
||||
) : null}
|
||||
<AvatarFallback className="bg-primary/10 text-sm font-medium text-primary">
|
||||
{profile?.full_name?.charAt(0)?.toUpperCase() ??
|
||||
profile?.email?.charAt(0)?.toUpperCase() ??
|
||||
"U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{profile?.full_name ?? "User"}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{profile?.email ?? ""}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
className="min-w-56 bg-popover text-popover-foreground ring-border"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
href="/settings?tab=profile"
|
||||
onClick={onClose}
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<User className="size-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
href="/settings?tab=whatsapp"
|
||||
onClick={onClose}
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-border" />
|
||||
<DropdownMenuItem
|
||||
onClick={signOut}
|
||||
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<LogOut className="size-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user