Files
nlcc-itinerary/components/Menu.vue
Joshua Ryder cee37c78c4 feat: Unify navigation with hamburger menu across all screen sizes
- Renamed MobileMenu to Menu component (no longer mobile-only)
- Added 500ms debounce to prevent accidental double-tap menu toggles
- Improved click-outside detection using ref-based containment check
- Removed mobile/desktop navigation split - menu now consistent everywhere
- All pages now use single hamburger menu on both mobile and desktop
- Simplified header layouts across index, sermon, profile, admin, and users pages

This provides a cleaner, more consistent UX with the hamburger menu available
on all screen sizes. The debounce prevents the menu from closing accidentally
when navigating between pages or double-tapping.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 09:51:09 -05:00

172 lines
4.9 KiB
Vue

<template>
<div class="relative" ref="menuContainer">
<!-- Hamburger Button -->
<button
@click="toggleMenu"
:disabled="isToggling"
class="p-2 text-gray-700 hover:bg-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
aria-label="Menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="!isOpen"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</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="isOpen"
class="absolute right-0 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
>
<div class="py-1">
<!-- Home Link -->
<NuxtLink
v-if="showHome"
to="/"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
🏠 Home
</NuxtLink>
<!-- Authentication Links -->
<ClientOnly>
<template v-if="isAuthenticated">
<!-- Profile Link -->
<NuxtLink
to="/profile"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
👤 Edit Profile
</NuxtLink>
<!-- Admin Links -->
<template v-if="isAdmin">
<NuxtLink
to="/admin"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
📝 Manage Sermons
</NuxtLink>
<NuxtLink
to="/users"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
👥 Manage Users
</NuxtLink>
</template>
<!-- Logout -->
<button
@click="handleLogout"
class="block w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-red-50"
>
🚪 Log Out
</button>
</template>
<!-- Not authenticated -->
<template v-else>
<NuxtLink
:to="loginRedirectUrl"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-green-700 hover:bg-green-50 font-medium"
>
🔑 Log In
</NuxtLink>
<NuxtLink
to="/register"
@click="isOpen = false"
class="block px-4 py-2 text-sm text-blue-700 hover:bg-blue-50 font-medium"
>
Register
</NuxtLink>
</template>
</ClientOnly>
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
isAuthenticated?: boolean
isAdmin?: boolean
showHome?: boolean
currentPath?: string
}>()
const isOpen = ref(false)
const isToggling = ref(false)
const menuContainer = ref<HTMLElement | null>(null)
const route = useRoute()
const loginRedirectUrl = computed(() => {
const redirectPath = props.currentPath || route.fullPath
return `/login?redirect=${encodeURIComponent(redirectPath)}`
})
// Toggle menu with debounce to prevent accidental double-taps
const toggleMenu = () => {
if (isToggling.value) return
isToggling.value = true
isOpen.value = !isOpen.value
// Re-enable after 500ms
setTimeout(() => {
isToggling.value = false
}, 500)
}
const handleLogout = async () => {
isOpen.value = false
await $fetch('/api/auth/logout', { method: 'POST' })
await navigateTo('/login')
}
// Close menu when clicking outside
const handleClickOutside = (event: MouseEvent) => {
if (!isOpen.value || !menuContainer.value) return
const target = event.target as HTMLElement
// Check if click is outside the menu container
if (!menuContainer.value.contains(target)) {
isOpen.value = false
}
}
onMounted(() => {
// Use capture phase to ensure we catch the event before other handlers
document.addEventListener('click', handleClickOutside, true)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside, true)
})
</script>