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:
Ovidiu U
2026-04-11 13:21:27 +01:00
parent ea7a5b4f10
commit 3895356b0d

View File

@@ -15,42 +15,50 @@
</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>
<div <Transition
x-show="open" enter-active-class="transition ease-out duration-100"
@click.away="open = false" enter-from-class="transform opacity-0 scale-95"
x-transition enter-to-class="transform opacity-100 scale-100"
class="absolute right-0 top-full mt-2 w-64 bg-white border border-[#e5ded7] rounded-2xl shadow-lg overflow-hidden z-50" leave-active-class="transition ease-in duration-75"
style="display: none" leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
> >
<div class="px-4 py-3 border-b border-[#e5ded7]"> <div
<p class="text-sm font-black text-[#4a3f3b]">{{ user?.name }}</p> v-show="dropdownOpen"
<p class="text-xs text-[#89726c] truncate">{{ user?.email }}</p> class="absolute right-0 top-full mt-2 w-64 bg-white border border-[#e5ded7] rounded-2xl shadow-lg overflow-hidden z-50"
>
<div class="px-4 py-3 border-b border-[#e5ded7]">
<p class="text-sm font-black text-[#4a3f3b]">{{ user?.name }}</p>
<p class="text-xs text-[#89726c] truncate">{{ user?.email }}</p>
</div>
<div class="py-1">
<RouterLink
to="/dashboard/settings"
@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"
>
<iconify-icon icon="lucide:settings"></iconify-icon>
Settings
</RouterLink>
<button
@click="handleLogout"
class="w-full 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:log-out"></iconify-icon>
Log out
</button>
</div>
</div> </div>
<div class="py-1"> </Transition>
<RouterLink
to="/dashboard/settings"
@click="open = 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"
>
<iconify-icon icon="lucide:settings"></iconify-icon>
Settings
</RouterLink>
<button
@click="handleLogout"
class="w-full 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:log-out"></iconify-icon>
Log out
</button>
</div>
</div>
</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('/')
} }