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-tooltip": "^1.1.4",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -2855,6 +2856,32 @@
|
||||
"@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": {
|
||||
"version": "0.5.15",
|
||||
"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-tooltip": "^1.1.4",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@ -3,15 +3,17 @@ import { useState } from 'react';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
import ModernLayout from './ModernLayout';
|
||||
import OnboardingTour from '@/components/onboarding/OnboardingTour';
|
||||
import HelpIcon from '@/components/help/HelpIcon';
|
||||
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
||||
|
||||
interface LayoutProps {
|
||||
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 isMobile = useIsMobile();
|
||||
|
||||
@ -23,6 +25,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
closeTour
|
||||
} = useOnboardingTour();
|
||||
|
||||
// Se useModernSidebar for true, usa o novo layout moderno
|
||||
if (useModernSidebar) {
|
||||
return <ModernLayout>{children}</ModernLayout>;
|
||||
}
|
||||
|
||||
// Layout antigo para compatibilidade
|
||||
const handleSidebarClose = () => {
|
||||
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