feat: Add mobile hamburger menu and Register button

Implemented comprehensive navigation improvements to make registration
more discoverable and provide a cleaner mobile experience.

## Mobile Improvements
- Created reusable MobileMenu component with hamburger icon
- Replaces cluttered button layout with clean dropdown menu
- Includes Home, Profile, Admin links, and authentication options
- Auto-closes when clicking outside or navigating
- Smooth transition animations

## Register Button Added
- Sermon page: Added Register button next to Login in notes section
- Sermon page: Added Register button in header for non-authenticated users
- Home page: Added Register button next to Login for easy discovery
- All Register links redirect to login page in register mode

## Navigation Consistency
- Home button now visible on sermon pages (desktop and mobile)
- Consistent navigation experience across all pages
- Mobile menu available on both home and sermon pages
- Better mobile UX with less header crowding

Benefits:
- Users can easily find how to register
- Cleaner mobile interface
- Consistent navigation patterns
- Better discoverability of account features

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-06 09:02:41 -05:00
parent 514c72a4fa
commit c7dc88cdf6
4 changed files with 230 additions and 80 deletions

151
components/MobileMenu.vue Normal file
View File

@@ -0,0 +1,151 @@
<template>
<div class="relative">
<!-- Hamburger Button -->
<button
@click="isOpen = !isOpen"
class="p-2 text-gray-700 hover:bg-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
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 route = useRoute()
const loginRedirectUrl = computed(() => {
const redirectPath = props.currentPath || route.fullPath
return `/login?redirect=${encodeURIComponent(redirectPath)}`
})
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) => {
const target = event.target as HTMLElement
if (isOpen.value && !target.closest('.relative')) {
isOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>

View File

@@ -11,20 +11,12 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<QRCodeButton v-if="sermon" :sermon="sermon" /> <QRCodeButton v-if="sermon" :sermon="sermon" />
<ClientOnly fallback-tag="div"> <ClientOnly fallback-tag="div">
<button <MobileMenu
v-if="isAuthenticated" :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>
<NuxtLink
v-else
: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>
</ClientOnly> </ClientOnly>
</div> </div>
</div> </div>
@@ -49,6 +41,12 @@
<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-end gap-4">
<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"> <div class="flex flex-col gap-1">
<label for="font-size" class="text-xs font-medium text-gray-700">Font Size:</label> <label for="font-size" class="text-xs font-medium text-gray-700">Font Size:</label>
<select <select
@@ -63,20 +61,28 @@
</div> </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">
<button <button
v-if="isAuthenticated"
@click="handleLogout" @click="handleLogout"
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100 whitespace-nowrap" class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100 whitespace-nowrap"
> >
Log Out Log Out
</button> </button>
</template>
<template v-else>
<NuxtLink <NuxtLink
v-else
:to="`/login?redirect=${encodeURIComponent(route.fullPath)}`" :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" 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 Log In
</NuxtLink> </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>
@@ -208,12 +214,20 @@
</svg> </svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">Want to take notes?</h3> <h3 class="text-lg font-medium text-gray-900 mb-2">Want to take notes?</h3>
<p class="text-gray-600 mb-4">Log in or create an account to save your sermon notes!</p> <p class="text-gray-600 mb-4">Log in or create an account to save your sermon notes!</p>
<div class="flex gap-3 justify-center">
<NuxtLink <NuxtLink
:to="`/login?redirect=${encodeURIComponent(route.fullPath)}`" :to="`/login?redirect=${encodeURIComponent(route.fullPath)}`"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium" class="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium"
> >
Log In Log In
</NuxtLink> </NuxtLink>
<NuxtLink
to="/register"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Register
</NuxtLink>
</div>
</div> </div>
</ClientOnly> </ClientOnly>
</section> </section>

View File

@@ -8,47 +8,13 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<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" />
<ClientOnly fallback-tag="div"> <ClientOnly fallback-tag="div">
<template v-if="isAuthenticated"> <MobileMenu
<div class="flex items-center gap-2"> :is-authenticated="isAuthenticated"
<NuxtLink :is-admin="isAdmin"
to="/profile" :show-home="false"
class="px-3 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 whitespace-nowrap" />
>
Edit Profile
</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>
</template>
<NuxtLink
v-else
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>
</ClientOnly> </ClientOnly>
</div> </div>
<ClientOnly fallback-tag="div">
<div v-if="isAuthenticated && isAdmin" class="flex items-center justify-center gap-2">
<NuxtLink
to="/admin"
class="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium text-xs whitespace-nowrap"
>
Manage Sermons
</NuxtLink>
<NuxtLink
to="/users"
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium text-xs whitespace-nowrap"
>
Manage Users
</NuxtLink>
</div>
</ClientOnly>
</div> </div>
<!-- Desktop Layout --> <!-- Desktop Layout -->
@@ -86,13 +52,20 @@
Log Out Log Out
</button> </button>
</template> </template>
<template v-else>
<NuxtLink <NuxtLink
v-else
to="/login" to="/login"
class="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 rounded-md hover:bg-green-100" class="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 rounded-md hover:bg-green-100"
> >
Log In Log In
</NuxtLink> </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> <template #fallback>
<div class="h-10"></div> <div class="h-10"></div>
</template> </template>

12
pages/register.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div></div>
</template>
<script setup lang="ts">
// Redirect to login page in register mode
definePageMeta({
middleware: () => {
return navigateTo('/login?mode=register')
}
})
</script>