diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout-client.tsx
deleted file mode 100644
index 9e42daf6d3..0000000000
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout-client.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-"use client";
-
-import useWorkspace from "@/lib/swr/use-workspace";
-import SettingsLayout from "@/ui/layout/settings-layout";
-import {
- CircleInfo,
- ConnectedDots,
- CubeSettings,
- Gear2,
- Gift,
- Globe,
- Key,
- Receipt2,
- ShieldCheck,
- Tag,
- Users6,
-} from "@dub/ui/src/icons";
-import { Webhook } from "lucide-react";
-import { ReactNode, useMemo } from "react";
-
-export default function WorkspaceSettingsLayoutClient({
- children,
-}: {
- children: ReactNode;
-}) {
- const { flags } = useWorkspace();
-
- const tabs = useMemo(
- () => [
- // Workspace Settings
- {
- group: "Workspace Settings",
- tabs: [
- {
- name: "General",
- icon: Gear2,
- segment: null,
- },
- {
- name: "Domains",
- icon: Globe,
- segment: "domains",
- },
- {
- name: "Tags",
- icon: Tag,
- segment: "tags",
- },
- {
- name: "Billing",
- icon: Receipt2,
- segment: "billing",
- },
- {
- name: "People",
- icon: Users6,
- segment: "people",
- },
- {
- name: "Integrations",
- icon: ConnectedDots,
- segment: "integrations",
- },
- {
- name: "Security",
- icon: ShieldCheck,
- segment: "security",
- },
- ...(flags?.referrals
- ? [{ name: "Referrals", icon: Gift, segment: "referrals" }]
- : []),
- ],
- },
-
- // Developer Settings
- {
- group: "Developer Settings",
- tabs: [
- {
- name: "API Keys",
- icon: Key,
- segment: "tokens",
- },
- {
- name: "OAuth Apps",
- icon: CubeSettings,
- segment: "oauth-apps",
- },
- ...(flags?.webhooks
- ? [
- {
- name: "Webhooks",
- icon: Webhook,
- segment: "webhooks",
- },
- ]
- : []),
- ],
- },
-
- // Account Settings
- {
- group: "Account Settings",
- tabs: [
- {
- name: "Notifications",
- icon: CircleInfo,
- segment: "notifications",
- },
- ],
- },
- ],
- [flags],
- );
-
- return (
-
- {children}
-
- );
-}
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout.tsx
index bc2ecb0794..173a5c5fec 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/layout.tsx
@@ -1,12 +1,10 @@
import { ReactNode } from "react";
-import WorkspaceSettingsLayoutClient from "./layout-client";
+import SettingsLayout from "../../../../../ui/layout/settings-layout";
export default function WorkspaceSettingsLayout({
children,
}: {
children: ReactNode;
}) {
- return (
-
{children}
- );
+ return
{children};
}
diff --git a/apps/web/app/app.dub.co/(dashboard)/account/settings/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/account/settings/layout.tsx
index 8b23f97f71..1254680b89 100644
--- a/apps/web/app/app.dub.co/(dashboard)/account/settings/layout.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/account/settings/layout.tsx
@@ -1,5 +1,4 @@
import SettingsLayout from "@/ui/layout/settings-layout";
-import { Gear2, Key, ShieldCheck } from "@dub/ui";
import { ReactNode } from "react";
export default function PersonalSettingsLayout({
@@ -7,32 +6,5 @@ export default function PersonalSettingsLayout({
}: {
children: ReactNode;
}) {
- const tabs = [
- {
- group: "",
- tabs: [
- {
- name: "General",
- icon: Gear2,
- segment: null,
- },
- {
- name: "Security",
- icon: ShieldCheck,
- segment: "security",
- },
- {
- name: "API Keys",
- icon: Key,
- segment: "tokens",
- },
- ],
- },
- ];
-
- return (
-
- {children}
-
- );
+ return
{children};
}
diff --git a/apps/web/app/app.dub.co/(dashboard)/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/layout.tsx
index 15393dda32..141d2d31ad 100644
--- a/apps/web/app/app.dub.co/(dashboard)/layout.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/layout.tsx
@@ -12,7 +12,7 @@ export const metadata = constructMetadata();
export default async function Layout({ children }: { children: ReactNode }) {
return (
-
+
}>{children}
{/*
*/}
diff --git a/apps/web/ui/layout/main-nav.tsx b/apps/web/ui/layout/main-nav.tsx
index 47642ed2fa..0d95963080 100644
--- a/apps/web/ui/layout/main-nav.tsx
+++ b/apps/web/ui/layout/main-nav.tsx
@@ -49,14 +49,14 @@ export function MainNav({
useKeyboardShortcut("n", () => setIsOpen((o) => !o));
return (
-
+
{/* Side nav backdrop */}
{
if (e.target === e.currentTarget) {
@@ -68,7 +68,7 @@ export function MainNav({
{/* Side nav */}
-
+
{children}
diff --git a/apps/web/ui/layout/page-content/index.tsx b/apps/web/ui/layout/page-content/index.tsx
index a4731c02c6..f379ff4a2f 100644
--- a/apps/web/ui/layout/page-content/index.tsx
+++ b/apps/web/ui/layout/page-content/index.tsx
@@ -9,22 +9,22 @@ export function PageContent({
children,
}: PropsWithChildren<{ title: ReactNode }>) {
return (
-
-
+
+
-
+
{title}
-
-
diff --git a/apps/web/ui/layout/page-content/nav-button.tsx b/apps/web/ui/layout/page-content/nav-button.tsx
index ee5fc2759a..5de38376a4 100644
--- a/apps/web/ui/layout/page-content/nav-button.tsx
+++ b/apps/web/ui/layout/page-content/nav-button.tsx
@@ -14,7 +14,7 @@ export function NavButton() {
variant="outline"
onClick={() => setIsOpen((o) => !o)}
icon={
}
- className="h-auto w-fit p-1 sm:hidden"
+ className="h-auto w-fit p-1 md:hidden"
/>
);
}
diff --git a/apps/web/ui/layout/settings-layout.tsx b/apps/web/ui/layout/settings-layout.tsx
index 824ea642c6..e667f1466c 100644
--- a/apps/web/ui/layout/settings-layout.tsx
+++ b/apps/web/ui/layout/settings-layout.tsx
@@ -1,67 +1,15 @@
-import { Icon, MaxWidthWrapper } from "@dub/ui";
-import { cn } from "@dub/utils";
-import { ReactNode } from "react";
-import NavLink from "./settings-nav-link";
-import { SettingsNavMobile } from "./settings-nav-mobile";
+import { MaxWidthWrapper } from "@dub/ui";
+import { PropsWithChildren } from "react";
+import { PageContent } from "./page-content";
-interface Tab {
- name: string;
- icon: Icon;
- segment: string | null;
-}
-
-export interface SettingsLayoutProps {
- tabs: {
- group: string;
- tabs: Tab[];
- }[];
- tabContainerClassName?: string;
- children: ReactNode;
-}
-
-export default function SettingsLayout({
- tabs,
- tabContainerClassName,
- children,
-}: SettingsLayoutProps) {
+export default function SettingsLayout({ children }: PropsWithChildren) {
return (
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
- );
-}
-
-function Tabs({ tabs }: Pick
) {
- return (
- <>
- {tabs.map(({ group, tabs }) => (
-
- {group && (
- {group}
- )}
-
- {tabs.map(({ name, icon, segment }) => (
-
- {name}
-
- ))}
-
- ))}
- >
+
+
+
);
}
diff --git a/apps/web/ui/layout/settings-nav-link.tsx b/apps/web/ui/layout/settings-nav-link.tsx
deleted file mode 100644
index 95351e63d6..0000000000
--- a/apps/web/ui/layout/settings-nav-link.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"use client";
-
-import { Icon } from "@dub/ui";
-import { cn } from "@dub/utils";
-import Link from "next/link";
-import { useParams, useSelectedLayoutSegment } from "next/navigation";
-import { ReactNode } from "react";
-
-export default function NavLink({
- segment,
- icon: Icon,
- children,
-}: {
- segment: string | null;
- icon: Icon;
- children: ReactNode;
-}) {
- const selectedLayoutSegment = useSelectedLayoutSegment();
- const { slug } = useParams() as {
- slug?: string;
- };
-
- const href = `${slug ? `/${slug}` : "/account"}/settings${
- segment ? `/${segment}` : ""
- }`;
-
- const isSelected = selectedLayoutSegment === segment;
-
- return (
-
- {Icon && (
-
- )}
- {children}
-
- );
-}
diff --git a/apps/web/ui/layout/sidebar/icons/cursor-rays.tsx b/apps/web/ui/layout/sidebar/icons/cursor-rays.tsx
index 6624c349ec..46314e283f 100644
--- a/apps/web/ui/layout/sidebar/icons/cursor-rays.tsx
+++ b/apps/web/ui/layout/sidebar/icons/cursor-rays.tsx
@@ -5,7 +5,7 @@ export function CursorRays({
isActive,
className,
...rest
-}: { isActive: boolean } & SVGProps) {
+}: { isActive?: boolean } & SVGProps) {
const cursorRef = useRef(null);
const raysRef = useRef(null);
diff --git a/apps/web/ui/layout/sidebar/icons/hyperlink.tsx b/apps/web/ui/layout/sidebar/icons/hyperlink.tsx
index 07da83df95..e7899b54ed 100644
--- a/apps/web/ui/layout/sidebar/icons/hyperlink.tsx
+++ b/apps/web/ui/layout/sidebar/icons/hyperlink.tsx
@@ -3,7 +3,7 @@ import { SVGProps, useEffect, useRef } from "react";
export function Hyperlink({
isActive,
...rest
-}: { isActive: boolean } & SVGProps) {
+}: { isActive?: boolean } & SVGProps) {
const ref = useRef(null);
useEffect(() => {
diff --git a/apps/web/ui/layout/sidebar/icons/lines-y.tsx b/apps/web/ui/layout/sidebar/icons/lines-y.tsx
index 52f290bc04..770568c532 100644
--- a/apps/web/ui/layout/sidebar/icons/lines-y.tsx
+++ b/apps/web/ui/layout/sidebar/icons/lines-y.tsx
@@ -7,7 +7,7 @@ export function LinesY({
isActive,
className,
...rest
-}: { isActive: boolean } & SVGProps) {
+}: { isActive?: boolean } & SVGProps) {
const line1Ref = useRef(null);
const line2Ref = useRef(null);
const line3Ref = useRef(null);
diff --git a/apps/web/ui/layout/sidebar/items.ts b/apps/web/ui/layout/sidebar/items.ts
new file mode 100644
index 0000000000..4d80e28e06
--- /dev/null
+++ b/apps/web/ui/layout/sidebar/items.ts
@@ -0,0 +1,174 @@
+import { BetaFeatures } from "@/lib/types";
+import {
+ CircleInfo,
+ ConnectedDots,
+ CubeSettings,
+ Gear2,
+ Gift,
+ Globe,
+ Key,
+ Receipt2,
+ ShieldCheck,
+ Tag,
+ Users6,
+} from "@dub/ui/src/icons";
+import { Webhook } from "lucide-react";
+import { ComponentType, SVGProps } from "react";
+import { CursorRays } from "./icons/cursor-rays";
+import { Hyperlink } from "./icons/hyperlink";
+import { LinesY } from "./icons/lines-y";
+
+type NavItem = {
+ name: string;
+ icon: ComponentType & { isActive?: boolean }>;
+ href: string;
+ exact?: boolean;
+};
+
+export const ITEMS: Record<
+ string,
+ {
+ name?: string;
+ items: (args: {
+ slug: string;
+ flags?: Record;
+ }) => NavItem[];
+ }[]
+> = {
+ // Top-level
+ default: [
+ {
+ items: ({ slug }) => [
+ {
+ name: "Links",
+ icon: Hyperlink,
+ href: `/${slug}`,
+ exact: true,
+ },
+ {
+ name: "Analytics",
+ icon: LinesY,
+ href: `/${slug}/analytics`,
+ },
+ {
+ name: "Events",
+ icon: CursorRays,
+ href: `/${slug}/events`,
+ },
+ ],
+ },
+ ],
+
+ // Workspace settings
+ workspaceSettings: [
+ {
+ name: "Workspace",
+ items: ({ slug, flags }) => [
+ {
+ name: "General",
+ icon: Gear2,
+ href: `/${slug}/settings`,
+ exact: true,
+ },
+ {
+ name: "Domains",
+ icon: Globe,
+ href: `/${slug}/settings/domains`,
+ },
+ {
+ name: "Tags",
+ icon: Tag,
+ href: `/${slug}/settings/tags`,
+ },
+ {
+ name: "Billing",
+ icon: Receipt2,
+ href: `/${slug}/settings/billing`,
+ },
+ {
+ name: "People",
+ icon: Users6,
+ href: `/${slug}/settings/people`,
+ },
+ {
+ name: "Integrations",
+ icon: ConnectedDots,
+ href: `/${slug}/settings/integrations`,
+ },
+ {
+ name: "Security",
+ icon: ShieldCheck,
+ href: `/${slug}/settings/security`,
+ },
+ ...(flags?.referrals
+ ? [
+ {
+ name: "Referrals",
+ icon: Gift,
+ href: `/${slug}/settings/referrals`,
+ },
+ ]
+ : []),
+ ],
+ },
+ {
+ name: "Developer",
+ items: ({ slug, flags }) => [
+ {
+ name: "API Keys",
+ icon: Key,
+ href: `/${slug}/settings/tokens`,
+ },
+ {
+ name: "OAuth Apps",
+ icon: CubeSettings,
+ href: `/${slug}/settings/oauth-apps`,
+ },
+ ...(flags?.webhooks
+ ? [
+ {
+ name: "Webhooks",
+ icon: Webhook,
+ href: `/${slug}/settings/webhooks`,
+ },
+ ]
+ : []),
+ ],
+ },
+ {
+ name: "Account",
+ items: ({ slug }) => [
+ {
+ name: "Notifications",
+ icon: CircleInfo,
+ href: `/${slug}/settings/notifications`,
+ },
+ ],
+ },
+ ],
+
+ // User settings
+ userSettings: [
+ {
+ name: "Account",
+ items: () => [
+ {
+ name: "General",
+ icon: Gear2,
+ href: "/account/settings",
+ exact: true,
+ },
+ {
+ name: "Security",
+ icon: ShieldCheck,
+ href: "/account/settings/security",
+ },
+ {
+ name: "API Keys",
+ icon: Key,
+ href: "/account/settings/tokens",
+ },
+ ],
+ },
+ ],
+};
diff --git a/apps/web/ui/layout/sidebar/sidebar-nav.tsx b/apps/web/ui/layout/sidebar/sidebar-nav.tsx
index 91f544f89c..d78f3f3328 100644
--- a/apps/web/ui/layout/sidebar/sidebar-nav.tsx
+++ b/apps/web/ui/layout/sidebar/sidebar-nav.tsx
@@ -1,65 +1,121 @@
+import useWorkspace from "@/lib/swr/use-workspace";
import { Wordmark } from "@dub/ui";
import { cn } from "@dub/utils";
+import { AnimatePresence, motion } from "framer-motion";
+import { ChevronLeft } from "lucide-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { ReactNode, Suspense, useMemo } from "react";
-import { CursorRays } from "./icons/cursor-rays";
-import { Hyperlink } from "./icons/hyperlink";
-import { LinesY } from "./icons/lines-y";
+import { ITEMS } from "./items";
import UserDropdown from "./user-dropdown";
import { WorkspaceDropdown } from "./workspace-dropdown";
export function SidebarNav({ toolContent }: { toolContent?: ReactNode }) {
const { slug } = useParams() as { slug?: string };
+ const { flags } = useWorkspace();
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],
- );
+ const area = useMemo(() => {
+ return pathname.startsWith("/account/settings")
+ ? "userSettings"
+ : pathname.startsWith(`/${slug}/settings`)
+ ? "workspaceSettings"
+ : "default";
+ }, [slug, pathname]);
+
+ const itemGroups = useMemo(() => ITEMS[area], [area]);
return (
-
-
-
+
+
+
+ {area === "default" ? (
+
+ ) : (
+
+
+ Settings
+
+ )}
+
+
+
{toolContent}
+
+
+
+ {area === "default" && (
+
+
+
+ )}
-
-
-
-
-
- {tabs.map(({ name, icon: Icon, href }) => {
- const isActive =
- href === `/${slug}` ? pathname === href : pathname.startsWith(href);
+
+ {itemGroups.map(({ name, items }, idx) => (
+
+ {name && (
+
+ {name}
+
+ )}
+ {items({ slug: slug || "", flags }).map(
+ ({ name, icon: Icon, href, exact }) => {
+ const isActive = exact
+ ? pathname === href
+ : pathname.startsWith(href);
- return (
-
-
- {name}
-
- );
- })}
+ return (
+
+
+ {name}
+
+ );
+ },
+ )}
+
+ ))}
+
+
+
);
diff --git a/apps/web/ui/layout/sidebar/workspace-dropdown.tsx b/apps/web/ui/layout/sidebar/workspace-dropdown.tsx
index dc7b9a24ec..2653c87b18 100644
--- a/apps/web/ui/layout/sidebar/workspace-dropdown.tsx
+++ b/apps/web/ui/layout/sidebar/workspace-dropdown.tsx
@@ -15,16 +15,29 @@ import { ChevronsUpDown } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
-import { useCallback, useContext, useMemo, useRef, useState } from "react";
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
export function WorkspaceDropdown() {
const { workspaces } = useWorkspaces();
const { data: session, status } = useSession();
- const { slug, key } = useParams() as {
+ const { slug: currentSlug, key } = useParams() as {
slug?: string;
key?: string;
};
+ // Prevent slug from changing to empty to avoid UI switching during nav animation
+ const [slug, setSlug] = useState(currentSlug);
+ useEffect(() => {
+ if (currentSlug) setSlug(currentSlug);
+ }, [currentSlug]);
+
const selected = useMemo(() => {
const selectedWorkspace = workspaces?.find(
(workspace) => workspace.slug === slug,
@@ -175,8 +188,6 @@ function WorkspaceList({
[domain, key, pathname, selected.slug],
);
- console.log(scrollProgress);
-
return (