390 lines
13 KiB
Vue
390 lines
13 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-gray-50">
|
||
<!-- 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>
|
||
<div class="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="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||
<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 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>
|