- Updated profile.vue to use MobileMenu on mobile, desktop buttons on desktop - Updated admin.vue to use MobileMenu on mobile, desktop buttons on desktop - Updated users.vue to use MobileMenu on mobile, desktop buttons on desktop - All pages with header navigation now have consistent mobile UX - Mobile menu provides clean, organized navigation with all options - Desktop retains traditional button layout for familiarity This ensures consistent navigation experience across the entire application, with the hamburger menu appearing on all pages with headers when viewed on mobile devices. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
406 lines
14 KiB
Vue
406 lines
14 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>
|
||
|
||
<!-- Mobile Menu -->
|
||
<div class="md:hidden">
|
||
<ClientOnly>
|
||
<MobileMenu
|
||
:is-authenticated="true"
|
||
:is-admin="false"
|
||
:show-home="true"
|
||
:current-path="route.fullPath"
|
||
/>
|
||
</ClientOnly>
|
||
</div>
|
||
|
||
<!-- Desktop Navigation -->
|
||
<div class="hidden md:flex items-center gap-2">
|
||
<NuxtLink
|
||
to="/"
|
||
class="px-3 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 whitespace-nowrap"
|
||
>
|
||
Home
|
||
</NuxtLink>
|
||
<button
|
||
@click="handleLogout"
|
||
class="px-3 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100 whitespace-nowrap"
|
||
>
|
||
Log Out
|
||
</button>
|
||
</div>
|
||
</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'
|
||
})
|
||
|
||
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>
|