feat: update Sidebar component for improved responsiveness and menu interaction; add showTitle prop to SidebarItem
This commit is contained in:
parent
1e9d5c2742
commit
cf9fd767f3
@ -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",
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user