Enhance frontend components and introduce new features
- Updated PasswordInput component with improved styling and touch manipulation support. - Added new IconMenu component for consistent icon representation in the UI. - Refactored MapControls and introduced MapControlsContent for better organization and usability. - Implemented suppress-leaflet-deprecation plugin to handle known warnings in Firefox. - Enhanced default layout with a responsive drawer for mobile navigation and improved user experience.
This commit is contained in:
@@ -1,75 +1,165 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-base-100 overflow-hidden">
|
||||
<header class="navbar relative z-[1100] bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
||||
<NuxtLink to="/" class="flex items-center gap-2 text-lg font-semibold hover:opacity-80 transition-all duration-200">
|
||||
<icons-icon-map class="size-6 text-primary shrink-0" aria-hidden="true" />
|
||||
<span>{{ title }}</span>
|
||||
</NuxtLink>
|
||||
<div class="flex-1" />
|
||||
<!-- When not logged in: show Login link (except on login page) -->
|
||||
<NuxtLink
|
||||
v-if="!me && !isLogin"
|
||||
to="/login"
|
||||
class="btn btn-ghost btn-sm btn-primary"
|
||||
>
|
||||
Login
|
||||
</NuxtLink>
|
||||
<!-- Connection status: pulsing dot + tooltip when logged in -->
|
||||
<div
|
||||
v-if="!isLogin && me"
|
||||
class="tooltip tooltip-bottom shrink-0"
|
||||
:data-tip="live ? 'Connected to live updates' : 'Disconnected'"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs text-base-content/70"
|
||||
:class="live ? 'text-success' : 'text-base-content/50'"
|
||||
>
|
||||
<span
|
||||
class="size-2 rounded-full"
|
||||
:class="live ? 'bg-success animate-pulse' : 'bg-base-content/40'"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ live ? 'Live' : 'Offline' }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- User dropdown when logged in: details/summary for reliable click-to-open -->
|
||||
<div v-if="!isLogin && me" class="dropdown dropdown-end">
|
||||
<details ref="userDropdownRef" class="dropdown group">
|
||||
<summary class="btn btn-ghost btn-sm gap-2 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-8">
|
||||
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="max-w-[8rem] truncate font-medium hidden sm:inline">{{ me.username }}</span>
|
||||
<svg class="size-4 opacity-70 shrink-0 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
<ul
|
||||
class="dropdown-content menu bg-base-200 rounded-box z-[1100] mt-2 w-52 border border-base-300/50 shadow-xl p-2"
|
||||
<div class="drawer h-screen flex flex-col bg-base-100 overflow-hidden">
|
||||
<input id="nav-drawer" ref="drawerCheckboxRef" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content flex flex-col h-screen overflow-hidden">
|
||||
<header class="navbar relative z-[1100] bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
||||
<NuxtLink to="/" class="flex items-center gap-2 text-lg font-semibold hover:opacity-80 transition-all duration-200">
|
||||
<icons-icon-map class="size-6 text-primary shrink-0" aria-hidden="true" />
|
||||
<span>{{ title }}</span>
|
||||
</NuxtLink>
|
||||
<div class="flex-1" />
|
||||
<!-- Desktop (md+): Login / connection + user dropdown -->
|
||||
<template v-if="!isLogin">
|
||||
<NuxtLink
|
||||
v-if="!me"
|
||||
to="/login"
|
||||
class="btn btn-ghost btn-sm btn-primary hidden md:inline-flex"
|
||||
>
|
||||
Login
|
||||
</NuxtLink>
|
||||
<div
|
||||
v-if="me"
|
||||
class="hidden md:flex items-center gap-2 shrink-0"
|
||||
>
|
||||
<div
|
||||
class="tooltip tooltip-bottom shrink-0"
|
||||
:data-tip="live ? 'Connected to live updates' : 'Disconnected'"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs text-base-content/70"
|
||||
:class="live ? 'text-success' : 'text-base-content/50'"
|
||||
>
|
||||
<span
|
||||
class="size-2 rounded-full"
|
||||
:class="live ? 'bg-success animate-pulse' : 'bg-base-content/40'"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ live ? 'Live' : 'Offline' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dropdown dropdown-end">
|
||||
<details ref="userDropdownRef" class="dropdown group">
|
||||
<summary class="btn btn-ghost btn-sm gap-2 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-8">
|
||||
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="max-w-[8rem] truncate font-medium">{{ me.username }}</span>
|
||||
<svg class="size-4 opacity-70 shrink-0 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
<ul
|
||||
class="dropdown-content menu bg-base-200 rounded-box z-[1100] mt-2 w-52 border border-base-300/50 shadow-xl p-2"
|
||||
>
|
||||
<li>
|
||||
<NuxtLink to="/" :class="route.path === '/' ? 'active' : ''" @click="closeDropdown">
|
||||
<icons-icon-map />
|
||||
Map
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/profile" :class="route.path === '/profile' ? 'active' : ''" @click="closeDropdown">
|
||||
<icons-icon-user />
|
||||
Profile
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li v-if="isAdmin">
|
||||
<NuxtLink to="/admin" :class="route.path.startsWith('/admin') ? 'active' : ''" @click="closeDropdown">
|
||||
<icons-icon-shield />
|
||||
Admin
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="menu-divider my-1" />
|
||||
<li class="[&_.menu-item]:flex [&_.menu-item]:items-center [&_.menu-item]:gap-3">
|
||||
<label class="flex cursor-pointer items-center gap-3 py-2 px-2 rounded-lg hover:bg-base-300/50 w-full min-h-0">
|
||||
<icons-icon-sun v-if="!dark" class="size-4 shrink-0 opacity-80" />
|
||||
<icons-icon-moon v-else class="size-4 shrink-0 opacity-80" />
|
||||
<span class="flex-1 text-sm">Тёмная тема</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary shrink-0"
|
||||
:checked="dark"
|
||||
@change="onThemeToggle"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="text-error hover:bg-error/10 rounded-lg" @click="doLogout">
|
||||
<icons-icon-logout />
|
||||
Logout
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Mobile: hamburger to open drawer -->
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="btn btn-ghost btn-square md:hidden flex items-center justify-center min-h-[2.75rem] min-w-[2.75rem]"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<icons-icon-menu class="size-6" />
|
||||
</label>
|
||||
</header>
|
||||
<main class="flex-1 min-h-0 overflow-y-auto relative flex flex-col">
|
||||
<AdminBreadcrumbs v-if="route.path.startsWith('/admin') && route.path !== '/admin'" />
|
||||
<div class="flex-1 min-h-0">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<ToastContainer />
|
||||
</div>
|
||||
<!-- Drawer sidebar (mobile menu) -->
|
||||
<div class="drawer-side z-[1100]">
|
||||
<label for="nav-drawer" aria-label="Close menu" class="drawer-overlay" />
|
||||
<aside class="bg-base-200/95 backdrop-blur-xl min-h-full w-72 p-4 flex flex-col">
|
||||
<!-- Mobile: user + live when logged in -->
|
||||
<div v-if="!isLogin && me" class="flex items-center gap-3 pb-4 mb-2 border-b border-base-300/50">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-10">
|
||||
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium truncate">{{ me.username }}</p>
|
||||
<p class="text-xs flex items-center gap-1.5" :class="live ? 'text-success' : 'text-base-content/60'">
|
||||
<span class="size-1.5 rounded-full" :class="live ? 'bg-success animate-pulse' : 'bg-base-content/40'" />
|
||||
{{ live ? 'Live' : 'Offline' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="menu p-0 flex-1 text-base">
|
||||
<li v-if="!me && !isLogin">
|
||||
<NuxtLink to="/login" class="min-h-12 touch-manipulation" @click="closeDrawer">
|
||||
Login
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<template v-else>
|
||||
<li>
|
||||
<NuxtLink to="/" :class="route.path === '/' ? 'active' : ''" @click="closeDropdown">
|
||||
<NuxtLink to="/" :class="route.path === '/' ? 'active' : ''" class="min-h-12 touch-manipulation" @click="closeDrawer">
|
||||
<icons-icon-map />
|
||||
Map
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/profile" :class="route.path === '/profile' ? 'active' : ''" @click="closeDropdown">
|
||||
<NuxtLink to="/profile" :class="route.path === '/profile' ? 'active' : ''" class="min-h-12 touch-manipulation" @click="closeDrawer">
|
||||
<icons-icon-user />
|
||||
Profile
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li v-if="isAdmin">
|
||||
<NuxtLink to="/admin" :class="route.path.startsWith('/admin') ? 'active' : ''" @click="closeDropdown">
|
||||
<NuxtLink to="/admin" :class="route.path.startsWith('/admin') ? 'active' : ''" class="min-h-12 touch-manipulation" @click="closeDrawer">
|
||||
<icons-icon-shield />
|
||||
Admin
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="menu-divider my-1" />
|
||||
<li class="[&_.menu-item]:flex [&_.menu-item]:items-center [&_.menu-item]:gap-3">
|
||||
<label class="flex cursor-pointer items-center gap-3 py-2 px-2 rounded-lg hover:bg-base-300/50 w-full min-h-0">
|
||||
<li class="[&_.menu-item]:flex [&_.menu-item]:items-center [&_.menu-item]:gap-3 min-h-12">
|
||||
<label class="flex cursor-pointer items-center gap-3 py-3 px-2 rounded-lg hover:bg-base-300/50 w-full min-h-0 touch-manipulation">
|
||||
<icons-icon-sun v-if="!dark" class="size-4 shrink-0 opacity-80" />
|
||||
<icons-icon-moon v-else class="size-4 shrink-0 opacity-80" />
|
||||
<span class="flex-1 text-sm">Тёмная тема</span>
|
||||
@@ -82,22 +172,15 @@
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="text-error hover:bg-error/10 rounded-lg" @click="doLogout">
|
||||
<button type="button" class="text-error hover:bg-error/10 rounded-lg min-h-12 touch-manipulation w-full justify-start" @click="doLogout">
|
||||
<icons-icon-logout />
|
||||
Logout
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 min-h-0 overflow-y-auto relative flex flex-col">
|
||||
<AdminBreadcrumbs v-if="route.path.startsWith('/admin')" />
|
||||
<div class="flex-1 min-h-0">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<ToastContainer />
|
||||
</template>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -121,11 +204,16 @@ const dark = ref(false)
|
||||
const live = ref(false)
|
||||
const me = ref<{ username?: string; auths?: string[] } | null>(null)
|
||||
const userDropdownRef = ref<HTMLDetailsElement | null>(null)
|
||||
const drawerCheckboxRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function closeDropdown() {
|
||||
userDropdownRef.value?.removeAttribute('open')
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
if (drawerCheckboxRef.value) drawerCheckboxRef.value.checked = false
|
||||
}
|
||||
|
||||
const { isLoginPath } = useAppPaths()
|
||||
const isLogin = computed(() => isLoginPath(route.path))
|
||||
const isAdmin = computed(() => !!me.value?.auths?.includes('admin'))
|
||||
@@ -175,6 +263,7 @@ function applyTheme() {
|
||||
|
||||
async function doLogout() {
|
||||
await useMapApi().logout()
|
||||
closeDrawer()
|
||||
await router.push('/login')
|
||||
me.value = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user