first
This commit is contained in:
commit
8538ab969a
434 changed files with 44051 additions and 0 deletions
33
apps/web/modules/saas/shared/components/AppWrapper.tsx
Normal file
33
apps/web/modules/saas/shared/components/AppWrapper.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { config } from "@repo/config";
|
||||
import { NavBar } from "@saas/shared/components/NavBar";
|
||||
import { cn } from "@ui/lib";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export function AppWrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-[radial-gradient(farthest-corner_at_0%_0%,color-mix(in_oklch,var(--color-primary),transparent_95%)_0%,var(--color-background)_50%)] dark:bg-[radial-gradient(farthest-corner_at_0%_0%,color-mix(in_oklch,var(--color-primary),transparent_90%)_0%,var(--color-background)_50%)]",
|
||||
[config.ui.saas.useSidebarLayout ? "" : ""],
|
||||
)}
|
||||
>
|
||||
<NavBar />
|
||||
<div
|
||||
className={cn(" md:pr-4 py-4 flex", [
|
||||
config.ui.saas.useSidebarLayout
|
||||
? "min-h-[calc(100vh)] md:ml-[280px]"
|
||||
: "",
|
||||
])}
|
||||
>
|
||||
<main
|
||||
className={cn(
|
||||
"py-6 border rounded-2xl bg-card px-4 md:p-8 min-h-full w-full",
|
||||
[config.ui.saas.useSidebarLayout ? "" : ""],
|
||||
)}
|
||||
>
|
||||
<div className="container px-0">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
apps/web/modules/saas/shared/components/AuthWrapper.tsx
Normal file
49
apps/web/modules/saas/shared/components/AuthWrapper.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { config } from "@repo/config";
|
||||
import { Footer } from "@saas/shared/components/Footer";
|
||||
import { ColorModeToggle } from "@shared/components/ColorModeToggle";
|
||||
import { LocaleSwitch } from "@shared/components/LocaleSwitch";
|
||||
import { Logo } from "@shared/components/Logo";
|
||||
import { cn } from "@ui/lib";
|
||||
import Link from "next/link";
|
||||
import { type PropsWithChildren, Suspense } from "react";
|
||||
|
||||
export function AuthWrapper({
|
||||
children,
|
||||
contentClass,
|
||||
}: PropsWithChildren<{ contentClass?: string }>) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full py-6">
|
||||
<div className="flex w-full flex-col items-center justify-between gap-8">
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="block">
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{config.i18n.enabled && (
|
||||
<Suspense>
|
||||
<LocaleSwitch withLocaleInUrl={false} />
|
||||
</Suspense>
|
||||
)}
|
||||
<ColorModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container flex justify-center">
|
||||
<main
|
||||
className={cn(
|
||||
"w-full max-w-md rounded-3xl bg-card p-6 border lg:p-8",
|
||||
contentClass,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@ui/components/alert-dialog";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type ConfirmOptions = {
|
||||
title: string;
|
||||
message?: string;
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: string;
|
||||
destructive?: boolean;
|
||||
onConfirm: () => Promise<void> | void;
|
||||
};
|
||||
const ConfirmationAlertContext = createContext<{
|
||||
confirm: (options: ConfirmOptions) => void;
|
||||
}>({
|
||||
confirm: async () => false,
|
||||
});
|
||||
|
||||
export function ConfirmationAlertProvider({ children }: PropsWithChildren) {
|
||||
const t = useTranslations();
|
||||
const [confirmOptions, setConfirmOptions] = useState<ConfirmOptions | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const confirm = (options: ConfirmOptions) => {
|
||||
setConfirmOptions(options);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationAlertContext.Provider value={{ confirm }}>
|
||||
{children}
|
||||
|
||||
<AlertDialog
|
||||
open={!!confirmOptions}
|
||||
onOpenChange={(open) =>
|
||||
setConfirmOptions(open ? confirmOptions : null)
|
||||
}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{confirmOptions?.title}
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
{confirmOptions?.message}
|
||||
</AlertDialogDescription>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{confirmOptions?.cancelLabel ??
|
||||
t("common.confirmation.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
variant={
|
||||
confirmOptions?.destructive
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
onClick={async () => {
|
||||
await confirmOptions?.onConfirm();
|
||||
setConfirmOptions(null);
|
||||
}}
|
||||
>
|
||||
{confirmOptions?.confirmLabel ??
|
||||
t("common.confirmation.confirm")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</ConfirmationAlertContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useConfirmationAlert = () => {
|
||||
const context = useContext(ConfirmationAlertContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useConfirmationAlert must be used within a ConfirmationAlertProvider",
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
20
apps/web/modules/saas/shared/components/Footer.tsx
Normal file
20
apps/web/modules/saas/shared/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { LocaleLink } from "@i18n/routing";
|
||||
import { cn } from "@ui/lib";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"container max-w-6xl py-6 text-center text-foreground/60 text-xs",
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
<a href="https://supastarter.dev">Built with supastarter</a>
|
||||
</span>
|
||||
<span className="opacity-50"> | </span>
|
||||
<LocaleLink href="/legal/privacy-policy">Privacy policy</LocaleLink>
|
||||
<span className="opacity-50"> | </span>
|
||||
<LocaleLink href="/legal/terms">Terms and conditions</LocaleLink>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
189
apps/web/modules/saas/shared/components/NavBar.tsx
Normal file
189
apps/web/modules/saas/shared/components/NavBar.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
"use client";
|
||||
import { config } from "@repo/config";
|
||||
import { useSession } from "@saas/auth/hooks/use-session";
|
||||
import { useActiveOrganization } from "@saas/organizations/hooks/use-active-organization";
|
||||
import { UserMenu } from "@saas/shared/components/UserMenu";
|
||||
import { Logo } from "@shared/components/Logo";
|
||||
import { cn } from "@ui/lib";
|
||||
import {
|
||||
BotMessageSquareIcon,
|
||||
ChevronRightIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
UserCog2Icon,
|
||||
UserCogIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { OrganzationSelect } from "../../organizations/components/OrganizationSelect";
|
||||
|
||||
export function NavBar() {
|
||||
const t = useTranslations();
|
||||
const pathname = usePathname();
|
||||
const { user } = useSession();
|
||||
const { activeOrganization } = useActiveOrganization();
|
||||
|
||||
const { useSidebarLayout } = config.ui.saas;
|
||||
|
||||
const basePath = activeOrganization
|
||||
? `/app/${activeOrganization.slug}`
|
||||
: "/app";
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: t("app.menu.start"),
|
||||
href: basePath,
|
||||
icon: HomeIcon,
|
||||
isActive: pathname === basePath,
|
||||
},
|
||||
{
|
||||
label: t("app.menu.aiChatbot"),
|
||||
href: activeOrganization
|
||||
? `/app/${activeOrganization.slug}/chatbot`
|
||||
: "/app/chatbot",
|
||||
icon: BotMessageSquareIcon,
|
||||
isActive: pathname.includes("/chatbot"),
|
||||
},
|
||||
...(activeOrganization
|
||||
? [
|
||||
{
|
||||
label: t("app.menu.organizationSettings"),
|
||||
href: `${basePath}/settings`,
|
||||
icon: SettingsIcon,
|
||||
isActive: pathname.startsWith(`${basePath}/settings/`),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: t("app.menu.accountSettings"),
|
||||
href: "/app/settings",
|
||||
icon: UserCog2Icon,
|
||||
isActive: pathname.startsWith("/app/settings/"),
|
||||
},
|
||||
...(user?.role === "admin"
|
||||
? [
|
||||
{
|
||||
label: t("app.menu.admin"),
|
||||
href: "/app/admin",
|
||||
icon: UserCogIcon,
|
||||
isActive: pathname.startsWith("/app/admin/"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn("w-full", {
|
||||
"w-full md:fixed md:top-0 md:left-0 md:h-full md:w-[280px]":
|
||||
useSidebarLayout,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn("container max-w-6xl py-4", {
|
||||
"container max-w-6xl py-4 md:flex md:h-full md:flex-col md:px-6 md:pt-6 md:pb-0":
|
||||
useSidebarLayout,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div
|
||||
className={cn("flex items-center gap-4 md:gap-2", {
|
||||
"md:flex md:w-full md:flex-col md:items-stretch md:align-stretch":
|
||||
useSidebarLayout,
|
||||
})}
|
||||
>
|
||||
<Link href="/app" className="block">
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
{config.organizations.enable &&
|
||||
!config.organizations.hideOrganization && (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"hidden opacity-30 md:block",
|
||||
{
|
||||
"md:hidden": useSidebarLayout,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</span>
|
||||
|
||||
<OrganzationSelect
|
||||
className={cn({
|
||||
"md:-mx-2 md:mt-2":
|
||||
useSidebarLayout,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"mr-0 ml-auto flex items-center justify-end gap-4",
|
||||
{
|
||||
"md:hidden": useSidebarLayout,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
className={cn(
|
||||
"no-scrollbar -mx-4 -mb-4 mt-6 flex list-none items-center justify-start gap-4 overflow-x-auto px-4 text-sm",
|
||||
{
|
||||
"md:mx-0 md:my-4 md:flex md:flex-col md:items-stretch md:gap-1 md:px-0":
|
||||
useSidebarLayout,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{menuItems.map((menuItem) => (
|
||||
<li key={menuItem.href}>
|
||||
<Link
|
||||
href={menuItem.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 whitespace-nowrap border-b-2 px-1 pb-3",
|
||||
[
|
||||
menuItem.isActive
|
||||
? "border-primary font-bold"
|
||||
: "border-transparent",
|
||||
],
|
||||
{
|
||||
"md:-mx-6 md:border-b-0 md:border-l-2 md:px-6 md:py-2":
|
||||
useSidebarLayout,
|
||||
},
|
||||
)}
|
||||
prefetch
|
||||
>
|
||||
<menuItem.icon
|
||||
className={`size-4 shrink-0 ${
|
||||
menuItem.isActive
|
||||
? "text-primary"
|
||||
: "opacity-50"
|
||||
}`}
|
||||
/>
|
||||
<span>{menuItem.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"-mx-4 md:-mx-6 mt-auto mb-0 hidden p-4 md:p-4",
|
||||
{
|
||||
"md:block": useSidebarLayout,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<UserMenu showUserName />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
16
apps/web/modules/saas/shared/components/PageHeader.tsx
Normal file
16
apps/web/modules/saas/shared/components/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"use client";
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h2 className="font-bold text-2xl lg:text-3xl">{title}</h2>
|
||||
<p className="mt-1 opacity-60">{subtitle}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
apps/web/modules/saas/shared/components/Pagination.tsx
Normal file
53
apps/web/modules/saas/shared/components/Pagination.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Button } from "@ui/components/button";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
|
||||
export type PaginatioProps = {
|
||||
className?: string;
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
currentPage: number;
|
||||
onChangeCurrentPage: (page: number) => void;
|
||||
};
|
||||
|
||||
const Pagination = ({
|
||||
currentPage,
|
||||
totalItems,
|
||||
itemsPerPage,
|
||||
className,
|
||||
onChangeCurrentPage,
|
||||
}: PaginatioProps) => {
|
||||
const numberOfPages = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onChangeCurrentPage(currentPage - 1)}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{currentPage * itemsPerPage - itemsPerPage + 1} -{" "}
|
||||
{currentPage * itemsPerPage > totalItems
|
||||
? totalItems
|
||||
: currentPage * itemsPerPage}{" "}
|
||||
of {totalItems}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={currentPage === numberOfPages}
|
||||
onClick={() => onChangeCurrentPage(currentPage + 1)}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Pagination.displayName = "Pagination";
|
||||
|
||||
export { Pagination };
|
||||
37
apps/web/modules/saas/shared/components/SettingsItem.tsx
Normal file
37
apps/web/modules/saas/shared/components/SettingsItem.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Card } from "@ui/components/card";
|
||||
import { cn } from "@ui/lib";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
export function SettingsItem({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
danger,
|
||||
}: PropsWithChildren<{
|
||||
title: string | ReactNode;
|
||||
description?: string | ReactNode;
|
||||
danger?: boolean;
|
||||
}>) {
|
||||
return (
|
||||
<Card className="@container p-4 rounded-2xl border md:p-6">
|
||||
<div className="grid @-xl:grid-cols-[min(100%/3,280px)_auto] grid-cols-1 @xl:gap-8 gap-4">
|
||||
<div className="flex shrink-0 flex-col gap-1.5">
|
||||
<h3
|
||||
className={cn(
|
||||
"m-0 font-semibold leading-tight",
|
||||
danger && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
{description && (
|
||||
<p className="m-0 text-foreground/60 text-xs">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
13
apps/web/modules/saas/shared/components/SettingsList.tsx
Normal file
13
apps/web/modules/saas/shared/components/SettingsList.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export function SettingsList({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="@container flex flex-col gap-4">
|
||||
{Array.isArray(children)
|
||||
? children.filter(Boolean).map((child, i) => {
|
||||
return <div key={`settings-item-${i}`}>{child}</div>;
|
||||
})
|
||||
: children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
export function SidebarContentLayout({
|
||||
children,
|
||||
sidebar,
|
||||
}: { children: React.ReactNode; sidebar: ReactNode }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex flex-col items-start gap-4 lg:flex-row lg:gap-8">
|
||||
<div className="top-4 w-full lg:sticky lg:max-w-[180px]">
|
||||
{sidebar}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
apps/web/modules/saas/shared/components/TabGroup.tsx
Normal file
36
apps/web/modules/saas/shared/components/TabGroup.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function TabGroup({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: { label: string; href: string; segment: string }[];
|
||||
className?: string;
|
||||
}) {
|
||||
const selectedSegment = useSelectedLayoutSegment();
|
||||
const activeItem = useMemo(() => {
|
||||
return items.find((item) => item.segment === selectedSegment);
|
||||
}, [items, selectedSegment]);
|
||||
|
||||
return (
|
||||
<div className={` flex border-b-2 ${className}`}>
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`-mb-0.5 block border-b-2 px-6 py-3 ${
|
||||
item === activeItem
|
||||
? "border-primary font-bold"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
apps/web/modules/saas/shared/components/UserMenu.tsx
Normal file
176
apps/web/modules/saas/shared/components/UserMenu.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"use client";
|
||||
|
||||
import { DropdownMenuSub } from "@radix-ui/react-dropdown-menu";
|
||||
import { authClient } from "@repo/auth/client";
|
||||
import { config } from "@repo/config";
|
||||
import { useSession } from "@saas/auth/hooks/use-session";
|
||||
import { UserAvatar } from "@shared/components/UserAvatar";
|
||||
import { clearCache } from "@shared/lib/cache";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu";
|
||||
import {
|
||||
BookIcon,
|
||||
HardDriveIcon,
|
||||
HomeIcon,
|
||||
LogOutIcon,
|
||||
MoonIcon,
|
||||
MoreVerticalIcon,
|
||||
SettingsIcon,
|
||||
SunIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
export function UserMenu({ showUserName }: { showUserName?: boolean }) {
|
||||
const t = useTranslations();
|
||||
const { user } = useSession();
|
||||
const { setTheme: setCurrentTheme, theme: currentTheme } = useTheme();
|
||||
const [theme, setTheme] = useState<string>(currentTheme ?? "system");
|
||||
|
||||
const colorModeOptions = [
|
||||
{
|
||||
value: "system",
|
||||
label: "System",
|
||||
icon: HardDriveIcon,
|
||||
},
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
icon: SunIcon,
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
icon: MoonIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const onLogout = () => {
|
||||
authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: async () => {
|
||||
await clearCache();
|
||||
window.location.href = new URL(
|
||||
config.auth.redirectAfterLogout,
|
||||
window.location.origin,
|
||||
).toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, email, image } = user;
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer w-full items-center justify-between gap-2 rounded-lg outline-hidden focus-visible:ring-2 focus-visible:ring-primary md:w-[100%+1rem] md:px-2 md:py-1.5 md:hover:bg-primary/5"
|
||||
aria-label="User menu"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<UserAvatar name={name ?? ""} avatarUrl={image} />
|
||||
{showUserName && (
|
||||
<span className="text-left leading-tight">
|
||||
<span className="font-medium text-sm">
|
||||
{name}
|
||||
</span>
|
||||
<span className="block text-xs opacity-70">
|
||||
{email}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{showUserName && <MoreVerticalIcon className="size-4" />}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
{name}
|
||||
<span className="block font-normal text-xs opacity-70">
|
||||
{email}
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Color mode selection */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<SunIcon className="mr-2 size-4" />
|
||||
{t("app.userMenu.colorMode")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={theme}
|
||||
onValueChange={(value) => {
|
||||
setTheme(value);
|
||||
setCurrentTheme(value);
|
||||
}}
|
||||
>
|
||||
{colorModeOptions.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<option.icon className="mr-2 size-4 opacity-50" />
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/app/settings/general">
|
||||
<SettingsIcon className="mr-2 size-4" />
|
||||
{t("app.userMenu.accountSettings")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="https://supastarter.dev/docs/nextjs">
|
||||
<BookIcon className="mr-2 size-4" />
|
||||
{t("app.userMenu.documentation")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/">
|
||||
<HomeIcon className="mr-2 size-4" />
|
||||
{t("app.userMenu.home")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={onLogout}>
|
||||
<LogOutIcon className="mr-2 size-4" />
|
||||
{t("app.userMenu.logout")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue