feat: update Sidebar component for improved responsiveness and menu interaction; add showTitle prop to SidebarItem

This commit is contained in:
Rémi 2025-01-07 16:58:34 +01:00
parent 1e9d5c2742
commit cf9fd767f3
4 changed files with 77 additions and 107 deletions

View File

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4000", "dev": "next dev --turbopack -p 4000",
"build": "next build", "build": "next build",
"build:docker": "docker build -t toogether/webapp .", "build:docker": "docker build -t toogether/webapp .",
"start": "next start -p 4000", "start": "next start -p 4000",

View File

@ -6,6 +6,7 @@ import { FaDoorClosed, FaRegUser } from "react-icons/fa";
import { FiSettings } from "react-icons/fi"; import { FiSettings } from "react-icons/fi";
import { IoMdStats } from "react-icons/io"; import { IoMdStats } from "react-icons/io";
import { MdInbox } from "react-icons/md"; import { MdInbox } from "react-icons/md";
import { SidebarItemProps } from "../components/Sidebar/item";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const pathName = usePathname(); const pathName = usePathname();
@ -17,8 +18,14 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{ href: "/admin/classes", title: "Classes", icon: <MdInbox /> }, { href: "/admin/classes", title: "Classes", icon: <MdInbox /> },
{ href: "/admin/rooms", title: "Rooms", icon: <FaDoorClosed /> }, { href: "/admin/rooms", title: "Rooms", icon: <FaDoorClosed /> },
], ],
[{ href: "/admin/settings", title: "Settings", icon: <FiSettings /> }], [
]; {
href: "/admin/settings",
title: "Settings",
icon: <FiSettings />,
},
],
] as SidebarItemProps[][];
return ( return (
<div className="flex"> <div className="flex">

View File

@ -1,9 +1,10 @@
"use client";
import { Divider, Link } from "@nextui-org/react"; import { Divider, Link } from "@nextui-org/react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { BsChevronDoubleRight } from "react-icons/bs"; import {
BsLayoutSidebarInset,
BsReverseLayoutSidebarInsetReverse,
} from "react-icons/bs";
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher"; import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
import { SidebarItem } from "./item"; import { SidebarItem } from "./item";
@ -18,141 +19,99 @@ export const Sidebar = ({
}) => { }) => {
const router = useRouter(); const router = useRouter();
const pathName = usePathname(); const pathName = usePathname();
const sidebarRef = useRef<HTMLDivElement | null>(null);
// State to manage menu openness // State to manage menu openness
const [open, setOpen] = useState(false); const [open, setOpen] = useState(true);
const [startX, setStartX] = useState(0); const [isMobile, setIsMobile] = useState(false);
const [isSwiping, setIsSwiping] = useState(false);
// Effect to manage responsiveness
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (window.innerWidth >= 1024) { if (window.innerWidth <= 1024 && !isMobile) {
setOpen(true); // Menu always open on larger screens setIsMobile(true);
} else { } else if (window.innerWidth > 1024 && isMobile) {
setOpen(false); // Menu closed by default on smaller screens setIsMobile(false);
} }
}; };
handleResize(); // Set initial state based on current window size handleResize();
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); });
// Effect to close the menu when clicking outside
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { if (isMobile) {
if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) { setOpen(false);
setOpen(false); // Close the menu if clicked outside
}
};
document.addEventListener("click", handleClickOutside);
// Cleanup the event listener when component unmounts
return () => {
document.removeEventListener("click", handleClickOutside);
};
}, []);
// Handle swipe start (touchstart event)
const handleTouchStart = (e: React.TouchEvent) => {
const touchStart = e.touches[0].clientX;
setStartX(touchStart);
setIsSwiping(true);
};
// Handle swipe move (touchmove event)
const handleTouchMove = (e: React.TouchEvent) => {
if (!isSwiping) return;
const touchMove = e.touches[0].clientX;
const distance = touchMove - startX;
if (distance > 50 && !open) {
setOpen(true); // Open the menu if swiping right
} else if (distance < -50 && open) {
setOpen(false); // Close the menu if swiping left
} }
}; }, [isMobile]);
// Handle swipe end (touchend event)
const handleTouchEnd = () => {
setIsSwiping(false);
};
return ( return (
<aside <aside
ref={sidebarRef}
className={` className={`
${open ? "translate-x-0" : "-translate-x-full"} ${open ? "w-96" : "w-20"}
fixed top-0 left-0 z-40 w-64 h-screen transition-transform top-0 left-0 z-40 h-screen transition-all bg-foreground-100
lg:translate-x-0 lg:static
`} `}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
> >
<div className="flex flex-col h-full px-3 py-4 overflow-y-auto bg-foreground-100 scrollbar-hide"> {/* items-center lg:items-start */}
{/* Toggle Button - Hidden on large screens */} <div
<button className={`flex flex-col h-full px-3 py-4 overflow-y-auto scrollbar-hide ${open ? "items-start" : "items-center"}`}
className={` >
lg:hidden flex items-center justify-center w-12 h-12 <div
fixed left-64 transform bg-foreground-200 rounded-full shadow-lg className={`flex flex-col gap-2 p-2 mb-4 w-full ${!open ? "items-center" : ""}`}
transition-transform duration-500 z-50
-translate-x-1/2 top-1/2 -translate-y-1/2
${open ? "rotate-180" : "rotate-0"}
`}
onClick={() => setOpen(!open)}
> >
<span className="text-xl font-bold"> {open ? (
<BsChevronDoubleRight className="text-gray-900 dark:text-white" /> <div className="flex items-center justify-between w-full">
</span> <Link
</button> className="text-2xl font-semibold text-gray-900 dark:text-white rounded-lg cursor-pointer"
onPress={() => router.push("/")}
>
Toogether
</Link>
<button onClick={() => setOpen(!open)}>
<BsLayoutSidebarInset className="text-xl" />
</button>
</div>
) : (
<button onClick={() => setOpen(!open)}>
<BsReverseLayoutSidebarInsetReverse className="text-xl" />
</button>
)}
<div className="flex flex-col gap-2 p-2"> {open && (
<Link <p className="text-sm text-foreground-400 dark:text-gray-400">
className="text-2xl font-semibold text-gray-900 dark:text-white rounded-lg cursor-pointer" Manage classes, rooms, and users
onPress={() => router.push("/")} </p>
> )}
Toogether
</Link>
<p className="text-sm text-foreground-400 dark:text-gray-400">
Manage classes, rooms, and users
</p>
</div> </div>
{items.map((group, index) => ( {items.map((group, index) => (
<div key={index}> <div key={index} className={open ? "w-full" : ""}>
<section className="flex flex-col"> <section className="flex flex-col">
<ul className="flex flex-col gap-2"> <ul className={`flex flex-col gap-2 w-full`}>
{group.map((item, index) => ( {group.map((item, index) => (
<li <li
key={index} key={index}
className={` className={`${
${ pathName === item.href
pathName === item.href ? "bg-foreground-300"
? "bg-foreground-300" : "hover:bg-foreground-200"
: "hover:bg-foreground-200" } rounded-md cursor-pointer w-full flex `}
} rounded-md cursor-pointer
`}
> >
<SidebarItem <SidebarItem
href={item.href} href={item.href}
title={item.title} title={item.title}
icon={item.icon} icon={item.icon}
setOpen={setOpen} setOpen={setOpen}
showTitle={open}
/> />
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
{index < items.length - 1 && ( {index < items.length - 1 && (
<Divider className="bg-foreground-300 mt-4" /> <Divider className="bg-foreground-300 my-4" />
)} )}
</div> </div>
))} ))}
<section className="mt-auto flex justify-between"> <section className={`mt-auto ${!open && "justify-center"}`}>
<ThemeSwitcher /> <ThemeSwitcher />
</section> </section>
</div> </div>

View File

@ -2,17 +2,21 @@
import { Link } from "@nextui-org/react"; import { Link } from "@nextui-org/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export type SidebarItemProps = {
title: string;
icon: React.ReactNode;
href: string;
setOpen: (open: boolean) => void;
showTitle: boolean;
};
export const SidebarItem = ({ export const SidebarItem = ({
title, title,
icon, icon,
href, href,
setOpen, setOpen,
}: { showTitle = true,
title: string; }: SidebarItemProps) => {
icon: React.ReactNode;
href: string;
setOpen: (open: boolean) => void;
}) => {
const router = useRouter(); const router = useRouter();
return ( return (
@ -25,7 +29,7 @@ export const SidebarItem = ({
className="w-full p-2 gap-3 text-md" className="w-full p-2 gap-3 text-md"
> >
<span className="text-xl">{icon}</span> <span className="text-xl">{icon}</span>
{title} {showTitle && <span className="text-sm">{title}</span>}
</Link> </Link>
); );
}; };