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>
86 lines
2.4 KiB
TypeScript
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,
|
|
};
|
|
}
|