407 lines
12 KiB
TypeScript
407 lines
12 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
BarChart3,
|
|
ChevronDown,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
CloudSun,
|
|
Droplet,
|
|
Filter,
|
|
Gauge,
|
|
Home,
|
|
Lightbulb,
|
|
MonitorDot,
|
|
Settings,
|
|
TabletSmartphone,
|
|
Waves,
|
|
Wind,
|
|
} from "lucide-react";
|
|
|
|
import logo from "../../assets/logo.png";
|
|
import type { AppPage } from "../../app/App";
|
|
|
|
type SidebarProps = {
|
|
theme: "dark" | "light";
|
|
activePage: AppPage;
|
|
collapsed: boolean;
|
|
onNavigate: (page: AppPage) => void;
|
|
onToggleCollapsed: () => void;
|
|
};
|
|
|
|
const RADIUS = "rounded-[6px]";
|
|
|
|
const navigationItems: {
|
|
label: string;
|
|
page: AppPage;
|
|
icon: React.ElementType;
|
|
}[] = [
|
|
{ label: "Painel Principal", page: "dashboard", icon: Home },
|
|
{ label: "Meteorologia", page: "meteo", icon: CloudSun },
|
|
{ label: "Gráficos Gerais", page: "maincharts", icon: BarChart3 },
|
|
{ label: "Sinótico", page: "synoptic" , icon: MonitorDot }
|
|
|
|
];
|
|
|
|
const climateItems = [
|
|
{ label: "Iluminação", icon: Lightbulb },
|
|
{ label: "Ventilação", icon: Wind },
|
|
{ label: "Gráficos", icon: BarChart3 },
|
|
];
|
|
|
|
const irrigationItems = [
|
|
{ label: "Regas", icon: Droplet },
|
|
{ label: "Filtros de Rega", icon: Filter },
|
|
{ label: "Consumos", icon: Gauge },
|
|
{ label: "Drenagem", icon: Waves },
|
|
{ label: "Gráficos", icon: BarChart3 },
|
|
];
|
|
|
|
const utilityItems: {
|
|
label: string;
|
|
page: AppPage;
|
|
icon: React.ElementType;
|
|
}[] = [
|
|
{ label: "Consola (VNC)", page: "console", icon: TabletSmartphone },
|
|
{ label: "Configurações", page: "settings", icon: Settings },
|
|
];
|
|
|
|
export function Sidebar({
|
|
theme,
|
|
activePage,
|
|
collapsed,
|
|
onNavigate,
|
|
onToggleCollapsed,
|
|
}: SidebarProps) {
|
|
const isDark = theme === "dark";
|
|
|
|
const [climateOpen, setClimateOpen] = useState(false);
|
|
const [irrigationOpen, setIrrigationOpen] = useState(false);
|
|
const [activeTreeItem, setActiveTreeItem] = useState<string | null>(null);
|
|
|
|
const handleTreeClick = (key: string) => {
|
|
setActiveTreeItem(key);
|
|
|
|
if (key === "climate:Gráficos") {
|
|
onNavigate("climateCharts");
|
|
}
|
|
};
|
|
|
|
const handleTreeToggle = (section: "climate" | "irrigation") => {
|
|
if (collapsed) {
|
|
onToggleCollapsed();
|
|
}
|
|
|
|
if (section === "climate") {
|
|
setClimateOpen((current) => !current);
|
|
setIrrigationOpen(false);
|
|
return;
|
|
}
|
|
|
|
setIrrigationOpen((current) => !current);
|
|
setClimateOpen(false);
|
|
};
|
|
|
|
return (
|
|
<aside
|
|
className={
|
|
isDark
|
|
? `${collapsed ? "w-20" : "w-[290px]"
|
|
} flex h-full flex-col border-r border-[#263247] bg-[#0B1220] px-4 py-5 text-slate-100 transition-all duration-200`
|
|
: `${collapsed ? "w-20" : "w-[290px]"
|
|
} flex h-full flex-col border-r border-[#D7DEE8] bg-[#F3F6FA] px-4 py-5 text-[#0F172A] transition-all duration-200`
|
|
}
|
|
>
|
|
<div
|
|
className={
|
|
collapsed
|
|
? "mb-10 flex items-center justify-center"
|
|
: "mb-10 flex items-center gap-3 px-1"
|
|
}
|
|
>
|
|
<div
|
|
className={
|
|
isDark
|
|
? `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#2A3950] bg-[#111A2B] p-1.5`
|
|
: `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#CBD5E1] bg-white p-1.5`
|
|
}
|
|
>
|
|
<img
|
|
src={logo}
|
|
alt="Litoral Central"
|
|
className="h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
|
|
{!collapsed && (
|
|
<div className="min-w-0 flex-1">
|
|
<div
|
|
className={
|
|
isDark
|
|
? "truncate text-[15px] font-extrabold tracking-[-0.02em] text-white"
|
|
: "truncate text-[15px] font-extrabold tracking-[-0.02em] text-[#0F172A]"
|
|
}
|
|
>
|
|
Litoral Central
|
|
</div>
|
|
|
|
<div className="mt-1 whitespace-nowrap text-[10px] font-bold uppercase tracking-[0.18em] text-[#7D8EA8]">
|
|
Operações agrícolas
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<nav className="space-y-1.5">
|
|
{navigationItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const active = activePage === item.page && activeTreeItem === null;
|
|
|
|
return (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
onClick={() => {
|
|
setActiveTreeItem(null);
|
|
onNavigate(item.page);
|
|
}}
|
|
title={collapsed ? item.label : undefined}
|
|
className={navButtonClass(isDark, active, collapsed)}
|
|
>
|
|
{active && <ActiveIndicator isDark={isDark} />}
|
|
<Icon className={navIconClass(isDark, active)} />
|
|
{!collapsed && <span className="truncate">{item.label}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
|
|
<SectionLabel collapsed={collapsed} label="Operação" />
|
|
|
|
<TreeSection
|
|
theme={theme}
|
|
collapsed={collapsed}
|
|
label="Clima"
|
|
icon={Wind}
|
|
open={climateOpen}
|
|
onToggle={() => handleTreeToggle("climate")}
|
|
items={climateItems}
|
|
sectionKey="climate"
|
|
activeTreeItem={activeTreeItem}
|
|
onItemClick={handleTreeClick}
|
|
/>
|
|
|
|
<TreeSection
|
|
theme={theme}
|
|
collapsed={collapsed}
|
|
label="Rega"
|
|
icon={Droplet}
|
|
open={irrigationOpen}
|
|
onToggle={() => handleTreeToggle("irrigation")}
|
|
items={irrigationItems}
|
|
sectionKey="irrigation"
|
|
activeTreeItem={activeTreeItem}
|
|
onItemClick={handleTreeClick}
|
|
/>
|
|
|
|
<SectionLabel collapsed={collapsed} label="Sistema" />
|
|
|
|
{utilityItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const active = activePage === item.page && activeTreeItem === null;
|
|
|
|
return (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
onClick={() => {
|
|
setActiveTreeItem(null);
|
|
onNavigate(item.page);
|
|
}}
|
|
title={collapsed ? item.label : undefined}
|
|
className={navButtonClass(isDark, active, collapsed)}
|
|
>
|
|
{active && <ActiveIndicator isDark={isDark} />}
|
|
<Icon className={navIconClass(isDark, active)} />
|
|
{!collapsed && <span className="truncate">{item.label}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={onToggleCollapsed}
|
|
className={
|
|
isDark
|
|
? `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#263247] bg-[#111A2B] px-4 py-3 text-[14px] font-semibold text-[#9BAAC1] transition hover:border-[#33445F] hover:bg-[#162033] hover:text-white`
|
|
: `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#CBD5E1] bg-white px-4 py-3 text-[14px] font-semibold text-slate-600 transition hover:bg-[#EAF0F7] hover:text-[#0F172A]`
|
|
}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="h-5 w-5" />
|
|
) : (
|
|
<>
|
|
<ChevronLeft className="h-5 w-5" />
|
|
<span>Recolher menu</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function TreeSection({
|
|
theme,
|
|
collapsed,
|
|
label,
|
|
icon: Icon,
|
|
open,
|
|
onToggle,
|
|
items,
|
|
sectionKey,
|
|
activeTreeItem,
|
|
onItemClick,
|
|
}: {
|
|
theme: "dark" | "light";
|
|
collapsed: boolean;
|
|
label: string;
|
|
icon: React.ElementType;
|
|
open: boolean;
|
|
onToggle: () => void;
|
|
items: { label: string; icon: React.ElementType }[];
|
|
sectionKey: string;
|
|
activeTreeItem: string | null;
|
|
onItemClick: (key: string) => void;
|
|
}) {
|
|
const isDark = theme === "dark";
|
|
const hasActiveChild = items.some(
|
|
(item) => activeTreeItem === `${sectionKey}:${item.label}`,
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<button
|
|
type="button"
|
|
onClick={onToggle}
|
|
title={collapsed ? label : undefined}
|
|
className={navButtonClass(isDark, hasActiveChild, collapsed)}
|
|
>
|
|
{hasActiveChild && <ActiveIndicator isDark={isDark} />}
|
|
<Icon className={navIconClass(isDark, hasActiveChild)} />
|
|
|
|
{!collapsed && (
|
|
<>
|
|
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
|
|
<ChevronDown
|
|
className={`h-4 w-4 shrink-0 text-[#6F819B] transition-transform duration-200 ${open ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{!collapsed && open && (
|
|
<div
|
|
className={
|
|
isDark
|
|
? "ml-[18px] mt-2 space-y-1 border-l border-[#263247] pl-3"
|
|
: "ml-[18px] mt-2 space-y-1 border-l border-[#CBD5E1] pl-3"
|
|
}
|
|
>
|
|
{items.map((item) => {
|
|
const SubIcon = item.icon;
|
|
const key = `${sectionKey}:${item.label}`;
|
|
const active = activeTreeItem === key;
|
|
|
|
return (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
onClick={() => onItemClick(key)}
|
|
className={
|
|
active
|
|
? isDark
|
|
? "flex w-full items-center gap-2 rounded-[5px] border border-[#2C3D56] bg-[#131D2F] px-3 py-2.5 text-left text-[13px] font-bold text-white"
|
|
: "flex w-full items-center gap-2 rounded-[5px] border border-[#CBD5E1] bg-white px-3 py-2.5 text-left text-[13px] font-bold text-[#0F172A]"
|
|
: isDark
|
|
? "flex w-full items-center gap-2 rounded-[5px] px-3 py-2.5 text-left text-[13px] font-semibold text-[#7D8EA8] transition hover:bg-[#111A2B] hover:text-slate-200"
|
|
: "flex w-full items-center gap-2 rounded-[5px] px-3 py-2.5 text-left text-[13px] font-semibold text-slate-500 transition hover:bg-white hover:text-[#0F172A]"
|
|
}
|
|
>
|
|
<SubIcon
|
|
className={
|
|
active
|
|
? isDark
|
|
? "h-4 w-4 shrink-0 text-[#4FD1C5]"
|
|
: "h-4 w-4 shrink-0 text-[#0F766E]"
|
|
: "h-4 w-4 shrink-0"
|
|
}
|
|
/>
|
|
|
|
<span className="truncate">{item.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionLabel({
|
|
collapsed,
|
|
label,
|
|
}: {
|
|
collapsed: boolean;
|
|
label: string;
|
|
}) {
|
|
if (collapsed) {
|
|
return <div className="py-2" />;
|
|
}
|
|
|
|
return (
|
|
<div className="pb-1 pt-5">
|
|
<p className="px-4 text-[10px] font-black uppercase tracking-[0.2em] text-[#61738C]">
|
|
{label}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ActiveIndicator({ isDark }: { isDark: boolean }) {
|
|
return (
|
|
<span
|
|
className={
|
|
isDark
|
|
? "absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-sm bg-[#4FD1C5]"
|
|
: "absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-sm bg-[#0F766E]"
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function navButtonClass(isDark: boolean, active: boolean, collapsed: boolean) {
|
|
const alignment = collapsed ? "justify-center px-0" : "px-4";
|
|
|
|
if (active) {
|
|
return isDark
|
|
? `relative flex w-full items-center gap-3 ${RADIUS} border border-[#2C3D56] bg-[#131D2F] ${alignment} py-3.5 text-left text-[14px] font-extrabold text-white`
|
|
: `relative flex w-full items-center gap-3 ${RADIUS} border border-[#CBD5E1] bg-white ${alignment} py-3.5 text-left text-[14px] font-extrabold text-[#0F172A]`;
|
|
}
|
|
|
|
return isDark
|
|
? `relative flex w-full items-center gap-3 ${RADIUS} ${alignment} py-3.5 text-left text-[14px] font-semibold text-[#8EA0BA] transition hover:bg-[#111A2B] hover:text-white`
|
|
: `relative flex w-full items-center gap-3 ${RADIUS} ${alignment} py-3.5 text-left text-[14px] font-semibold text-slate-600 transition hover:bg-white hover:text-[#0F172A]`;
|
|
}
|
|
|
|
function navIconClass(isDark: boolean, active: boolean) {
|
|
if (active) {
|
|
return isDark
|
|
? "h-5 w-5 shrink-0 text-[#4FD1C5]"
|
|
: "h-5 w-5 shrink-0 text-[#0F766E]";
|
|
}
|
|
|
|
return isDark
|
|
? "h-5 w-5 shrink-0 text-[#6F819B]"
|
|
: "h-5 w-5 shrink-0 text-slate-500";
|
|
} |