Merge branch 'main' into feature/multiplesections

This commit is contained in:
Julian Appel 2026-07-31 09:00:39 +02:00
commit f4bfef9071
6 changed files with 568 additions and 127 deletions

View 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>
);
}

View 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- &amp; 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>
);
}