Files
papercrate/frontend/src/settings/SettingsModal.tsx
T
2025-11-18 13:47:43 +01:00

128 lines
3.6 KiB
TypeScript

import React, {
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import PanelHeader from '../ui/PanelHeader';
import { CloseIcon } from '../ui/icons';
import { DEFAULT_SETTINGS_SECTIONS } from './sections';
export interface SettingsSectionConfig {
id: string;
label: string;
component?: React.ComponentType<any>;
render?: (props: Record<string, unknown>) => ReactNode;
}
interface SettingsModalProps {
open?: boolean;
onClose?: () => void;
sections?: SettingsSectionConfig[];
defaultSectionId?: string;
[key: string]: unknown;
}
const SettingsModal: React.FC<SettingsModalProps> = ({
open = false,
onClose,
sections,
defaultSectionId,
...sectionProps
}) => {
const sectionList: SettingsSectionConfig[] = useMemo(() => {
if (Array.isArray(sections) && sections.length) {
return sections;
}
return DEFAULT_SETTINGS_SECTIONS as SettingsSectionConfig[];
}, [sections]);
const firstSectionId = sectionList[0]?.id ?? null;
const resolvedDefaultSection = defaultSectionId || firstSectionId;
const [activeSection, setActiveSection] = useState(resolvedDefaultSection);
useEffect(() => {
if (!open) {
setActiveSection(resolvedDefaultSection);
return;
}
const hasActiveSection = sectionList.some((section) => section.id === activeSection);
if (!hasActiveSection) {
setActiveSection(resolvedDefaultSection);
}
}, [open, sectionList, resolvedDefaultSection, activeSection]);
const handleBackdropClick = useCallback(() => {
onClose?.();
}, [onClose]);
const handleInnerClick = useCallback((event) => {
event.stopPropagation();
}, []);
if (!open) {
return null;
}
const activeSectionConfig = sectionList.find((section) => section.id === activeSection);
let sectionContent = null;
if (activeSectionConfig) {
if (activeSectionConfig.component) {
const SectionComponent = activeSectionConfig.component;
sectionContent = <SectionComponent {...sectionProps} />;
} else if (activeSectionConfig.render) {
sectionContent = activeSectionConfig.render(sectionProps);
}
}
return (
<div className="modal-backdrop" role="presentation" onClick={handleBackdropClick}>
<div
className="modal modal--panel settings-modal"
role="dialog"
aria-modal="true"
aria-labelledby="settings-modal-title"
onClick={handleInnerClick}
>
<PanelHeader
className="panel-modal__header"
title="Settings"
titleTag="h3"
titleProps={{ id: 'settings-modal-title' }}
actions={(
<button type="button" className="icon-button" onClick={onClose} aria-label="Close">
<CloseIcon size={16} />
</button>
)}
/>
<div className="settings-modal__body">
<nav className="settings-modal__sidebar" aria-label="Settings sections">
<ul>
{sectionList.map((section) => (
<li key={section.id}>
<button
type="button"
className={section.id === activeSection ? 'active' : ''}
onClick={() => setActiveSection(section.id)}
>
{section.label}
</button>
</li>
))}
</ul>
</nav>
<div className="settings-modal__content">
{sectionContent || (
<p>Select a settings section.</p>
)}
</div>
</div>
</div>
</div>
);
};
export default SettingsModal;