diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx index 4ca4ff121a..a1700955d1 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/page-client.tsx @@ -71,7 +71,7 @@ function WorkspaceLinks() {
-
+
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}

-
+
-
+
{children}
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} - - ))} -
- ))} - + +
+ +
{children}
+
+
+
); } 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 (