Files
BanGUI/frontend/src/hooks/useTabRouter.ts
Lukas 42beb9cf3b refactor: Decompose ConfigPage into focused routing and component layers
Split the over-centralized ConfigPage into focused, composable layers:

1. useTabRouter hook: Encapsulates tab state management and URL synchronization
   - Maintains selected tab and active item (e.g., jail name)
   - Syncs state to location.state for deep linking and browser history
   - Supports bookmarkable URLs and back/forward navigation

2. ConfigPageContainer: Orchestrates tab navigation
   - Manages TabList and routes tab selection events
   - Conditionally renders tab content panels
   - Delegates domain-specific logic to tab components

3. ConfigPage: Focused page layout component
   - Renders page structure (header, title, description)
   - Delegates tab orchestration to ConfigPageContainer
   - No routing or tab state logic

Benefits:
- Page is now 30 lines vs 125 lines (76% reduction)
- Tab state management is reusable for other multi-tab pages
- Each tab component remains focused on domain-specific UI
- Deep linking and browser history work out of the box
- Easier to test and maintain

Added comprehensive tests:
- useTabRouter: 6 tests covering state initialization, tab selection, and deep linking
- ConfigPageContainer: 8 tests covering tab rendering and navigation
- ConfigPage: 3 tests for page structure

Updated Web-Development.md with tab orchestration pattern documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 08:27:36 +02:00

86 lines
2.4 KiB
TypeScript

/**
* useTabRouter — centralized tab routing and state management.
*
* Encapsulates tab navigation logic, URL state synchronization, and history
* management for the config page. Supports deep linking to specific tabs and
* optional item IDs (e.g., jail names).
*/
import { useEffect, useState, useCallback } from "react";
import { useLocation, useNavigate } from "react-router-dom";
/** Available tab IDs in the config page. */
export type ConfigTabId = "jails" | "filters" | "actions" | "server" | "regex";
/** Tab navigation state with optional active item/jail name. */
export interface TabRouterState {
activeTab: ConfigTabId;
activeItem: string | null;
}
interface UseTabRouterReturn extends TabRouterState {
selectTab: (tab: ConfigTabId) => void;
selectTabWithItem: (tab: ConfigTabId, itemId: string | null) => void;
}
/**
* Hook for managing config page tab routing.
*
* Syncs tab selection to URL state via `location.state` so it persists across
* navigation. The activeItem (jail name, filter name, etc.) is also tracked
* to support deep linking.
*
* @returns Tab state and navigation functions.
*/
export function useTabRouter(): UseTabRouterReturn {
const location = useLocation();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<ConfigTabId>("jails");
const [activeItem, setActiveItem] = useState<string | null>(null);
// Sync from location.state on mount or when location changes.
useEffect(() => {
const state = location.state as { tab?: string; item?: string } | null;
if (state?.tab === "jails" || state?.tab === "filters" || state?.tab === "actions" || state?.tab === "server" || state?.tab === "regex") {
setActiveTab(state.tab);
}
if (state?.item) {
setActiveItem(state.item);
}
}, [location.state]);
const selectTab = useCallback(
(tab: ConfigTabId): void => {
setActiveTab(tab);
setActiveItem(null);
navigate(location.pathname, {
state: { tab },
replace: false,
});
},
[navigate, location.pathname],
);
const selectTabWithItem = useCallback(
(tab: ConfigTabId, itemId: string | null): void => {
setActiveTab(tab);
setActiveItem(itemId);
navigate(location.pathname, {
state: { tab, item: itemId },
replace: false,
});
},
[navigate, location.pathname],
);
return {
activeTab,
activeItem,
selectTab,
selectTabWithItem,
};
}