Files
nlcc-itinerary/pages/index.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

247 lines
8.0 KiB
Vue

<template>
<div class="min-h-screen bg-gray-50 flex flex-col">
<!-- Header -->
<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="flex items-center justify-between mb-3">
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
<ClientOnly fallback-tag="div">
<Menu
:is-authenticated="isAuthenticated"
:is-admin="isAdmin"
:show-home="false"
/>
</ClientOnly>
</div>
<div class="text-center mt-6">
<ClientOnly>
<h2 v-if="isAuthenticated && firstName" class="text-xl text-gray-700 mb-2">
{{ greeting }}
</h2>
</ClientOnly>
<h1 class="text-3xl font-bold text-gray-900">
Welcome to New Life!
</h1>
</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12 w-full">
<ClientOnly>
<!-- Today's Sermon -->
<div v-if="todaysSermon">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Today's Sermon</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<SermonCard :sermon="todaysSermon" />
</div>
</div>
<!-- Upcoming Sermons -->
<div v-if="upcomingSermons.length > 0">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Upcoming Sermons</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<SermonCard v-for="sermon in upcomingSermons" :key="sermon.id" :sermon="sermon" />
</div>
</div>
<!-- Empty State (if no current or upcoming sermons) -->
<div v-if="!hasCurrentSermons" class="text-center py-12">
<p class="text-gray-500 text-lg">No upcoming sermons available yet.</p>
</div>
<template #fallback>
<div class="text-center py-12">
<p class="text-gray-500 text-lg">Loading sermons...</p>
</div>
</template>
</ClientOnly>
<!-- Previous Sermons Dropdown -->
<div v-if="previousSermons.length > 0" class="max-w-2xl mx-auto">
<h2 class="text-2xl font-semibold text-gray-900 mb-4 text-center">Previous Sermons</h2>
<p class="text-center text-gray-700 mb-4">Use the dropdown below to view a previous sermon</p>
<div class="flex flex-col sm:flex-row gap-3">
<select
v-model="selectedSermonSlug"
class="flex-1 px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-base"
>
<option value="">-- Select a previous sermon --</option>
<option v-for="sermon in previousSermons" :key="sermon.id" :value="sermon.slug">
{{ sermon.title }} ({{ formatDate(sermon.date) }})
</option>
</select>
<button
@click="navigateToSermon"
:disabled="!selectedSermonSlug"
class="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed font-medium sm:whitespace-nowrap"
>
View
</button>
</div>
</div>
</main>
<Footer />
</div>
</template>
<script setup lang="ts">
// Check authentication status
const { data: authData } = await useFetch('/api/auth/verify')
const isAuthenticated = computed(() => authData.value?.authenticated || false)
const isAdmin = computed(() => authData.value?.isAdmin || false)
const firstName = computed(() => authData.value?.firstName || '')
// Time-based greeting
const greeting = computed(() => {
if (!firstName.value) return ''
const hour = new Date().getHours()
let timeGreeting = ''
if (hour >= 5 && hour < 12) {
timeGreeting = 'Good morning'
} else if (hour >= 12 && hour < 18) {
timeGreeting = 'Good afternoon'
} else if (hour >= 18 && hour < 21) {
timeGreeting = 'Good evening'
} else {
timeGreeting = 'Greetings'
}
return `${timeGreeting}, ${firstName.value}!`
})
// Fetch all sermons in a single request (more efficient than two separate calls)
const { data: allSermons } = await useFetch('/api/sermons?includeArchived=true')
const selectedSermonSlug = ref('')
// Filter active (non-archived) sermons client-side
const activeSermons = computed(() => {
if (!allSermons.value) return []
return allSermons.value.filter((s: any) => s.archived !== 1)
})
// Get today's sermon (if one exists for today's date)
const todaysSermon = computed(() => {
if (activeSermons.value.length === 0) return null
const today = new Date()
today.setHours(0, 0, 0, 0)
for (const s of activeSermons.value) {
// Check primary date
const sermonDate = new Date(s.date + 'T00:00:00')
sermonDate.setHours(0, 0, 0, 0)
if (sermonDate.getTime() === today.getTime()) {
return { ...s, displayDate: s.date }
}
// Check additional dates if they exist
if (s.dates) {
try {
const additionalDates = JSON.parse(s.dates)
const matchingDate = additionalDates.find((dateStr: string) => {
const additionalDate = new Date(dateStr + 'T00:00:00')
additionalDate.setHours(0, 0, 0, 0)
return additionalDate.getTime() === today.getTime()
})
if (matchingDate) {
return { ...s, displayDate: matchingDate }
}
} catch {
// Continue to next sermon if parsing fails
}
}
}
return null
})
// Get upcoming sermons (future dates)
const upcomingSermons = computed(() => {
if (activeSermons.value.length === 0) return []
const today = new Date()
today.setHours(0, 0, 0, 0)
return activeSermons.value
.map((s: any) => {
// Check primary date
const sermonDate = new Date(s.date + 'T00:00:00')
sermonDate.setHours(0, 0, 0, 0)
// If primary date is in the future, use it
if (sermonDate.getTime() > today.getTime()) {
return { ...s, displayDate: s.date }
}
// Check additional dates if they exist
if (s.dates) {
try {
const additionalDates = JSON.parse(s.dates)
// Find the first future date from additional dates
const futureDate = additionalDates.find((dateStr: string) => {
const additionalDate = new Date(dateStr + 'T00:00:00')
additionalDate.setHours(0, 0, 0, 0)
return additionalDate.getTime() > today.getTime()
})
if (futureDate) {
return { ...s, displayDate: futureDate }
}
} catch {
return null
}
}
return null
})
.filter((s: any) => s !== null)
})
// Get archived sermons only for the previous sermons dropdown
const previousSermons = computed(() => {
if (!allSermons.value) return []
return allSermons.value.filter((s: any) => s.archived === 1)
})
// Check if there are any current or upcoming sermons
const hasCurrentSermons = computed(() => {
return todaysSermon.value !== null || upcomingSermons.value.length > 0
})
function formatDate(dateString: string) {
// Add T00:00:00 to ensure the date is interpreted as local time, not UTC
const date = new Date(dateString + 'T00:00:00')
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
function navigateToSermon() {
if (selectedSermonSlug.value) {
navigateTo(`/${selectedSermonSlug.value}`)
}
}
async function handleLogout() {
await $fetch('/api/auth/logout', { method: 'POST' })
window.location.reload()
}
// Watch for authentication changes (e.g., if user is deleted)
watch(() => authData.value?.authenticated, (newAuth, oldAuth) => {
// If user was authenticated but now isn't (and it's not initial load)
if (oldAuth === true && newAuth === false) {
window.location.reload()
}
})
</script>