Files
nlcc-itinerary/pages/profile.vue
Joshua Ryder 3f1c573a67 feat: Make menu context-aware and hide current page links
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>
2025-11-07 09:40:42 -05:00

391 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>