Implement intelligent menu filtering that hides self-referencing links and shows all applicable pages based on user role and authentication state. Changes: - Menu component now uses currentPath to filter out the current page link - Added computed properties to detect which page user is on - Fixed profile, admin, and users pages to dynamically detect admin status from API - Added menu to forgot-password page for consistent navigation - All pages now pass correct authentication state to Menu component This ensures menus always show relevant navigation options while avoiding redundant links to the current page. Admin users now see all admin options (Manage Sermons, Manage Users) regardless of which page they're on, except the current one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
391 lines
13 KiB
Vue
391 lines
13 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-4">
|
||
<NuxtLink to="/">
|
||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
||
</NuxtLink>
|
||
<ClientOnly>
|
||
<Menu
|
||
:is-authenticated="isAuthenticated"
|
||
:is-admin="isAdmin"
|
||
:show-home="true"
|
||
:current-path="route.fullPath"
|
||
/>
|
||
</ClientOnly>
|
||
</div>
|
||
<h1 class="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main class="flex-1 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12 w-full">
|
||
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
||
<div class="px-6 py-4 border-b border-gray-200">
|
||
<h2 class="text-xl font-semibold text-gray-900">Profile Information</h2>
|
||
</div>
|
||
|
||
<form @submit.prevent="handleSubmit" class="p-6 space-y-6">
|
||
<!-- Username (read-only) -->
|
||
<div>
|
||
<label for="username" class="block text-sm font-medium text-gray-700">
|
||
Username
|
||
</label>
|
||
<input
|
||
id="username"
|
||
v-model="profile.username"
|
||
type="text"
|
||
disabled
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-500 cursor-not-allowed"
|
||
/>
|
||
<p class="mt-1 text-xs text-gray-500">Username cannot be changed</p>
|
||
</div>
|
||
|
||
<!-- First Name -->
|
||
<div>
|
||
<label for="firstName" class="block text-sm font-medium text-gray-700">
|
||
First Name
|
||
</label>
|
||
<input
|
||
id="firstName"
|
||
v-model="profile.firstName"
|
||
type="text"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Last Name -->
|
||
<div>
|
||
<label for="lastName" class="block text-sm font-medium text-gray-700">
|
||
Last Name
|
||
</label>
|
||
<input
|
||
id="lastName"
|
||
v-model="profile.lastName"
|
||
type="text"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Email -->
|
||
<div>
|
||
<label for="email" class="block text-sm font-medium text-gray-700">
|
||
Email
|
||
</label>
|
||
<input
|
||
id="email"
|
||
v-model="profile.email"
|
||
type="email"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Change Password Section -->
|
||
<div class="pt-6 border-t border-gray-200">
|
||
<h3 class="text-lg font-medium text-gray-900 mb-4">Change Password (Optional)</h3>
|
||
|
||
<!-- Current Password -->
|
||
<div class="mb-4">
|
||
<label for="currentPassword" class="block text-sm font-medium text-gray-700">
|
||
Current Password
|
||
</label>
|
||
<input
|
||
id="currentPassword"
|
||
v-model="passwords.current"
|
||
type="password"
|
||
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>
|
||
|
||
<!-- New Password -->
|
||
<div class="mb-4">
|
||
<label for="newPassword" class="block text-sm font-medium text-gray-700">
|
||
New Password
|
||
</label>
|
||
<input
|
||
id="newPassword"
|
||
v-model="passwords.new"
|
||
type="password"
|
||
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 -->
|
||
<div v-if="passwords.new.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 New Password -->
|
||
<div>
|
||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
|
||
Confirm New Password
|
||
</label>
|
||
<input
|
||
id="confirmPassword"
|
||
v-model="passwords.confirm"
|
||
type="password"
|
||
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="passwords.confirm.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>
|
||
|
||
<div v-if="error" class="text-red-600 text-sm">
|
||
{{ error }}
|
||
</div>
|
||
|
||
<div v-if="success" class="text-green-600 text-sm">
|
||
{{ success }}
|
||
</div>
|
||
|
||
<div class="flex gap-4">
|
||
<button
|
||
type="submit"
|
||
:disabled="loading"
|
||
class="flex-1 px-4 py-2 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"
|
||
>
|
||
{{ loading ? 'Saving...' : 'Save Changes' }}
|
||
</button>
|
||
<NuxtLink
|
||
to="/"
|
||
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-center flex items-center justify-center"
|
||
>
|
||
Cancel
|
||
</NuxtLink>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Delete Profile Section -->
|
||
<div class="px-6 py-4 border-t border-gray-200 bg-red-50">
|
||
<h3 class="text-lg font-medium text-red-900 mb-2">Delete Profile</h3>
|
||
<p class="text-sm text-red-700 mb-4">
|
||
⚠️ Warning: Deleting your profile will permanently remove your account and all sermon notes that you haven't emailed or downloaded. This action cannot be undone.
|
||
</p>
|
||
<button
|
||
@click="showDeleteConfirmation = true"
|
||
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||
>
|
||
Delete My Profile
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Confirmation Modal -->
|
||
<div
|
||
v-if="showDeleteConfirmation"
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||
@click.self="showDeleteConfirmation = false"
|
||
>
|
||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||
<h3 class="text-xl font-bold text-gray-900 mb-4">Confirm Profile Deletion</h3>
|
||
<p class="text-gray-700 mb-4">
|
||
Are you absolutely sure you want to delete your profile? This will:
|
||
</p>
|
||
<ul class="list-disc list-inside text-gray-700 mb-6 space-y-2">
|
||
<li>Permanently delete your account</li>
|
||
<li>Remove all your sermon notes</li>
|
||
</ul>
|
||
<p class="text-sm text-red-600 font-semibold mb-6">
|
||
This cannot be undone. Make sure you have emailed or downloaded any notes you want to keep!
|
||
</p>
|
||
<div class="flex gap-4">
|
||
<button
|
||
@click="handleDeleteProfile"
|
||
:disabled="deleteLoading"
|
||
class="flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50"
|
||
>
|
||
{{ deleteLoading ? 'Deleting...' : 'Yes, Delete My Profile' }}
|
||
</button>
|
||
<button
|
||
@click="showDeleteConfirmation = false"
|
||
:disabled="deleteLoading"
|
||
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 disabled:opacity-50"
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
definePageMeta({
|
||
middleware: 'auth'
|
||
})
|
||
|
||
// 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 profile = ref({
|
||
username: '',
|
||
firstName: '',
|
||
lastName: '',
|
||
email: ''
|
||
})
|
||
|
||
const passwords = ref({
|
||
current: '',
|
||
new: '',
|
||
confirm: ''
|
||
})
|
||
|
||
const error = ref('')
|
||
const success = ref('')
|
||
const loading = ref(false)
|
||
const showDeleteConfirmation = ref(false)
|
||
const deleteLoading = ref(false)
|
||
|
||
const passwordRequirements = computed(() => ({
|
||
minLength: passwords.value.new.length >= 8,
|
||
hasUppercase: /[A-Z]/.test(passwords.value.new),
|
||
hasLowercase: /[a-z]/.test(passwords.value.new),
|
||
hasNumberOrSymbol: /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(passwords.value.new)
|
||
}))
|
||
|
||
const isPasswordValid = computed(() => {
|
||
if (passwords.value.new.length === 0) return true // Password change is optional
|
||
return passwordRequirements.value.minLength &&
|
||
passwordRequirements.value.hasUppercase &&
|
||
passwordRequirements.value.hasLowercase &&
|
||
passwordRequirements.value.hasNumberOrSymbol
|
||
})
|
||
|
||
const passwordsMatch = computed(() => {
|
||
return passwords.value.new === passwords.value.confirm
|
||
})
|
||
|
||
async function loadProfile() {
|
||
try {
|
||
const data = await $fetch('/api/profile')
|
||
profile.value = data
|
||
} catch (e: any) {
|
||
error.value = e.data?.message || 'Failed to load profile'
|
||
}
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
error.value = ''
|
||
success.value = ''
|
||
|
||
// Validate password change if attempted
|
||
if (passwords.value.new || passwords.value.current || passwords.value.confirm) {
|
||
if (!passwords.value.current) {
|
||
error.value = 'Current password is required to change password'
|
||
return
|
||
}
|
||
if (!passwords.value.new) {
|
||
error.value = 'New password is required'
|
||
return
|
||
}
|
||
if (!isPasswordValid.value) {
|
||
error.value = 'Please meet all password requirements'
|
||
return
|
||
}
|
||
if (!passwordsMatch.value) {
|
||
error.value = 'New passwords do not match'
|
||
return
|
||
}
|
||
}
|
||
|
||
loading.value = true
|
||
|
||
try {
|
||
await $fetch('/api/profile/update', {
|
||
method: 'PUT',
|
||
body: {
|
||
firstName: profile.value.firstName,
|
||
lastName: profile.value.lastName,
|
||
email: profile.value.email,
|
||
currentPassword: passwords.value.current || undefined,
|
||
newPassword: passwords.value.new || undefined,
|
||
confirmPassword: passwords.value.confirm || undefined
|
||
}
|
||
})
|
||
|
||
success.value = 'Profile updated successfully!'
|
||
|
||
// Clear password fields
|
||
passwords.value = {
|
||
current: '',
|
||
new: '',
|
||
confirm: ''
|
||
}
|
||
|
||
// Redirect to home page after successful save
|
||
setTimeout(() => {
|
||
navigateTo('/')
|
||
}, 1000)
|
||
} catch (e: any) {
|
||
error.value = e.data?.message || 'Failed to update profile'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleLogout() {
|
||
await $fetch('/api/auth/logout', { method: 'POST' })
|
||
await navigateTo('/login')
|
||
}
|
||
|
||
async function handleDeleteProfile() {
|
||
deleteLoading.value = true
|
||
|
||
try {
|
||
await $fetch('/api/profile/delete', {
|
||
method: 'DELETE'
|
||
})
|
||
|
||
// Redirect to home page after successful deletion
|
||
await navigateTo('/')
|
||
} catch (e: any) {
|
||
error.value = e.data?.message || 'Failed to delete profile'
|
||
showDeleteConfirmation.value = false
|
||
} finally {
|
||
deleteLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadProfile()
|
||
})
|
||
</script>
|