Skip to content

Commit aef3f74

Browse files
committed
update app sidebar nav
1 parent 145fd54 commit aef3f74

8 files changed

Lines changed: 143 additions & 145 deletions

File tree

apps/web/app/api/links/crawl/bitly/[domain]/[key]/route.ts

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1-
import { createId } from "@/lib/api/create-id";
2-
import { encodeKeyIfCaseSensitive } from "@/lib/api/links/case-sensitivity";
3-
import { conn } from "@/lib/planetscale";
41
import { redis } from "@/lib/upstash";
52
import z from "@/lib/zod";
6-
import {
7-
DUB_HEADERS,
8-
getUrlFromStringIfValid,
9-
linkConstructorSimple,
10-
} from "@dub/utils";
11-
import { waitUntil } from "@vercel/functions";
3+
import { DUB_HEADERS, getUrlFromStringIfValid } from "@dub/utils";
124
import { NextRequest, NextResponse } from "next/server";
135

146
const workspaceId = "cm05wnnpo000711ztj05wwdbu";
15-
const userId = "cm05wnd49000411ztg2xbup0i";
16-
const folderId = "fold_1JNQBVZV8P0NA0YGB11W2HHSQ";
177

188
// GET /api/links/crawl/bitly – crawl a bitly link and redirect to the destination if exists
199
export const GET = async (_req: NextRequest, { params }) => {
@@ -34,55 +24,6 @@ export const GET = async (_req: NextRequest, { params }) => {
3424
const sanitizedUrl = getUrlFromStringIfValid(link.long_url);
3525

3626
if (sanitizedUrl) {
37-
const processedKey = encodeKeyIfCaseSensitive({
38-
domain,
39-
key,
40-
});
41-
42-
const newLink = {
43-
id: createId({ prefix: "link_" }),
44-
projectId: workspaceId,
45-
userId,
46-
domain,
47-
key: processedKey,
48-
url: sanitizedUrl,
49-
shortLink: linkConstructorSimple({
50-
domain,
51-
key: processedKey,
52-
}),
53-
archived: false,
54-
folderId,
55-
createdAt: new Date(link.created_at),
56-
updatedAt: new Date(link.created_at),
57-
};
58-
59-
console.log(
60-
`[Bitly] Creating link ${newLink.shortLink} -> ${newLink.url}`,
61-
);
62-
63-
waitUntil(
64-
(async () => {
65-
try {
66-
await conn.execute(
67-
"INSERT INTO Link (id, projectId, userId, domain, `key`, url, shortLink, archived, folderId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
68-
[
69-
newLink.id,
70-
newLink.projectId,
71-
newLink.userId,
72-
newLink.domain,
73-
newLink.key,
74-
newLink.url,
75-
newLink.shortLink,
76-
newLink.archived,
77-
newLink.folderId,
78-
newLink.createdAt,
79-
newLink.updatedAt,
80-
],
81-
);
82-
} catch (_e) {}
83-
})(),
84-
);
85-
8627
return NextResponse.redirect(sanitizedUrl, {
8728
headers: DUB_HEADERS,
8829
status: 302,

apps/web/lib/middleware/partners.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export default async function PartnersMiddleware(req: NextRequest) {
4343
return NextResponse.redirect(new URL("/onboarding", req.url));
4444
}
4545

46-
if (searchParamsObj.next) {
46+
// special case for handling ?next= query param
47+
// only redirect if next is a valid relative path (not an absolute URL)
48+
if (searchParamsObj.next && searchParamsObj.next.startsWith("/")) {
4749
return NextResponse.redirect(new URL(searchParamsObj.next, req.url));
4850
}
4951

apps/web/lib/swr/use-partner-profile.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import { CONNECT_SUPPORTED_COUNTRIES, fetcher } from "@dub/utils";
22
import { useSession } from "next-auth/react";
3-
import { useEffect, useState } from "react";
43
import useSWR from "swr";
54
import { PartnerProps } from "../types";
65

76
export default function usePartnerProfile() {
87
const { data: session, status } = useSession();
9-
const partnerId = session?.user?.["defaultPartnerId"];
10-
11-
const [isPartnerPage, setIsPartnerPage] = useState(false);
12-
13-
useEffect(() => {
14-
setIsPartnerPage(window.location.hostname.startsWith("partners"));
15-
}, []);
8+
const defaultPartnerId = session?.user?.["defaultPartnerId"];
169

1710
const {
1811
data: partner,
1912
error,
2013
isLoading,
2114
mutate,
2215
} = useSWR<PartnerProps>(
23-
isPartnerPage && partnerId && "/api/partner-profile",
16+
defaultPartnerId && "/api/partner-profile",
2417
fetcher,
2518
{
2619
dedupingInterval: 60000,

apps/web/ui/layout/sidebar/app-sidebar-nav.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { ReactNode, useMemo } from "react";
2323
import UserSurveyButton from "../user-survey";
2424
import { ConnectedDots4 } from "./icons/connected-dots4";
2525
import { CursorRays } from "./icons/cursor-rays";
26-
import { Gear } from "./icons/gear";
2726
import { Hyperlink } from "./icons/hyperlink";
2827
import { LinesY } from "./icons/lines-y";
2928
import { User } from "./icons/user";
@@ -67,20 +66,10 @@ const NAV_AREAS: SidebarNavAreas<{
6766
icon: User,
6867
href: `/${slug}/customers`,
6968
},
70-
{
71-
name: "Settings",
72-
icon: Gear,
73-
href: `/${slug}/settings`,
74-
},
75-
],
76-
},
77-
...(defaultProgramId
78-
? [
79-
{
80-
name: "Programs",
81-
items: [
69+
...(defaultProgramId
70+
? [
8271
{
83-
name: "Affiliate",
72+
name: "Program",
8473
icon: ConnectedDots4,
8574
href: `/${slug}/programs/${defaultProgramId}`,
8675
items: [
@@ -111,10 +100,10 @@ const NAV_AREAS: SidebarNavAreas<{
111100
},
112101
],
113102
},
114-
],
115-
},
116-
]
117-
: []),
103+
]
104+
: []),
105+
],
106+
},
118107
],
119108
}),
120109

apps/web/ui/layout/sidebar/user-dropdown.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"use client";
22

3-
import { Avatar, Gift, Icon, Popover, User } from "@dub/ui";
3+
import usePartnerProfile from "@/lib/swr/use-partner-profile";
4+
import {
5+
ArrowsOppositeDirectionX,
6+
Avatar,
7+
Gift,
8+
Icon,
9+
Popover,
10+
User,
11+
} from "@dub/ui";
412
import { cn } from "@dub/utils";
513
import { LogOut } from "lucide-react";
614
import { signOut, useSession } from "next-auth/react";
@@ -9,12 +17,13 @@ import { ComponentPropsWithoutRef, ElementType, useState } from "react";
917

1018
export default function UserDropdown() {
1119
const { data: session } = useSession();
20+
const { partner } = usePartnerProfile();
1221
const [openPopover, setOpenPopover] = useState(false);
1322

1423
return (
1524
<Popover
1625
content={
17-
<div className="flex w-full flex-col space-y-px rounded-md bg-white p-2 sm:w-56">
26+
<div className="flex w-full flex-col space-y-px rounded-md bg-white p-2">
1827
{session?.user ? (
1928
<div className="p-2">
2029
<p className="truncate text-sm font-medium text-neutral-900">
@@ -44,6 +53,14 @@ export default function UserDropdown() {
4453
href="/account/settings/referrals"
4554
onClick={() => setOpenPopover(false)}
4655
/>
56+
{partner && (
57+
<UserOption
58+
as={Link}
59+
label="Switch to partner account"
60+
icon={ArrowsOppositeDirectionX}
61+
href="https://partners.dub.co"
62+
/>
63+
)}
4764
<UserOption
4865
as="button"
4966
type="button"
@@ -99,7 +116,7 @@ function UserOption<T extends ElementType = "button">({
99116

100117
return (
101118
<Component
102-
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"
119+
className="flex items-center gap-x-4 rounded-md px-2.5 py-1.5 text-sm transition-all duration-75 hover:bg-neutral-200/50 active:bg-neutral-200/80"
103120
{...rest}
104121
>
105122
<Icon className="size-4 text-neutral-500" />

apps/web/ui/layout/sidebar/workspace-dropdown.tsx

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
Popover,
1010
useScrollProgress,
1111
} from "@dub/ui";
12-
import { Book2, Check2, Plus } from "@dub/ui/icons";
12+
import { Check2, Gear, Plus, UserPlus } from "@dub/ui/icons";
1313
import { cn, OG_AVATAR_URL } from "@dub/utils";
14-
import { ChevronsUpDown, HelpCircle } from "lucide-react";
14+
import { ChevronsUpDown } from "lucide-react";
1515
import { useSession } from "next-auth/react";
1616
import Link from "next/link";
1717
import { useParams, usePathname } from "next/navigation";
@@ -139,21 +139,6 @@ function WorkspaceDropdownPlaceholder() {
139139
);
140140
}
141141

142-
const LINKS = [
143-
{
144-
name: "Help Center",
145-
icon: HelpCircle,
146-
href: "https://dub.co/help",
147-
target: "_blank",
148-
},
149-
{
150-
name: "Documentation",
151-
icon: Book2,
152-
href: "https://dub.co/docs",
153-
target: "_blank",
154-
},
155-
];
156-
157142
function WorkspaceList({
158143
selected,
159144
workspaces,
@@ -180,9 +165,9 @@ function WorkspaceList({
180165

181166
const href = useCallback(
182167
(slug: string) => {
183-
if (link || selected.slug === "/") {
168+
if (link) {
184169
// if we're on a link page, navigate back to the workspace root
185-
return `/${slug}`;
170+
return `/${slug}/links`;
186171
} else if (programId) {
187172
// if we're on a program page, navigate to the program page
188173
return `/${slug}/programs`;
@@ -201,31 +186,59 @@ function WorkspaceList({
201186
onScroll={updateScrollProgress}
202187
className="relative max-h-80 w-full space-y-0.5 overflow-auto rounded-lg bg-white text-base sm:w-64 sm:text-sm"
203188
>
204-
<div className="flex flex-col gap-0.5 border-b border-neutral-200 p-2">
205-
{LINKS.map(({ name, icon: Icon, href, target }) => (
206-
<Link
207-
key={name}
208-
href={href}
209-
target={target}
210-
className={cn(
211-
"flex items-center gap-x-4 rounded-md px-2.5 py-2 transition-all duration-75 hover:bg-neutral-200/50 active:bg-neutral-200/80",
212-
"outline-none focus-visible:ring-2 focus-visible:ring-black/50",
189+
{/* Current workspace section */}
190+
<div className="border-b border-neutral-200 p-2">
191+
<div className="flex items-center gap-x-2.5 rounded-md p-2">
192+
<BlurImage
193+
src={selected.image || `${OG_AVATAR_URL}${selected.name}`}
194+
width={28}
195+
height={28}
196+
alt={selected.name}
197+
className="size-8 shrink-0 overflow-hidden rounded-full"
198+
/>
199+
<div className="min-w-0">
200+
<div className="truncate text-sm font-medium leading-5 text-neutral-900">
201+
{selected.name}
202+
</div>
203+
{selected.slug !== "/" && (
204+
<div
205+
className={cn(
206+
"truncate text-xs capitalize leading-tight",
207+
getPlanColor(selected.plan),
208+
)}
209+
>
210+
{selected.plan}
211+
</div>
213212
)}
213+
</div>
214+
</div>
215+
216+
{/* Settings and Invite members options */}
217+
<div className="mt-2 flex flex-col gap-0.5">
218+
<Link
219+
href={`/${selected.slug}/settings`}
220+
className="flex w-full items-center gap-x-2 rounded-md px-2 py-1.5 text-neutral-700 outline-none transition-all duration-75 hover:bg-neutral-200/50 focus-visible:ring-2 focus-visible:ring-black/50 active:bg-neutral-200/80"
214221
onClick={() => setOpenPopover(false)}
215222
>
216-
<Icon className="size-4 text-neutral-500" />
217-
<span className="block truncate text-neutral-600">{name}</span>
223+
<Gear className="size-4 text-neutral-500" />
224+
<span className="block truncate text-sm">Settings</span>
218225
</Link>
219-
))}
226+
<Link
227+
href={`/${selected.slug}/settings/people`}
228+
className="flex w-full items-center gap-x-2 rounded-md px-2 py-1.5 text-neutral-700 outline-none transition-all duration-75 hover:bg-neutral-200/50 focus-visible:ring-2 focus-visible:ring-black/50 active:bg-neutral-200/80"
229+
onClick={() => setOpenPopover(false)}
230+
>
231+
<UserPlus className="size-4 text-neutral-500" />
232+
<span className="block truncate text-sm">Invite members</span>
233+
</Link>
234+
</div>
220235
</div>
236+
237+
{/* Workspaces section */}
221238
<div className="p-2">
222-
<div className="flex items-center justify-between pb-1">
223-
<p className="px-1 text-xs font-medium text-neutral-500">
224-
Workspaces
225-
</p>
226-
</div>
239+
<p className="p-1 text-xs font-medium text-neutral-500">Workspaces</p>
227240
<div className="flex flex-col gap-0.5">
228-
{workspaces.map(({ id, name, slug, logo, plan }) => {
241+
{workspaces.map(({ id, name, slug, logo }) => {
229242
const isActive = selected.slug === slug;
230243
return (
231244
<Link
@@ -245,23 +258,11 @@ function WorkspaceList({
245258
width={28}
246259
height={28}
247260
alt={id}
248-
className="size-7 shrink-0 overflow-hidden rounded-full"
261+
className="size-6 shrink-0 overflow-hidden rounded-full"
249262
/>
250-
<div>
251-
<span className="block truncate text-sm leading-5 text-neutral-900 sm:max-w-[140px]">
252-
{name}
253-
</span>
254-
{slug !== "/" && (
255-
<div
256-
className={cn(
257-
"truncate text-xs capitalize leading-tight",
258-
getPlanColor(plan),
259-
)}
260-
>
261-
{plan}
262-
</div>
263-
)}
264-
</div>
263+
<span className="block truncate text-sm leading-5 text-neutral-900 sm:max-w-[140px]">
264+
{name}
265+
</span>
265266
{selected.slug === slug ? (
266267
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-black">
267268
<Check2 className="size-4" aria-hidden="true" />
@@ -278,7 +279,7 @@ function WorkspaceList({
278279
}}
279280
className="group flex w-full cursor-pointer items-center gap-x-2 rounded-md p-2 text-neutral-700 transition-all duration-75 hover:bg-neutral-200/50 active:bg-neutral-200/80"
280281
>
281-
<Plus className="mx-1.5 size-4 text-neutral-500" />
282+
<Plus className="ml-1.5 size-4 text-neutral-500" />
282283
<span className="block truncate">Create new workspace</span>
283284
</button>
284285
</div>

0 commit comments

Comments
 (0)