Make app navigation mobile usable
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 47s

This commit is contained in:
Cauê Faleiros
2026-08-03 13:39:21 -03:00
parent 59fe9104d6
commit 2d738073bf
3 changed files with 77 additions and 23 deletions

View File

@@ -94,12 +94,12 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
}; };
return ( return (
<div className="flex flex-wrap items-center gap-3"> <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
{/* Date Presets Dropdown */} {/* Date Presets Dropdown */}
<div className="relative"> <div className="relative w-full sm:w-auto">
<button <button
onClick={() => setIsPresetOpen(!isPresetOpen)} onClick={() => setIsPresetOpen(!isPresetOpen)}
className="flex items-center gap-2 bg-dark-card border border-dark-border px-4 py-2.5 rounded-xl shadow-sm hover:border-brand-primary transition-colors text-sm font-medium text-dark-text cursor-pointer" className="flex min-h-11 w-full items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-4 py-2.5 text-sm font-medium text-dark-text shadow-sm transition-colors hover:border-brand-primary cursor-pointer sm:w-auto"
> >
<Calendar size={16} className="text-dark-muted shrink-0" /> <Calendar size={16} className="text-dark-muted shrink-0" />
<span>{formatDisplayDate(dateRange.start)} - {formatDisplayDate(dateRange.end)}</span> <span>{formatDisplayDate(dateRange.start)} - {formatDisplayDate(dateRange.end)}</span>
@@ -109,7 +109,7 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
{isPresetOpen && ( {isPresetOpen && (
<> <>
<div className="fixed inset-0 z-10" onClick={() => setIsPresetOpen(false)}></div> <div className="fixed inset-0 z-10" onClick={() => setIsPresetOpen(false)}></div>
<div className="absolute right-0 mt-2 w-56 bg-dark-card border border-dark-border rounded-xl shadow-xl z-20 py-2 max-h-[32rem] overflow-y-auto"> <div className="fixed inset-x-3 top-20 z-20 mt-0 max-h-[calc(100dvh-6rem)] overflow-y-auto rounded-xl border border-dark-border bg-dark-card py-2 shadow-xl sm:absolute sm:right-0 sm:top-auto sm:inset-x-auto sm:mt-2 sm:max-h-[32rem] sm:w-56">
<div className="px-4 pb-3 pt-1 border-b border-dark-border mb-2 flex flex-col gap-2"> <div className="px-4 pb-3 pt-1 border-b border-dark-border mb-2 flex flex-col gap-2">
<span className="text-xs font-bold text-dark-muted uppercase tracking-widest">Período Customizado</span> <span className="text-xs font-bold text-dark-muted uppercase tracking-widest">Período Customizado</span>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
@@ -177,7 +177,7 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
{/* Auto Refresh Dropdown */} {/* Auto Refresh Dropdown */}
{setRefreshInterval && onManualRefresh && ( {setRefreshInterval && onManualRefresh && (
<div className="flex items-center bg-dark-card border border-dark-border rounded-xl shadow-sm overflow-hidden hover:border-brand-primary transition-colors"> <div className="flex min-h-11 w-full items-center overflow-hidden rounded-xl border border-dark-border bg-dark-card shadow-sm transition-colors hover:border-brand-primary sm:w-auto">
<button <button
onClick={onManualRefresh} onClick={onManualRefresh}
className="flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium text-dark-text hover:text-brand-primary hover:bg-dark-input transition-colors border-r border-dark-border cursor-pointer" className="flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium text-dark-text hover:text-brand-primary hover:bg-dark-input transition-colors border-r border-dark-border cursor-pointer"

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Outlet, Link, useLocation } from 'react-router-dom'; import { Outlet, Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, Tags, Boxes, ClipboardList } from 'lucide-react'; import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, Tags, Boxes, ClipboardList, Menu, X } from 'lucide-react';
import type { DateRange, OrderData } from '../types'; import type { DateRange, OrderData } from '../types';
import { isSuperAdmin, logout } from '../dataService'; import { isSuperAdmin, logout } from '../dataService';
import { rangeForLastDays } from '../dateRanges'; import { rangeForLastDays } from '../dateRanges';
@@ -13,6 +13,7 @@ const Layout = () => {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
return localStorage.getItem('graph_sidebar_collapsed') === 'true'; return localStorage.getItem('graph_sidebar_collapsed') === 'true';
}); });
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
const [themeMode, setThemeMode] = useState<ThemeMode>(() => { const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
return localStorage.getItem('nexstar_theme') === 'offwhite' ? 'offwhite' : 'dark'; return localStorage.getItem('nexstar_theme') === 'offwhite' ? 'offwhite' : 'dark';
}); });
@@ -58,6 +59,7 @@ const Layout = () => {
const toggleTheme = () => { const toggleTheme = () => {
setThemeMode(current => current === 'dark' ? 'offwhite' : 'dark'); setThemeMode(current => current === 'dark' ? 'offwhite' : 'dark');
}; };
const showSidebarLabels = !isSidebarCollapsed || isMobileNavOpen;
const adminNavigation = isSuperAdmin() const adminNavigation = isSuperAdmin()
? [{ name: 'Usuários', href: '/admin/users', icon: Shield }] ? [{ name: 'Usuários', href: '/admin/users', icon: Shield }]
@@ -90,25 +92,36 @@ const Layout = () => {
]; ];
return ( return (
<div className="app-shell flex h-screen text-dark-text overflow-hidden"> <div className="app-shell flex h-dvh min-w-0 text-dark-text overflow-hidden">
{isMobileNavOpen && (
<button
type="button"
className="fixed inset-0 z-40 bg-black/55 md:hidden"
onClick={() => setIsMobileNavOpen(false)}
aria-label="Fechar menu"
/>
)}
{/* Sidebar */} {/* Sidebar */}
<aside className={`bg-dark-sidebar border-r border-dark-border flex flex-col transition-all duration-300 ${isSidebarCollapsed ? 'w-20' : 'w-64'}`}> <aside className={`fixed inset-y-0 left-0 z-50 flex w-72 flex-col border-r border-dark-border bg-dark-sidebar transition-transform duration-300 md:relative md:z-auto md:translate-x-0 md:transition-[width] ${isSidebarCollapsed ? 'md:w-20' : 'md:w-64'} ${isMobileNavOpen ? 'translate-x-0' : '-translate-x-full'}`}>
<div className={`h-20 px-6 border-b border-dark-border flex items-center ${isSidebarCollapsed ? 'justify-center' : 'justify-between'}`}> <div className={`flex h-16 items-center border-b border-dark-border px-5 md:h-20 md:px-6 ${isSidebarCollapsed ? 'md:justify-center' : 'justify-between'}`}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="rounded-lg border border-brand-primary/25 bg-brand-primary/15 p-1.5 text-brand-primary shadow-sm shadow-brand-primary/10"> <div className="rounded-lg border border-brand-primary/25 bg-brand-primary/15 p-1.5 text-brand-primary shadow-sm shadow-brand-primary/10">
<BarChart3 className="w-6 h-6" /> <BarChart3 className="w-6 h-6" />
</div> </div>
{!isSidebarCollapsed && <span className="text-xl font-bold text-dark-text">Nexstar</span>} {showSidebarLabels && <span className="text-xl font-bold text-dark-text">Nexstar</span>}
</div> </div>
<button onClick={() => setIsMobileNavOpen(false)} className="text-dark-muted transition-colors hover:text-dark-text md:hidden" aria-label="Fechar menu">
<X className="h-5 w-5" />
</button>
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
<button onClick={toggleSidebar} className="text-dark-muted hover:text-dark-text transition-colors cursor-pointer"> <button onClick={toggleSidebar} className="hidden cursor-pointer text-dark-muted transition-colors hover:text-dark-text md:block">
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
)} )}
</div> </div>
{isSidebarCollapsed && ( {isSidebarCollapsed && (
<div className="p-4 border-b border-dark-border flex justify-center"> <div className="hidden border-b border-dark-border p-4 md:flex md:justify-center">
<button onClick={toggleSidebar} className="text-dark-muted hover:text-dark-text transition-colors cursor-pointer"> <button onClick={toggleSidebar} className="text-dark-muted hover:text-dark-text transition-colors cursor-pointer">
<ChevronRight className="w-5 h-5" /> <ChevronRight className="w-5 h-5" />
</button> </button>
@@ -120,10 +133,10 @@ const Layout = () => {
<div <div
key={section.label} key={section.label}
className={`space-y-1.5 ${ className={`space-y-1.5 ${
isSidebarCollapsed ? 'border-t border-dark-border pt-5 first:border-t-0 first:pt-0' : '' isSidebarCollapsed ? 'md:border-t md:border-dark-border md:pt-5 md:first:border-t-0 md:first:pt-0' : ''
}`} }`}
> >
{!isSidebarCollapsed && ( {showSidebarLabels && (
<div className="px-3 pb-1 text-[11px] font-bold uppercase tracking-widest text-dark-muted"> <div className="px-3 pb-1 text-[11px] font-bold uppercase tracking-widest text-dark-muted">
{section.label} {section.label}
</div> </div>
@@ -134,8 +147,9 @@ const Layout = () => {
<Link <Link
key={item.name} key={item.name}
to={item.href} to={item.href}
onClick={() => setIsMobileNavOpen(false)}
className={`group flex min-h-11 items-center rounded-lg px-3 py-2.5 text-sm transition-all ${ className={`group flex min-h-11 items-center rounded-lg px-3 py-2.5 text-sm transition-all ${
isSidebarCollapsed ? 'justify-center' : 'gap-3' isSidebarCollapsed ? 'gap-3 md:justify-center' : 'gap-3'
} ${ } ${
isActive isActive
? 'bg-brand-primary/12 text-brand-primary font-semibold shadow-sm shadow-brand-primary/10 ring-1 ring-brand-primary/15' ? 'bg-brand-primary/12 text-brand-primary font-semibold shadow-sm shadow-brand-primary/10 ring-1 ring-brand-primary/15'
@@ -144,7 +158,7 @@ const Layout = () => {
title={isSidebarCollapsed ? item.name : undefined} title={isSidebarCollapsed ? item.name : undefined}
> >
<item.icon className="h-4.5 w-4.5 shrink-0" /> <item.icon className="h-4.5 w-4.5 shrink-0" />
{!isSidebarCollapsed && <span className="truncate font-medium">{item.name}</span>} {showSidebarLabels && <span className="truncate font-medium">{item.name}</span>}
</Link> </Link>
); );
})} })}
@@ -155,23 +169,33 @@ const Layout = () => {
<div className="p-4 border-t border-dark-border"> <div className="p-4 border-t border-dark-border">
<button <button
onClick={logout} onClick={logout}
className={`w-full flex items-center text-red-500 hover:bg-red-500/10 px-4 py-3 rounded-xl transition-all cursor-pointer ${isSidebarCollapsed ? 'justify-center' : 'space-x-3'}`} className={`flex w-full items-center rounded-xl px-4 py-3 text-red-500 transition-all hover:bg-red-500/10 cursor-pointer ${isSidebarCollapsed ? 'gap-3 md:justify-center' : 'gap-3'}`}
> >
<LogOut className="w-5 h-5 shrink-0" /> <LogOut className="w-5 h-5 shrink-0" />
{!isSidebarCollapsed && <span className="font-medium">Sair</span>} {showSidebarLabels && <span className="font-medium">Sair</span>}
</button> </button>
</div> </div>
</aside> </aside>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 flex flex-col h-screen overflow-hidden"> <main className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Header */} {/* Header */}
<header className="h-20 bg-dark-header border-b border-dark-border flex items-center justify-between px-8 shrink-0"> <header className="flex h-16 shrink-0 items-center justify-between border-b border-dark-border bg-dark-header px-4 md:h-20 md:px-8">
<h2 className="text-xl font-bold text-dark-text">Painel de Análise</h2> <div className="flex min-w-0 items-center gap-3">
<button
type="button"
onClick={() => setIsMobileNavOpen(true)}
className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-dark-text md:hidden"
aria-label="Abrir menu"
>
<Menu className="h-5 w-5" />
</button>
<h2 className="truncate text-lg font-bold text-dark-text md:text-xl">Painel de Análise</h2>
</div>
<button <button
type="button" type="button"
onClick={toggleTheme} onClick={toggleTheme}
className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-transparent bg-transparent text-dark-text transition-colors hover:border-dark-border hover:bg-dark-input hover:text-brand-primary cursor-pointer" className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-transparent bg-transparent text-dark-text transition-colors hover:border-dark-border hover:bg-dark-input hover:text-brand-primary cursor-pointer"
title={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'} title={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'}
aria-label={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'} aria-label={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'}
> >
@@ -180,7 +204,7 @@ const Layout = () => {
</header> </header>
{/* Content Area */} {/* Content Area */}
<div className="app-content flex-1 overflow-y-auto p-8 relative"> <div className="app-content relative flex-1 overflow-y-auto px-4 py-5 pb-24 sm:px-5 md:p-8">
<Outlet context={{ dateRange, setDateRange, ordersData: emptyOrdersData, isDataLoading: false, refreshInterval, setRefreshInterval, themeMode }} /> <Outlet context={{ dateRange, setDateRange, ordersData: emptyOrdersData, isDataLoading: false, refreshInterval, setRefreshInterval, themeMode }} />
</div> </div>
</main> </main>

View File

@@ -149,6 +149,36 @@
linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 16rem), linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 16rem),
transparent; transparent;
} }
@media (max-width: 767px) {
html,
body,
#root {
min-width: 0;
overflow-x: hidden;
}
.app-content {
scroll-padding-bottom: 5rem;
-webkit-overflow-scrolling: touch;
}
.app-content .overflow-x-auto {
overscroll-behavior-x: contain;
scrollbar-width: thin;
scrollbar-color: var(--color-dark-border) transparent;
}
.app-content .overflow-x-auto::-webkit-scrollbar {
height: 5px;
}
.app-content input,
.app-content select,
.app-content textarea {
font-size: 16px;
}
}
} }
@layer utilities { @layer utilities {