refactor: enhance sidebar functionality with cookie state management and animations

This commit is contained in:
Marco Pedone 2025-10-21 14:34:24 +02:00
parent d2548e8854
commit 2cb7067beb
7 changed files with 99 additions and 39 deletions

View file

@ -1,3 +1,4 @@
import { getCookie } from "cookies-next";
import { import {
ExternalLink, ExternalLink,
Mail, Mail,
@ -14,7 +15,10 @@ import { usePathname } from "next/navigation";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactNode, useContext } from "react"; import { type ReactNode, useContext } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Sidebar } from "~/components/area-riservata/sidebar"; import {
SIDEBAR_COOKIE_NAME,
Sidebar,
} from "~/components/area-riservata/sidebar";
import { BannerFactory } from "~/components/banners"; import { BannerFactory } from "~/components/banners";
import { Footer, MiniFooter } from "~/components/footer"; import { Footer, MiniFooter } from "~/components/footer";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
@ -114,7 +118,10 @@ export const AreaRiservataLayout = ({
<main className="flex h-full flex-1 overflow-auto"> <main className="flex h-full flex-1 overflow-auto">
<div className="flex h-auto w-full flex-col md:flex-row"> <div className="flex h-auto w-full flex-col md:flex-row">
{noSidebar ? null : ( {noSidebar ? null : (
<Sidebar isAdmin={session.user?.isAdmin || false} /> <Sidebar
defaultOpen={getCookie(SIDEBAR_COOKIE_NAME) === "true"}
isAdmin={session.user?.isAdmin || false}
/>
)} )}
<EnforcedSessionContext.Provider value={session as ValidSession}> <EnforcedSessionContext.Provider value={session as ValidSession}>
{children} {children}
@ -150,7 +157,10 @@ export const AreaRiservataLayoutUserView = ({
/> />
</Head> </Head>
<div className="flex h-auto w-full flex-col md:flex-row"> <div className="flex h-auto w-full flex-col md:flex-row">
<Sidebar isAdmin /> <Sidebar
defaultOpen={getCookie(SIDEBAR_COOKIE_NAME) === "true"}
isAdmin
/>
<div className="mx-4 my-3 flex-1 overflow-auto"> <div className="mx-4 my-3 flex-1 overflow-auto">
<UserViewHeader /> <UserViewHeader />

View file

@ -1,44 +1,47 @@
import { getCookie, setCookie } from "cookies-next/client"; import { setCookie } from "cookies-next/client";
import { add } from "date-fns"; import { add } from "date-fns";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Expand, Minimize2 } from "lucide-react"; import { Expand, Minimize2 } from "lucide-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useState } from "react";
import { ARMinimizableLinks } from "~/components/navbar/mobile-nav"; import { ARMinimizableLinks } from "~/components/navbar/mobile-nav";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
export const SIDEBAR_COOKIE_NAME = "sidebar_minimized";
export const Sidebar = ({ export const Sidebar = ({
defaultOpen,
isAdmin, isAdmin,
className, className,
}: { }: {
defaultOpen: boolean;
isAdmin: boolean; isAdmin: boolean;
className?: string; className?: string;
}) => { }) => {
const pathname = usePathname(); const pathname = usePathname();
const [isAnimating, setIsAnimating] = useState(false);
const [minimized, setMinimized] = useState(defaultOpen);
const [minimized, setMinimized] = useState(false);
useEffect(() => {
const defaultOpen = getCookie("sidebar_minimized") === "true";
setMinimized(defaultOpen);
}, []);
const toggleSidebar = (v: boolean) => { const toggleSidebar = (v: boolean) => {
setIsAnimating(true);
setMinimized(v); setMinimized(v);
setCookie("sidebar_minimized", v.toString(), { setCookie(SIDEBAR_COOKIE_NAME, v.toString(), {
expires: add(new Date(), { days: 30 }), expires: add(new Date(), { days: 30 }),
}); });
setTimeout(() => {
setIsAnimating(false);
}, 300);
}; };
return ( return (
<motion.nav <motion.nav
animate={{ width: minimized ? 65 : 192 }} animate={{ width: minimized ? 55 : 192 }}
aria-label="Sidebar Menu" aria-label="Sidebar Menu"
className={cn( className={cn(
"z-30 hidden h-auto w-48 shrink-0 space-y-4 border-r bg-white pt-3 text-accent-foreground md:block", "z-30 hidden h-auto w-48 shrink-0 space-y-4 border-r bg-white pt-3 text-accent-foreground md:block",
className, className,
minimized && "cursor-pointer", minimized && "cursor-pointer",
//defaultOpen && "w-[65px]",
)} )}
onClick={ onClick={
minimized minimized
@ -52,7 +55,7 @@ export const Sidebar = ({
> >
<div <div
className={cn( className={cn(
"flex items-center justify-between px-7", "flex h-8 items-center justify-between px-4",
minimized && "justify-center px-0", minimized && "justify-center px-0",
)} )}
> >
@ -66,16 +69,17 @@ export const Sidebar = ({
variant="ghost" variant="ghost"
> >
{minimized ? ( {minimized ? (
<Expand className="size-6" /> <Expand className="size-5" />
) : ( ) : (
<Minimize2 className="size-6" /> <Minimize2 className="size-5" />
)} )}
<span className="sr-only">Toggle Menu</span> <span className="sr-only">Toggle Menu</span>
</Button> </Button>
</div> </div>
<div className={cn("px-4 text-lg", minimized && "px-0")}> <div className={cn("px-2 text-lg")}>
<ARMinimizableLinks <ARMinimizableLinks
isAdmin={isAdmin} isAdmin={isAdmin}
isAnimating={isAnimating}
minimized={minimized} minimized={minimized}
pathname={pathname} pathname={pathname}
setOpen={toggleSidebar} setOpen={toggleSidebar}

View file

@ -262,11 +262,13 @@ const AreaRiservataLinks = ({
}; };
export const ARMinimizableLinks = ({ export const ARMinimizableLinks = ({
isAnimating,
isAdmin, isAdmin,
setOpen, setOpen,
pathname, pathname,
minimized, minimized,
}: { }: {
isAnimating: boolean;
isAdmin: boolean; isAdmin: boolean;
setOpen?: (status: boolean) => void; setOpen?: (status: boolean) => void;
pathname: string; pathname: string;
@ -276,7 +278,7 @@ export const ARMinimizableLinks = ({
return ( return (
<div <div
className={cn( className={cn(
"flex w-full flex-col space-y-4 text-base text-primary", "flex w-full flex-col items-start gap-4 text-base text-primary",
minimized && "justify-center", minimized && "justify-center",
)} )}
> >
@ -287,17 +289,23 @@ export const ARMinimizableLinks = ({
<div key={item.title}> <div key={item.title}>
<div <div
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex h-8 items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
minimized && "justify-center",
)} )}
> >
<IconMatrix className="size-5" type={item.icon} /> <IconMatrix className="size-5 shrink-0" type={item.icon} />
<span
{!minimized && item.title} className={cn(
"truncate duration-500",
minimized && "hidden",
isAnimating && "animate-expand",
)}
>
{!minimized && item.title}
</span>
</div> </div>
<div> <div>
<ul className="truncate"> <ul className="text-ellipsis">
{item.items.map((item) => { {item.items.map((item) => {
return ( return (
<MobileLink <MobileLink
@ -324,34 +332,48 @@ export const ARMinimizableLinks = ({
return ( return (
<MobileLink <MobileLink
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex h-8 items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
minimized && "justify-center",
)} )}
disabled={minimized} disabled={minimized}
href={item.href} href={item.href}
key={item.title} key={item.title}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<IconMatrix className="size-5" type={item.icon} /> <IconMatrix className="size-5 shrink-0" type={item.icon} />
{!minimized && item.title} <span
className={cn(
"truncate duration-500",
minimized && "hidden",
isAnimating && "animate-expand",
)}
>
{!minimized && item.title}
</span>
</MobileLink> </MobileLink>
); );
}) })
: t.nav.areaRiservataUserNav.map((item) => ( : t.nav.areaRiservataUserNav.map((item) => (
<MobileLink <MobileLink
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex h-8 items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
minimized && "justify-center",
)} )}
disabled={minimized} disabled={minimized}
href={item.href} href={item.href}
key={item.title} key={item.title}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<IconMatrix className="size-5" type={item.icon} /> <IconMatrix className="size-5 shrink-0" type={item.icon} />
{!minimized && item.title} <span
className={cn(
"truncate duration-500",
minimized && "hidden",
isAnimating && "animate-expand",
)}
>
{!minimized && item.title}
</span>
</MobileLink> </MobileLink>
))} ))}
</div> </div>

View file

@ -1,3 +1,4 @@
import { getCookie } from "cookies-next";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { Sidebar } from "~/components/area-riservata/sidebar"; import { Sidebar } from "~/components/area-riservata/sidebar";
@ -41,7 +42,10 @@ const AdminChats: NextPageWithLayout = () => {
<meta content={t.heads.area_riservata_description} name="description" /> <meta content={t.heads.area_riservata_description} name="description" />
</Head> </Head>
<div className="flex h-full w-full flex-col md:flex-row"> <div className="flex h-full w-full flex-col md:flex-row">
<Sidebar isAdmin /> <Sidebar
defaultOpen={getCookie("sidebar_minimized") === "true"}
isAdmin
/>
<div className="h-full flex-1 grow"> <div className="h-full flex-1 grow">
<div className="flex h-full w-full flex-row"> <div className="flex h-full w-full flex-row">

View file

@ -1,3 +1,4 @@
import { getCookie } from "cookies-next";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { Suspense } from "react"; import { Suspense } from "react";
@ -32,7 +33,11 @@ const ChatPage: NextPageWithLayout<ChatPageProps> = ({
<div className="h-full flex-1 grow"> <div className="h-full flex-1 grow">
<div className="flex h-full w-full flex-row"> <div className="flex h-full w-full flex-row">
<div className="h-full"> <div className="h-full">
<Sidebar className="h-full" isAdmin /> <Sidebar
className="h-full"
defaultOpen={getCookie("sidebar_minimized") === "true"}
isAdmin
/>
</div> </div>
<div className="h-full flex-1 grow"> <div className="h-full flex-1 grow">
<div className="flex h-full w-full flex-row"> <div className="flex h-full w-full flex-row">

View file

@ -1,3 +1,4 @@
import { getCookie } from "cookies-next";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { Suspense } from "react"; import { Suspense } from "react";
@ -31,7 +32,11 @@ const UserChat: NextPageWithLayout<UserChatProps> = ({
<div className="h-full flex-1 grow"> <div className="h-full flex-1 grow">
<div className="flex h-full w-full flex-row"> <div className="flex h-full w-full flex-row">
<div className="h-full"> <div className="h-full">
<Sidebar className="h-full" isAdmin={false} /> <Sidebar
className="h-full"
defaultOpen={getCookie("sidebar_minimized") === "true"}
isAdmin={false}
/>
</div> </div>
<Suspense fallback={<LoadingPage />}> <Suspense fallback={<LoadingPage />}>
<Chat chatId={chatId} userData={session} /> <Chat chatId={chatId} userData={session} />

View file

@ -126,7 +126,6 @@
--animate-collapsible-down: collapsible-down 0.3s ease-out; --animate-collapsible-down: collapsible-down 0.3s ease-out;
--animate-collapsible-up: collapsible-up 0.3s ease-in; --animate-collapsible-up: collapsible-up 0.3s ease-in;
@keyframes caret-blink { @keyframes caret-blink {
0%, 0%,
@ -154,6 +153,19 @@
border-color: hsl(var(--border)) / 10; border-color: hsl(var(--border)) / 10;
} }
} }
--animate-expand: expand 0.3s ease-out forwards;
@keyframes expand {
0% {
width: 0%;
/*opacity: 0;*/
}
100% {
width: 100%;
/*opacity: 1;*/
}
}
} }
:root { :root {
@ -287,8 +299,6 @@
@apply !py-2 hover:border-muted-foreground focus:border-foreground focus:ring-foreground focus-visible:ring-1 @apply !py-2 hover:border-muted-foreground focus:border-foreground focus:ring-foreground focus-visible:ring-1
} }
@utility no-spinner { @utility no-spinner {
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield; appearance: textfield;