feat: add new theme (#157)
This commit is contained in:
236
apps/web/src/components/AppSideBar.tsx
Normal file
236
apps/web/src/components/AppSideBar.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BookUser,
|
||||
Calendar,
|
||||
Code,
|
||||
Cog,
|
||||
Globe,
|
||||
Home,
|
||||
Inbox,
|
||||
LayoutDashboard,
|
||||
LayoutTemplate,
|
||||
LogOut,
|
||||
Mail,
|
||||
Search,
|
||||
Server,
|
||||
Settings,
|
||||
Volume2,
|
||||
BookOpenText,
|
||||
ChartColumnBig,
|
||||
ChartArea,
|
||||
} 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";
|
||||
|
||||
// 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 onClick={() => signOut()} tooltip="Logout">
|
||||
<LogOut />
|
||||
<span>Logout</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Docs">
|
||||
<Link href="https://docs.unsend.dev" target="_blank">
|
||||
<BookOpenText />
|
||||
<span>Docs</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem className="px-2">
|
||||
<ThemeSwitcher />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
@@ -123,11 +123,11 @@ export default function JoinTeam({
|
||||
<DialogTitle>Accept Team Invitation</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to join{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
<span className="font-semibold text-foreground">
|
||||
{selectedInvite?.team.name}
|
||||
</span>
|
||||
? You will be added as a{" "}
|
||||
<span className="font-semibold text-primary lowercase">
|
||||
<span className="font-semibold text-foreground lowercase">
|
||||
{selectedInvite?.role.toLowerCase()}
|
||||
</span>
|
||||
.
|
||||
|
@@ -3,19 +3,22 @@ import { Button } from "@unsend/ui/src/button";
|
||||
import { Monitor, Sun, Moon, SunMoonIcon } from "lucide-react";
|
||||
|
||||
export const ThemeSwitcher = () => {
|
||||
const { theme, setTheme, systemTheme } = useTheme();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center justify-between w-full">
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<p className="text-sm text-sidebar-foreground flex items-center gap-2">
|
||||
<SunMoonIcon className="h-4 w-4" />
|
||||
Theme
|
||||
</p>
|
||||
<div className="flex gap-2 border rounded-md p-0.5 ">
|
||||
<div className="flex gap-2 border rounded-md p-1 ">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("p-0.5 h-5 w-5", theme === "system" ? "bg-muted" : "")}
|
||||
className={cn(
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "system" ? " bg-muted" : ""
|
||||
)}
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
<Monitor className="h-3 w-3" />
|
||||
@@ -24,8 +27,8 @@ export const ThemeSwitcher = () => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"p-0.5 h-5 w-5",
|
||||
theme === "light" ? " bg-gray-200" : ""
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "light" ? " bg-muted" : ""
|
||||
)}
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
@@ -34,7 +37,10 @@ export const ThemeSwitcher = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("p-0.5 h-5 w-5", theme === "dark" ? "bg-muted" : "")}
|
||||
className={cn(
|
||||
"p-0.5 rounded-[0.20rem] h-5 w-5",
|
||||
theme === "dark" ? "bg-muted" : ""
|
||||
)}
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
<Moon className="h-3 w-3" />
|
||||
@@ -43,3 +49,34 @@ export const ThemeSwitcher = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MiniThemeSwitcher = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const cycleTheme = () => {
|
||||
if (theme === "light") {
|
||||
setTheme("dark");
|
||||
} else if (theme === "dark") {
|
||||
setTheme("system");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
};
|
||||
|
||||
const renderIcon = () => {
|
||||
switch (theme) {
|
||||
case "light":
|
||||
return <Sun className="h-4 w-4" />;
|
||||
case "dark":
|
||||
return <Moon className="h-4 w-4" />;
|
||||
default:
|
||||
return <Monitor className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="ghost" size="icon" onClick={cycleTheme}>
|
||||
{renderIcon()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user