Add user icon with dropdown menu to BottomNav

This commit is contained in:
rafaeldpsilva
2025-10-03 11:16:50 +01:00
parent e2cf2bc782
commit 544c1a3a4f
3 changed files with 326 additions and 3 deletions

View File

@@ -10,7 +10,6 @@
</div>
</div>
</main>
<bottom-nav />
<!-- <app-footer /> -->
</div>
@@ -22,4 +21,3 @@ import AppHeader from './components/common/AppHeader.vue'
import BottomNav from './components/common/BottomNav.vue'
import { RouterView } from 'vue-router'
</script>

View File

@@ -13,6 +13,7 @@
class="absolute bottom-0 left-0 right-0 bg-white md:bg-transparent border-t md:border-t-0 border-gray-200 md:shadow-none shadow-lg"
:class="getNavigationClasses()"
>
<UserIcon />
<div class="flex justify-center md:pb-4 pb-2">
<ul
class="flex space-x-4 md:space-x-8 md:bg-white md:rounded-lg md:shadow-md px-6 py-3 w-full md:w-auto justify-around md:justify-center"
@@ -126,9 +127,10 @@
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { onMounted } from 'vue'
import { useSettingsStore } from '@/stores/settings'
import UserIcon from './UserIcon.vue'
const settingsStore = useSettingsStore()
// Compute navigation classes based on settings

View File

@@ -0,0 +1,323 @@
<template>
<!-- User Icon Container -->
<div
v-if="settingsStore.settings.ui.navigationMode !== 'hidden'"
class="absolute bottom-0 left-0 h-16 group md:h-16"
>
<!-- Invisible hover trigger area for desktop (only for hover mode) -->
<div
v-if="settingsStore.settings.ui.navigationMode === 'hover'"
class="absolute inset-0 hidden md:block"
></div>
<div :class="getNavigationClasses()">
<div class="flex md:pb-4 pb-2 pl-4">
<!-- User Avatar Button -->
<button
@click="toggleMenu"
class="relative bg-white rounded-full shadow-md hover:shadow-lg transition-shadow duration-200 w-12 h-12 flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-blue-500"
:class="{ 'ring-2 ring-blue-400': isMenuOpen }"
>
<!-- User Initials or Icon -->
<div v-if="authStore.isAuthenticated" class="flex items-center justify-center">
<span class="text-sm font-semibold text-gray-700">{{ userInitials }}</span>
</div>
<div v-else class="flex items-center justify-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
<!-- Status Indicator -->
<div
class="absolute bottom-0 right-0 w-3 h-3 rounded-full border-2 border-white"
:class="statusIndicatorColor"
></div>
</button>
<!-- Dropdown Menu -->
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="isMenuOpen"
class="absolute bottom-16 left-4 w-64 bg-white rounded-lg shadow-xl border border-gray-200 py-2 z-50"
@click.stop
>
<!-- User Info Section -->
<div class="px-4 py-3 border-b border-gray-100">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<span class="text-lg font-semibold text-blue-600">{{ userInitials }}</span>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">{{ userName }}</p>
<p class="text-xs text-gray-500">{{ authStatus }}</p>
</div>
</div>
</div>
<!-- Token Info (if authenticated) -->
<div v-if="authStore.isAuthenticated" class="px-4 py-3 border-b border-gray-100">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">Token Expires:</span>
<span class="font-medium" :class="expiryTextColor">{{ tokenExpiryText }}</span>
</div>
<div class="mt-1 w-full bg-gray-200 rounded-full h-1.5">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="expiryBarColor"
:style="{ width: tokenExpiryPercentage + '%' }"
></div>
</div>
</div>
<!-- Menu Items -->
<div class="py-1">
<button
v-if="!authStore.isAuthenticated"
@click="handleLogin"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
<span>Login / Generate Token</span>
</button>
<button
@click="handleProfile"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>Profile</span>
</button>
<button
v-if="authStore.isAuthenticated"
@click="handleTokenInfo"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg>
<span>Token Details</span>
</button>
<button
@click="handleSettings"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Settings</span>
</button>
<div v-if="authStore.isAuthenticated" class="border-t border-gray-100 mt-1 pt-1">
<button
@click="handleLogout"
class="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center space-x-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>Logout</span>
</button>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const settingsStore = useSettingsStore()
const authStore = useAuthStore()
const isMenuOpen = ref(false)
// Compute navigation classes based on settings (same as BottomNav)
const getNavigationClasses = () => {
const mode = settingsStore.settings.ui.navigationMode
if (mode === 'always') {
return 'transform-none md:transform-none'
} else if (mode === 'hover') {
return 'transform-none md:transform md:translate-y-full md:group-hover:translate-y-0 md:transition-transform md:duration-300 md:ease-in-out'
}
return 'transform-none md:transform md:translate-y-full md:group-hover:translate-y-0 md:transition-transform md:duration-300 md:ease-in-out'
}
// User display info
const userName = computed(() => {
if (authStore.isAuthenticated) {
return 'Dashboard User'
}
return 'Guest'
})
const userInitials = computed(() => {
const name = userName.value
const parts = name.split(' ')
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase()
}
return name.substring(0, 2).toUpperCase()
})
const authStatus = computed(() => {
if (authStore.isAuthenticated) {
return 'Authenticated'
}
return 'Not authenticated'
})
const statusIndicatorColor = computed(() => {
if (authStore.isAuthenticated) {
return 'bg-green-500'
}
return 'bg-gray-400'
})
// Token expiry info
const tokenExpiryText = computed(() => {
if (!authStore.timeUntilExpiry) return 'N/A'
const { hours, minutes } = authStore.timeUntilExpiry
if (hours > 0) {
return `${hours}h ${minutes}m`
}
return `${minutes}m`
})
const tokenExpiryPercentage = computed(() => {
if (!authStore.timeUntilExpiry) return 0
const { milliseconds } = authStore.timeUntilExpiry
const totalMs = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
return Math.max(0, Math.min(100, (milliseconds / totalMs) * 100))
})
const expiryTextColor = computed(() => {
const percentage = tokenExpiryPercentage.value
if (percentage > 50) return 'text-green-600'
if (percentage > 25) return 'text-yellow-600'
return 'text-red-600'
})
const expiryBarColor = computed(() => {
const percentage = tokenExpiryPercentage.value
if (percentage > 50) return 'bg-green-500'
if (percentage > 25) return 'bg-yellow-500'
return 'bg-red-500'
})
// Menu handlers
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value
}
const closeMenu = () => {
isMenuOpen.value = false
}
const handleLogin = async () => {
const success = await authStore.generateToken()
if (success) {
console.log('Token generated successfully')
}
closeMenu()
}
const handleProfile = () => {
console.log('Navigate to profile')
closeMenu()
}
const handleTokenInfo = () => {
console.log('Show token details:', {
token: authStore.token,
expiry: authStore.tokenExpiry,
resources: authStore.tokenResources,
})
closeMenu()
}
const handleSettings = () => {
router.push('/settings')
closeMenu()
}
const handleLogout = () => {
authStore.clearToken()
console.log('User logged out')
closeMenu()
}
// Close menu when clicking outside
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (isMenuOpen.value && !target.closest('.absolute.bottom-16')) {
closeMenu()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
// Initialize settings store if needed
if (!settingsStore.lastSaved) {
settingsStore.initialize()
}
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>