Skip to content

Commit

Permalink
collapsable sidebar with nice transition (#878)
Browse files Browse the repository at this point in the history
Co-authored-by: Lowell Torola <[email protected]>
  • Loading branch information
nour-massri and lowtorola authored Jan 9, 2025
1 parent 79f2ab0 commit 9a77fb3
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 21 deletions.
55 changes: 50 additions & 5 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
"@eslint/migrate-config": "^1.3.4",
"@headlessui/react": "^1.7.15",
"@headlessui/tailwindcss": "^0.2.0",
"@heroicons/react": "^2.0.18",
"@heroicons/react": "^2.2.0",
"@tanstack/react-query": "^5.8.7",
"@tanstack/react-query-devtools": "^5.12.2",
"@types/node": ">=21.1.0",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"framer-motion": "^11.16.1",
"highcharts": "^11.4.8",
"highcharts-react-official": "^3.2.1",
"react": "^18.2.0",
Expand Down
53 changes: 48 additions & 5 deletions frontend/src/components/EpisodeLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type React from "react";
import { useEffect } from "react";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import Header from "./Header";
import Sidebar from "./sidebar";
import { Outlet, useParams } from "react-router-dom";
import { useEpisodeId } from "../contexts/EpisodeContext";
import Cookies from "js-cookie";
const SIDEBAR_WIDTH = 240;

// This component contains the Header and SideBar.
// Child route components are rendered with <Outlet />
Expand All @@ -15,13 +18,53 @@ const EpisodeLayout: React.FC = () => {
episodeContext.setEpisodeId(episodeId);
}
}, [episodeId]);

// Detect screen size and set default collapsed value
const isLargeScreen = window.matchMedia("(min-width: 640px)").matches;
const defaultCollapsed =
!isLargeScreen || Cookies.get("sidebar-collapsed") === "true";

const [collapsed, setCollapsed] = useState(defaultCollapsed);

//to collapse the sidebar when the screen size changes
useEffect(() => {
const mediaQuery = window.matchMedia("(min-width: 640px)");

const handleMediaQueryChange = (event: MediaQueryListEvent): void => {
if (!event.matches) {
setCollapsed(true);
} else {
const cookieCollapsed = Cookies.get("sidebar-collapsed") === "true";
setCollapsed(cookieCollapsed);
}
};

mediaQuery.addEventListener("change", handleMediaQueryChange);

return () => {
mediaQuery.removeEventListener("change", handleMediaQueryChange);
};
}, []);

const toggleSidebar = (): void => {
const newCollapsedState = !collapsed;
setCollapsed(newCollapsedState);
Cookies.set("sidebar-collapsed", newCollapsedState.toString());
};
return (
<div className="h-screen bg-gray-200/80">
<Header />
<Sidebar />
<div className="h-full pt-16 sm:pl-60">
<Header toggleSidebar={toggleSidebar} />
<Sidebar collapsed={collapsed} />
<motion.div
className="h-full pt-16"
initial={false}
animate={{
paddingLeft: collapsed ? 0 : SIDEBAR_WIDTH, // Assuming sidebar width is 240px
}}
transition={{ duration: 0.5, ease: "easeInOut" }}
>
<Outlet />
</div>
</motion.div>
</div>
);
};
Expand Down
20 changes: 18 additions & 2 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { useQueryClient } from "@tanstack/react-query";
import { useUserTeam } from "api/team/useTeam";
import { useEpisodeInfo } from "api/episode/useEpisode";

const Header: React.FC = () => {
interface HeaderProps {
toggleSidebar: () => void;
}

const Header: React.FC<HeaderProps> = ({ toggleSidebar }) => {
const { authState, user } = useCurrentUser();
const { episodeId } = useEpisodeId();
const queryClient = useQueryClient();
Expand All @@ -30,9 +34,21 @@ const Header: React.FC = () => {
<nav className="fixed top-0 z-30 h-16 w-full bg-gray-700">
<div className="w-full px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 items-center justify-between">
{/* desktop sidebar button */}
<button
className="z-20 mr-4 hidden p-2 sm:block"
onClick={toggleSidebar}
>
<span className="sr-only">Toggle Sidebar</span>
<Icon
name="bars_3"
className="text-gray-300 hover:text-white"
size="lg"
/>
</button>
{/* mobile menu */}
<Menu>
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden md:left-3">
<div className="absolute inset-y-0 left-3 flex items-center sm:hidden">
<Menu.Button className="rounded-md px-1 py-1.5 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<span className="sr-only">Open main menu</span>
<Icon
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/elements/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
ChevronLeftIcon as ChevronLeftIcon24,
ChevronRightIcon as ChevronRightIcon24,
ComputerDesktopIcon as ComputerDesktopIcon24,
UserCircleIcon as UserCircleIcon24,
UserPlusIcon as UserPlusIcon24,
} from "@heroicons/react/24/outline";

import {
Expand All @@ -47,6 +49,8 @@ import {
ChevronLeftIcon as ChevronLeftIcon20,
ChevronRightIcon as ChevronRightIcon20,
ComputerDesktopIcon as ComputerDesktopIcon20,
UserCircleIcon as UserCircleIcon20,
UserPlusIcon as UserPlusIcon20,
} from "@heroicons/react/20/solid";

const icons24 = {
Expand All @@ -72,6 +76,8 @@ const icons24 = {
chevron_left: ChevronLeftIcon24,
chevron_right: ChevronRightIcon24,
computer_desktop: ComputerDesktopIcon24,
user_circle: UserCircleIcon24,
user_plus: UserPlusIcon24,
};

const icons20 = {
Expand All @@ -97,6 +103,8 @@ const icons20 = {
chevron_left: ChevronLeftIcon20,
chevron_right: ChevronRightIcon20,
computer_desktop: ComputerDesktopIcon20,
user_circle: UserCircleIcon20,
user_plus: UserPlusIcon20,
};

export type IconName = keyof typeof icons24;
Expand Down
36 changes: 28 additions & 8 deletions frontend/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type React from "react";
import { motion, AnimatePresence } from "framer-motion";
import SidebarSection from "./SidebarSection";
import type { IconName } from "../elements/Icon";
import type { UseQueryResult } from "@tanstack/react-query";
import { type Episode, type TeamPrivate, Status526Enum } from "api/_autogen";
import { type AuthState, AuthStateEnum } from "contexts/CurrentUserContext";

interface SidebarProps {
collapsed?: boolean;
collapsed: boolean;
}

enum UserAuthLevel {
Expand Down Expand Up @@ -158,13 +159,32 @@ export const renderableItems = (
};

// IMPORTANT: When changing this file, also remember to change the mobile menu that appears on small screens.
const Sidebar: React.FC<SidebarProps> = ({ collapsed = false }) =>
collapsed ? null : (
<nav className="fixed top-16 z-10 hidden h-full w-60 flex-col gap-8 overflow-y-auto bg-gray-50 pb-24 pt-4 drop-shadow-[2px_0_2px_rgba(0,0,0,0.25)] sm:flex">
<SidebarSection title="" items={GENERAL_ITEMS} />
<SidebarSection title="compete" items={COMPETE_ITEMS} />
<SidebarSection title="team management" items={TEAM_MANAGEMENT_ITEMS} />
</nav>
const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
const sidebarVariants = {
open: { x: 0, transition: { duration: 0.5, ease: "easeInOut" } },
closed: { x: "-100%", transition: { duration: 0.5, ease: "easeInOut" } },
};

return (
<AnimatePresence>
{!collapsed && (
<motion.nav
initial="closed"
animate="open"
exit="closed"
variants={sidebarVariants}
className="fixed top-16 z-10 flex h-full w-60 flex-col gap-8 overflow-y-auto rounded-br-xl bg-gray-50 pb-24 pt-4 drop-shadow-[2px_0_2px_rgba(0,0,0,0.25)] "
>
<SidebarSection title="" items={GENERAL_ITEMS} />
<SidebarSection title="compete" items={COMPETE_ITEMS} />
<SidebarSection
title="team management"
items={TEAM_MANAGEMENT_ITEMS}
/>
</motion.nav>
)}
</AnimatePresence>
);
};

export default Sidebar;

0 comments on commit 9a77fb3

Please sign in to comment.