feat: setup dashboard 2.0
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
TbLayoutSidebarLeftCollapse,
|
TbLayoutSidebarLeftCollapse,
|
||||||
@@ -11,7 +10,6 @@ import { authClient } from "@kan/auth/client";
|
|||||||
|
|
||||||
import { useClickOutside } from "~/hooks/useClickOutside";
|
import { useClickOutside } from "~/hooks/useClickOutside";
|
||||||
import { useTheme } from "~/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
import FeedbackButton from "./FeedbackButton";
|
|
||||||
import SideNavigation from "./SideNavigation";
|
import SideNavigation from "./SideNavigation";
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
@@ -77,67 +75,57 @@ export default function Dashboard({
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
background-color: ${!isDarkMode ? "hsl(0deg 0% 97.3%)" : "#161616"};
|
background-color: ${!isDarkMode ? "hsl(0deg 0% 97.3%)" : "#1c1c1c"};
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<div className="flex h-screen flex-col items-center bg-light-100 dark:bg-dark-50 md:min-w-[800px]">
|
<div className="relative flex h-screen flex-col bg-light-100 px-3 py-3 dark:bg-dark-100">
|
||||||
<div className="flex h-12 min-h-12 w-full justify-between border-b border-light-600 px-5 py-2 align-middle dark:border-dark-400 md:h-16 md:min-h-16">
|
{/* Mobile Navigation Controls - positioned absolutely over content */}
|
||||||
<div className="my-auto flex w-full items-center justify-between">
|
<div className="absolute left-8 top-8 z-50 flex items-center gap-2 md:hidden">
|
||||||
|
<button
|
||||||
|
ref={sideNavButtonRef}
|
||||||
|
onClick={toggleSideNav}
|
||||||
|
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
||||||
|
>
|
||||||
|
{isSideNavOpen ? (
|
||||||
|
<TbLayoutSidebarLeftCollapse
|
||||||
|
size={20}
|
||||||
|
className="text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TbLayoutSidebarLeftExpand
|
||||||
|
size={20}
|
||||||
|
className="text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* {hasRightPanel && (
|
||||||
|
<div className="absolute right-8 top-8 z-50 md:hidden">
|
||||||
<button
|
<button
|
||||||
ref={sideNavButtonRef}
|
ref={rightPanelButtonRef}
|
||||||
onClick={toggleSideNav}
|
onClick={toggleRightPanel}
|
||||||
className="text-neutral-900 dark:text-dark-1000 md:hidden"
|
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
|
||||||
>
|
>
|
||||||
{isSideNavOpen ? (
|
{isRightPanelOpen ? (
|
||||||
<TbLayoutSidebarLeftCollapse
|
<TbLayoutSidebarRightCollapse
|
||||||
size={20}
|
size={20}
|
||||||
className="text-light-900 dark:text-dark-900"
|
className="text-light-900 dark:text-dark-900"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TbLayoutSidebarLeftExpand
|
<TbLayoutSidebarRightExpand
|
||||||
size={20}
|
size={20}
|
||||||
className="text-light-900 dark:text-dark-900"
|
className="text-light-900 dark:text-dark-900"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link href="/" className="hidden md:block">
|
|
||||||
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
|
||||||
kan.bn
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{hasRightPanel && (
|
|
||||||
<button
|
|
||||||
ref={rightPanelButtonRef}
|
|
||||||
onClick={toggleRightPanel}
|
|
||||||
className="text-neutral-900 dark:text-dark-1000 md:hidden"
|
|
||||||
>
|
|
||||||
{isRightPanelOpen ? (
|
|
||||||
<TbLayoutSidebarRightCollapse
|
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TbLayoutSidebarRightExpand
|
|
||||||
size={20}
|
|
||||||
className="text-light-900 dark:text-dark-900"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="hidden md:block">
|
|
||||||
<FeedbackButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)} */}
|
||||||
|
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-[calc(100vh-1.5rem)] min-h-0 w-full">
|
||||||
<div
|
<div
|
||||||
ref={sideNavRef}
|
ref={sideNavRef}
|
||||||
className={`fixed left-0 top-12 z-50 h-[calc(100dvh-3rem)] transform transition-transform duration-300 ease-in-out md:relative md:top-0 md:h-full md:translate-x-0 ${isSideNavOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"} `}
|
className={`fixed left-3 top-3 z-40 h-[calc(100dvh-1.5rem)] transform transition-transform duration-300 ease-in-out md:relative md:left-0 md:top-0 md:h-full md:translate-x-0 ${isSideNavOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"} `}
|
||||||
>
|
>
|
||||||
<SideNavigation
|
<SideNavigation
|
||||||
user={{ email: session?.user.email, image: session?.user.image }}
|
user={{ email: session?.user.email, image: session?.user.image }}
|
||||||
@@ -145,25 +133,29 @@ export default function Dashboard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex min-h-0 w-full overflow-hidden">
|
<div className="relative h-full min-h-0 w-full overflow-hidden rounded-2xl border border-light-300 bg-light-50 dark:border-dark-200 dark:bg-dark-50">
|
||||||
<div className="h-full w-full overflow-y-auto">{children}</div>
|
<div className="relative flex h-full min-h-0 w-full overflow-hidden">
|
||||||
|
<div className="h-full w-full overflow-y-auto">{children}</div>
|
||||||
|
|
||||||
{/* Mobile Right Panel */}
|
{/* Mobile Right Panel */}
|
||||||
{hasRightPanel && rightPanel && (
|
{hasRightPanel && rightPanel && (
|
||||||
<div
|
<div
|
||||||
ref={rightPanelRef}
|
ref={rightPanelRef}
|
||||||
className={`fixed right-0 top-12 z-50 h-[calc(100dvh-3rem)] w-80 transform bg-light-200 transition-transform duration-300 ease-in-out dark:bg-dark-100 md:hidden ${
|
className={`fixed right-3 top-3 z-40 h-[calc(100dvh-1.5rem)] w-80 transform bg-light-200 transition-transform duration-300 ease-in-out dark:bg-dark-100 md:hidden ${
|
||||||
isRightPanelOpen ? "translate-x-0" : "translate-x-full"
|
isRightPanelOpen ? "translate-x-0" : "translate-x-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-full overflow-y-auto">{rightPanel}</div>
|
<div className="h-full overflow-y-auto rounded-md border dark:border-dark-200">
|
||||||
</div>
|
{rightPanel}
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Desktop Right Panel */}
|
{/* Desktop Right Panel */}
|
||||||
{hasRightPanel && rightPanel && (
|
{hasRightPanel && rightPanel && (
|
||||||
<div className="hidden md:block">{rightPanel}</div>
|
<div className="hidden md:block">{rightPanel}</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import dynamic from "next/dynamic";
|
|||||||
|
|
||||||
const Lottie = dynamic(() => import("react-lottie-player"), { ssr: false });
|
const Lottie = dynamic(() => import("react-lottie-player"), { ssr: false });
|
||||||
|
|
||||||
type IconProps = {
|
interface IconProps {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
json: object;
|
json: object;
|
||||||
};
|
}
|
||||||
|
|
||||||
const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +15,7 @@ const Icon: React.FC<IconProps> = ({ isPlaying, index, json }) => {
|
|||||||
animationData={json}
|
animationData={json}
|
||||||
play={isPlaying}
|
play={isPlaying}
|
||||||
loop={false}
|
loop={false}
|
||||||
style={{ width: 20, height: 20, fill: "white" }}
|
style={{ width: 18, height: 18, fill: "white" }}
|
||||||
rendererSettings={{ preserveAspectRatio: "xMidYMid slice" }}
|
rendererSettings={{ preserveAspectRatio: "xMidYMid slice" }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ const PatternedBackground = () => (
|
|||||||
<svg className="h-full w-full">
|
<svg className="h-full w-full">
|
||||||
<pattern
|
<pattern
|
||||||
id="pattern"
|
id="pattern"
|
||||||
x="0.034759358288862785"
|
x="10"
|
||||||
y="3.335370511841166"
|
y="10"
|
||||||
width="14.423223834988539"
|
width="14.423223834988539"
|
||||||
height="14.423223834988539"
|
height="14.423223834988539"
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const Button: React.FC<{
|
|||||||
current: boolean;
|
current: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
json: object;
|
json: object;
|
||||||
}> = ({ href, current, name, json }) => {
|
isCollapsed?: boolean;
|
||||||
|
}> = ({ href, current, name, json, isCollapsed = false }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
@@ -26,12 +27,16 @@ const Button: React.FC<{
|
|||||||
href={href}
|
href={href}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
current ? "bg-light-200 dark:bg-dark-200" : "dark:bg-dark-50",
|
"group flex h-[34px] items-center rounded-md p-1.5 text-sm font-normal leading-6 text-neutral-900 hover:bg-light-200 dark:hover:bg-dark-200 dark:hover:text-dark-1000",
|
||||||
"group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6 text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-200",
|
current
|
||||||
|
? "bg-light-200 dark:bg-dark-200 dark:text-dark-1000"
|
||||||
|
: "dark:bg-dark-100 dark:text-dark-900",
|
||||||
|
isCollapsed ? "justify-center" : "gap-x-3",
|
||||||
)}
|
)}
|
||||||
|
title={isCollapsed ? name : undefined}
|
||||||
>
|
>
|
||||||
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
||||||
{name}
|
{!isCollapsed && name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { Button } from "@headlessui/react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
TbLayoutSidebarLeftCollapse,
|
||||||
|
TbLayoutSidebarLeftExpand,
|
||||||
|
} from "react-icons/tb";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import boardsIconDark from "~/assets/boards-dark.json";
|
import boardsIconDark from "~/assets/boards-dark.json";
|
||||||
import boardsIconLight from "~/assets/boards-light.json";
|
import boardsIconLight from "~/assets/boards-light.json";
|
||||||
@@ -11,6 +19,7 @@ import ReactiveButton from "~/components/ReactiveButton";
|
|||||||
import UserMenu from "~/components/UserMenu";
|
import UserMenu from "~/components/UserMenu";
|
||||||
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
||||||
import { useTheme } from "~/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
|
import FeedbackButton from "./FeedbackButton";
|
||||||
|
|
||||||
interface SideNavigationProps {
|
interface SideNavigationProps {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
@@ -27,6 +36,25 @@ export default function SideNavigation({
|
|||||||
isLoading,
|
isLoading,
|
||||||
}: SideNavigationProps) {
|
}: SideNavigationProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const [isInitialised, setIsInitialised] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedState = localStorage.getItem("kan_sidebar-collapsed");
|
||||||
|
if (savedState !== null) {
|
||||||
|
setIsCollapsed(JSON.parse(savedState));
|
||||||
|
}
|
||||||
|
setIsInitialised(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialised) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"kan_sidebar-collapsed",
|
||||||
|
JSON.stringify(isCollapsed),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [isCollapsed, isInitialised]);
|
||||||
|
|
||||||
const { pathname } = router;
|
const { pathname } = router;
|
||||||
|
|
||||||
@@ -52,11 +80,50 @@ export default function SideNavigation({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
setIsCollapsed(!isCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="flex h-full w-64 flex-col justify-between border-r border-light-600 bg-light-50 px-3 pb-3 pt-5 dark:border-dark-400 dark:bg-dark-50">
|
<nav
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-full flex-col justify-between bg-light-100 pr-3 transition-all duration-300 dark:bg-dark-100",
|
||||||
|
isCollapsed ? "w-auto" : "w-64",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<WorkspaceMenu />
|
<div className="flex h-14 items-center justify-between pb-[18px] pt-1.5">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<Link href="/" className="block">
|
||||||
|
<h1 className="pl-2 text-lg font-bold tracking-tight text-neutral-700 dark:text-dark-950">
|
||||||
|
kan.bn
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={toggleCollapse}
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-8 items-center justify-center rounded-md hover:bg-light-200 dark:hover:bg-dark-200",
|
||||||
|
isCollapsed ? "w-full" : "w-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<TbLayoutSidebarLeftExpand
|
||||||
|
size={18}
|
||||||
|
className="text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TbLayoutSidebarLeftCollapse
|
||||||
|
size={18}
|
||||||
|
className="text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 mb-4 w-auto border-b border-light-300 dark:border-dark-400" />
|
||||||
|
|
||||||
|
<WorkspaceMenu isCollapsed={isCollapsed} />
|
||||||
<ul role="list" className="space-y-1">
|
<ul role="list" className="space-y-1">
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
@@ -65,16 +132,26 @@ export default function SideNavigation({
|
|||||||
current={pathname.includes(item.href)}
|
current={pathname.includes(item.href)}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
json={item.icon}
|
json={item.icon}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<UserMenu
|
|
||||||
email={user.email ?? ""}
|
<div className="space-y-3">
|
||||||
imageUrl={user.image ?? undefined}
|
{/* Feedback button above user menu */}
|
||||||
isLoading={isLoading}
|
{/* <div className="flex justify-start">
|
||||||
/>
|
<FeedbackButton />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<UserMenu
|
||||||
|
email={user.email ?? ""}
|
||||||
|
imageUrl={user.image ?? undefined}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface UserMenuProps {
|
|||||||
imageUrl: string | undefined;
|
imageUrl: string | undefined;
|
||||||
email: string;
|
email: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function classNames(...classes: string[]): string {
|
function classNames(...classes: string[]): string {
|
||||||
@@ -23,6 +24,7 @@ export default function UserMenu({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
email,
|
email,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isCollapsed = false,
|
||||||
}: UserMenuProps) {
|
}: UserMenuProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { themePreference, switchTheme } = useTheme();
|
const { themePreference, switchTheme } = useTheme();
|
||||||
@@ -39,12 +41,17 @@ export default function UserMenu({
|
|||||||
<Menu as="div" className="relative inline-block w-full text-left">
|
<Menu as="div" className="relative inline-block w-full text-left">
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex">
|
<div className={classNames(isCollapsed ? "" : "flex")}>
|
||||||
<div className="h-[30px] w-[30px] animate-pulse rounded-full bg-light-200 dark:bg-dark-200" />
|
<div className="h-[30px] w-[30px] animate-pulse rounded-full bg-light-200 dark:bg-dark-200" />
|
||||||
<div className="mx-2 h-[30px] w-[175px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
{!isCollapsed && (
|
||||||
|
<div className="mx-2 h-[30px] w-[175px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000">
|
<Menu.Button
|
||||||
|
className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000"
|
||||||
|
title={isCollapsed ? email : undefined}
|
||||||
|
>
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
@@ -64,7 +71,9 @@ export default function UserMenu({
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="mx-2 truncate text-sm">{email}</span>
|
{!isCollapsed && (
|
||||||
|
<span className="mx-2 truncate text-sm">{email}</span>
|
||||||
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +87,12 @@ export default function UserMenu({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute bottom-[40px] left-0 z-10 mt-2 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
<Menu.Items
|
||||||
|
className={classNames(
|
||||||
|
"absolute bottom-[40px] z-10 mt-2 origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300",
|
||||||
|
isCollapsed ? "left-0 w-48" : "left-0 w-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ import { Menu, Transition } from "@headlessui/react";
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { HiCheck } from "react-icons/hi2";
|
import { HiCheck } from "react-icons/hi2";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
export default function WorkspaceMenu() {
|
export default function WorkspaceMenu({
|
||||||
|
isCollapsed = false,
|
||||||
|
}: {
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}) {
|
||||||
const { workspace, isLoading, availableWorkspaces, switchWorkspace } =
|
const { workspace, isLoading, availableWorkspaces, switchWorkspace } =
|
||||||
useWorkspace();
|
useWorkspace();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
@@ -15,24 +20,36 @@ export default function WorkspaceMenu() {
|
|||||||
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="mb-1 flex p-1.5">
|
<div className={twMerge("mb-1", isCollapsed && "flex p-1.5")}>
|
||||||
<div className="h-6 w-6 animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-200" />
|
<div className="h-6 w-6 animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
||||||
<div className="ml-2 h-6 w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-200" />
|
{!isCollapsed && (
|
||||||
|
<div className="ml-2 h-6 w-[150px] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button className="mb-1 flex w-full items-center rounded-[5px] p-1.5 hover:bg-light-200 dark:hover:bg-dark-200">
|
<Menu.Button
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-[5px] bg-indigo-700">
|
className={twMerge(
|
||||||
|
"mb-1 flex h-[34px] w-full items-center rounded-md p-1.5 hover:bg-light-200 dark:hover:bg-dark-200",
|
||||||
|
isCollapsed && "mb-1.5 h-9 p-1",
|
||||||
|
)}
|
||||||
|
title={isCollapsed ? workspace.name : undefined}
|
||||||
|
>
|
||||||
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-indigo-700">
|
||||||
<span className="text-xs font-bold leading-none text-white">
|
<span className="text-xs font-bold leading-none text-white">
|
||||||
{workspace.name.charAt(0).toUpperCase()}
|
{workspace.name.charAt(0).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
{!isCollapsed && (
|
||||||
{workspace.name}
|
<>
|
||||||
</span>
|
<span className="ml-2 text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
{workspace.plan === "pro" && (
|
{workspace.name}
|
||||||
<span className="ml-2 inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-[10px] font-medium text-indigo-700">
|
</span>
|
||||||
Pro
|
{workspace.plan === "pro" && (
|
||||||
</span>
|
<span className="ml-2 inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-[10px] font-medium text-indigo-700">
|
||||||
|
Pro
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
@@ -47,7 +64,12 @@ export default function WorkspaceMenu() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute left-0 z-10 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
<Menu.Items
|
||||||
|
className={twMerge(
|
||||||
|
"absolute left-0 z-10 origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300",
|
||||||
|
isCollapsed ? "w-48" : "w-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{availableWorkspaces.map((availableWorkspace) => (
|
{availableWorkspaces.map((availableWorkspace) => (
|
||||||
<div key={availableWorkspace.publicId} className="flex">
|
<div key={availableWorkspace.publicId} className="flex">
|
||||||
|
|||||||
Reference in New Issue
Block a user