Files
nlcc-itinerary/pages/login.vue
Joshua Ryder 599b2f0685 feat: Add navigation menu to login/register page
Add consistent header with logo and hamburger menu to authentication pages, matching the navigation pattern used throughout the site. The menu adapts to authentication state and provides easy access to home and other sections.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:36:17 -05:00

313 lines
10 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-4">
<div class="flex items-center justify-between">
<NuxtLink to="/">
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
</NuxtLink>
<ClientOnly fallback-tag="div">
<Menu
:is-authenticated="isAuthenticated"
:is-admin="isAdmin"
:show-home="true"
/>
</ClientOnly>
</div>
</div>
</header>
<div class="flex-1 flex items-center justify-center px-4">
<div class="max-w-md w-full">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900">{{ isRegistering ? 'Create Account' : 'Log In' }}</h2>
<p class="mt-2 text-sm text-gray-600">{{ isRegistering ? 'Create a new account' : 'Sign in to your account' }}</p>
</div>
<div class="bg-white py-8 px-6 shadow-lg rounded-lg">
<form @submit.prevent="isRegistering ? handleRegister() : handleLogin()" class="space-y-6">
<!-- First Name (only show when registering) -->
<div v-if="isRegistering">
<label for="firstName" class="block text-sm font-medium text-gray-700">
First Name
</label>
<input
id="firstName"
v-model="firstName"
type="text"
required
autocomplete="given-name"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Last Name (only show when registering) -->
<div v-if="isRegistering">
<label for="lastName" class="block text-sm font-medium text-gray-700">
Last Name
</label>
<input
id="lastName"
v-model="lastName"
type="text"
required
autocomplete="family-name"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Email (only show when registering) -->
<div v-if="isRegistering">
<label for="email" class="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
v-model="email"
type="email"
required
autocomplete="email"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label for="username" class="block text-sm font-medium text-gray-700">
Username
</label>
<input
id="username"
v-model="username"
type="text"
required
autocapitalize="none"
autocomplete="username"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
v-model="password"
type="password"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<!-- Password Requirements (only show when registering) -->
<div v-if="isRegistering && password.length > 0" class="mt-2 space-y-1 text-xs">
<div :class="passwordRequirements.minLength ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.minLength"></span>
<span v-else></span>
At least 8 characters
</div>
<div :class="passwordRequirements.hasUppercase ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasUppercase"></span>
<span v-else></span>
One uppercase letter
</div>
<div :class="passwordRequirements.hasLowercase ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasLowercase"></span>
<span v-else></span>
One lowercase letter
</div>
<div :class="passwordRequirements.hasNumberOrSymbol ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasNumberOrSymbol"></span>
<span v-else></span>
One number or symbol
</div>
</div>
</div>
<!-- Confirm Password (only show when registering) -->
<div v-if="isRegistering">
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
Confirm Password
</label>
<input
id="confirmPassword"
v-model="confirmPassword"
type="password"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<!-- Password Match Indicator -->
<div v-if="confirmPassword.length > 0" class="mt-2 text-xs">
<div :class="passwordsMatch ? 'text-green-600' : 'text-red-600'">
<span v-if="passwordsMatch"> Passwords match</span>
<span v-else> Passwords do not match</span>
</div>
</div>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{{ loading ? (isRegistering ? 'Creating account...' : 'Signing in...') : (isRegistering ? 'Create Account' : 'Sign In') }}
</button>
</form>
<div class="mt-6 text-center space-y-3">
<button
@click="toggleMode"
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{{ isRegistering ? 'Already have an account? Sign in' : "Don't have an account? Create one" }}
</button>
<div v-if="!isRegistering">
<NuxtLink to="/forgot-password" class="text-sm text-blue-600 hover:text-blue-700">
Forgot Password?
</NuxtLink>
</div>
<div>
<NuxtLink to="/" class="text-sm text-gray-600 hover:text-gray-700">
Back to Sermons
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
<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 route = useRoute()
const router = useRouter()
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const email = ref('')
const firstName = ref('')
const lastName = ref('')
const error = ref('')
const loading = ref(false)
// Initialize from URL query parameter
const isRegistering = ref(route.query.mode === 'register')
const passwordRequirements = computed(() => ({
minLength: password.value.length >= 8,
hasUppercase: /[A-Z]/.test(password.value),
hasLowercase: /[a-z]/.test(password.value),
hasNumberOrSymbol: /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password.value)
}))
const isPasswordValid = computed(() => {
return passwordRequirements.value.minLength &&
passwordRequirements.value.hasUppercase &&
passwordRequirements.value.hasLowercase &&
passwordRequirements.value.hasNumberOrSymbol
})
const passwordsMatch = computed(() => {
return password.value === confirmPassword.value
})
function toggleMode() {
isRegistering.value = !isRegistering.value
error.value = ''
username.value = ''
password.value = ''
confirmPassword.value = ''
email.value = ''
firstName.value = ''
lastName.value = ''
// Update URL to reflect mode
const query = { ...route.query }
if (isRegistering.value) {
query.mode = 'register'
} else {
delete query.mode
}
router.replace({ query })
}
async function handleLogin() {
error.value = ''
loading.value = true
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: {
username: username.value,
password: password.value
}
})
if (response.success) {
// Check if there's a redirect parameter
const redirect = route.query.redirect as string
await navigateTo(redirect || '/')
}
} catch (e: any) {
error.value = e.data?.message || 'Invalid credentials'
} finally {
loading.value = false
}
}
async function handleRegister() {
error.value = ''
// Validate password requirements
if (!isPasswordValid.value) {
error.value = 'Please meet all password requirements'
return
}
// Validate passwords match
if (!passwordsMatch.value) {
error.value = 'Passwords do not match'
return
}
loading.value = true
try {
const response = await $fetch('/api/auth/register', {
method: 'POST',
body: {
username: username.value,
password: password.value,
email: email.value,
firstName: firstName.value,
lastName: lastName.value
}
})
if (response.success) {
// Check if there's a redirect parameter
const redirect = route.query.redirect as string
await navigateTo(redirect || '/')
}
} catch (e: any) {
error.value = e.data?.message || 'Registration failed'
} finally {
loading.value = false
}
}
</script>