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>
313 lines
10 KiB
Vue
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>
|