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 <bram.suurd@infracom.nl>
This commit is contained in:
Bram Suurd 2025-09-03 14:45:24 +02:00 committed by GitHub
parent 45a2163e66
commit a24169e9b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -36,19 +36,24 @@ export function formattedBadge(type: string) {
return null; return null;
} }
// random Script function getRandomScript(categories: Category[], previouslySelected: Set<string> = new Set()): Script | null {
function getRandomScript(categories: Category[]): Script | null {
const allScripts = categories.flatMap(cat => cat.scripts || []); const allScripts = categories.flatMap(cat => cat.scripts || []);
if (allScripts.length === 0) if (allScripts.length === 0)
return null; return null;
const idx = Math.floor(Math.random() * allScripts.length);
return allScripts[idx]; const availableScripts = allScripts.filter(script => !previouslySelected.has(script.slug));
if (availableScripts.length === 0) {
return allScripts[Math.floor(Math.random() * allScripts.length)];
}
const idx = Math.floor(Math.random() * availableScripts.length);
return availableScripts[idx];
} }
export default function CommandMenu() { function CommandMenu() {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [links, setLinks] = React.useState<Category[]>([]); const [links, setLinks] = React.useState<Category[]>([]);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
const router = useRouter(); const router = useRouter();
const fetchSortedCategories = () => { const fetchSortedCategories = () => {
@ -65,25 +70,26 @@ export default function CommandMenu() {
}; };
React.useEffect(() => { React.useEffect(() => {
const down = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
fetchSortedCategories(); fetchSortedCategories();
setOpen(open => !open); setOpen(open => !open);
} }
}; };
document.addEventListener("keydown", down); document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", down); return () => document.removeEventListener("keydown", handleKeyDown);
}, []); }, []);
const openRandomScript = async () => { const handleOpenRandomScript = async () => {
if (links.length === 0) { if (links.length === 0) {
setIsLoading(true); setIsLoading(true);
try { try {
const categories = await fetchCategories(); const categories = await fetchCategories();
setLinks(categories); setLinks(categories);
const randomScript = getRandomScript(categories); const randomScript = getRandomScript(categories, selectedScripts);
if (randomScript) { if (randomScript) {
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
router.push(`/scripts?id=${randomScript.slug}`); router.push(`/scripts?id=${randomScript.slug}`);
} }
} }
@ -92,13 +98,54 @@ export default function CommandMenu() {
} }
} }
else { else {
const randomScript = getRandomScript(links); const randomScript = getRandomScript(links, selectedScripts);
if (randomScript) { if (randomScript) {
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
router.push(`/scripts?id=${randomScript.slug}`); router.push(`/scripts?id=${randomScript.slug}`);
} }
} }
}; };
const getUniqueScriptsMap = React.useCallback(() => {
const scriptMap = new Map<string, { script: Script; categoryName: string }>();
for (const category of links) {
for (const script of category.scripts) {
if (!scriptMap.has(script.slug)) {
scriptMap.set(script.slug, { script, categoryName: category.name });
}
}
}
return scriptMap;
}, [links]);
const getUniqueScriptsByCategory = React.useCallback(() => {
const scriptMap = getUniqueScriptsMap();
const categoryOrder = links.map(cat => cat.name);
const grouped: Record<string, Script[]> = {};
for (const name of categoryOrder) {
grouped[name] = [];
}
for (const { script, categoryName } of scriptMap.values()) {
if (grouped[categoryName]) {
grouped[categoryName].push(script);
}
else {
grouped[categoryName] = [script];
}
}
Object.keys(grouped).forEach((cat) => {
if (grouped[cat].length === 0)
delete grouped[cat];
});
return grouped;
}, [getUniqueScriptsMap, links]);
const uniqueScriptsByCategory = getUniqueScriptsByCategory();
return ( return (
<> <>
<div className="flex gap-2"> <div className="flex gap-2">
@ -122,7 +169,20 @@ export default function CommandMenu() {
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={100}> <Tooltip delayDuration={100}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={openRandomScript} disabled={isLoading} className="hidden lg:flex"> <Button
variant="outline"
size="icon"
onClick={handleOpenRandomScript}
disabled={isLoading}
className="hidden lg:flex"
aria-label="Open Random Script"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleOpenRandomScript();
}
}}
>
<Sparkles className="size-4" /> <Sparkles className="size-4" />
<span className="sr-only">Open Random Script</span> <span className="sr-only">Open Random Script</span>
</Button> </Button>
@ -139,16 +199,24 @@ export default function CommandMenu() {
<CommandInput placeholder="Search for a script..." /> <CommandInput placeholder="Search for a script..." />
<CommandList> <CommandList>
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty> <CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
{links.map(category => ( {Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
<CommandGroup key={`category:${category.name}`} heading={category.name}> <CommandGroup key={`category:${categoryName}`} heading={categoryName}>
{category.scripts.map(script => ( {scripts.map(script => (
<CommandItem <CommandItem
key={`script:${script.slug}`} key={`script:${script.slug}`}
value={`${script.slug}-${script.name}`} value={`${script.name}-${script.type}`}
onSelect={() => { onSelect={() => {
setOpen(false); setOpen(false);
router.push(`/scripts?id=${script.slug}`); router.push(`/scripts?id=${script.slug}`);
}} }}
tabIndex={0}
aria-label={`Open script ${script.name}`}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setOpen(false);
router.push(`/scripts?id=${script.slug}`);
}
}}
> >
<div className="flex gap-2" onClick={() => setOpen(false)}> <div className="flex gap-2" onClick={() => setOpen(false)}>
<Image <Image
@ -172,3 +240,5 @@ export default function CommandMenu() {
</> </>
); );
} }
export default CommandMenu;