Skip to content

Commit 32d63cb

Browse files
committed
feat(nx-dev): display technology icons
1 parent 3ae2ade commit 32d63cb

File tree

1 file changed

+158
-25
lines changed

1 file changed

+158
-25
lines changed

nx-dev/ui-common/src/lib/sidebar.tsx

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { XMarkIcon } from '@heroicons/react/24/outline';
99
import { AlgoliaSearch } from '@nx/nx-dev/feature-search';
1010
import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
11+
import { iconsMap } from '@nx/nx-dev/ui-references';
1112
import cx from 'classnames';
1213
import Link from 'next/link';
1314
import { useRouter } from 'next/router';
@@ -31,16 +32,30 @@ export function Sidebar({ menu }: SidebarProps): JSX.Element {
3132
data-testid="navigation"
3233
className="pb-4 text-base lg:text-sm"
3334
>
34-
{menu.sections.map((section, index) => (
35-
<SidebarSection key={section.id + '-' + index} section={section} />
36-
))}
35+
{menu.sections.map((section, index) => {
36+
return (
37+
<SidebarSection
38+
key={section.id + '-' + index}
39+
section={section}
40+
isInTechnologiesPath={false}
41+
/>
42+
);
43+
})}
3744
</nav>
3845
</div>
3946
);
4047
}
4148

42-
function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
49+
function SidebarSection({
50+
section,
51+
isInTechnologiesPath,
52+
}: {
53+
section: MenuSection;
54+
isInTechnologiesPath: boolean;
55+
}): JSX.Element {
4356
const router = useRouter();
57+
58+
// Get all items with refs
4459
const itemList = section.itemList.map((i) => ({
4560
...i,
4661
ref: createRef<HTMLDivElement>(),
@@ -56,6 +71,7 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
5671
}, 0);
5772
});
5873
}, [currentItem]);
74+
5975
return (
6076
<>
6177
{section.hideSectionHeader ? null : (
@@ -70,11 +86,21 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
7086
<li className="mt-2">
7187
{itemList
7288
.filter((i) => !!i.children?.length)
73-
.map((item, index) => (
74-
<div key={item.id + '-' + index} ref={item.ref}>
75-
<SidebarSectionItems key={item.id + '-' + index} item={item} />
76-
</div>
77-
))}
89+
.map((item, index) => {
90+
// Check if this specific item is the Technologies item
91+
const isTechnologiesItem = item.id === 'technologies';
92+
93+
return (
94+
<div key={item.id + '-' + index} ref={item.ref}>
95+
<SidebarSectionItems
96+
key={item.id + '-' + index}
97+
item={item}
98+
isNested={false}
99+
isInTechnologiesPath={isTechnologiesItem}
100+
/>
101+
</div>
102+
);
103+
})}
78104
</li>
79105
</ul>
80106
</>
@@ -84,51 +110,88 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
84110
function SidebarSectionItems({
85111
item,
86112
isNested = false,
113+
isInTechnologiesPath = false,
87114
}: {
88115
item: MenuItem;
89116
isNested?: boolean;
117+
isInTechnologiesPath?: boolean;
90118
}): JSX.Element {
91119
const router = useRouter();
92120
const [collapsed, setCollapsed] = useState(!item.disableCollapsible);
93121

122+
// Check if this is the Technologies main item
123+
const isTechnologiesItem = item.id === 'technologies';
124+
125+
// If this is direct child of the Technologies item, show an icon
126+
const isDirectTechnologyChild = isInTechnologiesPath;
127+
128+
// Get the icon key for this technology
129+
let iconKey = null;
130+
if (isDirectTechnologyChild) {
131+
iconKey = getIconKeyForTechnology(item.id);
132+
}
133+
94134
const handleCollapseToggle = useCallback(() => {
95135
if (!item.disableCollapsible) {
96136
setCollapsed(!collapsed);
97137
}
98138
}, [collapsed, setCollapsed, item]);
139+
99140
function withoutAnchors(linkText: string): string {
100141
return linkText?.includes('#')
101142
? linkText.substring(0, linkText.indexOf('#'))
102143
: linkText;
103144
}
104145

146+
// Update the children mapping to safely handle cases where item.children might be undefined
147+
const children = item.children || [];
148+
105149
return (
106150
<>
107151
<h5
108152
data-testid={`section-h5:${item.id}`}
109153
className={cx(
110-
'flex py-2',
154+
'group flex items-center py-2',
155+
isDirectTechnologyChild
156+
? '-ml-1 rounded-md px-1 hover:bg-slate-50 dark:hover:bg-slate-800/60'
157+
: '',
111158
'text-sm font-semibold text-slate-800 lg:text-sm dark:text-slate-200',
112159
item.disableCollapsible ? 'cursor-text' : 'cursor-pointer'
113160
)}
114161
onClick={handleCollapseToggle}
115162
>
116-
{item.disableCollapsible ? (
117-
<Link
118-
href={item.path as string}
119-
className="hover:underline"
120-
prefetch={false}
121-
>
122-
{item.name}
123-
</Link>
124-
) : (
125-
<>
126-
{item.name} <CollapsibleIcon isCollapsed={collapsed} />
127-
</>
163+
{isDirectTechnologyChild && (
164+
<div className="mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center">
165+
<img
166+
className="h-5 w-5 object-cover opacity-100 dark:invert"
167+
loading="lazy"
168+
src={iconsMap[iconKey || 'nx']}
169+
alt={item.name + ' illustration'}
170+
aria-hidden="true"
171+
/>
172+
</div>
128173
)}
174+
<div className={cx('flex flex-grow items-center justify-between')}>
175+
{item.disableCollapsible ? (
176+
<Link
177+
href={item.path as string}
178+
className="hover:underline"
179+
prefetch={false}
180+
>
181+
{item.name}
182+
</Link>
183+
) : (
184+
<>
185+
<span className={isDirectTechnologyChild ? 'flex-grow' : ''}>
186+
{item.name}
187+
</span>
188+
<CollapsibleIcon isCollapsed={collapsed} />
189+
</>
190+
)}
191+
</div>
129192
</h5>
130193
<ul className={cx('mb-6', collapsed ? 'hidden' : '')}>
131-
{(item.children as MenuItem[]).map((subItem, index) => {
194+
{children.map((subItem, index) => {
132195
const isActiveLink = withoutAnchors(router.asPath).startsWith(
133196
subItem.path
134197
);
@@ -150,8 +213,12 @@ function SidebarSectionItems({
150213
: 'border-l-transparent hover:border-blue-300 dark:border-l-transparent dark:hover:border-sky-400')
151214
)}
152215
>
153-
{subItem.children.length ? (
154-
<SidebarSectionItems item={subItem} isNested={true} />
216+
{(subItem.children || []).length ? (
217+
<SidebarSectionItems
218+
item={subItem}
219+
isNested={true}
220+
isInTechnologiesPath={isTechnologiesItem}
221+
/>
155222
) : (
156223
<Link
157224
href={subItem.path}
@@ -369,6 +436,7 @@ export function SidebarMobile({
369436
<SidebarSection
370437
key={section.id + '-' + index}
371438
section={section}
439+
isInTechnologiesPath={false}
372440
/>
373441
))}
374442
</nav>
@@ -381,3 +449,68 @@ export function SidebarMobile({
381449
</Transition>
382450
);
383451
}
452+
453+
function getIconKeyForTechnology(idOrName: string): string {
454+
// Normalize the input to lowercase for more reliable matching
455+
const normalized = idOrName.toLowerCase();
456+
457+
// Technology icon mapping
458+
const technologyIconMap: Record<string, string> = {
459+
// JavaScript/TypeScript
460+
typescript: 'js',
461+
js: 'js',
462+
463+
// Angular
464+
angular: 'angular',
465+
'angular-rspack': 'angular-rspack',
466+
'angular-rsbuild': 'angular-rsbuild',
467+
468+
// React
469+
react: 'react',
470+
'react-native': 'react-native',
471+
remix: 'remix',
472+
next: 'next',
473+
expo: 'expo',
474+
475+
// Vue
476+
vue: 'vue',
477+
nuxt: 'nuxt',
478+
479+
// Node
480+
nodejs: 'node',
481+
'node.js': 'node',
482+
node: 'node',
483+
484+
// Java
485+
java: 'gradle',
486+
gradle: 'gradle',
487+
488+
// Module Federation
489+
'module-federation': 'module-federation',
490+
491+
// Linting
492+
eslint: 'eslint',
493+
'eslint-technology': 'eslint',
494+
495+
// Testing
496+
'testing-tools': 'jest',
497+
cypress: 'cypress',
498+
jest: 'jest',
499+
playwright: 'playwright',
500+
storybook: 'storybook',
501+
detox: 'detox',
502+
503+
// Build tools
504+
'build-tools': 'webpack',
505+
'build tools': 'webpack',
506+
webpack: 'webpack',
507+
vite: 'vite',
508+
rollup: 'rollup',
509+
esbuild: 'esbuild',
510+
rspack: 'rspack',
511+
rsbuild: 'rsbuild',
512+
};
513+
514+
// Return the mapped icon or 'nx' as default
515+
return technologyIconMap[normalized] || 'nx';
516+
}

0 commit comments

Comments
 (0)