forked from jappel/leistungsbilanz-ts
Apply Petrol/Volt/Kupfer design system and project sidebar
Bring the Econsult-family design tokens (see design-system-econsult.md) into leistungsbilanz-ts and add a persistent sidebar for the projects area, mirroring elt-planung-suite / Einsatz-Orga. The sidebar lists all projects and, inside a project, jumps to its sections; the tree editor stays sidebar-free since it needs the full width. The projects overview page is decluttered to a single stacked column (create/import/global devices) now that the project list lives in the sidebar.
This commit is contained in:
parent
cfd4778305
commit
704f4df2a8
6 changed files with 568 additions and 127 deletions
22
src/frontend/components/AppShell.tsx
Normal file
22
src/frontend/components/AppShell.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
const PROJECT_AREA_PATTERN = /^\/projects(\/[^/]+)?$/;
|
||||
|
||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const showSidebar = PROJECT_AREA_PATTERN.test(pathname ?? "");
|
||||
|
||||
if (!showSidebar) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Sidebar />
|
||||
<div className="app-shell-content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
121
src/frontend/components/Sidebar.tsx
Normal file
121
src/frontend/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { listProjects } from "../utils/api";
|
||||
import type { ProjectDto } from "../types";
|
||||
|
||||
const PROJECT_SECTIONS = [
|
||||
{ id: "verlauf", label: "Verlauf", icon: "↺" },
|
||||
{ id: "verteilungen", label: "Verteilungen", icon: "⌁" },
|
||||
{ id: "etagen", label: "Etagen", icon: "▥" },
|
||||
{ id: "raeume", label: "Räume", icon: "⌂" },
|
||||
{ id: "projektgeraete", label: "Projektgeräte", icon: "⚙" },
|
||||
] as const;
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const projectMatch = pathname?.match(/^\/projects\/([^/]+)$/);
|
||||
const activeProjectId = projectMatch ? projectMatch[1] : null;
|
||||
|
||||
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function refreshProjects() {
|
||||
listProjects()
|
||||
.then(setProjects)
|
||||
.catch(() => setProjects([]));
|
||||
}
|
||||
|
||||
refreshProjects();
|
||||
window.addEventListener("leistungsbilanz:projects-changed", refreshProjects);
|
||||
return () =>
|
||||
window.removeEventListener("leistungsbilanz:projects-changed", refreshProjects);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeProjectId) {
|
||||
setActiveSection(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = PROJECT_SECTIONS.map((section) =>
|
||||
document.getElementById(section.id)
|
||||
).filter((element): element is HTMLElement => element !== null);
|
||||
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
if (visible[0]) {
|
||||
setActiveSection(visible[0].target.id);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-20% 0px -70% 0px", threshold: 0 }
|
||||
);
|
||||
|
||||
elements.forEach((element) => observer.observe(element));
|
||||
return () => observer.disconnect();
|
||||
}, [activeProjectId]);
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-wordmark">Leistungsbilanz</div>
|
||||
<div className="sidebar-bar" />
|
||||
<div className="sidebar-tag">Stromkreis- & Lastberechnung</div>
|
||||
</div>
|
||||
<nav className="sidebar-nav">
|
||||
<Link
|
||||
href="/projects"
|
||||
className={pathname === "/projects" ? "sidebar-link active" : "sidebar-link"}
|
||||
>
|
||||
<span className="sidebar-icon">▦</span>
|
||||
Alle Projekte
|
||||
</Link>
|
||||
|
||||
<div className="sidebar-section-label">Projekte</div>
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/projects/${project.id}`}
|
||||
className={
|
||||
project.id === activeProjectId ? "sidebar-link active" : "sidebar-link"
|
||||
}
|
||||
>
|
||||
<span className="sidebar-icon">▣</span>
|
||||
{project.name}
|
||||
</Link>
|
||||
))}
|
||||
{!projects.length ? (
|
||||
<div className="sidebar-section-label" style={{ opacity: 0.6 }}>
|
||||
Noch keine Projekte
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeProjectId && (
|
||||
<>
|
||||
<div className="sidebar-section-label">In diesem Projekt</div>
|
||||
{PROJECT_SECTIONS.map(({ id, label, icon }) => (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={activeSection === id ? "sidebar-link active" : "sidebar-link"}
|
||||
>
|
||||
<span className="sidebar-icon">{icon}</span>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue