fix(deploy): reuse shared server n8n
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { BrandMark, Icon, type IconName } from "./Icons";
|
||||
@@ -25,16 +25,70 @@ const NAV_GROUPS: Array<{ label: string; items: Array<{ to: string; label: strin
|
||||
|
||||
const MOBILE_ITEMS = NAV_GROUPS.flatMap((group) => group.items).slice(0, 5);
|
||||
|
||||
const SEARCH_DESTINATIONS = [
|
||||
{ to: "/dashboard", terms: ["overview", "dashboard", "readiness"] },
|
||||
{ to: "/vehicles", terms: ["fleet", "vehicle", "vehicles"] },
|
||||
{ to: "/bookings", terms: ["booking", "bookings", "rental"] },
|
||||
{ to: "/data-quality", terms: ["quality", "data quality", "issues"] },
|
||||
{ to: "/knowledge", terms: ["knowledge", "procedures"] },
|
||||
{ to: "/automation", terms: ["automation", "integrations", "systems", "n8n"] },
|
||||
{ to: "/audit", terms: ["audit", "history"] },
|
||||
];
|
||||
|
||||
export function Layout() {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchStatus, setSearchStatus] = useState("");
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function focusGlobalSearch(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
searchInput.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", focusGlobalSearch);
|
||||
return () => window.removeEventListener("keydown", focusGlobalSearch);
|
||||
}, []);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
function handleSearch(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const query = searchQuery.trim();
|
||||
if (!query) {
|
||||
setSearchStatus("Enter a section or a vehicle, booking or issue reference.");
|
||||
return;
|
||||
}
|
||||
const publicRef = query.toUpperCase();
|
||||
let destination: string | undefined;
|
||||
|
||||
if (/^MO-\d+$/.test(publicRef)) destination = `/vehicles/${publicRef}`;
|
||||
else if (/^BK-[A-Z0-9-]+$/.test(publicRef)) destination = `/bookings/${publicRef}`;
|
||||
else if (/^DQ-[A-Z0-9-]+$/.test(publicRef)) destination = `/data-quality/${publicRef}`;
|
||||
else {
|
||||
const normalized = query.toLowerCase();
|
||||
destination = SEARCH_DESTINATIONS.find(({ terms }) =>
|
||||
terms.some((term) => term.includes(normalized) || normalized.includes(term)),
|
||||
)?.to;
|
||||
}
|
||||
|
||||
if (destination) {
|
||||
setSearchStatus("");
|
||||
navigate(destination);
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchStatus(`No destination found for ${query}. Try a vehicle, booking or issue reference.`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<a className="skip-link" href="#main-content">Skip to main content</a>
|
||||
@@ -74,12 +128,24 @@ export function Layout() {
|
||||
<button className="icon-button mobile-menu" type="button" onClick={() => setMobileOpen(true)} aria-label="Open navigation">
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
<label className="global-search">
|
||||
<form className="global-search" role="search" onSubmit={handleSearch}>
|
||||
<Icon name="search" />
|
||||
<span className="visually-hidden">Search MobilityOps</span>
|
||||
<input type="search" placeholder="Search fleet, booking or customer…" />
|
||||
<label className="visually-hidden" htmlFor="global-search-input">Search MobilityOps</label>
|
||||
<input
|
||||
id="global-search-input"
|
||||
ref={searchInput}
|
||||
type="search"
|
||||
value={searchQuery}
|
||||
placeholder="Search fleet, booking or section…"
|
||||
aria-describedby="global-search-status"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
setSearchStatus("");
|
||||
}}
|
||||
/>
|
||||
<kbd>Ctrl K</kbd>
|
||||
</label>
|
||||
<span id="global-search-status" className="visually-hidden" aria-live="polite">{searchStatus}</span>
|
||||
</form>
|
||||
<div className="topbar-meta">
|
||||
<span className="timezone"><Icon name="clock" /> Europe/Brussels</span>
|
||||
{user && (
|
||||
|
||||
Reference in New Issue
Block a user