Files
platform/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx
T
2026-02-23 22:52:27 +00:00

158 lines
5.6 KiB
TypeScript

import type { ComponentType, ReactNode } from 'react';
import { useState } from 'react';
import type { LucideIcon } from 'lucide-react';
import { useGlobal } from 'hooks/useGlobal';
import { Input } from '@/components/ui/input';
export type SettingsSection = {
key: string;
icon: LucideIcon;
title: string;
description: string;
content: ReactNode;
};
export type SettingsSectionGroup = {
label: string;
icon: LucideIcon;
sections: SettingsSection[];
};
type SettingsSidebarProps = {
globalKey: string;
icon: LucideIcon;
label: string;
sections: SettingsSection[];
groups?: SettingsSectionGroup[];
hideHeader?: boolean;
};
const SectionButton = ({
section,
isActive,
onClick,
}: {
section: SettingsSection;
isActive: boolean;
onClick: () => void;
}) => (
<button
key={section.key}
onClick={onClick}
className={`flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${
isActive ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground'
}`}
>
<section.icon className={`h-3.5 w-3.5 shrink-0 mt-0.5 ${isActive ? 'text-duck-teal' : 'text-duck-dark/40 dark:text-foreground/40'}`} />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">{section.title}</div>
<div className="text-xs text-duck-dark/40 dark:text-foreground/40 truncate">{section.description}</div>
</div>
</button>
);
export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections, groups, hideHeader }: SettingsSidebarProps) => {
const allSections = groups ? groups.flatMap((g) => g.sections) : sections;
const [selectedKey, setSelectedKey] = useGlobal<string | null>(globalKey, allSections[0]?.key ?? null);
const [search, setSearch] = useState('');
const query = search.toLowerCase();
const matchesSearch = (s: SettingsSection) =>
s.title.toLowerCase().includes(query) || s.description.toLowerCase().includes(query);
return (
<div className="flex flex-col h-full overflow-y-auto">
{!hideHeader && (
<div className="p-3 pb-2">
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
<Icon className="h-4 w-4" />
{label}
</div>
</div>
)}
<div className="px-3 pb-2">
<Input placeholder="Search..." value={search} onChange={(ev) => setSearch(ev.target.value)} className="h-8 text-xs" />
</div>
<div className="flex flex-col gap-0.5 px-3 overflow-y-auto flex-1">
{groups
? groups.map((group) => {
const filtered = group.sections.filter(matchesSearch);
if (filtered.length === 0) return null;
return (
<div key={group.label}>
<div className="flex items-center gap-2 px-3 pt-3 pb-1">
<group.icon className="h-3 w-3 text-duck-dark/30 dark:text-foreground/30" />
<span className="text-[11px] font-semibold uppercase tracking-wider text-duck-dark/30 dark:text-foreground/30">
{group.label}
</span>
</div>
{filtered.map((s) => (
<SectionButton
key={s.key}
section={s}
isActive={selectedKey === s.key}
onClick={() => setSelectedKey(s.key)}
/>
))}
</div>
);
})
: sections.filter(matchesSearch).map((s) => (
<SectionButton
key={s.key}
section={s}
isActive={selectedKey === s.key}
onClick={() => setSelectedKey(s.key)}
/>
))}
</div>
</div>
);
};
type SettingsContentProps = {
globalKey: string;
sections: SettingsSection[];
};
export const SettingsContent = ({ globalKey, sections }: SettingsContentProps) => {
const [selectedKey] = useGlobal<string | null>(globalKey, sections[0]?.key ?? null);
const section = sections.find((s) => s.key === selectedKey);
if (!section) {
return (
<div className="h-full flex items-center justify-center">
<p className="text-sm text-duck-dark/30 dark:text-foreground/30">Select a section</p>
</div>
);
}
return (
<div className="h-full overflow-y-auto p-6">
<div className="flex items-center gap-2.5 mb-1">
<section.icon className="h-4.5 w-4.5 text-duck-teal shrink-0" />
<h2 className="text-lg font-bold text-duck-dark dark:text-foreground">{section.title}</h2>
</div>
<p className="text-sm text-duck-dark/50 dark:text-foreground/50 mb-6">{section.description}</p>
<div>{section.content}</div>
</div>
);
};
type CreateSettingsPanelParams = {
globalKey: string;
sidebarIcon: LucideIcon;
sidebarLabel: string;
sections?: SettingsSection[];
groups?: SettingsSectionGroup[];
};
export const createSettingsPanelComponents = ({ globalKey, sidebarIcon, sidebarLabel, sections = [], groups }: CreateSettingsPanelParams) => {
const allSections = groups ? groups.flatMap((g) => g.sections) : sections;
const Sidebar: ComponentType = () => (
<SettingsSidebar globalKey={globalKey} icon={sidebarIcon} label={sidebarLabel} sections={allSections} groups={groups} />
);
const Content: ComponentType = () => <SettingsContent globalKey={globalKey} sections={allSections} />;
return { Sidebar, Content };
};