Skip to content

Commit 07f2849

Browse files
BramSuurdjeCopilot
andauthored
Improve mobile ui: added a hamburger navigation to the mobile view. (#7987)
* Update GitHubStarsButton component to be hidden on smaller screens * feat: added a mobile navigation to the front-end. * refactor: replace useQueryState with useSuspenseQueryState in ScriptContent and MobileSidebar components; add use-suspense-query-state hook * Revert "refactor: replace useQueryState with useSuspenseQueryState in ScriptContent and MobileSidebar components; add use-suspense-query-state hook" This reverts commit bfad01f. * refactor: wrap MobileSidebar component in Suspense for improved loading handling * Update mobile-sidebar.tsx Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 7602992 commit 07f2849

File tree

6 files changed

+165
-28
lines changed

6 files changed

+165
-28
lines changed

frontend/src/app/layout.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,22 @@ export default function RootLayout({
103103
<body className={inter.className}>
104104
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
105105
<div className="flex w-full flex-col justify-center">
106-
<Navbar />
107-
<div className="flex min-h-screen flex-col justify-center">
108-
<div className="flex w-full justify-center">
109-
<div className="w-full max-w-[1440px] ">
110-
<QueryProvider>
111-
<NuqsAdapter>{children}</NuqsAdapter>
112-
</QueryProvider>
113-
<Toaster richColors />
106+
<NuqsAdapter>
107+
<QueryProvider>
108+
109+
<Navbar />
110+
<div className="flex min-h-screen flex-col justify-center">
111+
<div className="flex w-full justify-center">
112+
<div className="w-full max-w-[1440px] ">
113+
{children}
114+
<Toaster richColors />
115+
</div>
116+
</div>
117+
<Footer />
114118
</div>
115-
</div>
116-
<Footer />
117-
</div>
119+
</QueryProvider>
120+
121+
</NuqsAdapter>
118122
</div>
119123
</ThemeProvider>
120124
</body>

frontend/src/app/scripts/_components/script-accordion.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ export default function ScriptAccordion({
2727
setSelectedScript,
2828
selectedCategory,
2929
setSelectedCategory,
30+
onItemSelect,
3031
}: {
3132
items: Category[];
3233
selectedScript: string | null;
3334
setSelectedScript: (script: string | null) => void;
3435
selectedCategory: string | null;
3536
setSelectedCategory: (category: string | null) => void;
37+
onItemSelect?: () => void;
3638
}) {
3739
const [expandedItem, setExpandedItem] = useState<string | undefined>(undefined);
3840
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
@@ -77,7 +79,7 @@ export default function ScriptAccordion({
7779
value={expandedItem}
7880
onValueChange={handleAccordionChange}
7981
collapsible
80-
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
82+
className="overflow-y-scroll sm:max-h-[calc(100vh-209px)] overflow-x-hidden p-1"
8183
>
8284
{items.map(category => (
8385
<AccordionItem
@@ -125,6 +127,7 @@ export default function ScriptAccordion({
125127
onClick={() => {
126128
handleSelected(script.slug);
127129
setSelectedCategory(category.name);
130+
onItemSelect?.();
128131
}}
129132
ref={(el) => {
130133
linkRefs.current[script.slug] = el;

frontend/src/app/scripts/_components/sidebar.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22

33
import type { Category, Script } from "@/lib/types";
44

5+
import { cn } from "@/lib/utils";
6+
57
import ScriptAccordion from "./script-accordion";
68

9+
type SidebarProps = {
10+
items: Category[];
11+
selectedScript: string | null;
12+
setSelectedScript: (script: string | null) => void;
13+
selectedCategory: string | null;
14+
setSelectedCategory: (category: string | null) => void;
15+
onItemSelect?: () => void;
16+
className?: string;
17+
};
18+
719
function Sidebar({
820
items,
921
selectedScript,
1022
setSelectedScript,
1123
selectedCategory,
1224
setSelectedCategory,
13-
}: {
14-
items: Category[];
15-
selectedScript: string | null;
16-
setSelectedScript: (script: string | null) => void;
17-
selectedCategory: string | null;
18-
setSelectedCategory: (category: string | null) => void;
19-
}) {
25+
onItemSelect,
26+
className,
27+
}: SidebarProps) {
2028
const uniqueScripts = items.reduce((acc, category) => {
2129
for (const script of category.scripts) {
2230
if (!acc.some(s => s.name === script.name)) {
@@ -27,7 +35,7 @@ function Sidebar({
2735
}, [] as Script[]);
2836

2937
return (
30-
<div className="flex min-w-[350px] flex-col sm:max-w-[350px]">
38+
<div className={cn("flex w-full flex-col sm:min-w-[350px] sm:max-w-[350px]", className)}>
3139
<div className="flex items-end justify-between pb-4">
3240
<h1 className="text-xl font-bold">Categories</h1>
3341
<p className="text-xs italic text-muted-foreground">
@@ -43,6 +51,7 @@ function Sidebar({
4351
setSelectedScript={setSelectedScript}
4452
selectedCategory={selectedCategory}
4553
setSelectedCategory={setSelectedCategory}
54+
onItemSelect={onItemSelect}
4655
/>
4756
</div>
4857
</div>

frontend/src/components/navbar.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import { useEffect, useState } from "react";
2+
import { Suspense, useEffect, useState } from "react";
33
import Image from "next/image";
44
import Link from "next/link";
55

@@ -8,6 +8,7 @@ import { navbarLinks } from "@/config/site-config";
88
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
99
import { GitHubStarsButton } from "./animate-ui/components/buttons/github-stars";
1010
import { Button } from "./animate-ui/components/buttons/button";
11+
import MobileSidebar from "./navigation/mobile-sidebar";
1112
import { ThemeToggle } from "./ui/theme-toggle";
1213
import CommandMenu from "./command-menu";
1314

@@ -30,21 +31,25 @@ function Navbar() {
3031
return (
3132
<>
3233
<div
33-
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
34-
isScrolled ? "glass border-b bg-background/50" : ""
34+
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${isScrolled ? "glass border-b bg-background/50" : ""
3535
}`}
3636
>
3737
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
3838
<Link
3939
href="/"
40-
className="flex cursor-pointer w-full justify-center sm:justify-start flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
40+
className="cursor-pointer w-full justify-center sm:justify-start flex-row-reverse hidden sm:flex items-center gap-2 font-semibold sm:flex-row"
4141
>
4242
<Image height={18} unoptimized width={18} alt="logo" src="/ProxmoxVE/logo.png" className="" />
43-
<span className="hidden md:block">Proxmox VE Helper-Scripts</span>
43+
<span className="">Proxmox VE Helper-Scripts</span>
4444
</Link>
45-
<div className="flex gap-2">
45+
<div className="flex items-center gap-2">
46+
<div className="flex sm:hidden">
47+
<Suspense>
48+
<MobileSidebar />
49+
</Suspense>
50+
</div>
4651
<CommandMenu />
47-
<GitHubStarsButton username="community-scripts" repo="ProxmoxVE" />
52+
<GitHubStarsButton username="community-scripts" repo="ProxmoxVE" className="hidden md:flex" />
4853
{navbarLinks.map(({ href, event, icon, text, mobileHidden }) => (
4954
<TooltipProvider key={event}>
5055
<Tooltip delayDuration={100}>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"use client";
2+
3+
import { useCallback, useEffect, useState } from "react";
4+
import { useQueryState } from "nuqs";
5+
import { Menu } from "lucide-react";
6+
7+
import type { Category, Script } from "@/lib/types";
8+
9+
import { ScriptItem } from "@/app/scripts/_components/script-item";
10+
import Sidebar from "@/app/scripts/_components/sidebar";
11+
import { fetchCategories } from "@/lib/data";
12+
13+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "../ui/sheet";
14+
import { Button } from "../ui/button";
15+
16+
function MobileSidebar() {
17+
const [isOpen, setIsOpen] = useState(false);
18+
const [isLoading, setIsLoading] = useState(false);
19+
const [categories, setCategories] = useState<Category[]>([]);
20+
const [lastViewedScript, setLastViewedScript] = useState<Script | undefined>(undefined);
21+
const [selectedScript, setSelectedScript] = useQueryState("id");
22+
const [selectedCategory, setSelectedCategory] = useQueryState("category");
23+
24+
const loadCategories = useCallback(async () => {
25+
setIsLoading(true);
26+
try {
27+
const response = await fetchCategories();
28+
setCategories(response);
29+
}
30+
catch (error) {
31+
console.error(error);
32+
}
33+
finally {
34+
setIsLoading(false);
35+
}
36+
}, []);
37+
38+
useEffect(() => {
39+
void loadCategories();
40+
}, [loadCategories]);
41+
42+
useEffect(() => {
43+
if (!selectedScript || categories.length === 0) {
44+
return;
45+
}
46+
47+
const scriptMatch = categories
48+
.flatMap(category => category.scripts)
49+
.find(script => script.slug === selectedScript);
50+
51+
setLastViewedScript(scriptMatch);
52+
}, [selectedScript, categories]);
53+
54+
const handleOpenChange = (openState: boolean) => {
55+
setIsOpen(openState);
56+
};
57+
58+
const handleItemSelect = () => {
59+
setIsOpen(false);
60+
};
61+
62+
const hasLinks = categories.length > 0;
63+
64+
return (
65+
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
66+
<SheetTrigger asChild>
67+
<Button
68+
variant="ghost"
69+
size="icon"
70+
aria-label="Open navigation menu"
71+
tabIndex={0}
72+
onKeyDown={(event) => {
73+
if (event.key === "Enter" || event.key === " ") {
74+
setIsOpen(true);
75+
}
76+
}}
77+
>
78+
<Menu className="size-5" aria-hidden="true" />
79+
</Button>
80+
</SheetTrigger>
81+
<SheetHeader className="border-b border-border px-6 pb-4 pt-2"><SheetTitle className="sr-only">Categories</SheetTitle></SheetHeader>
82+
<SheetContent side="left" className="flex w-full max-w-xs flex-col gap-4 overflow-hidden px-0 pb-6">
83+
<div className="flex h-full flex-col gap-4 overflow-y-auto">
84+
{isLoading && !hasLinks
85+
? (
86+
<div className="flex w-full flex-col items-center justify-center gap-2 px-6 py-4 text-sm text-muted-foreground">
87+
Loading categories...
88+
</div>
89+
)
90+
: (
91+
<div className="flex flex-col gap-4 px-4">
92+
<Sidebar
93+
items={categories}
94+
selectedScript={selectedScript}
95+
setSelectedScript={setSelectedScript}
96+
selectedCategory={selectedCategory}
97+
setSelectedCategory={setSelectedCategory}
98+
onItemSelect={handleItemSelect}
99+
/>
100+
</div>
101+
)}
102+
{selectedScript && lastViewedScript
103+
? (
104+
<div className="flex flex-col gap-3 px-4">
105+
<p className="text-sm font-medium">Last Viewed</p>
106+
<ScriptItem item={lastViewedScript} setSelectedScript={setSelectedScript} />
107+
</div>
108+
)
109+
: null}
110+
</div>
111+
</SheetContent>
112+
</Sheet>
113+
);
114+
}
115+
116+
export default MobileSidebar;

frontend/src/components/ui/command.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Command as CommandPrimitive } from "cmdk";
66
import { Search } from "lucide-react";
77
import * as React from "react";
88

9-
import { Dialog, DialogContent } from "@/components/ui/dialog";
9+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
1010
import { cn } from "@/lib/utils";
1111

1212
const Command = React.forwardRef<

0 commit comments

Comments
 (0)