fix: replace Alpine dropdown with Vue reactive state in DashboardLayout
Alpine.js is not loaded in the Vue SPA bundle, causing the avatar dropdown to never open and making Settings and Log out inaccessible. Replaced x-data/ x-show/x-transition/@click.away with Vue refs, onMounted/onUnmounted click- outside listener, and Vue's built-in <Transition> component. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,19 +15,26 @@
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<!-- User dropdown -->
|
<!-- User dropdown -->
|
||||||
<div x-data="{ open: false }" class="relative">
|
<div ref="dropdownRef" class="relative">
|
||||||
<button
|
<button
|
||||||
@click="open = !open"
|
@click="dropdownOpen = !dropdownOpen"
|
||||||
class="w-9 h-9 rounded-full bg-[#bb5b3e] flex items-center justify-center text-white text-sm font-black hover:bg-[#a34a31] transition-colors"
|
class="w-9 h-9 rounded-full bg-[#bb5b3e] flex items-center justify-center text-white text-sm font-black hover:bg-[#a34a31] transition-colors"
|
||||||
|
:aria-expanded="dropdownOpen"
|
||||||
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
{{ userInitials }}
|
{{ userInitials }}
|
||||||
</button>
|
</button>
|
||||||
|
<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
|
<div
|
||||||
x-show="open"
|
v-show="dropdownOpen"
|
||||||
@click.away="open = false"
|
|
||||||
x-transition
|
|
||||||
class="absolute right-0 top-full mt-2 w-64 bg-white border border-[#e5ded7] rounded-2xl shadow-lg overflow-hidden z-50"
|
class="absolute right-0 top-full mt-2 w-64 bg-white border border-[#e5ded7] rounded-2xl shadow-lg overflow-hidden z-50"
|
||||||
style="display: none"
|
|
||||||
>
|
>
|
||||||
<div class="px-4 py-3 border-b border-[#e5ded7]">
|
<div class="px-4 py-3 border-b border-[#e5ded7]">
|
||||||
<p class="text-sm font-black text-[#4a3f3b]">{{ user?.name }}</p>
|
<p class="text-sm font-black text-[#4a3f3b]">{{ user?.name }}</p>
|
||||||
@@ -36,7 +43,7 @@
|
|||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/dashboard/settings"
|
to="/dashboard/settings"
|
||||||
@click="open = false"
|
@click="dropdownOpen = false"
|
||||||
class="flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-[#89726c] hover:bg-[#faf6f3] hover:text-[#4a3f3b] transition-colors"
|
class="flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-[#89726c] hover:bg-[#faf6f3] hover:text-[#4a3f3b] transition-colors"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="lucide:settings"></iconify-icon>
|
<iconify-icon icon="lucide:settings"></iconify-icon>
|
||||||
@@ -51,6 +58,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +92,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
|
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuth } from '../../composables/useAuth.js'
|
import { useAuth } from '../../composables/useAuth.js'
|
||||||
|
|
||||||
@@ -92,6 +100,23 @@ const { user, logout } = useAuth()
|
|||||||
const $route = useRoute()
|
const $route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const dropdownOpen = ref(false)
|
||||||
|
const dropdownRef = ref(null)
|
||||||
|
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
|
||||||
|
dropdownOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
const userInitials = computed(() => {
|
const userInitials = computed(() => {
|
||||||
if (!user.value?.name) {
|
if (!user.value?.name) {
|
||||||
return '?'
|
return '?'
|
||||||
@@ -105,6 +130,7 @@ const userInitials = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
|
dropdownOpen.value = false
|
||||||
await logout()
|
await logout()
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user