finished meteorologia and main dashboard
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CloudSun,
|
||||
Droplet,
|
||||
Filter,
|
||||
Gauge,
|
||||
Home,
|
||||
Lightbulb,
|
||||
MonitorDot,
|
||||
Settings,
|
||||
TabletSmartphone,
|
||||
Waves,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -20,6 +28,8 @@ type SidebarProps = {
|
||||
onToggleCollapsed: () => void;
|
||||
};
|
||||
|
||||
const RADIUS = "rounded-[10px]";
|
||||
|
||||
const navigationItems: {
|
||||
label: string;
|
||||
page: AppPage;
|
||||
@@ -29,10 +39,24 @@ const navigationItems: {
|
||||
{ label: "Meteorologia", page: "meteo", icon: CloudSun },
|
||||
];
|
||||
|
||||
const disabledItems = [
|
||||
const climateItems = [
|
||||
{ label: "Iluminação", icon: Lightbulb },
|
||||
{ label: "Ventilação", icon: Wind },
|
||||
{ label: "Sinótico", icon: MonitorDot },
|
||||
{ label: "Gráficos", icon: BarChart3 },
|
||||
];
|
||||
|
||||
const irrigationItems = [
|
||||
{ label: "Regas", icon: Droplet },
|
||||
{ label: "Sinótico", icon: MonitorDot },
|
||||
{ label: "Filtros de Rega", icon: Filter },
|
||||
{ label: "Consumos", icon: Gauge },
|
||||
{ label: "Drenagem", icon: Waves },
|
||||
{ label: "Gráficos", icon: BarChart3 },
|
||||
];
|
||||
|
||||
const utilityItems = [
|
||||
{ label: "Consola (VNC)", icon: TabletSmartphone },
|
||||
{ label: "Rega", icon: Droplet },
|
||||
{ label: "Clima", icon: Wind },
|
||||
{ label: "Configurações", icon: Settings },
|
||||
];
|
||||
|
||||
@@ -45,47 +69,72 @@ export function Sidebar({
|
||||
}: 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);
|
||||
};
|
||||
|
||||
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-64"} flex h-full flex-col bg-[#0B1620] px-4 py-5 shadow-[inset_-1px_0_0_0_rgba(80,100,120,0.25),2px_0_12px_rgba(0,0,0,0.15)] transition-all duration-200`
|
||||
: `${collapsed ? "w-20" : "w-64"} flex h-full flex-col bg-[#EEF3F7] px-4 py-5 shadow-[inset_-1px_0_0_0_rgba(180,190,200,0.5)] transition-all duration-200`
|
||||
? `${collapsed ? "w-20" : "w-[290px]"} flex h-full flex-col border-r border-white/10 bg-[#0F172A] px-4 py-5 text-slate-100 shadow-[8px_0_30px_rgba(0,0,0,0.18)] transition-all duration-200`
|
||||
: `${collapsed ? "w-20" : "w-[290px]"} flex h-full flex-col border-r border-[#D8DEE7] bg-[#F8FAFC] px-4 py-5 text-[#0F172A] shadow-[8px_0_28px_rgba(15,23,42,0.04)] transition-all duration-200`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
collapsed
|
||||
? "mb-10 flex items-center justify-center"
|
||||
: "mb-10 flex items-center gap-3 px-2"
|
||||
? "mb-12 flex items-center justify-center"
|
||||
: "mb-12 flex items-center gap-4 px-1"
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt="LitoralRegas"
|
||||
className="h-12 w-12 shrink-0 object-contain"
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
isDark
|
||||
? `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden border border-white/10 bg-white/[0.04] shadow-[0_10px_24px_rgba(0,0,0,0.18)]`
|
||||
: `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden border border-[#D8DEE7] bg-white shadow-sm`
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Litoral Central"
|
||||
className="h-12 w-12 scale-[1.35] object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={
|
||||
isDark
|
||||
? "truncate text-[16px] font-bold tracking-wide text-white"
|
||||
: "truncate text-[16px] font-bold tracking-wide text-[#162434]"
|
||||
? "truncate text-[16px] font-black tracking-[-0.02em] text-white"
|
||||
: "truncate text-[16px] font-black tracking-[-0.02em] text-[#0F172A]"
|
||||
}
|
||||
>
|
||||
LITORAL CENTRAL
|
||||
Litoral Central
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
isDark
|
||||
? "mt-0.5 text-[10px] uppercase tracking-[0.18em] text-[#8FA3B8]"
|
||||
: "mt-0.5 text-[10px] uppercase tracking-[0.18em] text-[#607284]"
|
||||
}
|
||||
>
|
||||
OPERAÇÕES AGRÍCOLAS
|
||||
<div className="mt-1 whitespace-nowrap text-[10px] font-bold uppercase tracking-[0.1em] text-slate-500">
|
||||
Operações agrícolas
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -94,67 +143,257 @@ export function Sidebar({
|
||||
<nav className="space-y-2">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = activePage === item.page;
|
||||
const active = activePage === item.page && activeTreeItem === null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={() => onNavigate(item.page)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTreeItem(null);
|
||||
onNavigate(item.page);
|
||||
}}
|
||||
title={collapsed ? item.label : undefined}
|
||||
className={
|
||||
active
|
||||
? isDark
|
||||
? "flex w-full items-center gap-3 rounded-xl bg-[#18304B] px-4 py-3 text-left text-sm font-semibold text-white"
|
||||
: "flex w-full items-center gap-3 rounded-xl bg-[#DCE8F5] px-4 py-3 text-left text-sm font-semibold text-[#162434]"
|
||||
: isDark
|
||||
? "flex w-full items-center gap-3 rounded-xl px-4 py-3 text-left text-sm font-medium text-[#D8E2EC] hover:bg-[#132434] hover:text-white"
|
||||
: "flex w-full items-center gap-3 rounded-xl px-4 py-3 text-left text-sm font-medium text-[#445569] hover:bg-[#E2E8F0] hover:text-[#162434]"
|
||||
}
|
||||
className={navButtonClass(isDark, active, collapsed)}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
{active && <ActiveIndicator isDark={isDark} />}
|
||||
|
||||
<Icon className={navIconClass(isDark, active)} />
|
||||
|
||||
{!collapsed && <span className="truncate">{item.label}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{disabledItems.map((item) => {
|
||||
<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 key = `utility:${item.label}`;
|
||||
const active = activeTreeItem === key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.label}
|
||||
disabled
|
||||
type="button"
|
||||
onClick={() => handleTreeClick(key)}
|
||||
title={collapsed ? item.label : undefined}
|
||||
className={
|
||||
isDark
|
||||
? "flex w-full cursor-not-allowed items-center gap-3 rounded-xl px-4 py-3 text-left text-sm font-medium text-[#607284] opacity-60"
|
||||
: "flex w-full cursor-not-allowed items-center gap-3 rounded-xl px-4 py-3 text-left text-sm font-medium text-[#8A9AAB] opacity-70"
|
||||
}
|
||||
className={navButtonClass(isDark, active, collapsed)}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
{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 rounded-xl px-4 py-3 text-sm text-slate-400 hover:bg-[#132434] hover:text-white"
|
||||
: "mt-auto flex items-center justify-center gap-2 rounded-xl px-4 py-3 text-sm text-[#607284] hover:bg-[#E2E8F0] hover:text-[#162434]"
|
||||
? `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-white/10 bg-white/[0.04] px-4 py-3.5 text-[15px] font-semibold text-slate-400 shadow-[0_10px_24px_rgba(0,0,0,0.12)] transition hover:bg-white/[0.06] hover:text-white`
|
||||
: `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#D8DEE7] bg-white px-4 py-3.5 text-[15px] font-semibold text-slate-500 shadow-sm transition hover:bg-[#F1F5F9] hover:text-[#0F172A]`
|
||||
}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
<ChevronRight className="h-[22px] w-[22px]" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
<ChevronLeft className="h-[22px] w-[22px]" />
|
||||
<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-slate-500 transition-transform duration-200 ${open ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!collapsed && open && (
|
||||
<div
|
||||
className={
|
||||
isDark
|
||||
? "ml-5 mt-2 space-y-1.5 border-l border-white/10 pl-3"
|
||||
: "ml-5 mt-2 space-y-1.5 border-l border-[#D8DEE7] 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-[8px] bg-white/[0.06] px-3.5 py-2.5 text-left text-[13px] font-bold text-white"
|
||||
: "flex w-full items-center gap-2 rounded-[8px] bg-white px-3.5 py-2.5 text-left text-[13px] font-bold text-[#0F172A] shadow-sm"
|
||||
: isDark
|
||||
? "flex w-full items-center gap-2 rounded-[8px] px-3.5 py-2.5 text-left text-[13px] font-semibold text-slate-500 transition hover:bg-white/[0.04] hover:text-slate-200"
|
||||
: "flex w-full items-center gap-2 rounded-[8px] px-3.5 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-emerald-300"
|
||||
: "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="py-3">
|
||||
<p className="px-4 text-[11px] font-bold uppercase tracking-[0.18em] text-slate-500">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveIndicator({ isDark }: { isDark: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
isDark
|
||||
? "absolute left-0 top-1/2 h-7 w-1 -translate-y-1/2 rounded-r-full bg-emerald-400"
|
||||
: "absolute left-0 top-1/2 h-7 w-1 -translate-y-1/2 rounded-r-full 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-emerald-400/20 bg-[#162033] ${alignment} py-[14px] text-left text-[15px] font-black text-white shadow-[0_12px_28px_rgba(0,0,0,0.22)]`
|
||||
: `relative flex w-full items-center gap-3 ${RADIUS} border border-[#D8DEE7] bg-white ${alignment} py-[14px] text-left text-[15px] font-black text-[#0F172A] shadow-[0_8px_20px_rgba(15,23,42,0.06)]`;
|
||||
}
|
||||
|
||||
return isDark
|
||||
? `flex w-full items-center gap-3 ${RADIUS} ${alignment} py-[14px] text-left text-[15px] font-semibold text-slate-400 transition hover:bg-white/[0.04] hover:text-white`
|
||||
: `flex w-full items-center gap-3 ${RADIUS} ${alignment} py-[14px] text-left text-[15px] font-semibold text-slate-600 transition hover:bg-white hover:text-[#0F172A] hover:shadow-sm`;
|
||||
}
|
||||
|
||||
function navIconClass(isDark: boolean, active: boolean) {
|
||||
if (active) {
|
||||
return isDark
|
||||
? "h-[22px] w-[22px] shrink-0 text-emerald-300"
|
||||
: "h-[22px] w-[22px] shrink-0 text-[#0F766E]";
|
||||
}
|
||||
|
||||
return isDark
|
||||
? "h-[22px] w-[22px] shrink-0 text-slate-500"
|
||||
: "h-[22px] w-[22px] shrink-0 text-slate-500";
|
||||
}
|
||||
Reference in New Issue
Block a user