mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
feat(ui): introduce shared PageSidebar for Settings and Admin
Replaces the inline tab bar on SettingsPage and AdminPage with a responsive sidebar layout (left nav on desktop, hamburger drawer on mobile). Each tab gets a lucide-react icon for quick scanning. Both pages drop max-w-6xl so the panel fills the viewport.
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import { Menu, X, type LucideIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export interface PageSidebarTab {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon: LucideIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageSidebarProps {
|
||||||
|
/** Uppercase label shown above the tab list, e.g. "SETTINGS". */
|
||||||
|
sidebarLabel: string
|
||||||
|
tabs: PageSidebarTab[]
|
||||||
|
activeTab: string
|
||||||
|
onTabChange: (id: string) => void
|
||||||
|
children: React.ReactNode
|
||||||
|
/** Small text at the very bottom of the sidebar (e.g. "v3.0 · self-hosted"). */
|
||||||
|
footer?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Left-sidebar + right-panel layout used by the Settings and Admin pages.
|
||||||
|
*
|
||||||
|
* Desktop (>=1024px): sidebar is always visible at 260px; panel fills rest.
|
||||||
|
* Mobile: sidebar collapses behind a hamburger at the top of the panel; tap
|
||||||
|
* the hamburger to slide the sidebar in as an overlay, tap a tab to close.
|
||||||
|
*/
|
||||||
|
export default function PageSidebar({
|
||||||
|
sidebarLabel,
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
}: PageSidebarProps): React.ReactElement {
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
|
const activeLabel = tabs.find(t => t.id === activeTab)?.label ?? ''
|
||||||
|
|
||||||
|
// Close the mobile drawer on Escape or on outside click.
|
||||||
|
const drawerRef = useRef<HTMLDivElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mobileOpen) return
|
||||||
|
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setMobileOpen(false) }
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [mobileOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-2xl overflow-hidden flex flex-col lg:flex-row relative"
|
||||||
|
style={{
|
||||||
|
background: 'var(--bg-card)',
|
||||||
|
border: '1px solid var(--border-primary)',
|
||||||
|
minHeight: 'min(820px, calc(100vh - var(--nav-h) - 120px))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mobile top bar with hamburger */}
|
||||||
|
<div
|
||||||
|
className="lg:hidden flex items-center justify-between px-4 py-3 border-b"
|
||||||
|
style={{ borderColor: 'var(--border-primary)' }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(true)}
|
||||||
|
className="w-9 h-9 rounded-lg flex items-center justify-center transition-colors hover:bg-[var(--bg-hover)]"
|
||||||
|
aria-label="Open navigation"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
<Menu size={18} />
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{activeLabel}
|
||||||
|
</div>
|
||||||
|
<div className="w-9" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop sidebar (always visible on lg) */}
|
||||||
|
<aside
|
||||||
|
className="hidden lg:flex flex-col shrink-0 relative"
|
||||||
|
style={{
|
||||||
|
width: 260,
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
borderRight: '1px solid var(--border-primary)',
|
||||||
|
padding: '24px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SidebarInner
|
||||||
|
sidebarLabel={sidebarLabel}
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
footer={footer}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Mobile drawer */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="lg:hidden fixed inset-0 z-40"
|
||||||
|
style={{ background: 'rgba(0,0,0,0.35)' }}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
<aside
|
||||||
|
ref={drawerRef}
|
||||||
|
className="lg:hidden fixed top-0 left-0 bottom-0 z-50 flex flex-col shadow-2xl"
|
||||||
|
style={{
|
||||||
|
width: 280,
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
padding: '18px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3 px-2">
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-bold tracking-widest uppercase"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
|
{sidebarLabel}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className="w-8 h-8 rounded-lg flex items-center justify-center transition-colors hover:bg-[var(--bg-hover)]"
|
||||||
|
aria-label="Close navigation"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<SidebarInner
|
||||||
|
sidebarLabel={null}
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(id) => {
|
||||||
|
onTabChange(id)
|
||||||
|
setMobileOpen(false)
|
||||||
|
}}
|
||||||
|
footer={footer}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Panel */}
|
||||||
|
<div className="flex-1 min-w-0" style={{ padding: '26px 28px' }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarInner({
|
||||||
|
sidebarLabel,
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
sidebarLabel: string | null
|
||||||
|
tabs: PageSidebarTab[]
|
||||||
|
activeTab: string
|
||||||
|
onTabChange: (id: string) => void
|
||||||
|
footer?: React.ReactNode
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sidebarLabel && (
|
||||||
|
<div
|
||||||
|
className="text-[11px] font-bold tracking-widest uppercase mb-3 px-3"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
|
{sidebarLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<nav className="flex flex-col gap-1 flex-1">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon
|
||||||
|
const active = tab.id === activeTab
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => onTabChange(tab.id)}
|
||||||
|
className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
|
||||||
|
style={{
|
||||||
|
background: active ? 'var(--bg-hover)' : 'transparent',
|
||||||
|
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
|
||||||
|
fontWeight: active ? 600 : 500,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!active) e.currentTarget.style.background = 'var(--bg-hover)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!active) e.currentTarget.style.background = 'transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={16} className="shrink-0" />
|
||||||
|
<span className="truncate">{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
{footer && (
|
||||||
|
<div
|
||||||
|
className="mt-4 pt-3 px-3 text-[10px] tracking-wide"
|
||||||
|
style={{ color: 'var(--text-faint)', borderTop: '1px solid var(--border-primary)' }}
|
||||||
|
>
|
||||||
|
{footer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,8 +20,9 @@ import PackingTemplateManager from '../components/Admin/PackingTemplateManager'
|
|||||||
import AuditLogPanel from '../components/Admin/AuditLogPanel'
|
import AuditLogPanel from '../components/Admin/AuditLogPanel'
|
||||||
import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
|
import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
|
||||||
import PermissionsPanel from '../components/Admin/PermissionsPanel'
|
import PermissionsPanel from '../components/Admin/PermissionsPanel'
|
||||||
import { Users, Map, Briefcase, Shield, Trash2, Edit2, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, Sun, Link2, Copy, Plus, RefreshCw, AlertTriangle } from 'lucide-react'
|
import { Users, Map, Briefcase, Shield, Trash2, Edit2, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, Sun, Link2, Copy, Plus, RefreshCw, AlertTriangle, SlidersHorizontal, UserCog, Puzzle, Settings as SettingsIcon, Bell, Database, ScrollText, KeyRound, GitBranch, Bug } from 'lucide-react'
|
||||||
import CustomSelect from '../components/shared/CustomSelect'
|
import CustomSelect from '../components/shared/CustomSelect'
|
||||||
|
import PageSidebar, { type PageSidebarTab } from '../components/Layout/PageSidebar'
|
||||||
|
|
||||||
interface AdminUser {
|
interface AdminUser {
|
||||||
id: number
|
id: number
|
||||||
@@ -183,18 +184,18 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
const hour12 = useSettingsStore(s => s.settings.time_format) === '12h'
|
const hour12 = useSettingsStore(s => s.settings.time_format) === '12h'
|
||||||
const mcpEnabled = useAddonStore(s => s.isEnabled('mcp'))
|
const mcpEnabled = useAddonStore(s => s.isEnabled('mcp'))
|
||||||
const devMode = useAuthStore(s => s.devMode)
|
const devMode = useAuthStore(s => s.devMode)
|
||||||
const TABS = [
|
const TABS: PageSidebarTab[] = [
|
||||||
{ id: 'users', label: t('admin.tabs.users') },
|
{ id: 'users', label: t('admin.tabs.users'), icon: Users },
|
||||||
{ id: 'config', label: t('admin.tabs.config') },
|
{ id: 'config', label: t('admin.tabs.config'), icon: SlidersHorizontal },
|
||||||
{ id: 'defaults', label: t('admin.tabs.defaults') },
|
{ id: 'defaults', label: t('admin.tabs.defaults'), icon: UserCog },
|
||||||
{ id: 'addons', label: t('admin.tabs.addons') },
|
{ id: 'addons', label: t('admin.tabs.addons'), icon: Puzzle },
|
||||||
{ id: 'settings', label: t('admin.tabs.settings') },
|
{ id: 'settings', label: t('admin.tabs.settings'), icon: SettingsIcon },
|
||||||
{ id: 'notifications', label: t('admin.tabs.notifications') },
|
{ id: 'notifications', label: t('admin.tabs.notifications'), icon: Bell },
|
||||||
{ id: 'backup', label: t('admin.tabs.backup') },
|
{ id: 'backup', label: t('admin.tabs.backup'), icon: Database },
|
||||||
{ id: 'audit', label: t('admin.tabs.audit') },
|
{ id: 'audit', label: t('admin.tabs.audit'), icon: ScrollText },
|
||||||
...(mcpEnabled ? [{ id: 'mcp-tokens', label: t('admin.tabs.mcpTokens') }] : []),
|
...(mcpEnabled ? [{ id: 'mcp-tokens', label: t('admin.tabs.mcpTokens'), icon: KeyRound }] : []),
|
||||||
{ id: 'github', label: t('admin.tabs.github') },
|
{ id: 'github', label: t('admin.tabs.github'), icon: GitBranch },
|
||||||
...(devMode ? [{ id: 'dev-notifications', label: 'Dev: Notifications' }] : []),
|
...(devMode ? [{ id: 'dev-notifications', label: 'Dev: Notifications', icon: Bug }] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>('users')
|
const [activeTab, setActiveTab] = useState<string>('users')
|
||||||
@@ -500,7 +501,7 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
||||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
<div className="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center">
|
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center">
|
||||||
@@ -586,24 +587,15 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Sidebar layout — nav on the left, active panel on the right */}
|
||||||
<div className="grid grid-cols-3 sm:flex gap-1 mb-6 rounded-xl p-1" style={{ background: 'var(--bg-card)', border: '1px solid var(--border-primary)' }}>
|
<PageSidebar
|
||||||
{TABS.map(tab => (
|
sidebarLabel={t('admin.title').toUpperCase()}
|
||||||
<button
|
tabs={TABS}
|
||||||
key={tab.id}
|
activeTab={activeTab}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onTabChange={setActiveTab}
|
||||||
className={`px-3 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-lg transition-colors ${
|
footer="admin · self-hosted"
|
||||||
activeTab === tab.id
|
>
|
||||||
? 'bg-slate-900 text-white'
|
{/* Tab content */}
|
||||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === 'users' && (
|
{activeTab === 'users' && (
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||||
@@ -1618,6 +1610,7 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
{activeTab === 'defaults' && <DefaultUserSettingsTab />}
|
{activeTab === 'defaults' && <DefaultUserSettingsTab />}
|
||||||
|
|
||||||
{activeTab === 'dev-notifications' && <DevNotificationsPanel />}
|
{activeTab === 'dev-notifications' && <DevNotificationsPanel />}
|
||||||
|
</PageSidebar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { Settings } from 'lucide-react'
|
import { Settings, Palette, Map, Bell, Plug, CloudOff, User, Info } from 'lucide-react'
|
||||||
import { useTranslation } from '../i18n'
|
import { useTranslation } from '../i18n'
|
||||||
import { authApi } from '../api/client'
|
import { authApi } from '../api/client'
|
||||||
import { useAddonStore } from '../store/addonStore'
|
import { useAddonStore } from '../store/addonStore'
|
||||||
import Navbar from '../components/Layout/Navbar'
|
import Navbar from '../components/Layout/Navbar'
|
||||||
|
import PageSidebar, { type PageSidebarTab } from '../components/Layout/PageSidebar'
|
||||||
import DisplaySettingsTab from '../components/Settings/DisplaySettingsTab'
|
import DisplaySettingsTab from '../components/Settings/DisplaySettingsTab'
|
||||||
import MapSettingsTab from '../components/Settings/MapSettingsTab'
|
import MapSettingsTab from '../components/Settings/MapSettingsTab'
|
||||||
import NotificationsTab from '../components/Settings/NotificationsTab'
|
import NotificationsTab from '../components/Settings/NotificationsTab'
|
||||||
@@ -37,14 +38,18 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const TABS = [
|
const tabs: PageSidebarTab[] = [
|
||||||
{ id: 'display', label: t('settings.tabs.display') },
|
{ id: 'display', label: t('settings.tabs.display'), icon: Palette },
|
||||||
{ id: 'map', label: t('settings.tabs.map') },
|
{ id: 'map', label: t('settings.tabs.map'), icon: Map },
|
||||||
{ id: 'notifications', label: t('settings.tabs.notifications') },
|
{ id: 'notifications', label: t('settings.tabs.notifications'), icon: Bell },
|
||||||
...(hasIntegrations ? [{ id: 'integrations', label: t('settings.tabs.integrations') }] : []),
|
...(hasIntegrations
|
||||||
{ id: 'offline', label: t('settings.tabs.offline') },
|
? [{ id: 'integrations', label: t('settings.tabs.integrations'), icon: Plug }]
|
||||||
{ id: 'account', label: t('settings.tabs.account') },
|
: []),
|
||||||
...(appVersion ? [{ id: 'about', label: t('settings.tabs.about') }] : []),
|
{ id: 'offline', label: t('settings.tabs.offline'), icon: CloudOff },
|
||||||
|
{ id: 'account', label: t('settings.tabs.account'), icon: User },
|
||||||
|
...(appVersion
|
||||||
|
? [{ id: 'about', label: t('settings.tabs.about'), icon: Info }]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +57,7 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
<div style={{ paddingTop: 'var(--nav-h)' }}>
|
||||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
<div className="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'var(--bg-tertiary)' }}>
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'var(--bg-tertiary)' }}>
|
||||||
@@ -64,33 +69,24 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Sidebar layout */}
|
||||||
<div className="grid grid-cols-3 sm:flex gap-1 mb-6 rounded-xl p-1" style={{ background: 'var(--bg-card)', border: '1px solid var(--border-primary)' }}>
|
<PageSidebar
|
||||||
{TABS.map(tab => (
|
sidebarLabel={t('settings.title').toUpperCase()}
|
||||||
<button
|
tabs={tabs}
|
||||||
key={tab.id}
|
activeTab={activeTab}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onTabChange={setActiveTab}
|
||||||
className={`px-3 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-lg transition-colors ${
|
footer={appVersion ? `v${appVersion} · self-hosted` : 'self-hosted'}
|
||||||
activeTab === tab.id
|
>
|
||||||
? 'bg-slate-900 text-white'
|
{activeTab === 'display' && <DisplaySettingsTab />}
|
||||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
{activeTab === 'map' && <MapSettingsTab />}
|
||||||
}`}
|
{activeTab === 'notifications' && <NotificationsTab />}
|
||||||
>
|
{activeTab === 'integrations' && hasIntegrations && <IntegrationsTab />}
|
||||||
{tab.label}
|
{activeTab === 'offline' && <OfflineTab />}
|
||||||
</button>
|
{activeTab === 'account' && <AccountTab />}
|
||||||
))}
|
{activeTab === 'about' && appVersion && <AboutTab appVersion={appVersion} />}
|
||||||
</div>
|
</PageSidebar>
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === 'display' && <DisplaySettingsTab />}
|
|
||||||
{activeTab === 'map' && <MapSettingsTab />}
|
|
||||||
{activeTab === 'notifications' && <NotificationsTab />}
|
|
||||||
{activeTab === 'integrations' && hasIntegrations && <IntegrationsTab />}
|
|
||||||
{activeTab === 'offline' && <OfflineTab />}
|
|
||||||
{activeTab === 'account' && <AccountTab />}
|
|
||||||
{activeTab === 'about' && appVersion && <AboutTab appVersion={appVersion} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user