317 lines
10 KiB
Vue
317 lines
10 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-4">
|
|
<NuxtLink
|
|
to="/"
|
|
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
|
>
|
|
Home
|
|
</NuxtLink>
|
|
<button
|
|
@click="handleLogout"
|
|
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
|
>
|
|
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"
|
|
>
|
|
Cancel
|
|
</NuxtLink>
|
|
</div>
|
|
</form>
|
|
</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 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: ''
|
|
}
|
|
|
|
// Reload profile data
|
|
await loadProfile()
|
|
} 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')
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadProfile()
|
|
})
|
|
</script>
|