Skip to content

Commit bcc3a32

Browse files
committed
cleanup sidebar, add standalone items
1 parent dfb2296 commit bcc3a32

File tree

5 files changed

+156
-39
lines changed

5 files changed

+156
-39
lines changed

apps/dashboard/components/layout/navigation/navigation-config.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
GlobeIcon,
2222
GlobeSimpleIcon,
2323
HeartbeatIcon,
24+
HouseIcon,
2425
IdentificationCardIcon,
2526
KeyIcon,
2627
LightningIcon,
@@ -41,7 +42,7 @@ import {
4142
UsersThreeIcon,
4243
WarningIcon,
4344
} from "@phosphor-icons/react";
44-
import type { Category, NavigationSection } from "./types";
45+
import type { Category, NavigationEntry, NavigationSection } from "./types";
4546

4647
const createNavItem = (
4748
name: string,
@@ -114,8 +115,11 @@ const createDynamicNavigation = <T extends { id: string; name: string | null }>(
114115

115116
export const createWebsitesNavigation = (
116117
websites: Array<{ id: string; name: string | null; domain: string }>
117-
): NavigationSection[] =>
118-
createDynamicNavigation(
118+
): NavigationEntry[] => [
119+
createNavItem("Home", HouseIcon, "/home", {
120+
highlight: true,
121+
}),
122+
...createDynamicNavigation(
119123
websites,
120124
"Websites",
121125
GlobeSimpleIcon,
@@ -125,7 +129,8 @@ export const createWebsitesNavigation = (
125129
"/websites",
126130
"Add Your First Website",
127131
(website) => ({ domain: website.domain })
128-
);
132+
),
133+
];
129134

130135
export const personalNavigation: NavigationSection[] = [
131136
createNavSection("Personal", UserGearIcon, [
@@ -315,7 +320,7 @@ export const websiteSettingsNavigation: NavigationSection[] = [
315320
const createCategoryConfig = (
316321
categories: Category[],
317322
defaultCategory: string,
318-
navigationMap: Record<string, NavigationSection[]>
323+
navigationMap: Record<string, NavigationEntry[]>
319324
) => ({ categories, defaultCategory, navigationMap });
320325

321326
export const categoryConfig = {
@@ -433,12 +438,16 @@ const createLoadingNavigation = (
433438
]),
434439
];
435440

436-
export const createLoadingWebsitesNavigation = (): NavigationSection[] =>
437-
createLoadingNavigation(
441+
export const createLoadingWebsitesNavigation = (): NavigationEntry[] => [
442+
createNavItem("Home", HouseIcon, "/home", {
443+
highlight: true,
444+
}),
445+
...createLoadingNavigation(
438446
"Websites",
439447
GlobeSimpleIcon,
440448
"Website Overview",
441449
"/websites",
442450
"Loading websites...",
443451
GlobeIcon
444-
);
452+
),
453+
];

apps/dashboard/components/layout/navigation/navigation-item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function NavigationItem({
6565
size={20}
6666
/>
6767
) : (
68-
<Icon aria-hidden className="size-4 shrink-0" />
68+
<Icon aria-hidden className="size-4 shrink-0" weight="duotone" />
6969
)}
7070
<span className="flex-1">{name}</span>
7171
</>

apps/dashboard/components/layout/navigation/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface NavigationSection {
3434
flag?: string;
3535
}
3636

37+
export type NavigationEntry = NavigationSection | NavigationItem;
38+
3739
export interface Category {
3840
id: string;
3941
name: string;

apps/dashboard/components/layout/sidebar.tsx

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ import {
2020
getContextConfig,
2121
getDefaultCategory,
2222
} from "./navigation/navigation-config";
23+
import { NavigationItem } from "./navigation/navigation-item";
2324
import { NavigationSection } from "./navigation/navigation-section";
24-
import type { NavigationSection as NavigationSectionType } from "./navigation/types";
25+
import type {
26+
NavigationEntry,
27+
NavigationItem as NavigationItemType,
28+
NavigationSection as NavigationSectionType,
29+
} from "./navigation/types";
2530
import { WebsiteHeader } from "./navigation/website-header";
2631
import { OrganizationSelector } from "./organization-selector";
2732

2833
interface NavigationConfig {
29-
navigation: NavigationSectionType[];
34+
navigation: NavigationEntry[];
3035
header: React.ReactNode;
3136
currentWebsiteId?: string | null;
3237
}
@@ -41,6 +46,44 @@ interface SidebarProps {
4146
user: User | null;
4247
}
4348

49+
const isNavigationSection = (
50+
entry: NavigationEntry
51+
): entry is NavigationSectionType => {
52+
return "items" in entry;
53+
};
54+
55+
const isNavigationItem = (
56+
entry: NavigationEntry
57+
): entry is NavigationItemType => {
58+
return "href" in entry && !("items" in entry);
59+
};
60+
61+
const isItemActive = (
62+
item: NavigationItemType,
63+
pathname: string,
64+
currentWebsiteId?: string | null
65+
): boolean => {
66+
if (item.rootLevel) {
67+
return pathname === item.href;
68+
}
69+
70+
const buildFullPath = (basePath: string, itemHref: string) =>
71+
itemHref === "" ? basePath : `${basePath}${itemHref}`;
72+
73+
const fullPath = (() => {
74+
if (pathname.startsWith("/demo")) {
75+
return buildFullPath(`/demo/${currentWebsiteId}`, item.href);
76+
}
77+
return buildFullPath(`/websites/${currentWebsiteId}`, item.href);
78+
})();
79+
80+
if (item.href === "") {
81+
return pathname === fullPath;
82+
}
83+
84+
return pathname === fullPath || pathname.startsWith(`${fullPath}/`);
85+
};
86+
4487
export function Sidebar({ user = null }: SidebarProps) {
4588
const pathname = usePathname();
4689
const [isMobileOpen, setIsMobileOpen] = useState(false);
@@ -287,27 +330,65 @@ export function Sidebar({ user = null }: SidebarProps) {
287330
/>
288331

289332
<nav aria-label="Main navigation" className="flex flex-col">
290-
{navigation.map((section, idx) => (
291-
<NavigationSection
292-
accordionStates={accordionStates}
293-
className={cn(
294-
navigation.length > 1 && idx === navigation.length - 1
295-
? "border-t"
296-
: idx === 0 && navigation.length < 2
297-
? "box-content border-b"
298-
: idx !== 0 && navigation.length > 1
333+
{navigation.map((entry, idx) => {
334+
if (isNavigationSection(entry)) {
335+
return (
336+
<NavigationSection
337+
accordionStates={accordionStates}
338+
className={cn(
339+
navigation.length > 1 && idx === navigation.length - 1
299340
? "border-t"
300-
: "border-transparent"
301-
)}
302-
currentWebsiteId={currentWebsiteId}
303-
flag={section.flag}
304-
icon={section.icon}
305-
items={section.items}
306-
key={section.title}
307-
pathname={pathname}
308-
title={section.title}
309-
/>
310-
))}
341+
: idx === 0 && navigation.length < 2
342+
? "box-content border-b"
343+
: idx !== 0 && navigation.length > 1
344+
? "border-t"
345+
: "border-transparent"
346+
)}
347+
currentWebsiteId={currentWebsiteId}
348+
flag={entry.flag}
349+
icon={entry.icon}
350+
items={entry.items}
351+
key={entry.title}
352+
pathname={pathname}
353+
title={entry.title}
354+
/>
355+
);
356+
}
357+
358+
if (isNavigationItem(entry)) {
359+
return (
360+
<div
361+
className={cn(idx !== 0 && "border-t")}
362+
key={entry.name}
363+
>
364+
<NavigationItem
365+
alpha={entry.alpha}
366+
badge={entry.badge}
367+
currentWebsiteId={currentWebsiteId}
368+
disabled={entry.disabled}
369+
domain={entry.domain}
370+
href={entry.href}
371+
icon={entry.icon}
372+
isActive={isItemActive(
373+
entry,
374+
pathname,
375+
currentWebsiteId
376+
)}
377+
isExternal={entry.external}
378+
isLocked={false}
379+
isRootLevel={!!entry.rootLevel}
380+
lockedPlanName={null}
381+
name={entry.name}
382+
production={entry.production}
383+
sectionName="main"
384+
tag={entry.tag}
385+
/>
386+
</div>
387+
);
388+
}
389+
390+
return null;
391+
})}
311392
</nav>
312393
</div>
313394
</ScrollArea>

apps/dashboard/components/ui/command-search.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {
1919
websiteNavigation,
2020
websiteSettingsNavigation,
2121
} from "@/components/layout/navigation/navigation-config";
22-
import type { NavigationItem, NavigationSection } from "@/components/layout/navigation/types";
22+
import type {
23+
NavigationEntry,
24+
NavigationItem,
25+
NavigationSection,
26+
} from "@/components/layout/navigation/types";
2327
import { Badge } from "@/components/ui/badge";
2428
import {
2529
Dialog,
@@ -68,13 +72,34 @@ function toSearchItem(item: NavigationItem, pathPrefix = ""): SearchItem {
6872
};
6973
}
7074

71-
function toSearchGroups(sections: NavigationSection[], pathPrefix = ""): SearchGroup[] {
72-
return sections.map((section) => ({
73-
category: section.title,
74-
items: section.items
75-
.filter((item) => !item.hideFromDemo)
76-
.map((item) => toSearchItem(item, pathPrefix)),
77-
}));
75+
const isSection = (entry: NavigationEntry): entry is NavigationSection =>
76+
"items" in entry;
77+
78+
function toSearchGroups(entries: NavigationEntry[], pathPrefix = ""): SearchGroup[] {
79+
const groups: SearchGroup[] = [];
80+
const standaloneItems: SearchItem[] = [];
81+
82+
for (const entry of entries) {
83+
if (isSection(entry)) {
84+
groups.push({
85+
category: entry.title,
86+
items: entry.items
87+
.filter((item) => !item.hideFromDemo)
88+
.map((item) => toSearchItem(item, pathPrefix)),
89+
});
90+
} else if (!entry.hideFromDemo) {
91+
standaloneItems.push(toSearchItem(entry, pathPrefix));
92+
}
93+
}
94+
95+
if (standaloneItems.length > 0) {
96+
groups.unshift({
97+
category: "Quick Access",
98+
items: standaloneItems,
99+
});
100+
}
101+
102+
return groups;
78103
}
79104

80105
function mergeGroups(groups: SearchGroup[]): SearchGroup[] {

0 commit comments

Comments
 (0)