feat: Add language and theme context providers to App component, implement global search functionality in Dashboard, and enhance ASNs, Domains, and IPRanges managers with drag-and-drop import feature and confirmation modals for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m48s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m48s
This commit is contained in:
+89
-15
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, createContext, useContext } from 'react';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import {
|
||||
IconBrandTabler,
|
||||
@@ -23,6 +23,50 @@ import CommunitiesManager from './CommunitiesManager';
|
||||
import Dashboard from './Dashboard';
|
||||
import './App.css';
|
||||
import axios from 'axios';
|
||||
|
||||
// --- Simple i18n (RU/EN) ---
|
||||
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
|
||||
|
||||
function LanguageProvider({ children }) {
|
||||
const [lang, setLang] = useState(() => localStorage.getItem('lang') || 'ru');
|
||||
useEffect(() => { localStorage.setItem('lang', lang); }, [lang]);
|
||||
const dict = {
|
||||
ru: {
|
||||
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
||||
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
||||
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
||||
light: 'Светлая', dark: 'Тёмная'
|
||||
},
|
||||
en: {
|
||||
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
||||
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
||||
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
||||
light: 'Light', dark: 'Dark'
|
||||
}
|
||||
};
|
||||
const t = (key) => (dict[lang] && key in dict[lang] ? dict[lang][key] : key);
|
||||
return (
|
||||
<LanguageContext.Provider value={{ lang, setLang, t }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Theme provider (light/dark) ---
|
||||
const ThemeContext = createContext({ theme: 'light', setTheme: () => {} });
|
||||
|
||||
function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
|
||||
useEffect(() => {
|
||||
localStorage.setItem('theme', theme);
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
}, [theme]);
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
@@ -35,13 +79,19 @@ import {
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<MainLayout />
|
||||
<LanguageProvider>
|
||||
<ThemeProvider>
|
||||
<MainLayout />
|
||||
</ThemeProvider>
|
||||
</LanguageProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function MainLayout() {
|
||||
const location = useLocation();
|
||||
const { lang, setLang, t } = useContext(LanguageContext);
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
// Навбар стал лаконичным: без неиспользуемых уведомлений/иконок
|
||||
|
||||
// Состояние для управления выпадающими меню
|
||||
@@ -88,38 +138,38 @@ function MainLayout() {
|
||||
const navCategories = [
|
||||
{
|
||||
id: 'home',
|
||||
title: 'Главная',
|
||||
title: t('home'),
|
||||
icon: IconHome,
|
||||
path: '/dashboard',
|
||||
single: true
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: 'Данные',
|
||||
title: t('data'),
|
||||
icon: IconDatabase,
|
||||
items: [
|
||||
{ id: 'domains', title: 'Домены', path: '/domains', icon: IconWorld },
|
||||
{ id: 'ip-ranges', title: 'IP-диапазоны', path: '/ip-ranges', icon: IconNetwork },
|
||||
{ id: 'asns', title: 'AS', path: '/asns', icon: IconNetwork },
|
||||
{ id: 'communities', title: 'Community', path: '/communities', icon: IconFilter }
|
||||
{ id: 'domains', title: t('domains'), path: '/domains', icon: IconWorld },
|
||||
{ id: 'ip-ranges', title: t('ipRanges'), path: '/ip-ranges', icon: IconNetwork },
|
||||
{ id: 'asns', title: t('asns'), path: '/asns', icon: IconNetwork },
|
||||
{ id: 'communities', title: t('communities'), path: '/communities', icon: IconFilter }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'management',
|
||||
title: 'Управление',
|
||||
title: t('management'),
|
||||
icon: IconServer,
|
||||
items: [
|
||||
{ id: 'servers', title: 'Серверы', path: '/servers', icon: IconServer },
|
||||
{ id: 'filters', title: 'Фильтры', path: '/filters', icon: IconFilter },
|
||||
{ id: 'billing', title: 'Биллинг', path: '/billing', icon: IconCreditCard }
|
||||
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
|
||||
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
|
||||
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Инструменты',
|
||||
title: t('tools'),
|
||||
icon: IconSettings,
|
||||
items: [
|
||||
{ id: 'auto-urls', title: 'Авто URL', path: '/auto-urls', icon: IconDownload }
|
||||
{ id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload }
|
||||
]
|
||||
},
|
||||
// Убрали неиспользуемые/неработающие разделы
|
||||
@@ -211,7 +261,31 @@ function MainLayout() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая часть навбара убрана по требованиям лаконичности */}
|
||||
{/* Правая часть навбара: язык и тема */}
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="nav-item me-2">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={lang}
|
||||
onChange={(e) => setLang(e.target.value)}
|
||||
aria-label="language"
|
||||
>
|
||||
<option value="ru">RU</option>
|
||||
<option value="en">EN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="nav-item">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
aria-label="theme"
|
||||
>
|
||||
<option value="light">{t('light')}</option>
|
||||
<option value="dark">{t('dark')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="container-xl mt-4">
|
||||
|
||||
Reference in New Issue
Block a user