diff --git a/src/shared/components/navigation/navbar/nav-dropdown.tsx b/src/shared/components/navigation/navbar/nav-dropdown.tsx index 609eefa..a80a79b 100644 --- a/src/shared/components/navigation/navbar/nav-dropdown.tsx +++ b/src/shared/components/navigation/navbar/nav-dropdown.tsx @@ -25,10 +25,10 @@ export function NavDropdown({ items }: NavDropdownProps) { href={item.href} preservePeriod={item.preservePeriod} className={cn( - "flex items-center gap-3 rounded-sm px-2 py-2 text-sm transition-colors", + "flex items-center gap-3 rounded-sm px-2 py-3 text-sm transition-colors", isActive - ? "bg-accent text-foreground" - : "text-foreground hover:bg-accent", + ? "border-primary bg-accent text-foreground" + : "border-transparent text-foreground hover:bg-accent", )} > {children} diff --git a/src/shared/components/navigation/navbar/nav-tools.tsx b/src/shared/components/navigation/navbar/nav-tools.tsx index 5f1f6c0..8ba14b8 100644 --- a/src/shared/components/navigation/navbar/nav-tools.tsx +++ b/src/shared/components/navigation/navbar/nav-tools.tsx @@ -5,7 +5,7 @@ import { usePrivacyMode } from "@/shared/components/providers/privacy-provider"; import { Badge } from "@/shared/components/ui/badge"; const itemClass = - "flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; + "flex w-full items-center gap-2.5 rounded-sm px-2 py-3 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; type NavToolsDropdownProps = { onOpenCalculator: () => void; @@ -22,7 +22,7 @@ export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) { - Calculadora + Calculadora Faça cálculos rápidos @@ -39,7 +39,7 @@ export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) { )} - Privacidade + Privacidade Oculta valores na tela diff --git a/src/shared/components/navigation/navbar/navbar-shell.tsx b/src/shared/components/navigation/navbar/navbar-shell.tsx index 89a8392..23cf96d 100644 --- a/src/shared/components/navigation/navbar/navbar-shell.tsx +++ b/src/shared/components/navigation/navbar/navbar-shell.tsx @@ -16,15 +16,21 @@ export function NavbarShell({ return (
{logoHref ? ( - + ) : ( - + )} {children}
diff --git a/src/shared/components/navigation/navbar/notification-bell/notification-bell-trigger.tsx b/src/shared/components/navigation/navbar/notification-bell/notification-bell-trigger.tsx index b1f892b..83b2216 100644 --- a/src/shared/components/navigation/navbar/notification-bell/notification-bell-trigger.tsx +++ b/src/shared/components/navigation/navbar/notification-bell/notification-bell-trigger.tsx @@ -34,9 +34,11 @@ export function NotificationBellTrigger({ className={cn( buttonVariants({ variant: "ghost", size: "icon-sm" }), "group relative shadow-none transition-all duration-200", - "hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-2 focus-visible:ring-black/20", - "data-[state=open]:bg-black/10 data-[state=open]:text-black", - hasAnySourceItems ? "text-black" : "text-black/75", + "hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-2 focus-visible:ring-black/20 dark:hover:border-white/20 dark:hover:bg-white/10 dark:hover:text-white dark:focus-visible:ring-white/20", + "data-[state=open]:bg-black/10 data-[state=open]:text-black dark:data-[state=open]:bg-white/10 dark:data-[state=open]:text-white", + hasAnySourceItems + ? "text-black dark:text-white" + : "text-black/75 dark:text-white/75", )} > { - user: AppUser; - pagadorAvatarUrl: string | null; - pagadores: PagadorLike[]; - preLancamentosCount?: number; -} - -export function AppSidebar({ - user, - pagadorAvatarUrl, - pagadores, - preLancamentosCount = 0, - ...props -}: AppSidebarProps) { - if (!user) { - throw new Error("AppSidebar requires a user but received undefined."); - } - - const navigation = React.useMemo( - () => createSidebarNavData({ pagadores, preLancamentosCount }), - [pagadores, preLancamentosCount], - ); - - return ( - - - - - - - - - - - - - - - - - - - - - ); -} - -function LogoContent() { - const { state } = useSidebar(); - const isCollapsed = state === "collapsed"; - - return ; -} diff --git a/src/shared/components/navigation/sidebar/nav-link.tsx b/src/shared/components/navigation/sidebar/nav-link.tsx deleted file mode 100644 index 58e36ba..0000000 --- a/src/shared/components/navigation/sidebar/nav-link.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { - type RemixiconComponentType, - RiArrowLeftRightLine, - RiAtLine, - RiBankCard2Line, - RiBankLine, - RiCalendarEventLine, - RiDashboardLine, - RiFileChartLine, - RiFundsLine, - RiGroupLine, - RiPriceTag3Line, - RiSecurePaymentLine, - RiSettings2Line, - RiSparklingLine, - RiTodoLine, -} from "@remixicon/react"; - -export type SidebarSubItem = { - title: string; - url: string; - avatarUrl?: string | null; - isShared?: boolean; - key?: string; - icon?: RemixiconComponentType; - badge?: number; -}; - -export type SidebarItem = { - title: string; - url: string; - icon: RemixiconComponentType; - isActive?: boolean; - items?: SidebarSubItem[]; -}; - -export type SidebarSection = { - title: string; - items: SidebarItem[]; -}; - -export type SidebarNavData = { - navMain: SidebarSection[]; - navSecondary: { - title: string; - url: string; - icon: RemixiconComponentType; - }[]; -}; - -export interface PagadorLike { - id: string; - name: string | null; - avatarUrl: string | null; - canEdit?: boolean; -} - -export interface SidebarNavOptions { - pagadores: PagadorLike[]; - preLancamentosCount?: number; -} - -export function createSidebarNavData( - options: SidebarNavOptions, -): SidebarNavData { - const { pagadores, preLancamentosCount = 0 } = options; - const pagadorItems = pagadores - .map((pagador) => ({ - title: pagador.name?.trim().length - ? pagador.name.trim() - : "Pagador sem nome", - url: `/payers/${pagador.id}`, - key: pagador.canEdit ? pagador.id : `${pagador.id}-shared`, - isShared: !pagador.canEdit, - avatarUrl: pagador.avatarUrl, - })) - .sort((a, b) => - a.title.localeCompare(b.title, "pt-BR", { sensitivity: "base" }), - ); - - const pagadorItemsWithHistory: SidebarSubItem[] = pagadorItems; - - return { - navMain: [ - { - title: "Gestão Financeira", - items: [ - { - title: "Dashboard", - url: "/dashboard", - icon: RiDashboardLine, - }, - { - title: "Lançamentos", - url: "/transactions", - icon: RiArrowLeftRightLine, - items: [ - { - title: "Pré-Lançamentos", - url: "/inbox", - key: "pre-lancamentos", - icon: RiAtLine, - badge: - preLancamentosCount > 0 ? preLancamentosCount : undefined, - }, - ], - }, - { - title: "Calendário", - url: "/calendar", - icon: RiCalendarEventLine, - }, - { - title: "Cartões", - url: "/cards", - icon: RiBankCard2Line, - }, - { - title: "Contas", - url: "/accounts", - icon: RiBankLine, - }, - { - title: "Orçamentos", - url: "/budgets", - icon: RiFundsLine, - }, - ], - }, - { - title: "Organização", - items: [ - { - title: "Pagadores", - url: "/payers", - icon: RiGroupLine, - items: pagadorItemsWithHistory, - }, - { - title: "Categorias", - url: "/categories", - icon: RiPriceTag3Line, - }, - { - title: "Anotações", - url: "/notes", - icon: RiTodoLine, - }, - ], - }, - { - title: "Análise", - items: [ - { - title: "Insights", - url: "/insights", - icon: RiSparklingLine, - }, - { - title: "Tendências", - url: "/reports/category-trends", - icon: RiFileChartLine, - }, - { - title: "Uso de Cartões", - url: "/reports/card-usage", - icon: RiBankCard2Line, - }, - { - title: "Análise de Parcelas", - url: "/reports/installment-analysis", - icon: RiSecurePaymentLine, - }, - ], - }, - ], - navSecondary: [ - { - title: "Ajustes", - url: "/settings", - icon: RiSettings2Line, - }, - ], - }; -} diff --git a/src/shared/components/navigation/sidebar/nav-main.tsx b/src/shared/components/navigation/sidebar/nav-main.tsx deleted file mode 100644 index d850d4f..0000000 --- a/src/shared/components/navigation/sidebar/nav-main.tsx +++ /dev/null @@ -1,212 +0,0 @@ -"use client"; - -import { - type RemixiconComponentType, - RiArrowRightSLine, - RiUserSharedLine, -} from "@remixicon/react"; -import Link from "next/link"; -import { usePathname, useSearchParams } from "next/navigation"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/shared/components/ui/avatar"; -import { Badge } from "@/shared/components/ui/badge"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/shared/components/ui/collapsible"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/shared/components/ui/sidebar"; -import { getAvatarSrc } from "@/shared/lib/payers/utils"; - -type NavItem = { - title: string; - url: string; - icon: RemixiconComponentType; - isActive?: boolean; - items?: { - title: string; - url: string; - avatarUrl?: string | null; - isShared?: boolean; - key?: string; - icon?: RemixiconComponentType; - badge?: number; - }[]; -}; - -type NavSection = { - title: string; - items: NavItem[]; -}; - -const MONTH_PERIOD_PARAM = "periodo"; - -const PERIOD_AWARE_PATHS = new Set(["/dashboard", "/transactions", "/budgets"]); - -export function NavMain({ sections }: { sections: NavSection[] }) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const periodParam = searchParams.get(MONTH_PERIOD_PARAM); - - const normalizedPathname = - pathname.endsWith("/") && pathname !== "/" - ? pathname.slice(0, -1) - : pathname; - - const isLinkActive = (url: string) => { - const normalizedUrl = - url.endsWith("/") && url !== "/" ? url.slice(0, -1) : url; - - // Verifica se é exatamente igual ou se o pathname começa com a URL - return ( - normalizedPathname === normalizedUrl || - normalizedPathname.startsWith(`${normalizedUrl}/`) - ); - }; - - const buildHrefWithPeriod = (url: string) => { - if (!periodParam) { - return url; - } - - const [rawPathname, existingSearch = ""] = url.split("?"); - const normalizedRawPathname = - rawPathname.endsWith("/") && rawPathname !== "/" - ? rawPathname.slice(0, -1) - : rawPathname; - - if (!PERIOD_AWARE_PATHS.has(normalizedRawPathname)) { - return url; - } - - const params = new URLSearchParams(existingSearch); - params.set(MONTH_PERIOD_PARAM, periodParam); - - const queryString = params.toString(); - return queryString - ? `${normalizedRawPathname}?${queryString}` - : normalizedRawPathname; - }; - - const activeLinkClasses = - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-dark! hover:text-primary!"; - - return ( - <> - {sections.map((section, index) => ( - - - {section.title} - - - - {section.items.map((item) => { - const itemIsActive = isLinkActive(item.url); - return ( - - - - - - {item.title} - - - {item.items?.length ? ( - <> - - - - Toggle - - - - - {item.items?.map((subItem) => { - const subItemIsActive = isLinkActive( - subItem.url, - ); - const avatarSrc = getAvatarSrc( - subItem.avatarUrl, - ); - const initial = - subItem.title.charAt(0).toUpperCase() || "?"; - return ( - - - - {subItem.icon ? ( - - ) : subItem.avatarUrl !== undefined ? ( - - - - {initial} - - - ) : null} - {subItem.title} - {subItem.badge ? ( - - {subItem.badge} - - ) : null} - {subItem.isShared ? ( - - ) : null} - - - - ); - })} - - - - ) : null} - - - ); - })} - - - - ))} - - ); -} diff --git a/src/shared/components/navigation/sidebar/nav-secondary.tsx b/src/shared/components/navigation/sidebar/nav-secondary.tsx deleted file mode 100644 index d9a814f..0000000 --- a/src/shared/components/navigation/sidebar/nav-secondary.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import type { RemixiconComponentType } from "@remixicon/react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import type * as React from "react"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/shared/components/ui/sidebar"; - -export function NavSecondary({ - items, - ...props -}: { - items: { - title: string; - url: string; - icon: RemixiconComponentType; - }[]; -} & React.ComponentPropsWithoutRef) { - const pathname = usePathname(); - - return ( - - - - {items.map((item) => { - const normalizedPathname = - pathname.endsWith("/") && pathname !== "/" - ? pathname.slice(0, -1) - : pathname; - const normalizedUrl = - item.url.endsWith("/") && item.url !== "/" - ? item.url.slice(0, -1) - : item.url; - const itemIsActive = - normalizedPathname === normalizedUrl || - normalizedPathname.startsWith(`${normalizedUrl}/`); - return ( - - - - - {item.title} - - - - ); - })} - - - - ); -} diff --git a/src/shared/components/navigation/sidebar/nav-user.tsx b/src/shared/components/navigation/sidebar/nav-user.tsx deleted file mode 100644 index a8afe09..0000000 --- a/src/shared/components/navigation/sidebar/nav-user.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/shared/components/ui/sidebar"; -import { getAvatarSrc } from "@/shared/lib/payers/utils"; - -type NavUserProps = { - user: { - id: string; - name: string; - email: string; - image: string | null; - }; - pagadorAvatarUrl: string | null; -}; - -export function NavUser({ user, pagadorAvatarUrl }: NavUserProps) { - const avatarSrc = pagadorAvatarUrl - ? getAvatarSrc(pagadorAvatarUrl) - : user.image || getAvatarSrc(null); - const isDataUrl = avatarSrc.startsWith("data:"); - - return ( - - - -
- {user.name} -
-
- {user.name} - - {user.email} - -
-
-
-
- ); -} diff --git a/src/shared/components/providers/index.ts b/src/shared/components/providers/index.ts deleted file mode 100644 index a043c89..0000000 --- a/src/shared/components/providers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { PrivacyProvider } from "./privacy-provider"; -export { QueryProvider } from "./query-provider"; -export { ThemeProvider } from "./theme-provider"; diff --git a/src/shared/components/ui/sidebar.tsx b/src/shared/components/ui/sidebar.tsx deleted file mode 100644 index 46d2a98..0000000 --- a/src/shared/components/ui/sidebar.tsx +++ /dev/null @@ -1,726 +0,0 @@ -"use client"; - -import { Slot } from "@radix-ui/react-slot"; -import { RiLayoutLeft2Line } from "@remixicon/react"; -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; -import { Button } from "@/shared/components/ui/button"; -import { Input } from "@/shared/components/ui/input"; -import { Separator } from "@/shared/components/ui/separator"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/shared/components/ui/sheet"; -import { Skeleton } from "@/shared/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/shared/components/ui/tooltip"; -import { cn } from "@/shared/utils/ui"; -import { useIsMobile } from "./use-mobile"; - -const SIDEBAR_COOKIE_NAME = "sidebar_state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; - -type SidebarContextProps = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } - - return context; -} - -function SidebarProvider({ - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props -}: React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; -}) { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open], - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); - }, [isMobile, setOpen]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, toggleSidebar], - ); - - return ( - - -
- {children} -
-
-
- ); -} - -function Sidebar({ - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props -}: React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; -}) { - const { state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === "none") { - return ( -
- {children} -
- ); - } - - return ( - <> - - - - Sidebar - Displays the mobile sidebar. - -
{children}
-
-
- -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- - ); -} - -function SidebarTrigger({ - className, - onClick, - ...props -}: React.ComponentProps) { - const { toggleSidebar } = useSidebar(); - - return ( - - ); -} - -function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar(); - - return ( -