feat: add new theme (#157)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -28,7 +28,7 @@ export default function CampaignDetailsPage({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Spinner className="w-5 h-5 text-primary" />
|
||||
<Spinner className="w-5 h-5 text-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export default function CampaignDetailsPage({
|
||||
<div className="capitalize">{card.status.toLowerCase()}</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="text-primary font-light text-2xl font-mono">
|
||||
<div className="text-foreground font-light text-2xl font-mono">
|
||||
{card.count}
|
||||
</div>
|
||||
{card.status !== "total" ? (
|
||||
|
@@ -89,7 +89,7 @@ export default function CampaignList() {
|
||||
<TableRow key={campaign.id} className="">
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
className="underline underline-offset-4 decoration-dashed text-foreground hover:text-primary"
|
||||
className="underline underline-offset-4 decoration-dashed text-foreground hover:text-foreground"
|
||||
href={
|
||||
campaign.status === CampaignStatus.DRAFT
|
||||
? `/campaigns/${campaign.id}/edit`
|
||||
@@ -154,170 +154,3 @@ export default function CampaignList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// "use client";
|
||||
|
||||
// import {
|
||||
// Table,
|
||||
// TableHeader,
|
||||
// TableRow,
|
||||
// TableHead,
|
||||
// TableBody,
|
||||
// TableCell,
|
||||
// } from "@unsend/ui/src/table";
|
||||
// import { api } from "~/trpc/react";
|
||||
// import { useUrlState } from "~/hooks/useUrlState";
|
||||
// import { Button } from "@unsend/ui/src/button";
|
||||
// import Spinner from "@unsend/ui/src/spinner";
|
||||
// import { formatDistanceToNow } from "date-fns";
|
||||
// import { CampaignStatus } from "@prisma/client";
|
||||
// import DeleteCampaign from "./delete-campaign";
|
||||
// import Link from "next/link";
|
||||
// import DuplicateCampaign from "./duplicate-campaign";
|
||||
// import { motion } from "framer-motion";
|
||||
// import { useRouter } from "next/navigation";
|
||||
// import {
|
||||
// Select,
|
||||
// SelectTrigger,
|
||||
// SelectContent,
|
||||
// SelectItem,
|
||||
// } from "@unsend/ui/src/select";
|
||||
|
||||
// export default function CampaignList() {
|
||||
// const [page, setPage] = useUrlState("page", "1");
|
||||
// const [status, setStatus] = useUrlState("status");
|
||||
|
||||
// const pageNumber = Number(page);
|
||||
|
||||
// const campaignsQuery = api.campaign.getCampaigns.useQuery({
|
||||
// page: pageNumber,
|
||||
// status: status as CampaignStatus | null,
|
||||
// });
|
||||
|
||||
// const router = useRouter();
|
||||
|
||||
// return (
|
||||
// <div className="mt-10 flex flex-col gap-4">
|
||||
// <div className="flex justify-end">
|
||||
// <Select
|
||||
// value={status ?? "all"}
|
||||
// onValueChange={(val) => setStatus(val === "all" ? null : val)}
|
||||
// >
|
||||
// <SelectTrigger className="w-[180px] capitalize">
|
||||
// {status ? status.toLowerCase() : "All statuses"}
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
// <SelectItem value="all" className=" capitalize">
|
||||
// All statuses
|
||||
// </SelectItem>
|
||||
// <SelectItem value={CampaignStatus.DRAFT} className=" capitalize">
|
||||
// Draft
|
||||
// </SelectItem>
|
||||
// <SelectItem
|
||||
// value={CampaignStatus.SCHEDULED}
|
||||
// className=" capitalize"
|
||||
// >
|
||||
// Scheduled
|
||||
// </SelectItem>
|
||||
// <SelectItem value={CampaignStatus.SENT} className=" capitalize">
|
||||
// Sent
|
||||
// </SelectItem>
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
// </div>
|
||||
|
||||
// {campaignsQuery.isLoading ? (
|
||||
// <div className="flex justify-center items-center mt-20">
|
||||
// <Spinner
|
||||
// className="w-5 h-5 text-primary"
|
||||
// innerSvgClass="stroke-primary"
|
||||
// />
|
||||
// </div>
|
||||
// ) : (
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 ">
|
||||
// {campaignsQuery.data?.campaigns.map((campaign) => (
|
||||
// <motion.div
|
||||
// whileHover={{ scale: 1.01 }}
|
||||
// transition={{ type: "spring", stiffness: 600, damping: 10 }}
|
||||
// whileTap={{ scale: 0.99 }}
|
||||
// className="border rounded-xl shadow hover:shadow-lg"
|
||||
// key={campaign.id}
|
||||
// >
|
||||
// <div className="flex flex-col">
|
||||
// <Link
|
||||
// href={
|
||||
// campaign.status === CampaignStatus.DRAFT
|
||||
// ? `/campaigns/${campaign.id}/edit`
|
||||
// : `/campaigns/${campaign.id}`
|
||||
// }
|
||||
// >
|
||||
// <div className="h-40 overflow-hidden flex justify-center rounded-t-xl bg-muted/10">
|
||||
// <div
|
||||
// className="transform scale-[0.5] "
|
||||
// dangerouslySetInnerHTML={{ __html: campaign.html ?? "" }}
|
||||
// />
|
||||
// </div>
|
||||
// </Link>
|
||||
|
||||
// <div className="flex justify-between items-center shadow-[0px_-5px_25px_-8px_rgba(0,0,0,0.3)] rounded-xl -mt-2 z-10 bg-background">
|
||||
// <div
|
||||
// className="cursor-pointer w-full py-3 pl-4 flex gap-2 items-start"
|
||||
// onClick={() => router.push(`/campaigns/${campaign.id}`)}
|
||||
// >
|
||||
// <div className="flex flex-col gap-2">
|
||||
// <div className="flex gap-4">
|
||||
// <div className="font-semibold text-sm">
|
||||
// {campaign.name}
|
||||
// </div>
|
||||
// <div
|
||||
// className={`text-center px-4 rounded capitalize py-0.5 text-xs ${
|
||||
// campaign.status === CampaignStatus.DRAFT
|
||||
// ? "bg-gray-500/15 dark:bg-gray-600/10 text-gray-700 dark:text-gray-600/90 border border-gray-500/25 dark:border-gray-700/25"
|
||||
// : campaign.status === CampaignStatus.SENT
|
||||
// ? "bg-green-500/15 dark:bg-green-600/10 text-green-700 dark:text-green-600/90 border border-green-500/25 dark:border-green-700/25"
|
||||
// : "bg-yellow-500/15 dark:bg-yellow-600/10 text-yellow-700 dark:text-yellow-600/90 border border-yellow-500/25 dark:border-yellow-700/25"
|
||||
// }`}
|
||||
// >
|
||||
// {campaign.status.toLowerCase()}
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="text-muted-foreground text-xs">
|
||||
// {formatDistanceToNow(campaign.createdAt, {
|
||||
// addSuffix: true,
|
||||
// })}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="flex gap-2 pr-4">
|
||||
// <DuplicateCampaign campaign={campaign} />
|
||||
// <DeleteCampaign campaign={campaign} />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </motion.div>
|
||||
// ))}
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {campaignsQuery.data?.totalPage && campaignsQuery.data.totalPage > 1 ? (
|
||||
// <div className="flex gap-4 justify-end">
|
||||
// <Button
|
||||
// size="sm"
|
||||
// onClick={() => setPage((pageNumber - 1).toString())}
|
||||
// disabled={pageNumber === 1}
|
||||
// >
|
||||
// Previous
|
||||
// </Button>
|
||||
// <Button
|
||||
// size="sm"
|
||||
// onClick={() => setPage((pageNumber + 1).toString())}
|
||||
// disabled={pageNumber >= (campaignsQuery.data?.totalPage ?? 0)}
|
||||
// >
|
||||
// Next
|
||||
// </Button>
|
||||
// </div>
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
@@ -83,8 +83,10 @@ export const DeleteCampaign: React.FC<{
|
||||
<DialogTitle>Delete Campaign</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">{campaign.name}</span>?
|
||||
You can't reverse this.
|
||||
<span className="font-semibold text-foreground">
|
||||
{campaign.name}
|
||||
</span>
|
||||
? You can't reverse this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -54,7 +54,10 @@ export const DuplicateCampaign: React.FC<{
|
||||
<DialogTitle>Duplicate Campaign</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to duplicate{" "}
|
||||
<span className="font-semibold text-primary">{campaign.name}</span>?
|
||||
<span className="font-semibold text-foreground">
|
||||
{campaign.name}
|
||||
</span>
|
||||
?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -87,8 +87,10 @@ export const DeleteContact: React.FC<{
|
||||
<DialogTitle>Delete Contact</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">{contact.email}</span>?
|
||||
You can't reverse this.
|
||||
<span className="font-semibold text-foreground">
|
||||
{contact.email}
|
||||
</span>
|
||||
? You can't reverse this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -145,7 +145,7 @@ export const EditContact: React.FC<{
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="data-[state=checked]:bg-emerald-500"
|
||||
className="data-[state=checked]:bg-success"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
|
@@ -86,7 +86,7 @@ export const DeleteContactBook: React.FC<{
|
||||
<DialogTitle>Delete Contact Book</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
<span className="font-semibold text-foreground">
|
||||
{contactBook.name}
|
||||
</span>
|
||||
? You can't reverse this.
|
||||
|
@@ -78,7 +78,7 @@ export const EditContactBook: React.FC<{
|
||||
className="p-0 hover:bg-transparent"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Edit className="h-4 w-4 text-primary/80 hover:text-primary/70" />
|
||||
<Edit className="h-4 w-4 text-foreground/80 hover:text-foreground/70" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
|
@@ -1,211 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { LogoutButton, NavButton } from "./nav-button";
|
||||
import {
|
||||
BookOpenText,
|
||||
BookUser,
|
||||
CircleUser,
|
||||
Code,
|
||||
Cog,
|
||||
Globe,
|
||||
Home,
|
||||
LayoutDashboard,
|
||||
LayoutTemplate,
|
||||
LineChart,
|
||||
Mail,
|
||||
Menu,
|
||||
Package,
|
||||
Package2,
|
||||
Server,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
Volume2,
|
||||
} from "lucide-react";
|
||||
import { env } from "~/env";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@unsend/ui/src/dropdown-menu";
|
||||
import { ThemeSwitcher } from "~/components/theme/ThemeSwitcher";
|
||||
import { isCloud, isSelfHosted } from "~/utils/common";
|
||||
import { AppSidebar } from "~/components/AppSideBar";
|
||||
import { SidebarInset, SidebarTrigger } from "@unsend/ui/src/sidebar";
|
||||
import { SidebarProvider } from "@unsend/ui/src/sidebar";
|
||||
import { useIsMobile } from "@unsend/ui/src/hooks/use-mobile";
|
||||
|
||||
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const { data: session } = useSession();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full h-full">
|
||||
<div className="hidden bg-muted/20 md:block md:w-[230px]">
|
||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||
<div className="flex h-14 gap-4 items-center px-4 lg:h-[60px] lg:px-6">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold">
|
||||
<span className=" text-lg">Unsend</span>
|
||||
</Link>
|
||||
<span className="text-[10px] text-muted-foreground bg-muted p-0.5 px-2 rounded-full">
|
||||
Early access
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 h-full">
|
||||
<nav className=" flex-1 h-full flex-col justify-between items-center px-2 text-sm font-medium lg:px-4">
|
||||
<div className="h-[calc(100%-120px)]">
|
||||
<NavButton href="/dashboard">
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Dashboard
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/emails">
|
||||
<Mail className="h-4 w-4" />
|
||||
Emails
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/contacts">
|
||||
<BookUser className="h-4 w-4" />
|
||||
Contacts
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/templates">
|
||||
<LayoutTemplate className="h-4 w-4" />
|
||||
Templates
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/campaigns">
|
||||
<Volume2 className="h-4 w-4" />
|
||||
Campaigns
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/domains">
|
||||
<Globe className="h-4 w-4" />
|
||||
Domains
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/dev-settings">
|
||||
<Code className="h-4 w-4" />
|
||||
Developer settings
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/settings">
|
||||
<Cog className="h-4 w-4" />
|
||||
Settings
|
||||
</NavButton>
|
||||
|
||||
{isSelfHosted() || session?.user.isAdmin ? (
|
||||
<NavButton href="/admin">
|
||||
<Server className="h-4 w-4" />
|
||||
Admin
|
||||
</NavButton>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="pl-4 flex flex-col gap-2 w-full">
|
||||
<Link
|
||||
href="https://docs.unsend.dev"
|
||||
target="_blank"
|
||||
className="flex gap-2 items-center hover:text-primary text-muted-foreground"
|
||||
>
|
||||
<BookOpenText className="h-4 w-4" />
|
||||
<span className="">Docs</span>
|
||||
</Link>
|
||||
<LogoutButton />
|
||||
<div>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="mt-auto p-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<header className=" h-14 items-center gap-4 hidden bg-muted/20 px-4 lg:h-[60px] lg:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col">
|
||||
<nav className="grid gap-2 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl bg-muted px-3 py-2 text-foreground hover:text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="mt-auto"></div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
<CircleUser className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto h-full">
|
||||
<div className="flex flex-col gap-4 p-4 w-full lg:max-w-6xl mx-auto lg:gap-6 lg:p-6">
|
||||
<div className="h-full bg-sidebar-background">
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<main className="flex-1 overflow-auto h-full p-4 px-40">
|
||||
{isMobile ? (
|
||||
<SidebarTrigger className="h-5 w-5 text-muted-foreground" />
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -235,11 +235,13 @@ const DashboardItemCard: React.FC<DashboardItemCardProps> = ({
|
||||
<div className=" capitalize">{status.toLowerCase()}</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="text-primary font-light text-2xl font-mono">
|
||||
<div className="text-foreground font-light text-2xl font-mono">
|
||||
{count}
|
||||
</div>
|
||||
{status !== "total" ? (
|
||||
<div className="text-sm pb-1">{count > 0 ? (percentage * 100).toFixed(0) : 0}%</div>
|
||||
<div className="text-sm pb-1">
|
||||
{count > 0 ? (percentage * 100).toFixed(0) : 0}%
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -83,8 +83,8 @@ export const DeleteApiKey: React.FC<{
|
||||
<DialogTitle>Delete API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">{apiKey.name}</span>?
|
||||
You can't reverse this.
|
||||
<span className="font-semibold text-foreground">{apiKey.name}</span>
|
||||
? You can't reverse this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -15,9 +15,9 @@ export const SettingsNavButton: React.FC<{
|
||||
|
||||
if (comingSoon) {
|
||||
return (
|
||||
<div className="flex items-center justify-between hover:text-primary cursor-not-allowed mt-1">
|
||||
<div className="flex items-center justify-between hover:text-foreground cursor-not-allowed mt-1">
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary cursor-not-allowed ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-foreground cursor-not-allowed ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@ export const SettingsNavButton: React.FC<{
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex text-sm items-center mt-1 gap-3 rounded px-2 py-1 transition-all hover:text-primary ${isActive ? " bg-accent" : "text-muted-foreground"}`}
|
||||
className={`flex text-sm items-center mt-1 gap-3 rounded px-2 py-1 transition-all hover:text-foreground ${isActive ? " bg-accent" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
|
@@ -5,9 +5,10 @@ import { Input } from "@unsend/ui/src/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription, DialogHeader,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
|
||||
import {
|
||||
@@ -84,8 +85,8 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
<DialogTitle>Delete domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">{domain.name}</span>?
|
||||
You can't reverse this.
|
||||
<span className="font-semibold text-foreground">{domain.name}</span>
|
||||
? You can't reverse this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...domainForm}>
|
||||
|
@@ -274,7 +274,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
<Switch
|
||||
checked={clickTracking}
|
||||
onCheckedChange={handleClickTrackingChange}
|
||||
className="data-[state=checked]:bg-emerald-500"
|
||||
className="data-[state=checked]:bg-success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -288,7 +288,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
<Switch
|
||||
checked={openTracking}
|
||||
onCheckedChange={handleOpenTrackingChange}
|
||||
className="data-[state=checked]:bg-emerald-500"
|
||||
className="data-[state=checked]:bg-success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@@ -104,7 +104,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
<Switch
|
||||
checked={clickTracking}
|
||||
onCheckedChange={handleClickTrackingChange}
|
||||
className="data-[state=checked]:bg-emerald-500"
|
||||
className="data-[state=checked]:bg-success"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
@@ -112,7 +112,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
<Switch
|
||||
checked={openTracking}
|
||||
onCheckedChange={handleOpenTrackingChange}
|
||||
className="data-[state=checked]:bg-emerald-500"
|
||||
className="data-[state=checked]:bg-success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -115,7 +115,7 @@ export default function EmailDetails({ emailId }: { emailId: string }) {
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
{formatDate(evt.createdAt, "MMM dd, hh:mm a")}
|
||||
</div>
|
||||
<div className="mt-1 text-primary/80">
|
||||
<div className="mt-1 text-foreground/80">
|
||||
<EmailStatusText
|
||||
status={evt.status}
|
||||
data={evt.data}
|
||||
|
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { LogOut } from "lucide-react";
|
||||
import { signOut } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export const NavButton: React.FC<{
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
comingSoon?: boolean;
|
||||
}> = ({ href, children, comingSoon }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = pathname?.startsWith(href);
|
||||
|
||||
if (comingSoon) {
|
||||
return (
|
||||
<div className="flex items-center justify-between hover:text-primary cursor-not-allowed mt-1">
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary cursor-not-allowed ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="text-muted-foreground px-4 py-0.5 text-xs bg-muted rounded-full">
|
||||
soon
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex items-center mt-1 gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogoutButton: React.FC = () => {
|
||||
return (
|
||||
<button
|
||||
className={` w-full justify-start flex items-center gap-2 rounded-lg py-2 transition-all hover:text-primary text-muted-foreground`}
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
);
|
||||
};
|
@@ -55,7 +55,10 @@ export const DeleteTeamInvite: React.FC<{
|
||||
<DialogTitle>Cancel Invite</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to cancel the invite for{" "}
|
||||
<span className="font-semibold text-primary">{invite.email}</span>?
|
||||
<span className="font-semibold text-foreground">
|
||||
{invite.email}
|
||||
</span>
|
||||
?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
|
@@ -83,8 +83,10 @@ export const DeleteTemplate: React.FC<{
|
||||
<DialogTitle>Delete Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-semibold text-primary">{template.name}</span>?
|
||||
You can't reverse this.
|
||||
<span className="font-semibold text-foreground">
|
||||
{template.name}
|
||||
</span>
|
||||
? You can't reverse this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -54,7 +54,10 @@ export const DuplicateTemplate: React.FC<{
|
||||
<DialogTitle>Duplicate Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to duplicate{" "}
|
||||
<span className="font-semibold text-primary">{template.name}</span>?
|
||||
<span className="font-semibold text-foreground">
|
||||
{template.name}
|
||||
</span>
|
||||
?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
|
@@ -57,7 +57,7 @@ export default function TemplateList() {
|
||||
<TableRow key={template.id} className="">
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
className="underline underline-offset-4 decoration-dashed text-foreground hover:text-primary"
|
||||
className="underline underline-offset-4 decoration-dashed text-foreground hover:text-foreground"
|
||||
href={`/templates/${template.id}/edit`}
|
||||
>
|
||||
{template.name}
|
||||
|
@@ -24,8 +24,8 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`font-sans ${inter.variable} app`}>
|
||||
<html lang="en" suppressHydrationWarning className="bg-sidebar-background">
|
||||
<body className={`font-sans ${inter.variable} app bg-sidebar-background`}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<Toaster />
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
|
@@ -130,7 +130,7 @@ export default function LoginPage({
|
||||
{isSignup ? "Already have an account?" : "New to Unsend?"}
|
||||
<Link
|
||||
href={isSignup ? "/login" : "/signup"}
|
||||
className=" text-primary hover:underline ml-1"
|
||||
className=" text-foreground hover:underline ml-1"
|
||||
>
|
||||
{isSignup ? "Sign in" : "Create new account"}
|
||||
</Link>
|
||||
|
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