mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Merge pull request #746 from mauriceboe/feat/settings-sidebar-layout
feat(ui): unified sidebar layout for Settings and Admin pages
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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ function TagChip({ tag }: { tag: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StyleDropdown({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
function StyleDropdown({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ function StyleDropdown({ value, onChange }: { value: string; onChange: (v: strin
|
|||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 min-w-0">
|
<span className="flex items-center gap-2 min-w-0">
|
||||||
<span className="text-slate-900 dark:text-white truncate">
|
<span className="text-slate-900 dark:text-white truncate">
|
||||||
{selected ? selected.name : 'Select a Mapbox style'}
|
{selected ? selected.name : t('settings.mapStylePlaceholder')}
|
||||||
</span>
|
</span>
|
||||||
{selected && (
|
{selected && (
|
||||||
<span className="flex items-center gap-1 flex-shrink-0">
|
<span className="flex items-center gap-1 flex-shrink-0">
|
||||||
@@ -213,7 +214,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
<Section title={t('settings.map')} icon={Map}>
|
<Section title={t('settings.map')} icon={Map}>
|
||||||
{/* Provider picker — big cards so the choice is obvious */}
|
{/* Provider picker — big cards so the choice is obvious */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-2">Map Provider</label>
|
<label className="block text-sm font-medium text-slate-700 mb-2">{t('settings.mapProvider')}</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -227,7 +228,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
<Layers size={18} className="mt-0.5 flex-shrink-0 text-slate-700 dark:text-slate-300" />
|
<Layers size={18} className="mt-0.5 flex-shrink-0 text-slate-700 dark:text-slate-300" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-slate-900 dark:text-white">Leaflet</div>
|
<div className="text-sm font-medium text-slate-900 dark:text-white">Leaflet</div>
|
||||||
<div className="text-xs text-slate-500 mt-0.5">Classic 2D, any raster tiles</div>
|
<div className="hidden sm:block text-xs text-slate-500 mt-0.5">{t('settings.mapLeafletSubtitle')}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -240,17 +241,17 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="absolute top-2 right-2 text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
<span className="absolute top-2 right-2 text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
||||||
Experimental
|
{t('settings.mapExperimental')}
|
||||||
</span>
|
</span>
|
||||||
<Box size={18} className="mt-0.5 flex-shrink-0 text-slate-700 dark:text-slate-300" />
|
<Box size={18} className="mt-0.5 flex-shrink-0 text-slate-700 dark:text-slate-300" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-slate-900 dark:text-white">Mapbox GL</div>
|
<div className="text-sm font-medium text-slate-900 dark:text-white">Mapbox GL</div>
|
||||||
<div className="text-xs text-slate-500 mt-0.5">Vector tiles, 3D buildings & terrain</div>
|
<div className="hidden sm:block text-xs text-slate-500 mt-0.5">{t('settings.mapMapboxSubtitle')}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-400 mt-2">
|
<p className="text-xs text-slate-400 mt-2">
|
||||||
Affects Trip Planner and Journey maps. Atlas always uses Leaflet.
|
{t('settings.mapProviderHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -281,7 +282,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
{provider === 'mapbox-gl' && (
|
{provider === 'mapbox-gl' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Mapbox Access Token</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('settings.mapMapboxToken')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={mapboxToken}
|
value={mapboxToken}
|
||||||
@@ -290,15 +291,15 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-400 mt-1">
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
Public token (pk.*) from{' '}
|
{t('settings.mapMapboxTokenHint')}{' '}
|
||||||
<a href="https://account.mapbox.com/access-tokens/" target="_blank" rel="noreferrer" className="underline">
|
<a href="https://account.mapbox.com/access-tokens/" target="_blank" rel="noreferrer" className="underline">
|
||||||
mapbox.com → Access tokens
|
{t('settings.mapMapboxTokenLink')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Map Style</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('settings.mapStyle')}</label>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<StyleDropdown value={mapboxStyle} onChange={setMapboxStyle} />
|
<StyleDropdown value={mapboxStyle} onChange={setMapboxStyle} />
|
||||||
</div>
|
</div>
|
||||||
@@ -310,7 +311,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-400 mt-1">
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
Preset or your own <code className="text-[11px]">mapbox://styles/USER/ID</code> URL
|
{t('settings.mapStyleHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -320,9 +321,9 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
: 'border-slate-200 opacity-60 dark:border-slate-700'
|
: 'border-slate-200 opacity-60 dark:border-slate-700'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm font-medium text-slate-900 dark:text-white">3D Buildings & Terrain</div>
|
<div className="text-sm font-medium text-slate-900 dark:text-white">{t('settings.map3dBuildings')}</div>
|
||||||
<div className="text-xs text-slate-500 mt-0.5">
|
<div className="text-xs text-slate-500 mt-0.5">
|
||||||
Pitch + real 3D building extrusions — works on every style, including satellite.
|
{t('settings.map3dHint')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -333,22 +334,22 @@ export default function MapSettingsTab(): React.ReactElement {
|
|||||||
|
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg border border-slate-200 dark:border-slate-700">
|
<div className="flex items-start gap-3 p-3 rounded-lg border border-slate-200 dark:border-slate-700">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm font-medium text-slate-900 dark:text-white flex items-center gap-2">
|
<div className="text-sm font-medium text-slate-900 dark:text-white flex flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2">
|
||||||
High Quality Mode
|
<span className="order-2 sm:order-1">{t('settings.mapHighQuality')}</span>
|
||||||
<span className="text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
<span className="order-1 sm:order-2 text-[9px] font-semibold tracking-wide uppercase px-1.5 py-[3px] rounded bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300 leading-none">
|
||||||
Experimental
|
{t('settings.mapExperimental')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500 mt-0.5">
|
<div className="text-xs text-slate-500 mt-0.5">
|
||||||
Antialiasing + globe projection for sharper edges and a realistic world view.{' '}
|
{t('settings.mapHighQualityHint')}{' '}
|
||||||
<span className="text-amber-600 dark:text-amber-400">May impact performance on lower-end devices.</span>
|
<span className="text-amber-600 dark:text-amber-400">{t('settings.mapHighQualityWarning')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch on={mapboxQuality} onToggle={() => setMapboxQuality(!mapboxQuality)} />
|
<ToggleSwitch on={mapboxQuality} onToggle={() => setMapboxQuality(!mapboxQuality)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-slate-400 p-3 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
<div className="text-xs text-slate-400 p-3 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
||||||
<strong className="text-slate-600 dark:text-slate-300">Tip:</strong> right-click and drag to rotate/pitch the map. Middle-click to add a place (right-click is reserved for rotation).
|
<strong className="text-slate-600 dark:text-slate-300">{t('settings.mapTipLabel')}</strong> {t('settings.mapTip')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -161,6 +161,24 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا',
|
'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'قالب URL لبلاطات الخريطة',
|
'settings.mapHint': 'قالب URL لبلاطات الخريطة',
|
||||||
|
'settings.mapProvider': 'مزود الخريطة',
|
||||||
|
'settings.mapProviderHint': 'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': '2D كلاسيكي، أي بلاطات نقطية',
|
||||||
|
'settings.mapMapboxSubtitle': 'بلاطات متجهية ومبانٍ ثلاثية الأبعاد وتضاريس',
|
||||||
|
'settings.mapExperimental': 'تجريبي',
|
||||||
|
'settings.mapMapboxToken': 'رمز وصول Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'الرمز العام (pk.*) من',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com ← رموز الوصول',
|
||||||
|
'settings.mapStyle': 'نمط الخريطة',
|
||||||
|
'settings.mapStylePlaceholder': 'اختر نمط Mapbox',
|
||||||
|
'settings.mapStyleHint': 'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
|
||||||
|
'settings.map3dBuildings': 'مبانٍ ثلاثية الأبعاد وتضاريس',
|
||||||
|
'settings.map3dHint': 'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
|
||||||
|
'settings.mapHighQuality': 'وضع الجودة العالية',
|
||||||
|
'settings.mapHighQualityHint': 'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
|
||||||
|
'settings.mapHighQualityWarning': 'قد يؤثر على الأداء في الأجهزة الأقل قدرة.',
|
||||||
|
'settings.mapTipLabel': 'نصيحة:',
|
||||||
|
'settings.mapTip': 'انقر بزر الماوس الأيمن واسحب لتدوير/إمالة الخريطة. النقر الأوسط لإضافة مكان (النقر الأيمن مخصص للتدوير).',
|
||||||
'settings.latitude': 'خط العرض',
|
'settings.latitude': 'خط العرض',
|
||||||
'settings.longitude': 'خط الطول',
|
'settings.longitude': 'خط الطول',
|
||||||
'settings.saveMap': 'حفظ الخريطة',
|
'settings.saveMap': 'حفظ الخريطة',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Deixe vazio para OpenStreetMap (padrão)',
|
'settings.mapDefaultHint': 'Deixe vazio para OpenStreetMap (padrão)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL do modelo de blocos do mapa',
|
'settings.mapHint': 'URL do modelo de blocos do mapa',
|
||||||
|
'settings.mapProvider': 'Provedor de mapa',
|
||||||
|
'settings.mapProviderHint': 'Afeta os mapas do Planejador de Viagem e Diário. Atlas sempre usa Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Clássico 2D, quaisquer blocos raster',
|
||||||
|
'settings.mapMapboxSubtitle': 'Blocos vetoriais, prédios 3D & terreno',
|
||||||
|
'settings.mapExperimental': 'Experimental',
|
||||||
|
'settings.mapMapboxToken': 'Token de acesso Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Token público (pk.*) de',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Tokens de acesso',
|
||||||
|
'settings.mapStyle': 'Estilo do mapa',
|
||||||
|
'settings.mapStylePlaceholder': 'Selecionar um estilo Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset ou sua própria URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': 'Prédios 3D & terreno',
|
||||||
|
'settings.map3dHint': 'Inclinação + extrusões 3D reais de prédios — funciona em todo estilo, incluindo satélite.',
|
||||||
|
'settings.mapHighQuality': 'Modo alta qualidade',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + projeção global para bordas mais nítidas e uma visão realista do mundo.',
|
||||||
|
'settings.mapHighQualityWarning': 'Pode afetar o desempenho em dispositivos menos potentes.',
|
||||||
|
'settings.mapTipLabel': 'Dica:',
|
||||||
|
'settings.mapTip': 'Clique direito e arraste para girar/inclinar o mapa. Clique do meio para adicionar um local (o clique direito é reservado para rotação).',
|
||||||
'settings.latitude': 'Latitude',
|
'settings.latitude': 'Latitude',
|
||||||
'settings.longitude': 'Longitude',
|
'settings.longitude': 'Longitude',
|
||||||
'settings.saveMap': 'Salvar mapa',
|
'settings.saveMap': 'Salvar mapa',
|
||||||
|
|||||||
@@ -157,6 +157,24 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Ponechte prázdné pro OpenStreetMap (výchozí)',
|
'settings.mapDefaultHint': 'Ponechte prázdné pro OpenStreetMap (výchozí)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL šablony pro mapové dlaždice',
|
'settings.mapHint': 'URL šablony pro mapové dlaždice',
|
||||||
|
'settings.mapProvider': 'Poskytovatel mapy',
|
||||||
|
'settings.mapProviderHint': 'Ovlivňuje mapy v Trip Planneru a Journey. Atlas vždy používá Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klasické 2D, libovolné rastrové dlaždice',
|
||||||
|
'settings.mapMapboxSubtitle': 'Vektorové dlaždice, 3D budovy a terén',
|
||||||
|
'settings.mapExperimental': 'Experimentální',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox přístupový token',
|
||||||
|
'settings.mapMapboxTokenHint': 'Veřejný token (pk.*) z',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Přístupové tokeny',
|
||||||
|
'settings.mapStyle': 'Styl mapy',
|
||||||
|
'settings.mapStylePlaceholder': 'Vyberte styl Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset nebo vaše vlastní URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': '3D budovy a terén',
|
||||||
|
'settings.map3dHint': 'Náklon + skutečné 3D vyvýšení budov — funguje s každým stylem, včetně satelitu.',
|
||||||
|
'settings.mapHighQuality': 'Režim vysoké kvality',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + zobrazení glóbu pro ostřejší hrany a realistický pohled na svět.',
|
||||||
|
'settings.mapHighQualityWarning': 'Může ovlivnit výkon na slabších zařízeních.',
|
||||||
|
'settings.mapTipLabel': 'Tip:',
|
||||||
|
'settings.mapTip': 'Pravé tlačítko myši a táhněte pro rotaci/náklon mapy. Prostřední tlačítko pro přidání místa (pravé tlačítko je vyhrazeno pro rotaci).',
|
||||||
'settings.latitude': 'Zeměpisná šířka',
|
'settings.latitude': 'Zeměpisná šířka',
|
||||||
'settings.longitude': 'Zeměpisná délka',
|
'settings.longitude': 'Zeměpisná délka',
|
||||||
'settings.saveMap': 'Uložit nastavení mapy',
|
'settings.saveMap': 'Uložit nastavení mapy',
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Leer lassen für OpenStreetMap (Standard)',
|
'settings.mapDefaultHint': 'Leer lassen für OpenStreetMap (Standard)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL-Template für die Kartenkacheln',
|
'settings.mapHint': 'URL-Template für die Kartenkacheln',
|
||||||
|
'settings.mapProvider': 'Kartenanbieter',
|
||||||
|
'settings.mapProviderHint': 'Gilt für Trip Planner und Journey. Atlas nutzt immer Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klassisch 2D, beliebige Raster-Kacheln',
|
||||||
|
'settings.mapMapboxSubtitle': 'Vektor-Kacheln, 3D-Gebäude & Terrain',
|
||||||
|
'settings.mapExperimental': 'Experimentell',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox Access Token',
|
||||||
|
'settings.mapMapboxTokenHint': 'Öffentliches Token (pk.*) von',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Access Tokens',
|
||||||
|
'settings.mapStyle': 'Kartenstil',
|
||||||
|
'settings.mapStylePlaceholder': 'Mapbox-Stil wählen',
|
||||||
|
'settings.mapStyleHint': 'Preset oder eigene mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D-Gebäude & Terrain',
|
||||||
|
'settings.map3dHint': 'Neigung + echte 3D-Gebäude-Extrusionen — funktioniert mit jedem Stil, auch Satellit.',
|
||||||
|
'settings.mapHighQuality': 'Hochqualitäts-Modus',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + Globus-Projektion für schärfere Kanten und eine realistische Weltsicht.',
|
||||||
|
'settings.mapHighQualityWarning': 'Kann die Performance auf schwächeren Geräten beeinträchtigen.',
|
||||||
|
'settings.mapTipLabel': 'Tipp:',
|
||||||
|
'settings.mapTip': 'Rechtsklick und ziehen, um die Karte zu drehen/neigen. Mittelklick, um einen Ort hinzuzufügen (Rechtsklick ist für die Rotation reserviert).',
|
||||||
'settings.latitude': 'Breitengrad',
|
'settings.latitude': 'Breitengrad',
|
||||||
'settings.longitude': 'Längengrad',
|
'settings.longitude': 'Längengrad',
|
||||||
'settings.saveMap': 'Karte speichern',
|
'settings.saveMap': 'Karte speichern',
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Leave empty for OpenStreetMap (default)',
|
'settings.mapDefaultHint': 'Leave empty for OpenStreetMap (default)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL template for map tiles',
|
'settings.mapHint': 'URL template for map tiles',
|
||||||
|
'settings.mapProvider': 'Map Provider',
|
||||||
|
'settings.mapProviderHint': 'Affects Trip Planner and Journey maps. Atlas always uses Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Classic 2D, any raster tiles',
|
||||||
|
'settings.mapMapboxSubtitle': 'Vector tiles, 3D buildings & terrain',
|
||||||
|
'settings.mapExperimental': 'Experimental',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox Access Token',
|
||||||
|
'settings.mapMapboxTokenHint': 'Public token (pk.*) from',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Access tokens',
|
||||||
|
'settings.mapStyle': 'Map Style',
|
||||||
|
'settings.mapStylePlaceholder': 'Select a Mapbox style',
|
||||||
|
'settings.mapStyleHint': 'Preset or your own mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D Buildings & Terrain',
|
||||||
|
'settings.map3dHint': 'Pitch + real 3D building extrusions — works on every style, including satellite.',
|
||||||
|
'settings.mapHighQuality': 'High Quality Mode',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + globe projection for sharper edges and a realistic world view.',
|
||||||
|
'settings.mapHighQualityWarning': 'May impact performance on lower-end devices.',
|
||||||
|
'settings.mapTipLabel': 'Tip:',
|
||||||
|
'settings.mapTip': 'right-click and drag to rotate/pitch the map. Middle-click to add a place (right-click is reserved for rotation).',
|
||||||
'settings.latitude': 'Latitude',
|
'settings.latitude': 'Latitude',
|
||||||
'settings.longitude': 'Longitude',
|
'settings.longitude': 'Longitude',
|
||||||
'settings.saveMap': 'Save Map',
|
'settings.saveMap': 'Save Map',
|
||||||
|
|||||||
@@ -157,6 +157,24 @@ const es: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': 'Déjalo vacío para OpenStreetMap (por defecto)',
|
'settings.mapDefaultHint': 'Déjalo vacío para OpenStreetMap (por defecto)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'Plantilla de URL para los mosaicos del mapa',
|
'settings.mapHint': 'Plantilla de URL para los mosaicos del mapa',
|
||||||
|
'settings.mapProvider': 'Proveedor de mapa',
|
||||||
|
'settings.mapProviderHint': 'Afecta a los mapas de Trip Planner y Journey. Atlas siempre usa Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Clásico 2D, cualquier mosaico raster',
|
||||||
|
'settings.mapMapboxSubtitle': 'Mosaicos vectoriales, edificios 3D y terreno',
|
||||||
|
'settings.mapExperimental': 'Experimental',
|
||||||
|
'settings.mapMapboxToken': 'Token de acceso de Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Token público (pk.*) de',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Tokens de acceso',
|
||||||
|
'settings.mapStyle': 'Estilo de mapa',
|
||||||
|
'settings.mapStylePlaceholder': 'Seleccionar un estilo de Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset o tu propia URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': 'Edificios 3D y terreno',
|
||||||
|
'settings.map3dHint': 'Inclinación + extrusiones 3D reales de edificios — funciona con todos los estilos, incluyendo satélite.',
|
||||||
|
'settings.mapHighQuality': 'Modo de alta calidad',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + proyección global para bordes más nítidos y una vista realista del mundo.',
|
||||||
|
'settings.mapHighQualityWarning': 'Puede afectar el rendimiento en dispositivos menos potentes.',
|
||||||
|
'settings.mapTipLabel': 'Consejo:',
|
||||||
|
'settings.mapTip': 'Clic derecho y arrastrar para rotar/inclinar el mapa. Clic central para añadir un lugar (el clic derecho está reservado para la rotación).',
|
||||||
'settings.latitude': 'Latitud',
|
'settings.latitude': 'Latitud',
|
||||||
'settings.longitude': 'Longitud',
|
'settings.longitude': 'Longitud',
|
||||||
'settings.saveMap': 'Guardar mapa',
|
'settings.saveMap': 'Guardar mapa',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const fr: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': 'Laissez vide pour OpenStreetMap (par défaut)',
|
'settings.mapDefaultHint': 'Laissez vide pour OpenStreetMap (par défaut)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'Modèle d\'URL pour les tuiles de carte',
|
'settings.mapHint': 'Modèle d\'URL pour les tuiles de carte',
|
||||||
|
'settings.mapProvider': 'Fournisseur de carte',
|
||||||
|
'settings.mapProviderHint': 'Affecte les cartes Trip Planner et Journey. Atlas utilise toujours Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Classique 2D, toutes tuiles raster',
|
||||||
|
'settings.mapMapboxSubtitle': 'Tuiles vectorielles, bâtiments 3D & terrain',
|
||||||
|
'settings.mapExperimental': 'Expérimental',
|
||||||
|
'settings.mapMapboxToken': 'Jeton d\'accès Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Jeton public (pk.*) depuis',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Jetons d\'accès',
|
||||||
|
'settings.mapStyle': 'Style de carte',
|
||||||
|
'settings.mapStylePlaceholder': 'Sélectionner un style Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset ou votre propre URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': 'Bâtiments 3D & terrain',
|
||||||
|
'settings.map3dHint': 'Inclinaison + extrusions 3D réelles des bâtiments — fonctionne avec tous les styles, y compris satellite.',
|
||||||
|
'settings.mapHighQuality': 'Mode haute qualité',
|
||||||
|
'settings.mapHighQualityHint': 'Anticrénelage + projection globe pour des bords plus nets et une vue réaliste du monde.',
|
||||||
|
'settings.mapHighQualityWarning': 'Peut affecter les performances sur les appareils moins puissants.',
|
||||||
|
'settings.mapTipLabel': 'Astuce :',
|
||||||
|
'settings.mapTip': 'Clic droit et glisser pour pivoter/incliner la carte. Clic milieu pour ajouter un lieu (le clic droit est réservé à la rotation).',
|
||||||
'settings.latitude': 'Latitude',
|
'settings.latitude': 'Latitude',
|
||||||
'settings.longitude': 'Longitude',
|
'settings.longitude': 'Longitude',
|
||||||
'settings.saveMap': 'Enregistrer la carte',
|
'settings.saveMap': 'Enregistrer la carte',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Hagyd üresen az OpenStreetMap használatához (alapértelmezett)',
|
'settings.mapDefaultHint': 'Hagyd üresen az OpenStreetMap használatához (alapértelmezett)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL sablon a térképcsempékhez',
|
'settings.mapHint': 'URL sablon a térképcsempékhez',
|
||||||
|
'settings.mapProvider': 'Térkép szolgáltató',
|
||||||
|
'settings.mapProviderHint': 'A Trip Planner és Journey térképekre érvényes. Az Atlas mindig Leafletet használ.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klasszikus 2D, bármilyen raszter csempe',
|
||||||
|
'settings.mapMapboxSubtitle': 'Vektoros csempék, 3D épületek és terep',
|
||||||
|
'settings.mapExperimental': 'Kísérleti',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox hozzáférési token',
|
||||||
|
'settings.mapMapboxTokenHint': 'Publikus token (pk.*) innen:',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Hozzáférési tokenek',
|
||||||
|
'settings.mapStyle': 'Térkép stílus',
|
||||||
|
'settings.mapStylePlaceholder': 'Válassz Mapbox stílust',
|
||||||
|
'settings.mapStyleHint': 'Preset vagy saját mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D épületek és terep',
|
||||||
|
'settings.map3dHint': 'Dőlés + valódi 3D épület-kiemelés — minden stílussal működik, beleértve a műholdast.',
|
||||||
|
'settings.mapHighQuality': 'Magas minőség mód',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + földgömb-vetítés az élesebb kontúrokért és egy valósághű világnézethez.',
|
||||||
|
'settings.mapHighQualityWarning': 'Gyengébb eszközökön befolyásolhatja a teljesítményt.',
|
||||||
|
'settings.mapTipLabel': 'Tipp:',
|
||||||
|
'settings.mapTip': 'Jobb klikk és húzás a térkép forgatásához/döntéséhez. Középső kattintás hely hozzáadásához (a jobb klikk a forgatáshoz van fenntartva).',
|
||||||
'settings.latitude': 'Szélességi fok',
|
'settings.latitude': 'Szélességi fok',
|
||||||
'settings.longitude': 'Hosszúsági fok',
|
'settings.longitude': 'Hosszúsági fok',
|
||||||
'settings.saveMap': 'Térkép mentése',
|
'settings.saveMap': 'Térkép mentése',
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Kosongkan untuk OpenStreetMap (default)',
|
'settings.mapDefaultHint': 'Kosongkan untuk OpenStreetMap (default)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'Template URL untuk tile peta',
|
'settings.mapHint': 'Template URL untuk tile peta',
|
||||||
|
'settings.mapProvider': 'Penyedia peta',
|
||||||
|
'settings.mapProviderHint': 'Berlaku untuk peta Trip Planner dan Journey. Atlas selalu menggunakan Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klasik 2D, tile raster apa pun',
|
||||||
|
'settings.mapMapboxSubtitle': 'Tile vektor, bangunan 3D & medan',
|
||||||
|
'settings.mapExperimental': 'Eksperimental',
|
||||||
|
'settings.mapMapboxToken': 'Token akses Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Token publik (pk.*) dari',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Token akses',
|
||||||
|
'settings.mapStyle': 'Gaya peta',
|
||||||
|
'settings.mapStylePlaceholder': 'Pilih gaya Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset atau URL mapbox://styles/USER/ID milikmu',
|
||||||
|
'settings.map3dBuildings': 'Bangunan 3D & medan',
|
||||||
|
'settings.map3dHint': 'Kemiringan + ekstrusi bangunan 3D nyata — bekerja di semua gaya, termasuk satelit.',
|
||||||
|
'settings.mapHighQuality': 'Mode kualitas tinggi',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + proyeksi globe untuk tepi yang lebih tajam dan tampilan dunia realistis.',
|
||||||
|
'settings.mapHighQualityWarning': 'Dapat memengaruhi performa pada perangkat kelas bawah.',
|
||||||
|
'settings.mapTipLabel': 'Tip:',
|
||||||
|
'settings.mapTip': 'Klik kanan dan seret untuk memutar/memiringkan peta. Klik tengah untuk menambah tempat (klik kanan untuk rotasi).',
|
||||||
'settings.latitude': 'Lintang',
|
'settings.latitude': 'Lintang',
|
||||||
'settings.longitude': 'Bujur',
|
'settings.longitude': 'Bujur',
|
||||||
'settings.saveMap': 'Simpan Peta',
|
'settings.saveMap': 'Simpan Peta',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Lascia vuoto per OpenStreetMap (predefinito)',
|
'settings.mapDefaultHint': 'Lascia vuoto per OpenStreetMap (predefinito)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'Modello URL per i tile della mappa',
|
'settings.mapHint': 'Modello URL per i tile della mappa',
|
||||||
|
'settings.mapProvider': 'Provider mappa',
|
||||||
|
'settings.mapProviderHint': 'Influisce sulle mappe Trip Planner e Journey. Atlas usa sempre Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Classica 2D, qualsiasi tile raster',
|
||||||
|
'settings.mapMapboxSubtitle': 'Tile vettoriali, edifici 3D e terreno',
|
||||||
|
'settings.mapExperimental': 'Sperimentale',
|
||||||
|
'settings.mapMapboxToken': 'Token di accesso Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Token pubblico (pk.*) da',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Token di accesso',
|
||||||
|
'settings.mapStyle': 'Stile mappa',
|
||||||
|
'settings.mapStylePlaceholder': 'Seleziona uno stile Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset o il tuo URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': 'Edifici 3D e terreno',
|
||||||
|
'settings.map3dHint': 'Inclinazione + estrusioni 3D reali degli edifici — funziona con ogni stile, incluso satellite.',
|
||||||
|
'settings.mapHighQuality': 'Modalità alta qualità',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + proiezione globo per bordi più nitidi e una vista realistica del mondo.',
|
||||||
|
'settings.mapHighQualityWarning': 'Può influire sulle prestazioni su dispositivi meno potenti.',
|
||||||
|
'settings.mapTipLabel': 'Suggerimento:',
|
||||||
|
'settings.mapTip': 'Click destro e trascina per ruotare/inclinare la mappa. Click centrale per aggiungere un luogo (il click destro è riservato alla rotazione).',
|
||||||
'settings.latitude': 'Latitudine',
|
'settings.latitude': 'Latitudine',
|
||||||
'settings.longitude': 'Longitudine',
|
'settings.longitude': 'Longitudine',
|
||||||
'settings.saveMap': 'Salva Mappa',
|
'settings.saveMap': 'Salva Mappa',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const nl: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': 'Laat leeg voor OpenStreetMap (standaard)',
|
'settings.mapDefaultHint': 'Laat leeg voor OpenStreetMap (standaard)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL-sjabloon voor kaarttegels',
|
'settings.mapHint': 'URL-sjabloon voor kaarttegels',
|
||||||
|
'settings.mapProvider': 'Kaartprovider',
|
||||||
|
'settings.mapProviderHint': 'Geldt voor Trip Planner en Journey kaarten. Atlas gebruikt altijd Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klassiek 2D, elke raster-tile',
|
||||||
|
'settings.mapMapboxSubtitle': 'Vector tiles, 3D-gebouwen & terrein',
|
||||||
|
'settings.mapExperimental': 'Experimenteel',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox Access Token',
|
||||||
|
'settings.mapMapboxTokenHint': 'Openbaar token (pk.*) van',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Access tokens',
|
||||||
|
'settings.mapStyle': 'Kaartstijl',
|
||||||
|
'settings.mapStylePlaceholder': 'Kies een Mapbox-stijl',
|
||||||
|
'settings.mapStyleHint': 'Preset of eigen mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D-gebouwen & terrein',
|
||||||
|
'settings.map3dHint': 'Kanteling + echte 3D-gebouwenextrusies — werkt op elke stijl, inclusief satelliet.',
|
||||||
|
'settings.mapHighQuality': 'Hoge kwaliteit modus',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + globeprojectie voor scherpere randen en een realistische wereldweergave.',
|
||||||
|
'settings.mapHighQualityWarning': 'Kan de prestaties op minder krachtige apparaten beïnvloeden.',
|
||||||
|
'settings.mapTipLabel': 'Tip:',
|
||||||
|
'settings.mapTip': 'Rechts-klik en sleep om de kaart te roteren/kantelen. Middenklik om een locatie toe te voegen (rechts-klik is voor rotatie).',
|
||||||
'settings.latitude': 'Breedtegraad',
|
'settings.latitude': 'Breedtegraad',
|
||||||
'settings.longitude': 'Lengtegraad',
|
'settings.longitude': 'Lengtegraad',
|
||||||
'settings.saveMap': 'Kaart opslaan',
|
'settings.saveMap': 'Kaart opslaan',
|
||||||
|
|||||||
@@ -139,6 +139,24 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'settings.mapDefaultHint': 'Pozostaw puste dla OpenStreetMap (domyślnie)',
|
'settings.mapDefaultHint': 'Pozostaw puste dla OpenStreetMap (domyślnie)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'Szablon URL dla kafelków mapy',
|
'settings.mapHint': 'Szablon URL dla kafelków mapy',
|
||||||
|
'settings.mapProvider': 'Dostawca mapy',
|
||||||
|
'settings.mapProviderHint': 'Dotyczy map Trip Planner i Journey. Atlas zawsze używa Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Klasyczne 2D, dowolne kafelki rastrowe',
|
||||||
|
'settings.mapMapboxSubtitle': 'Kafelki wektorowe, budynki 3D i teren',
|
||||||
|
'settings.mapExperimental': 'Eksperymentalne',
|
||||||
|
'settings.mapMapboxToken': 'Token dostępu Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Token publiczny (pk.*) z',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Tokeny dostępu',
|
||||||
|
'settings.mapStyle': 'Styl mapy',
|
||||||
|
'settings.mapStylePlaceholder': 'Wybierz styl Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset lub własny URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': 'Budynki 3D i teren',
|
||||||
|
'settings.map3dHint': 'Nachylenie + prawdziwe wytłaczanie budynków 3D — działa w każdym stylu, także satelitarnym.',
|
||||||
|
'settings.mapHighQuality': 'Tryb wysokiej jakości',
|
||||||
|
'settings.mapHighQualityHint': 'Antialiasing + projekcja globusa dla ostrzejszych krawędzi i realistycznego widoku świata.',
|
||||||
|
'settings.mapHighQualityWarning': 'Może wpływać na wydajność na słabszych urządzeniach.',
|
||||||
|
'settings.mapTipLabel': 'Wskazówka:',
|
||||||
|
'settings.mapTip': 'Kliknij prawym przyciskiem i przeciągnij, aby obrócić/pochylić mapę. Środkowy przycisk dodaje miejsce (prawy jest zarezerwowany dla obrotu).',
|
||||||
'settings.latitude': 'Szerokość',
|
'settings.latitude': 'Szerokość',
|
||||||
'settings.longitude': 'Długość',
|
'settings.longitude': 'Długość',
|
||||||
'settings.saveMap': 'Zapisz mapę',
|
'settings.saveMap': 'Zapisz mapę',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const ru: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': 'Оставьте пустым для OpenStreetMap (по умолчанию)',
|
'settings.mapDefaultHint': 'Оставьте пустым для OpenStreetMap (по умолчанию)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': 'URL-шаблон для тайлов карты',
|
'settings.mapHint': 'URL-шаблон для тайлов карты',
|
||||||
|
'settings.mapProvider': 'Провайдер карты',
|
||||||
|
'settings.mapProviderHint': 'Применяется к Trip Planner и Journey. Atlas всегда использует Leaflet.',
|
||||||
|
'settings.mapLeafletSubtitle': 'Классические 2D, любые растровые тайлы',
|
||||||
|
'settings.mapMapboxSubtitle': 'Векторные тайлы, 3D-здания и рельеф',
|
||||||
|
'settings.mapExperimental': 'Экспериментально',
|
||||||
|
'settings.mapMapboxToken': 'Токен доступа Mapbox',
|
||||||
|
'settings.mapMapboxTokenHint': 'Публичный токен (pk.*) с',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → Токены доступа',
|
||||||
|
'settings.mapStyle': 'Стиль карты',
|
||||||
|
'settings.mapStylePlaceholder': 'Выберите стиль Mapbox',
|
||||||
|
'settings.mapStyleHint': 'Preset или собственный URL mapbox://styles/USER/ID',
|
||||||
|
'settings.map3dBuildings': '3D-здания и рельеф',
|
||||||
|
'settings.map3dHint': 'Наклон + настоящие 3D-здания — работает со всеми стилями, включая спутник.',
|
||||||
|
'settings.mapHighQuality': 'Режим высокого качества',
|
||||||
|
'settings.mapHighQualityHint': 'Сглаживание + проекция глобуса для более чётких краёв и реалистичного вида мира.',
|
||||||
|
'settings.mapHighQualityWarning': 'Может повлиять на производительность на слабых устройствах.',
|
||||||
|
'settings.mapTipLabel': 'Совет:',
|
||||||
|
'settings.mapTip': 'Зажмите правую кнопку мыши и перетащите, чтобы повернуть/наклонить карту. Клик средней кнопкой — добавить место (правая кнопка зарезервирована для вращения).',
|
||||||
'settings.latitude': 'Широта',
|
'settings.latitude': 'Широта',
|
||||||
'settings.longitude': 'Долгота',
|
'settings.longitude': 'Долгота',
|
||||||
'settings.saveMap': 'Сохранить карту',
|
'settings.saveMap': 'Сохранить карту',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const zh: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': '留空则使用 OpenStreetMap(默认)',
|
'settings.mapDefaultHint': '留空则使用 OpenStreetMap(默认)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': '地图瓦片 URL 模板',
|
'settings.mapHint': '地图瓦片 URL 模板',
|
||||||
|
'settings.mapProvider': '地图提供商',
|
||||||
|
'settings.mapProviderHint': '影响行程规划和旅程地图。Atlas 始终使用 Leaflet。',
|
||||||
|
'settings.mapLeafletSubtitle': '经典 2D,任何栅格瓦片',
|
||||||
|
'settings.mapMapboxSubtitle': '矢量瓦片、3D 建筑和地形',
|
||||||
|
'settings.mapExperimental': '实验性',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox 访问令牌',
|
||||||
|
'settings.mapMapboxTokenHint': '公共令牌 (pk.*) 来自',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → 访问令牌',
|
||||||
|
'settings.mapStyle': '地图样式',
|
||||||
|
'settings.mapStylePlaceholder': '选择 Mapbox 样式',
|
||||||
|
'settings.mapStyleHint': '预设或您自己的 mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D 建筑和地形',
|
||||||
|
'settings.map3dHint': '倾斜 + 真实 3D 建筑拉伸 — 适用于所有样式,包括卫星。',
|
||||||
|
'settings.mapHighQuality': '高画质模式',
|
||||||
|
'settings.mapHighQualityHint': '抗锯齿 + 地球投影,带来更清晰的边缘和更真实的世界视图。',
|
||||||
|
'settings.mapHighQualityWarning': '可能影响低端设备的性能。',
|
||||||
|
'settings.mapTipLabel': '提示:',
|
||||||
|
'settings.mapTip': '右键点击并拖动以旋转/倾斜地图。中键点击添加地点(右键用于旋转)。',
|
||||||
'settings.latitude': '纬度',
|
'settings.latitude': '纬度',
|
||||||
'settings.longitude': '经度',
|
'settings.longitude': '经度',
|
||||||
'settings.saveMap': '保存地图',
|
'settings.saveMap': '保存地图',
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ const zhTw: Record<string, string> = {
|
|||||||
'settings.mapDefaultHint': '留空則使用 OpenStreetMap(預設)',
|
'settings.mapDefaultHint': '留空則使用 OpenStreetMap(預設)',
|
||||||
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
'settings.mapHint': '地圖瓦片 URL 模板',
|
'settings.mapHint': '地圖瓦片 URL 模板',
|
||||||
|
'settings.mapProvider': '地圖提供商',
|
||||||
|
'settings.mapProviderHint': '影響行程規劃和旅程地圖。Atlas 始終使用 Leaflet。',
|
||||||
|
'settings.mapLeafletSubtitle': '經典 2D,任何柵格瓦片',
|
||||||
|
'settings.mapMapboxSubtitle': '向量瓦片、3D 建築和地形',
|
||||||
|
'settings.mapExperimental': '實驗性',
|
||||||
|
'settings.mapMapboxToken': 'Mapbox 存取權杖',
|
||||||
|
'settings.mapMapboxTokenHint': '公開權杖 (pk.*) 來自',
|
||||||
|
'settings.mapMapboxTokenLink': 'mapbox.com → 存取權杖',
|
||||||
|
'settings.mapStyle': '地圖樣式',
|
||||||
|
'settings.mapStylePlaceholder': '選擇 Mapbox 樣式',
|
||||||
|
'settings.mapStyleHint': '預設或您自己的 mapbox://styles/USER/ID URL',
|
||||||
|
'settings.map3dBuildings': '3D 建築和地形',
|
||||||
|
'settings.map3dHint': '傾斜 + 真實 3D 建築拉伸 — 適用於所有樣式,包括衛星。',
|
||||||
|
'settings.mapHighQuality': '高畫質模式',
|
||||||
|
'settings.mapHighQualityHint': '抗鋸齒 + 地球投影,帶來更清晰的邊緣和更真實的世界視圖。',
|
||||||
|
'settings.mapHighQualityWarning': '可能影響低階裝置的效能。',
|
||||||
|
'settings.mapTipLabel': '提示:',
|
||||||
|
'settings.mapTip': '右鍵點擊並拖曳以旋轉/傾斜地圖。中鍵點擊新增地點(右鍵用於旋轉)。',
|
||||||
'settings.latitude': '緯度',
|
'settings.latitude': '緯度',
|
||||||
'settings.longitude': '經度',
|
'settings.longitude': '經度',
|
||||||
'settings.saveMap': '儲存地圖',
|
'settings.saveMap': '儲存地圖',
|
||||||
|
|||||||
@@ -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,23 +587,14 @@ 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'
|
|
||||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* 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">
|
||||||
@@ -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,24 +69,14 @@ 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'
|
|
||||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === 'display' && <DisplaySettingsTab />}
|
{activeTab === 'display' && <DisplaySettingsTab />}
|
||||||
{activeTab === 'map' && <MapSettingsTab />}
|
{activeTab === 'map' && <MapSettingsTab />}
|
||||||
{activeTab === 'notifications' && <NotificationsTab />}
|
{activeTab === 'notifications' && <NotificationsTab />}
|
||||||
@@ -89,6 +84,7 @@ export default function SettingsPage(): React.ReactElement {
|
|||||||
{activeTab === 'offline' && <OfflineTab />}
|
{activeTab === 'offline' && <OfflineTab />}
|
||||||
{activeTab === 'account' && <AccountTab />}
|
{activeTab === 'account' && <AccountTab />}
|
||||||
{activeTab === 'about' && appVersion && <AboutTab appVersion={appVersion} />}
|
{activeTab === 'about' && appVersion && <AboutTab appVersion={appVersion} />}
|
||||||
|
</PageSidebar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user