feat: Integrate auto-collapsing sidebar component
Integrate the provided sidebar component, ensuring it functions correctly on both desktop and mobile devices with automatic collapsing and expanding functionality.
This commit is contained in:
parent
a9ab6d566f
commit
576b6e5307
27
package-lock.json
generated
27
package-lock.json
generated
@ -37,6 +37,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -2855,6 +2856,32 @@
|
|||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tabler/icons": {
|
||||||
|
"version": "3.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz",
|
||||||
|
"integrity": "sha512-jtVqv0JC1WU2TTEBN32D9+R6mc1iEBuPwLnBsWaR02SIEciu9aq5806AWkCHuObhQ4ERhhXErLEK7Fs+tEZxiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tabler/icons-react": {
|
||||||
|
"version": "3.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.34.0.tgz",
|
||||||
|
"integrity": "sha512-OpEIR2iZsIXECtAIMbn1zfKfQ3zKJjXyIZlkgOGUL9UkMCFycEiF2Y8AVfEQsyre/3FnBdlWJvGr0NU47n2TbQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons": "3.34.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
||||||
|
|||||||
@ -40,6 +40,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@ -3,15 +3,17 @@ import { useState } from 'react';
|
|||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
|
import ModernLayout from './ModernLayout';
|
||||||
import OnboardingTour from '@/components/onboarding/OnboardingTour';
|
import OnboardingTour from '@/components/onboarding/OnboardingTour';
|
||||||
import HelpIcon from '@/components/help/HelpIcon';
|
import HelpIcon from '@/components/help/HelpIcon';
|
||||||
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
useModernSidebar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children, useModernSidebar = true }: LayoutProps) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -23,6 +25,12 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
closeTour
|
closeTour
|
||||||
} = useOnboardingTour();
|
} = useOnboardingTour();
|
||||||
|
|
||||||
|
// Se useModernSidebar for true, usa o novo layout moderno
|
||||||
|
if (useModernSidebar) {
|
||||||
|
return <ModernLayout>{children}</ModernLayout>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout antigo para compatibilidade
|
||||||
const handleSidebarClose = () => {
|
const handleSidebarClose = () => {
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
};
|
};
|
||||||
|
|||||||
199
src/components/layout/ModernLayout.tsx
Normal file
199
src/components/layout/ModernLayout.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { ModernSidebar, SidebarBody, SidebarLink } from '@/components/ui/modern-sidebar';
|
||||||
|
import OnboardingTour from '@/components/onboarding/OnboardingTour';
|
||||||
|
import HelpIcon from '@/components/help/HelpIcon';
|
||||||
|
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
||||||
|
import {
|
||||||
|
Home,
|
||||||
|
Receipt,
|
||||||
|
CreditCard,
|
||||||
|
ListFilter,
|
||||||
|
Target,
|
||||||
|
Calendar,
|
||||||
|
Crown,
|
||||||
|
MessageSquareText,
|
||||||
|
Users,
|
||||||
|
Settings
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface ModernLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logo = () => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="relative z-20 flex items-center space-x-2 py-1 text-sm font-normal text-black"
|
||||||
|
>
|
||||||
|
<div className="h-5 w-6 shrink-0 rounded-tl-lg rounded-tr-sm rounded-br-lg rounded-bl-sm bg-blue-700" />
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="font-medium whitespace-pre text-blue-700 dark:text-white"
|
||||||
|
>
|
||||||
|
Finance Home
|
||||||
|
</motion.span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogoIcon = () => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="relative z-20 flex items-center space-x-2 py-1 text-sm font-normal text-black"
|
||||||
|
>
|
||||||
|
<div className="h-5 w-6 shrink-0 rounded-tl-lg rounded-tr-sm rounded-br-lg rounded-bl-sm bg-blue-700" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModernLayout({ children }: ModernLayoutProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: tourOpen,
|
||||||
|
currentStep,
|
||||||
|
nextStep,
|
||||||
|
skipTour,
|
||||||
|
closeTour
|
||||||
|
} = useOnboardingTour();
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
href: "/",
|
||||||
|
icon: <Home className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Transações",
|
||||||
|
href: "/transacoes",
|
||||||
|
icon: <Receipt className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Cartões de Crédito",
|
||||||
|
href: "/cartoes",
|
||||||
|
icon: <CreditCard className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Categorias",
|
||||||
|
href: "/categorias",
|
||||||
|
icon: <ListFilter className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Metas",
|
||||||
|
href: "/metas",
|
||||||
|
icon: <Target className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Calendário",
|
||||||
|
href: "/calendario",
|
||||||
|
icon: <Calendar className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Assinatura",
|
||||||
|
href: "/assinatura",
|
||||||
|
icon: <Crown className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const whatsappLinks = [
|
||||||
|
{
|
||||||
|
label: "Conectar WhatsApp",
|
||||||
|
href: "/whatsapp",
|
||||||
|
icon: <MessageSquareText className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" data-tour="whatsapp-menu" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Grupos",
|
||||||
|
href: "/grupos-whatsapp",
|
||||||
|
icon: <Users className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" data-tour="grupos-menu" />,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const configLinks = [
|
||||||
|
{
|
||||||
|
label: "Configurações",
|
||||||
|
href: "/configuracoes",
|
||||||
|
icon: <Settings className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex w-full">
|
||||||
|
<ModernSidebar open={open} setOpen={setOpen}>
|
||||||
|
<SidebarBody className="justify-between gap-10">
|
||||||
|
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
|
||||||
|
{open ? <Logo /> : <LogoIcon />}
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-2">
|
||||||
|
{links.map((link, idx) => (
|
||||||
|
<SidebarLink key={idx} link={link} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
{open && (
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="mb-2 px-2 text-sm font-semibold tracking-tight text-gray-600"
|
||||||
|
>
|
||||||
|
WhatsApp
|
||||||
|
</motion.h2>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{whatsappLinks.map((link, idx) => (
|
||||||
|
<SidebarLink key={`whatsapp-${idx}`} link={link} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
{open && (
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="mb-2 px-2 text-sm font-semibold tracking-tight text-gray-600"
|
||||||
|
>
|
||||||
|
Configurações
|
||||||
|
</motion.h2>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{configLinks.map((link, idx) => (
|
||||||
|
<SidebarLink key={`config-${idx}`} link={link} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarBody>
|
||||||
|
</ModernSidebar>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Icon */}
|
||||||
|
<HelpIcon />
|
||||||
|
|
||||||
|
{/* Tour de Onboarding */}
|
||||||
|
<OnboardingTour
|
||||||
|
isOpen={tourOpen}
|
||||||
|
currentStep={currentStep}
|
||||||
|
onNext={nextStep}
|
||||||
|
onSkip={skipTour}
|
||||||
|
onClose={closeTour}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
192
src/components/ui/modern-sidebar.tsx
Normal file
192
src/components/ui/modern-sidebar.tsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
|
||||||
|
"use client";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import React, { useState, createContext, useContext } from "react";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface Links {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.JSX.Element | React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarContextProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
animate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarContext = createContext<SidebarContextProps | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useSidebar = () => {
|
||||||
|
const context = useContext(SidebarContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarProvider = ({
|
||||||
|
children,
|
||||||
|
open: openProp,
|
||||||
|
setOpen: setOpenProp,
|
||||||
|
animate = true,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
animate?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [openState, setOpenState] = useState(false);
|
||||||
|
|
||||||
|
const open = openProp !== undefined ? openProp : openState;
|
||||||
|
const setOpen = setOpenProp !== undefined ? setOpenProp : setOpenState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={{ open, setOpen, animate: animate }}>
|
||||||
|
{children}
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModernSidebar = ({
|
||||||
|
children,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
animate,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
animate?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SidebarProvider open={open} setOpen={setOpen} animate={animate}>
|
||||||
|
{children}
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarBody = (props: React.ComponentProps<typeof motion.div>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DesktopSidebar {...props} />
|
||||||
|
<MobileSidebar {...(props as React.ComponentProps<"div">)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DesktopSidebar = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof motion.div>) => {
|
||||||
|
const { open, setOpen, animate } = useSidebar();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"h-full px-4 py-4 hidden md:flex md:flex-col bg-white dark:bg-neutral-800 w-[300px] shrink-0 border-r border-gray-200 dark:border-neutral-700",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
animate={{
|
||||||
|
width: animate ? (open ? "300px" : "60px") : "300px",
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setOpen(true)}
|
||||||
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileSidebar = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) => {
|
||||||
|
const { open, setOpen } = useSidebar();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-16 px-4 py-4 flex flex-row md:hidden items-center justify-between bg-white dark:bg-neutral-800 w-full border-b border-gray-200 dark:border-neutral-700"
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-lg font-semibold text-blue-700">Finance Home</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end z-20">
|
||||||
|
<Menu
|
||||||
|
className="text-neutral-800 dark:text-neutral-200 h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "-100%", opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: "-100%", opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"fixed h-full w-full inset-0 bg-white dark:bg-neutral-900 p-6 z-[100] flex flex-col justify-between",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute right-6 top-6 z-50 text-neutral-800 dark:text-neutral-200 cursor-pointer"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarLink = ({
|
||||||
|
link,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
link: Links;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const { open, animate } = useSidebar();
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-start gap-3 group/sidebar py-3 px-2 rounded-md hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 hover:text-blue-700 transition-all duration-200",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{link.icon}
|
||||||
|
|
||||||
|
<motion.span
|
||||||
|
animate={{
|
||||||
|
display: animate ? (open ? "inline-block" : "none") : "inline-block",
|
||||||
|
opacity: animate ? (open ? 1 : 0) : 1,
|
||||||
|
}}
|
||||||
|
className="text-neutral-700 dark:text-neutral-200 text-sm group-hover/sidebar:translate-x-1 transition duration-150 whitespace-pre inline-block !p-0 !m-0"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</motion.span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user