Skip to content

Commit

Permalink
Continue filling in side nav
Browse files Browse the repository at this point in the history
  • Loading branch information
TWilson023 committed Oct 3, 2024
1 parent d045117 commit 56ca7f5
Show file tree
Hide file tree
Showing 4 changed files with 475 additions and 43 deletions.
50 changes: 7 additions & 43 deletions apps/web/ui/layout/main-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "react";
import { Divider } from "../shared/icons";
import NavTabs from "./nav-tabs";
import { SidebarNav } from "./sidebar/sidebar-nav";
import UpgradeBanner from "./upgrade-banner";
import UserDropdown from "./user-dropdown";
import WorkspaceSwitcher from "./workspace-switcher";
Expand Down Expand Up @@ -76,52 +77,15 @@ export function MainNav({ children }: PropsWithChildren) {
!isOpen && "-translate-x-full",
)}
>
<div className="flex size-full items-center justify-center text-gray-500">
[nav]
</div>
<SidebarNav />
</nav>
</div>
<div>
<div className="sticky -top-16 z-20 border-b border-gray-200 bg-white">
<MaxWidthWrapper>
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<Link
href={`/${slug || ""}`}
className={cn(
"hidden transition-all sm:block",
scrolled && "translate-y-[3.4rem]",
)}
>
<NavLogo
variant="symbol"
isInApp
{...(scrolled && { className: "h-6 w-6" })}
/>
</Link>
<Divider className="hidden h-8 w-8 text-gray-200 sm:ml-3 sm:block" />
<WorkspaceSwitcher />
</div>
<div className="flex items-center space-x-6">
<UpgradeBanner />
<a
href="https://dub.co/help"
className="hidden text-sm text-gray-500 transition-colors hover:text-gray-700 sm:block"
target="_blank"
>
Help
</a>
<UserDropdown />
</div>
</div>
<Suspense fallback={<div className="h-12 w-full" />}>
<NavTabs />
</Suspense>
</MaxWidthWrapper>
<div className="bg-neutral-100">
<div className="relative min-h-full bg-white pt-px sm:rounded-tl-[16px]">
<SideNavContext.Provider value={{ isOpen, setIsOpen }}>
{children}
</SideNavContext.Provider>
</div>
<SideNavContext.Provider value={{ isOpen, setIsOpen }}>
{children}
</SideNavContext.Provider>
</div>
</div>
) : (
Expand Down
56 changes: 56 additions & 0 deletions apps/web/ui/layout/sidebar/sidebar-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Wordmark } from "@dub/ui";
import { CursorRays, Hyperlink, LinesY } from "@dub/ui/src";
import { cn } from "@dub/utils";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { useMemo } from "react";
import UserDropdown from "./user-dropdown";
import { WorkspaceDropdown } from "./workspace-dropdown";

export function SidebarNav() {
const { slug } = useParams() as { slug?: string };
const pathname = usePathname();

const tabs = useMemo(
() => [
{ name: "Links", icon: Hyperlink, href: `/${slug}` },
{ name: "Analytics", icon: LinesY, href: `/${slug}/analytics` },
{ name: "Events", icon: CursorRays, href: `/${slug}/events` },
],
[slug],
);

return (
<div className="p-3 text-gray-500">
<div className="flex items-start justify-between gap-1">
<Wordmark className="ml-1 h-6" />
<UserDropdown />
</div>

<div className="mt-7">
<WorkspaceDropdown />
</div>

<div className="mt-4 flex flex-col gap-0.5">
{tabs.map(({ name, icon: Icon, href }) => {
const isActive =
href === `/${slug}` ? pathname === href : pathname.startsWith(href);

return (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-2.5 rounded-md p-2 text-sm text-neutral-600 transition-colors duration-75 hover:bg-neutral-200/50 active:bg-neutral-200/80",
isActive && "bg-neutral-200/50",
)}
>
<Icon className="size-4 text-gray-500" />
{name}
</Link>
);
})}
</div>
</div>
);
}
142 changes: 142 additions & 0 deletions apps/web/ui/layout/sidebar/user-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"use client";

import { Avatar, Badge, Popover } from "@dub/ui";
import { Gear, Icon } from "@dub/ui/src";
import Cookies from "js-cookie";
import { Edit3, HelpCircle, LogOut } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
import {
ComponentPropsWithoutRef,
ElementType,
useEffect,
useState,
} from "react";

export default function UserDropdown() {
const { data: session } = useSession();
const [openPopover, setOpenPopover] = useState(false);

const [unreadChangelogs, setUnreadChangelogs] = useState(0);
useEffect(() => {
const lastReadChangelog = Cookies.get("lastReadChangelog");
if (!lastReadChangelog) {
setUnreadChangelogs(2);
}
}, []);

return (
<Popover
content={
<div className="flex w-full flex-col space-y-px rounded-md bg-white p-2 sm:w-56">
{session?.user ? (
<div className="p-2">
<p className="truncate text-sm font-medium text-neutral-900">
{session.user.name || session.user.email?.split("@")[0]}
</p>
<p className="truncate text-sm text-neutral-500">
{session.user.email}
</p>
</div>
) : (
<div className="grid gap-2 px-2 py-3">
<div className="h-3 w-12 animate-pulse rounded-full bg-neutral-200" />
<div className="h-3 w-20 animate-pulse rounded-full bg-neutral-200" />
</div>
)}
<UserOption
as={Link}
label="Help Center"
icon={HelpCircle}
href="https://dub.co/help"
target="_blank"
onClick={() => setOpenPopover(false)}
/>
<UserOption
as={Link}
label="User Settings"
icon={Gear}
href="/account/settings"
onClick={() => setOpenPopover(false)}
/>
<UserOption
as={Link}
label="Changelog"
icon={Edit3}
href="https://dub.co/changelog"
target="_blank"
onClick={() => {
Cookies.set("lastReadChangelog", new Date().toISOString());
setOpenPopover(false);
}}
>
{unreadChangelogs > 0 && (
<div className="flex grow justify-end">
<Badge variant="blue">{unreadChangelogs}</Badge>
</div>
)}
</UserOption>
<UserOption
as="button"
type="button"
label="Logout"
icon={LogOut}
onClick={() =>
signOut({
callbackUrl: "/login",
})
}
/>
</div>
}
align="start"
openPopover={openPopover}
setOpenPopover={setOpenPopover}
>
<button
onClick={() => setOpenPopover(!openPopover)}
className="group relative rounded-full outline-none ring-offset-1 ring-offset-neutral-100 transition-all hover:ring-2 hover:ring-black/10 focus:outline-none active:ring-black/15 data-[focus-visible=true]:ring-2 data-[state='open']:ring-black/15"
>
{session?.user ? (
<Avatar
user={session.user}
className="size-7 border-none duration-75 sm:size-7"
/>
) : (
<div className="size-7 animate-pulse rounded-full bg-gray-100 sm:size-7" />
)}
{unreadChangelogs > 0 && (
<div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full border-2 border-white bg-blue-500" />
)}
</button>
</Popover>
);
}

type UserOptionProps<T extends ElementType> = {
as?: T;
label: string;
icon: Icon;
};

function UserOption<T extends ElementType = "button">({
as,
label,
icon: Icon,
children,
...rest
}: UserOptionProps<T> &
Omit<ComponentPropsWithoutRef<T>, keyof UserOptionProps<T>>) {
const Component = as ?? "button";

return (
<Component
className="flex items-center gap-x-4 rounded-md px-2.5 py-2 text-sm transition-all duration-75 hover:bg-neutral-200/50 active:bg-neutral-200/80"
{...rest}
>
<Icon className="size-4 text-neutral-500" />
<span className="block truncate text-neutral-600">{label}</span>
{children}
</Component>
);
}
Loading

0 comments on commit 56ca7f5

Please sign in to comment.