434 lines
20 KiB
TypeScript
434 lines
20 KiB
TypeScript
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
|
import GalpaoModule from './pages/GalpaoModule';
|
|
import CylinderModule from './pages/CylinderModule';
|
|
import VaultModule from './pages/VaultModule';
|
|
import DomeModule from './pages/DomeModule';
|
|
import SignModule from './pages/SignModule';
|
|
import IsolatedRoofModule from './pages/IsolatedRoofModule';
|
|
import BarSelectorModule from './pages/BarSelectorModule';
|
|
import BridgeModule from './pages/BridgeModule';
|
|
import DynamicsModule from './pages/DynamicsModule';
|
|
import SettingsModule from './pages/SettingsModule';
|
|
import TowerModule from './pages/TowerModule';
|
|
import PiperackModule from './pages/PiperackModule';
|
|
import CertificatePage from './pages/CertificatePage';
|
|
import {
|
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
|
Square, Layers, BarChart3, Activity, Warehouse,
|
|
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck
|
|
} from 'lucide-react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { useState } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ThemeProvider, useTheme } from '@/lib/theme';
|
|
import { useI18n } from './store/i18nStore';
|
|
import { GlobalWindSettingsModal } from './components/GlobalWindSettingsModal';
|
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
import { TermsOfUseDialog } from './components/TermsOfUseDialog';
|
|
// Ícones Customizados de Engenharia Premium
|
|
const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
{...props}
|
|
>
|
|
<path d="M3 18V6l3-1 6 3 6-3 3 1v12" />
|
|
<path d="M3 10h18" />
|
|
<path d="M8 10v8M16 10v8" />
|
|
<path d="M3 14c4-2 6-2 10 0s6 2 8 0" />
|
|
</svg>
|
|
);
|
|
|
|
const TowerIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
{...props}
|
|
>
|
|
<path d="M6 22L10 2M18 22L14 2" />
|
|
<path d="M10 2h4" />
|
|
<path d="M8.5 7h7" />
|
|
<path d="M7.5 12h9" />
|
|
<path d="M6.5 17h11" />
|
|
<path d="M6 22h12" />
|
|
<path d="M10 2l4 5M12 7l-4 5" />
|
|
<path d="M10 12l5 5M14 12l-5 5" />
|
|
<path d="M9.5 17l5.5 5M14.5 17l-5.5 5" />
|
|
</svg>
|
|
);
|
|
|
|
function AppLayout({ children }: { children: React.ReactNode }) {
|
|
const location = useLocation();
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
const { t } = useI18n();
|
|
const { setTheme, effectiveTheme } = useTheme();
|
|
|
|
const toggleTheme = () => {
|
|
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
|
};
|
|
|
|
const navItems = [
|
|
{ path: '/', icon: <Home className="w-5 h-5" />, labelKey: 'nav_home' as const },
|
|
{ path: '/galpao', icon: <Warehouse className="w-5 h-5" />, labelKey: 'nav_warehouse' as const },
|
|
{ path: '/cilindro', icon: <Cylinder className="w-5 h-5" />, labelKey: 'nav_cylinder' as const },
|
|
{ path: '/abobada', icon: <Church className="w-5 h-5" />, labelKey: 'nav_vault' as const },
|
|
{ path: '/cupula', icon: <CircleDot className="w-5 h-5" />, labelKey: 'nav_dome' as const },
|
|
{ path: '/muros', icon: <Square className="w-5 h-5" />, labelKey: 'nav_sign' as const },
|
|
{ path: '/cobertura-isolada', icon: <Layers className="w-5 h-5" />, labelKey: 'nav_isolated_roof' as const },
|
|
{ path: '/barras', icon: <BarChart3 className="w-5 h-5" />, labelKey: 'nav_bar' as const },
|
|
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
|
|
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
|
{ path: '/piperack', icon: <Frame className="w-5 h-5" />, labelKey: 'nav_piperack' as const },
|
|
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
|
|
];
|
|
|
|
return (
|
|
<div className="flex h-screen w-full bg-background overflow-hidden font-sans">
|
|
<aside
|
|
className={cn(
|
|
'hidden md:flex flex-col border-r bg-sidebar transition-all duration-300',
|
|
isCollapsed ? 'w-16' : 'w-56',
|
|
)}
|
|
>
|
|
<div className="h-14 flex items-center justify-between px-4 border-b">
|
|
{!isCollapsed && <img src="/logo_brainwind.png" alt="BrainWind" className="h-8 w-auto object-contain dark:brightness-[3] dark:grayscale" />}
|
|
<div className="flex items-center gap-1 shrink-0 ml-auto">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={toggleTheme}
|
|
className="h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full"
|
|
title={effectiveTheme === 'dark' ? 'Mudar para tema claro' : 'Mudar para tema escuro'}
|
|
>
|
|
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
title={isCollapsed ? t('nav_expand') : t('nav_collapse')}
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
|
|
{navItems.map((item) => {
|
|
const isActive = location.pathname === item.path;
|
|
const label = t(item.labelKey);
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors text-sm',
|
|
isActive
|
|
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
|
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
|
isCollapsed && 'justify-center px-0',
|
|
)}
|
|
title={isCollapsed ? label : undefined}
|
|
>
|
|
{item.icon}
|
|
{!isCollapsed && <span>{label}</span>}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
<div className="border-t p-2 flex flex-col gap-1 items-center justify-center">
|
|
<GlobalWindSettingsModal
|
|
trigger={
|
|
<button
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full text-left cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
|
isCollapsed && 'justify-center px-0'
|
|
)}
|
|
title={isCollapsed ? 'Parâmetros do Vento' : undefined}
|
|
>
|
|
<Settings2 className="w-5 h-5 shrink-0" />
|
|
{!isCollapsed && <span>Parâmetros do Vento</span>}
|
|
</button>
|
|
}
|
|
/>
|
|
<Link
|
|
to="/settings?tab=gerenciador"
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
|
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'gerenciador'
|
|
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
|
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
|
isCollapsed && 'justify-center px-0'
|
|
)}
|
|
title={isCollapsed ? 'Arquivos' : undefined}
|
|
>
|
|
<FolderOpen className="w-5 h-5 shrink-0" />
|
|
{!isCollapsed && <span>Arquivos</span>}
|
|
</Link>
|
|
|
|
<Link
|
|
to="/settings"
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
|
location.pathname === '/settings' && (!location.search || location.search.includes('tab=config') || location.search.includes('tab=testes'))
|
|
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
|
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
|
isCollapsed && 'justify-center px-0'
|
|
)}
|
|
title={isCollapsed ? t('nav_settings') : undefined}
|
|
>
|
|
<Settings className="w-5 h-5 shrink-0" />
|
|
{!isCollapsed && <span>Configurações</span>}
|
|
</Link>
|
|
|
|
<Link
|
|
to="/settings?tab=glossary"
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
|
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'glossary'
|
|
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
|
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
|
isCollapsed && 'justify-center px-0'
|
|
)}
|
|
title={isCollapsed ? 'Glossário' : undefined}
|
|
>
|
|
<BookOpen className="w-5 h-5 shrink-0" />
|
|
{!isCollapsed && <span>Glossário</span>}
|
|
</Link>
|
|
|
|
<TermsOfUseDialog isCollapsed={isCollapsed} />
|
|
|
|
<Link
|
|
to="/certificado"
|
|
className={cn(
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer mt-1 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-950/50',
|
|
location.pathname === '/certificado' && 'bg-emerald-100 dark:bg-emerald-900/50 font-medium shadow-sm',
|
|
isCollapsed && 'justify-center px-0'
|
|
)}
|
|
title={isCollapsed ? 'Certificado' : undefined}
|
|
>
|
|
<ShieldCheck className="w-5 h-5 shrink-0" />
|
|
{!isCollapsed && <span>Certificado de Fidelidade</span>}
|
|
</Link>
|
|
</div>
|
|
</aside>
|
|
|
|
<main className="flex-1 flex flex-col h-full overflow-hidden pb-16 md:pb-0">
|
|
<header className="h-14 border-b bg-card flex items-center justify-between px-4 md:hidden">
|
|
<img src="/logo_brainwind.png" alt="BrainWind" className="h-7 w-auto object-contain dark:brightness-[3] dark:grayscale" />
|
|
<div className="flex items-center gap-1.5">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={toggleTheme}
|
|
className="h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full"
|
|
title={effectiveTheme === 'dark' ? 'Tema Claro' : 'Tema Escuro'}
|
|
>
|
|
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn(
|
|
'h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
|
|
(location.pathname === '/settings' || location.pathname === '/arquivos') && 'text-primary border-primary/20 bg-primary/5'
|
|
)}
|
|
>
|
|
<MoreVertical className="h-4.5 w-4.5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<GlobalWindSettingsModal trigger={
|
|
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
|
<Settings2 className="w-4 h-4" /> Parâmetros do Vento
|
|
</DropdownMenuItem>
|
|
} />
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/settings?tab=gerenciador" className="cursor-pointer gap-2">
|
|
<FolderOpen className="w-4 h-4" /> Arquivos
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/settings" className="cursor-pointer gap-2">
|
|
<Settings className="w-4 h-4" /> Configurações
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/settings?tab=glossary" className="cursor-pointer gap-2">
|
|
<BookOpen className="w-4 h-4" /> Glossário
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<TermsOfUseDialog trigger={
|
|
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
|
<Info className="w-4 h-4" /> Termos de Uso
|
|
</DropdownMenuItem>
|
|
} />
|
|
<DropdownMenuItem asChild>
|
|
<Link to="/certificado" className="cursor-pointer gap-2 text-emerald-600 dark:text-emerald-400 focus:text-emerald-700 dark:focus:text-emerald-300">
|
|
<ShieldCheck className="w-4 h-4" /> Certificado
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
<div className="flex-1 overflow-auto">{children}</div>
|
|
</main>
|
|
|
|
<nav className="md:hidden fixed bottom-0 left-0 right-0 h-16 bg-card border-t flex items-center z-50 pb-safe overflow-hidden">
|
|
{/* Início (Fixo na esquerda) */}
|
|
{(() => {
|
|
const homeItem = navItems[0];
|
|
const isActive = location.pathname === homeItem.path;
|
|
const label = t(homeItem.labelKey);
|
|
return (
|
|
<Link
|
|
key={homeItem.path}
|
|
to={homeItem.path}
|
|
className={cn(
|
|
'flex flex-col items-center justify-center h-full px-3 text-xs transition-colors shrink-0 border-r bg-card z-20 shadow-[4px_0_8px_-4px_rgba(0,0,0,0.15)]',
|
|
isActive ? 'text-primary font-medium' : 'text-muted-foreground',
|
|
)}
|
|
>
|
|
<div className={cn('p-1 rounded-full transition-colors', isActive && 'bg-primary/10')}>
|
|
{homeItem.icon}
|
|
</div>
|
|
<span className="scale-90 text-[10px]">{label}</span>
|
|
</Link>
|
|
);
|
|
})()}
|
|
|
|
{/* Demais itens (Rolagem horizontal) */}
|
|
<div className="flex-1 flex items-center h-full overflow-x-auto no-scrollbar py-1 gap-1">
|
|
{navItems.slice(1).map((item) => {
|
|
const isActive = location.pathname === item.path;
|
|
const label = t(item.labelKey);
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={cn(
|
|
'flex flex-col items-center justify-center h-full px-3 text-xs transition-colors shrink-0 min-w-[4.25rem]',
|
|
isActive ? 'text-primary font-medium' : 'text-muted-foreground',
|
|
)}
|
|
>
|
|
<div className={cn('p-1 rounded-full transition-colors', isActive && 'bg-primary/10')}>
|
|
{item.icon}
|
|
</div>
|
|
<span className="scale-90 text-[10px] whitespace-nowrap">{label}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HomeMock() {
|
|
const { t } = useI18n();
|
|
const modules = [
|
|
{ to: '/galpao', icon: Warehouse, label: 'Galpão Retangular', desc: 'Paredes A/B/C/D, telhados E-J, excentricidade, atrito, alta turbulência.' },
|
|
{ to: '/cilindro', icon: Cylinder, label: 'Cilindro Vertical', desc: 'Silos, reservatórios, chaminés. Cpe por ângulo (Tab. 13), Reynolds.' },
|
|
{ to: '/abobada', icon: Church, label: 'Abóbada Cilíndrica', desc: 'Coberturas curvas em arco. Tab. 15-20, 6 zonas.' },
|
|
{ to: '/cupula', icon: CircleDot, label: 'Cúpula', desc: 'Sobre terreno (Tab. 21) ou parede cilíndrica (Tab. 22).' },
|
|
{ to: '/muros', icon: Square, label: 'Muros e Placas', desc: 'Cf para vento perpendicular e oblíquo (Tab. 23).' },
|
|
{ to: '/cobertura-isolada', icon: Layers, label: 'Coberturas Isoladas', desc: 'Uma ou duas águas, abas perpendiculares (Tab. 24-25).' },
|
|
{ to: '/barras', icon: BarChart3, label: 'Barras Prismáticas', desc: 'Faces planas (Tab. 26) ou circulares (Tab. 27).' },
|
|
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
|
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
|
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
|
];
|
|
|
|
return (
|
|
<div className="p-8 max-w-6xl mx-auto space-y-8">
|
|
<header className="text-center space-y-4 flex flex-col items-center mt-4 mb-6">
|
|
<div className="relative inline-block">
|
|
<img src="/logo_brainwind.png" alt="BrainWind" className="h-20 md:h-28 w-auto object-contain dark:drop-shadow-[0_0_15px_rgba(255,255,255,0.4)]" />
|
|
<span className="absolute -bottom-1 -right-2 md:-right-6 bg-primary text-primary-foreground text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full shadow-sm border border-primary-foreground/20">v1.0</span>
|
|
</div>
|
|
<p className="text-muted-foreground text-lg">
|
|
{t('app_subtitle')}
|
|
</p>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{modules.map((m) => (
|
|
<Link key={m.to} to={m.to} className="block p-6 rounded-xl border bg-card hover:shadow-lg hover:border-primary transition-all">
|
|
<m.icon className="w-8 h-8 text-primary mb-3" />
|
|
<h2 className="font-semibold text-lg mb-1">{m.label}</h2>
|
|
<p className="text-sm text-muted-foreground">{m.desc}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
<div className="rounded-lg border bg-muted/40 p-4 text-sm text-muted-foreground">
|
|
<p className="font-medium text-foreground mb-2">{t('home_full_coverage')}</p>
|
|
<ul className="grid grid-cols-2 md:grid-cols-4 gap-1 text-xs">
|
|
<li>✓ Sec. 5 — V₀, S₁, S₂, S₃</li>
|
|
<li>✓ Sec. 6.1 — Paralelepipédicas</li>
|
|
<li>✓ Sec. 6.2 — Cilindros, abóbadas, cúpulas</li>
|
|
<li>✓ Sec. 6.3 — Pressão interna (Cpi)</li>
|
|
<li>✓ Sec. 6.4 — Vizinhança</li>
|
|
<li>✓ Sec. 7 — Muros, coberturas isoladas</li>
|
|
<li>✓ Sec. 8 — Barras e reticulados</li>
|
|
<li>✓ Sec. 9 — Efeitos dinâmicos</li>
|
|
<li>✓ Sec. 10 — Vórtices</li>
|
|
<li>✓ Sec. 11 — Pontes</li>
|
|
<li>✓ Anexo A — S₂(qualquer t)</li>
|
|
<li>✓ Anexo B — S₃(Pₘ, vida útil)</li>
|
|
<li>✓ Anexo C — 49 estações meteorológicas</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<ThemeProvider>
|
|
<TooltipProvider>
|
|
<BrowserRouter>
|
|
<AppLayout>
|
|
<Routes>
|
|
<Route path="/" element={<HomeMock />} />
|
|
<Route path="/galpao" element={<GalpaoModule />} />
|
|
<Route path="/cilindro" element={<CylinderModule />} />
|
|
<Route path="/abobada" element={<VaultModule />} />
|
|
<Route path="/cupula" element={<DomeModule />} />
|
|
<Route path="/muros" element={<SignModule />} />
|
|
<Route path="/cobertura-isolada" element={<IsolatedRoofModule />} />
|
|
<Route path="/barras" element={<BarSelectorModule />} />
|
|
<Route path="/pontes" element={<BridgeModule />} />
|
|
<Route path="/torre" element={<TowerModule />} />
|
|
<Route path="/piperack" element={<PiperackModule />} />
|
|
<Route path="/dinamica" element={<DynamicsModule />} />
|
|
<Route path="/settings" element={<SettingsModule />} />
|
|
<Route path="/certificado" element={<CertificatePage />} />
|
|
</Routes>
|
|
</AppLayout>
|
|
</BrowserRouter>
|
|
</TooltipProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export default App; |