352 lines
9.8 KiB
TypeScript
352 lines
9.8 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
BookUser,
|
|
Calendar,
|
|
Code,
|
|
Cog,
|
|
Globe,
|
|
Home,
|
|
Inbox,
|
|
LayoutDashboard,
|
|
LayoutTemplate,
|
|
LogOut,
|
|
Mail,
|
|
Search,
|
|
Server,
|
|
Settings,
|
|
Volume2,
|
|
BookOpenText,
|
|
ChartColumnBig,
|
|
ChartArea,
|
|
BellIcon,
|
|
CreditCardIcon,
|
|
LogOutIcon,
|
|
MoreVerticalIcon,
|
|
UserCircleIcon,
|
|
UsersIcon,
|
|
GaugeIcon,
|
|
} from "lucide-react";
|
|
import { signOut } from "next-auth/react";
|
|
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
useSidebar,
|
|
} from "@unsend/ui/src/sidebar";
|
|
import Link from "next/link";
|
|
import { MiniThemeSwitcher, ThemeSwitcher } from "./theme/ThemeSwitcher";
|
|
import { useSession } from "next-auth/react";
|
|
import { isSelfHosted } from "~/utils/common";
|
|
import { usePathname } from "next/navigation";
|
|
import { Badge } from "@unsend/ui/src/badge";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@unsend/ui/src/avatar";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@unsend/ui/src/dropdown-menu";
|
|
|
|
// General items
|
|
const generalItems = [
|
|
{
|
|
title: "Analytics",
|
|
url: "/dashboard",
|
|
icon: ChartColumnBig,
|
|
},
|
|
{
|
|
title: "Emails",
|
|
url: "/emails",
|
|
icon: Mail,
|
|
},
|
|
{
|
|
title: "Templates",
|
|
url: "/templates",
|
|
icon: LayoutTemplate,
|
|
},
|
|
];
|
|
|
|
// Marketing items
|
|
const marketingItems = [
|
|
{
|
|
title: "Contacts",
|
|
url: "/contacts",
|
|
icon: BookUser,
|
|
},
|
|
{
|
|
title: "Campaigns",
|
|
url: "/campaigns",
|
|
icon: Volume2,
|
|
},
|
|
];
|
|
|
|
// Settings items
|
|
const settingsItems = [
|
|
{
|
|
title: "Domains",
|
|
url: "/domains",
|
|
icon: Globe,
|
|
},
|
|
{
|
|
title: "Developer settings",
|
|
url: "/dev-settings",
|
|
icon: Code,
|
|
},
|
|
{
|
|
title: "Settings",
|
|
url: "/settings",
|
|
icon: Cog,
|
|
},
|
|
// TODO: Add conditional logic for Admin item based on isSelfHosted() || session?.user.isAdmin
|
|
{
|
|
title: "Admin",
|
|
url: "/admin",
|
|
icon: Server,
|
|
isAdmin: true,
|
|
isSelfHosted: true,
|
|
},
|
|
];
|
|
|
|
export function AppSidebar() {
|
|
const { data: session } = useSession();
|
|
const { state, open } = useSidebar();
|
|
|
|
const pathname = usePathname();
|
|
|
|
return (
|
|
<Sidebar collapsible="icon" variant="sidebar">
|
|
<SidebarHeader>
|
|
<SidebarGroupLabel>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg font-semibold text-foreground">
|
|
Unsend
|
|
</span>
|
|
<Badge variant="outline">Beta</Badge>
|
|
</div>
|
|
</SidebarGroupLabel>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>
|
|
<span>General</span>
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{generalItems.map((item) => {
|
|
const isActive = pathname?.startsWith(item.url);
|
|
return (
|
|
<SidebarMenuItem key={item.title}>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip={item.title}
|
|
isActive={isActive}
|
|
>
|
|
<Link href={item.url}>
|
|
<item.icon />
|
|
<span>{item.title}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>
|
|
<span>Marketing</span>
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{marketingItems.map((item) => {
|
|
const isActive = pathname?.startsWith(item.url);
|
|
return (
|
|
<SidebarMenuItem key={item.title}>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip={item.title}
|
|
isActive={isActive}
|
|
className="text-sidebar-foreground"
|
|
>
|
|
<Link href={item.url}>
|
|
<item.icon />
|
|
<span>{item.title}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel>
|
|
<span>Settings</span>
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{settingsItems.map((item) => {
|
|
const isActive = pathname?.startsWith(item.url);
|
|
|
|
if (item.isAdmin && !session?.user.isAdmin) {
|
|
return null;
|
|
}
|
|
if (item.isSelfHosted && !isSelfHosted()) {
|
|
return null;
|
|
}
|
|
return (
|
|
<SidebarMenuItem key={item.title}>
|
|
<SidebarMenuButton
|
|
asChild
|
|
tooltip={item.title}
|
|
isActive={isActive}
|
|
>
|
|
<Link href={item.url}>
|
|
<item.icon />
|
|
<span>{item.title}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton asChild tooltip="Docs">
|
|
<Link href="https://docs.unsend.dev" target="_blank">
|
|
<BookOpenText />
|
|
<span>Docs</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
<NavUser
|
|
user={{
|
|
name: session?.user.name || "",
|
|
email: session?.user.email || "",
|
|
avatar: session?.user.image || "",
|
|
}}
|
|
/>
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
export function NavUser({
|
|
user,
|
|
}: {
|
|
user: {
|
|
name?: string | null;
|
|
email?: string | null;
|
|
avatar?: string | null;
|
|
};
|
|
}) {
|
|
const { isMobile } = useSidebar();
|
|
|
|
return (
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<SidebarMenuButton
|
|
size="lg"
|
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
>
|
|
<Avatar className="h-8 w-8 rounded-lg">
|
|
{user.avatar ? (
|
|
<AvatarImage
|
|
src={user.avatar}
|
|
alt={user.name ?? user.email ?? ""}
|
|
/>
|
|
) : null}
|
|
<AvatarFallback className="rounded-lg capitalize">
|
|
{user.name?.charAt(0) ?? user.email?.charAt(0) ?? ""}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
<span className="truncate font-medium">
|
|
{user.name ?? user.email ?? ""}
|
|
</span>
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
{user.name ? user.email : ""}
|
|
</span>
|
|
</div>
|
|
<MoreVerticalIcon className="ml-auto size-4" />
|
|
</SidebarMenuButton>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-xl"
|
|
side={isMobile ? "bottom" : "top"}
|
|
sideOffset={4}
|
|
>
|
|
<DropdownMenuLabel className="p-0 font-normal">
|
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
<Avatar className="h-8 w-8 rounded-lg">
|
|
{user.avatar ? (
|
|
<AvatarImage
|
|
src={user.avatar}
|
|
alt={user.name ?? user.email ?? ""}
|
|
/>
|
|
) : null}
|
|
<AvatarFallback className="rounded-lg capitalize">
|
|
{user.name?.charAt(0) ?? user.email?.charAt(0) ?? ""}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
<span className="truncate font-medium">
|
|
{user.name ?? user.email ?? ""}
|
|
</span>
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
{user.name ? user.email : ""}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/settings/team">
|
|
<UsersIcon />
|
|
Team
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/settings">
|
|
<GaugeIcon />
|
|
Usage
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<div className="px-2 py-0.5">
|
|
<ThemeSwitcher />
|
|
</div>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => signOut()}>
|
|
<LogOutIcon />
|
|
Log out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
);
|
|
}
|