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>
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative" ref="menuContainer">
|
||||||
<!-- Hamburger Button -->
|
<!-- Hamburger Button -->
|
||||||
<button
|
<button
|
||||||
@click="isOpen = !isOpen"
|
@click="toggleMenu"
|
||||||
class="p-2 text-gray-700 hover:bg-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
: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"
|
aria-label="Menu"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -120,6 +121,8 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
const isToggling = ref(false)
|
||||||
|
const menuContainer = ref<HTMLElement | null>(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const loginRedirectUrl = computed(() => {
|
const loginRedirectUrl = computed(() => {
|
||||||
@@ -127,6 +130,19 @@ const loginRedirectUrl = computed(() => {
|
|||||||
return `/login?redirect=${encodeURIComponent(redirectPath)}`
|
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 () => {
|
const handleLogout = async () => {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
await $fetch('/api/auth/logout', { method: 'POST' })
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
@@ -135,17 +151,21 @@ const handleLogout = async () => {
|
|||||||
|
|
||||||
// Close menu when clicking outside
|
// Close menu when clicking outside
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (!isOpen.value || !menuContainer.value) return
|
||||||
|
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (isOpen.value && !target.closest('.relative')) {
|
// Check if click is outside the menu container
|
||||||
|
if (!menuContainer.value.contains(target)) {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleClickOutside)
|
// Use capture phase to ensure we catch the event before other handlers
|
||||||
|
document.addEventListener('click', handleClickOutside, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside)
|
document.removeEventListener('click', handleClickOutside, true)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -2,90 +2,35 @@
|
|||||||
<div class="min-h-screen bg-gray-50 flex flex-col">
|
<div class="min-h-screen bg-gray-50 flex flex-col">
|
||||||
<header class="bg-white shadow-sm">
|
<header class="bg-white shadow-sm">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<!-- Mobile Layout -->
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="md:hidden">
|
<NuxtLink to="/" class="hover:opacity-80">
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<NuxtLink to="/" class="hover:opacity-80">
|
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<QRCodeButton v-if="sermon" :sermon="sermon" />
|
|
||||||
<ClientOnly fallback-tag="div">
|
|
||||||
<MobileMenu
|
|
||||||
:is-authenticated="isAuthenticated"
|
|
||||||
:is-admin="isAdmin"
|
|
||||||
:show-home="true"
|
|
||||||
:current-path="route.fullPath"
|
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Font Size Selector Below Buttons on Mobile -->
|
|
||||||
<div class="flex items-center justify-end gap-2">
|
|
||||||
<label for="font-size-mobile" class="text-xs font-medium text-gray-700 whitespace-nowrap">Size:</label>
|
|
||||||
<select
|
|
||||||
id="font-size-mobile"
|
|
||||||
v-model="fontSize"
|
|
||||||
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
||||||
>
|
|
||||||
<option value="small">Small</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="large">Large</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Layout -->
|
|
||||||
<div class="hidden md:flex items-center justify-between">
|
|
||||||
<NuxtLink to="/" class="flex items-center space-x-4 hover:opacity-80">
|
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div class="flex items-end gap-4">
|
<div class="flex items-center gap-2">
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
🏠 Home
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="font-size" class="text-xs font-medium text-gray-700">Font Size:</label>
|
|
||||||
<select
|
|
||||||
id="font-size"
|
|
||||||
v-model="fontSize"
|
|
||||||
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
||||||
>
|
|
||||||
<option value="small">Small</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="large">Large</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<QRCodeButton v-if="sermon" :sermon="sermon" />
|
<QRCodeButton v-if="sermon" :sermon="sermon" />
|
||||||
<ClientOnly fallback-tag="div">
|
<ClientOnly fallback-tag="div">
|
||||||
<template v-if="isAuthenticated">
|
<Menu
|
||||||
<button
|
:is-authenticated="isAuthenticated"
|
||||||
@click="handleLogout"
|
:is-admin="isAdmin"
|
||||||
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100 whitespace-nowrap"
|
:show-home="true"
|
||||||
>
|
:current-path="route.fullPath"
|
||||||
Log Out
|
/>
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/login?redirect=${encodeURIComponent(route.fullPath)}`"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 rounded-md hover:bg-green-100 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/register"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Font Size Selector -->
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<label for="font-size" class="text-xs font-medium text-gray-700 whitespace-nowrap">Font Size:</label>
|
||||||
|
<select
|
||||||
|
id="font-size"
|
||||||
|
v-model="fontSize"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||||
|
>
|
||||||
|
<option value="small">Small</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="large">Large</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -6,34 +6,14 @@
|
|||||||
<NuxtLink to="/">
|
<NuxtLink to="/">
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<ClientOnly>
|
||||||
<!-- Mobile Menu -->
|
<Menu
|
||||||
<div class="md:hidden">
|
:is-authenticated="true"
|
||||||
<ClientOnly>
|
:is-admin="true"
|
||||||
<MobileMenu
|
:show-home="true"
|
||||||
:is-authenticated="true"
|
:current-path="route.fullPath"
|
||||||
:is-admin="true"
|
/>
|
||||||
:show-home="true"
|
</ClientOnly>
|
||||||
:current-path="route.fullPath"
|
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
|
||||||
<div class="hidden md:flex items-center gap-4">
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</NuxtLink>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Manage Sermons</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Manage Sermons</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,74 +3,15 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="bg-white shadow-sm">
|
<header class="bg-white shadow-sm">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<!-- Mobile Layout -->
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="md:hidden">
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||||
<div class="flex items-center justify-between mb-3">
|
<ClientOnly fallback-tag="div">
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
<Menu
|
||||||
<ClientOnly fallback-tag="div">
|
:is-authenticated="isAuthenticated"
|
||||||
<MobileMenu
|
:is-admin="isAdmin"
|
||||||
:is-authenticated="isAuthenticated"
|
:show-home="false"
|
||||||
:is-admin="isAdmin"
|
/>
|
||||||
:show-home="false"
|
</ClientOnly>
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Layout -->
|
|
||||||
<div class="hidden md:flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<ClientOnly fallback-tag="div">
|
|
||||||
<template v-if="isAuthenticated">
|
|
||||||
<NuxtLink
|
|
||||||
v-if="isAdmin"
|
|
||||||
to="/admin"
|
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium text-sm whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Manage Sermons
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="isAdmin"
|
|
||||||
to="/users"
|
|
||||||
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium text-sm whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Manage Users
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/profile"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
Edit Profile
|
|
||||||
</NuxtLink>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<NuxtLink
|
|
||||||
to="/login"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 rounded-md hover:bg-green-100"
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/register"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
<template #fallback>
|
|
||||||
<div class="h-10"></div>
|
|
||||||
</template>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-6">
|
<div class="text-center mt-6">
|
||||||
|
|||||||
@@ -7,34 +7,14 @@
|
|||||||
<NuxtLink to="/">
|
<NuxtLink to="/">
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<ClientOnly>
|
||||||
<!-- Mobile Menu -->
|
<Menu
|
||||||
<div class="md:hidden">
|
:is-authenticated="true"
|
||||||
<ClientOnly>
|
:is-admin="false"
|
||||||
<MobileMenu
|
:show-home="true"
|
||||||
:is-authenticated="true"
|
:current-path="route.fullPath"
|
||||||
:is-admin="false"
|
/>
|
||||||
:show-home="true"
|
</ClientOnly>
|
||||||
:current-path="route.fullPath"
|
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
|
||||||
<div class="hidden md:flex items-center gap-2">
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="px-3 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</NuxtLink>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="px-3 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,34 +7,14 @@
|
|||||||
<NuxtLink to="/">
|
<NuxtLink to="/">
|
||||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<ClientOnly>
|
||||||
<!-- Mobile Menu -->
|
<Menu
|
||||||
<div class="md:hidden">
|
:is-authenticated="true"
|
||||||
<ClientOnly>
|
:is-admin="true"
|
||||||
<MobileMenu
|
:show-home="true"
|
||||||
:is-authenticated="true"
|
:current-path="route.fullPath"
|
||||||
:is-admin="true"
|
/>
|
||||||
:show-home="true"
|
</ClientOnly>
|
||||||
:current-path="route.fullPath"
|
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
|
||||||
<div class="hidden md:flex items-center gap-4">
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</NuxtLink>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Manage Users</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Manage Users</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user