feat(SettingsPage): implement sidebar navigation with search functionality for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import api from './lib/api.js';
|
||||
import {
|
||||
IconSettings,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
IconCloud,
|
||||
IconChartPie,
|
||||
IconWorld,
|
||||
IconChevronDown,
|
||||
IconSearch,
|
||||
} from '@tabler/icons-react';
|
||||
import FormField from './components/FormField';
|
||||
import ErrorAlert from './components/ErrorAlert';
|
||||
@@ -19,7 +20,18 @@ import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
|
||||
* Страница «Настройки интерфейса» — отдельный раздел в стиле Tabler UI и UniFi:
|
||||
* коллапсируемые карточки-секции, иконки, чёткая структура.
|
||||
*/
|
||||
// Разделы бокового меню (как в Tabler / UniFi)
|
||||
const SIDEBAR_SECTIONS = [
|
||||
{ id: 'live-doh', title: 'BGP Live и DoH', icon: IconPlugConnected },
|
||||
{ id: 'network-as', title: 'Сеть и AS', icon: IconNetwork },
|
||||
{ id: 'ping', title: 'Пинг через MikroTik', icon: IconCloud },
|
||||
{ id: 'ping-services', title: 'Пинг на главной', icon: IconChartPie },
|
||||
{ id: 'ptr-zone', title: 'PTR зона', icon: IconWorld },
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -38,6 +50,11 @@ export default function SettingsPage() {
|
||||
const [pingServicesGatewayIp, setPingServicesGatewayIp] = useState('');
|
||||
const [pingServicesCacheSeconds, setPingServicesCacheSeconds] = useState('');
|
||||
const [serversList, setServersList] = useState([]);
|
||||
const [sidebarSearch, setSidebarSearch] = useState('');
|
||||
const [activeSection, setActiveSection] = useState(() => {
|
||||
const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || '';
|
||||
return SIDEBAR_SECTIONS.some((s) => s.id === hash) ? hash : SIDEBAR_SECTIONS[0].id;
|
||||
});
|
||||
|
||||
const routerServersForPing = useMemo(() => {
|
||||
return (serversList || []).filter(
|
||||
@@ -48,6 +65,26 @@ export default function SettingsPage() {
|
||||
);
|
||||
}, [serversList]);
|
||||
|
||||
const sidebarItems = useMemo(() => {
|
||||
const q = (sidebarSearch || '').trim().toLowerCase();
|
||||
if (!q) return SIDEBAR_SECTIONS;
|
||||
return SIDEBAR_SECTIONS.filter(
|
||||
(s) => s.title.toLowerCase().includes(q) || s.id.toLowerCase().includes(q)
|
||||
);
|
||||
}, [sidebarSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = (location.hash || '').slice(1);
|
||||
if (hash && SIDEBAR_SECTIONS.some((s) => s.id === hash)) {
|
||||
setActiveSection(hash);
|
||||
}
|
||||
}, [location.hash]);
|
||||
|
||||
const goToSection = (id) => {
|
||||
setActiveSection(id);
|
||||
navigate(`/settings#${id}`, { replace: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
@@ -173,19 +210,11 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Коллапсируемая карточка в стиле UniFi
|
||||
function SectionCard({ id, title, icon: Icon, defaultOpen = true, children }) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
// Карточка секции (контент без коллапса — разделы переключаются через боковое меню)
|
||||
function SectionCard({ id, title, icon: Icon, children }) {
|
||||
return (
|
||||
<div className="card mb-3">
|
||||
<div
|
||||
className="card-header d-flex align-items-center justify-content-between cursor-pointer py-3"
|
||||
role="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
aria-controls={`settings-section-${id}`}
|
||||
id={`settings-heading-${id}`}
|
||||
>
|
||||
<div className="card mb-0" data-section-id={id}>
|
||||
<div className="card-header py-3">
|
||||
<h3 className="card-title m-0 d-flex align-items-center">
|
||||
{Icon && (
|
||||
<span className="me-2 d-flex align-items-center text-muted">
|
||||
@@ -194,20 +223,8 @@ export default function SettingsPage() {
|
||||
)}
|
||||
{title}
|
||||
</h3>
|
||||
<IconChevronDown
|
||||
className={`icon ms-2 text-muted transition ${open ? 'rotate-180' : ''}`}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
className="card-body pt-0"
|
||||
id={`settings-section-${id}`}
|
||||
aria-labelledby={`settings-heading-${id}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -285,13 +302,56 @@ export default function SettingsPage() {
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
{/* Секция: BGP Live и DoH */}
|
||||
{/* Боковое меню в стиле Tabler / UniFi */}
|
||||
<div className="col-lg-3 col-xl-2 mb-4 mb-lg-0">
|
||||
<div className="card sticky-top" style={{ top: '1rem' }}>
|
||||
<div className="card-body py-2">
|
||||
<div className="input-icon mb-2">
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={18} className="text-muted" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск разделов"
|
||||
value={sidebarSearch}
|
||||
onChange={(e) => setSidebarSearch(e.target.value)}
|
||||
aria-label="Поиск разделов"
|
||||
/>
|
||||
</div>
|
||||
<nav className="nav flex-column">
|
||||
{sidebarItems.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isActive = activeSection === section.id;
|
||||
return (
|
||||
<button
|
||||
key={section.id}
|
||||
type="button"
|
||||
className={`nav-link d-flex align-items-center text-start border-0 rounded mb-1 ${isActive ? 'active bg-primary text-white' : ''}`}
|
||||
onClick={() => goToSection(section.id)}
|
||||
>
|
||||
<span className="me-2 d-flex">
|
||||
<Icon size={18} />
|
||||
</span>
|
||||
{section.title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
{sidebarItems.length === 0 && (
|
||||
<p className="text-muted small mb-0 mt-2">Нет подходящих разделов</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Контент выбранного раздела */}
|
||||
<div className="col-lg-9 col-xl-10">
|
||||
{activeSection === 'live-doh' && (
|
||||
<SectionCard
|
||||
id="live-doh"
|
||||
title="BGP Live и DNS (DoH)"
|
||||
icon={IconPlugConnected}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12">
|
||||
@@ -322,13 +382,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* Секция: Сеть и AS */}
|
||||
{activeSection === 'network-as' && (
|
||||
<SectionCard
|
||||
id="network-as"
|
||||
title="Сеть и AS"
|
||||
icon={IconNetwork}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
@@ -345,13 +405,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* Секция: Пинг (домен и кеш) */}
|
||||
{activeSection === 'ping' && (
|
||||
<SectionCard
|
||||
id="ping"
|
||||
title="Пинг через MikroTik"
|
||||
icon={IconCloud}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
@@ -381,13 +441,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* Секция: Пинг сервисов на главной */}
|
||||
{activeSection === 'ping-services' && (
|
||||
<SectionCard
|
||||
id="ping-services"
|
||||
title="Пинг сервисов на главной"
|
||||
icon={IconChartPie}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Источник пинга</label>
|
||||
@@ -456,13 +516,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* Секция: PTR зона */}
|
||||
{activeSection === 'ptr-zone' && (
|
||||
<SectionCard
|
||||
id="ptr-zone"
|
||||
title="Настройка PTR зоны"
|
||||
icon={IconWorld}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
@@ -491,6 +551,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user