Add user icon with dropdown menu to BottomNav
This commit is contained in:
@@ -10,7 +10,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<bottom-nav />
|
<bottom-nav />
|
||||||
<!-- <app-footer /> -->
|
<!-- <app-footer /> -->
|
||||||
</div>
|
</div>
|
||||||
@@ -22,4 +21,3 @@ import AppHeader from './components/common/AppHeader.vue'
|
|||||||
import BottomNav from './components/common/BottomNav.vue'
|
import BottomNav from './components/common/BottomNav.vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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="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()"
|
:class="getNavigationClasses()"
|
||||||
>
|
>
|
||||||
|
<UserIcon />
|
||||||
<div class="flex justify-center md:pb-4 pb-2">
|
<div class="flex justify-center md:pb-4 pb-2">
|
||||||
<ul
|
<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"
|
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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
|
import UserIcon from './UserIcon.vue'
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
// Compute navigation classes based on settings
|
// Compute navigation classes based on settings
|
||||||
|
|||||||
323
src/components/common/UserIcon.vue
Normal file
323
src/components/common/UserIcon.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user