Skip to content

Commit a24169e

Browse files
authored
Fix navigation (#7376)
* Removed double entries from the search to improve navigation * change input on search field to improve searchability * added type to search to make sure that LXC and VM's dont get mixed up * run linting over changes --------- Co-authored-by: Bram Suurd <[email protected]>
1 parent 45a2163 commit a24169e

File tree

1 file changed

+86
-16
lines changed

1 file changed

+86
-16
lines changed

frontend/src/components/command-menu.tsx

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,24 @@ export function formattedBadge(type: string) {
3636
return null;
3737
}
3838

39-
// random Script
40-
function getRandomScript(categories: Category[]): Script | null {
39+
function getRandomScript(categories: Category[], previouslySelected: Set<string> = new Set()): Script | null {
4140
const allScripts = categories.flatMap(cat => cat.scripts || []);
4241
if (allScripts.length === 0)
4342
return null;
44-
const idx = Math.floor(Math.random() * allScripts.length);
45-
return allScripts[idx];
43+
44+
const availableScripts = allScripts.filter(script => !previouslySelected.has(script.slug));
45+
if (availableScripts.length === 0) {
46+
return allScripts[Math.floor(Math.random() * allScripts.length)];
47+
}
48+
const idx = Math.floor(Math.random() * availableScripts.length);
49+
return availableScripts[idx];
4650
}
4751

48-
export default function CommandMenu() {
52+
function CommandMenu() {
4953
const [open, setOpen] = React.useState(false);
5054
const [links, setLinks] = React.useState<Category[]>([]);
5155
const [isLoading, setIsLoading] = React.useState(false);
56+
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
5257
const router = useRouter();
5358

5459
const fetchSortedCategories = () => {
@@ -65,25 +70,26 @@ export default function CommandMenu() {
6570
};
6671

6772
React.useEffect(() => {
68-
const down = (e: KeyboardEvent) => {
73+
const handleKeyDown = (e: KeyboardEvent) => {
6974
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
7075
e.preventDefault();
7176
fetchSortedCategories();
7277
setOpen(open => !open);
7378
}
7479
};
75-
document.addEventListener("keydown", down);
76-
return () => document.removeEventListener("keydown", down);
80+
document.addEventListener("keydown", handleKeyDown);
81+
return () => document.removeEventListener("keydown", handleKeyDown);
7782
}, []);
7883

79-
const openRandomScript = async () => {
84+
const handleOpenRandomScript = async () => {
8085
if (links.length === 0) {
8186
setIsLoading(true);
8287
try {
8388
const categories = await fetchCategories();
8489
setLinks(categories);
85-
const randomScript = getRandomScript(categories);
90+
const randomScript = getRandomScript(categories, selectedScripts);
8691
if (randomScript) {
92+
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
8793
router.push(`/scripts?id=${randomScript.slug}`);
8894
}
8995
}
@@ -92,13 +98,54 @@ export default function CommandMenu() {
9298
}
9399
}
94100
else {
95-
const randomScript = getRandomScript(links);
101+
const randomScript = getRandomScript(links, selectedScripts);
96102
if (randomScript) {
103+
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
97104
router.push(`/scripts?id=${randomScript.slug}`);
98105
}
99106
}
100107
};
101108

109+
const getUniqueScriptsMap = React.useCallback(() => {
110+
const scriptMap = new Map<string, { script: Script; categoryName: string }>();
111+
for (const category of links) {
112+
for (const script of category.scripts) {
113+
if (!scriptMap.has(script.slug)) {
114+
scriptMap.set(script.slug, { script, categoryName: category.name });
115+
}
116+
}
117+
}
118+
return scriptMap;
119+
}, [links]);
120+
121+
const getUniqueScriptsByCategory = React.useCallback(() => {
122+
const scriptMap = getUniqueScriptsMap();
123+
const categoryOrder = links.map(cat => cat.name);
124+
const grouped: Record<string, Script[]> = {};
125+
126+
for (const name of categoryOrder) {
127+
grouped[name] = [];
128+
}
129+
130+
for (const { script, categoryName } of scriptMap.values()) {
131+
if (grouped[categoryName]) {
132+
grouped[categoryName].push(script);
133+
}
134+
else {
135+
grouped[categoryName] = [script];
136+
}
137+
}
138+
139+
Object.keys(grouped).forEach((cat) => {
140+
if (grouped[cat].length === 0)
141+
delete grouped[cat];
142+
});
143+
144+
return grouped;
145+
}, [getUniqueScriptsMap, links]);
146+
147+
const uniqueScriptsByCategory = getUniqueScriptsByCategory();
148+
102149
return (
103150
<>
104151
<div className="flex gap-2">
@@ -122,7 +169,20 @@ export default function CommandMenu() {
122169
<TooltipProvider>
123170
<Tooltip delayDuration={100}>
124171
<TooltipTrigger asChild>
125-
<Button variant="outline" size="icon" onClick={openRandomScript} disabled={isLoading} className="hidden lg:flex">
172+
<Button
173+
variant="outline"
174+
size="icon"
175+
onClick={handleOpenRandomScript}
176+
disabled={isLoading}
177+
className="hidden lg:flex"
178+
aria-label="Open Random Script"
179+
tabIndex={0}
180+
onKeyDown={(e) => {
181+
if (e.key === "Enter" || e.key === " ") {
182+
handleOpenRandomScript();
183+
}
184+
}}
185+
>
126186
<Sparkles className="size-4" />
127187
<span className="sr-only">Open Random Script</span>
128188
</Button>
@@ -139,16 +199,24 @@ export default function CommandMenu() {
139199
<CommandInput placeholder="Search for a script..." />
140200
<CommandList>
141201
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
142-
{links.map(category => (
143-
<CommandGroup key={`category:${category.name}`} heading={category.name}>
144-
{category.scripts.map(script => (
202+
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
203+
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
204+
{scripts.map(script => (
145205
<CommandItem
146206
key={`script:${script.slug}`}
147-
value={`${script.slug}-${script.name}`}
207+
value={`${script.name}-${script.type}`}
148208
onSelect={() => {
149209
setOpen(false);
150210
router.push(`/scripts?id=${script.slug}`);
151211
}}
212+
tabIndex={0}
213+
aria-label={`Open script ${script.name}`}
214+
onKeyDown={(e) => {
215+
if (e.key === "Enter" || e.key === " ") {
216+
setOpen(false);
217+
router.push(`/scripts?id=${script.slug}`);
218+
}
219+
}}
152220
>
153221
<div className="flex gap-2" onClick={() => setOpen(false)}>
154222
<Image
@@ -172,3 +240,5 @@ export default function CommandMenu() {
172240
</>
173241
);
174242
}
243+
244+
export default CommandMenu;

0 commit comments

Comments
 (0)