feat: allow editing user profile settings
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
|
||||
@@ -9,21 +9,86 @@
|
||||
const { t } = useI18n();
|
||||
const isPortraitDialogOpen = ref(false);
|
||||
const isSavingPortrait = ref(false);
|
||||
const settingsError = ref(null);
|
||||
const settingsStatus = ref(null);
|
||||
const form = reactive({
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
alias: '',
|
||||
email: '',
|
||||
});
|
||||
|
||||
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
||||
const alias = computed(() => userProfileStore.alias);
|
||||
const fullname = computed(() => userProfileStore.fullname);
|
||||
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
||||
|
||||
function syncFormFromUser(user) {
|
||||
form.firstname = user?.firstname ?? '';
|
||||
form.lastname = user?.lastname ?? '';
|
||||
form.alias = user?.alias ?? '';
|
||||
form.email = user?.email ?? '';
|
||||
}
|
||||
|
||||
async function submitSettings() {
|
||||
if (!form.email.trim()) {
|
||||
settingsError.value = t('userSettings.errors.emailRequired');
|
||||
settingsStatus.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userProfileStore.user ?? {};
|
||||
const nextFirstname = form.firstname.trim();
|
||||
const nextLastname = form.lastname.trim();
|
||||
const nextAlias = form.alias.trim();
|
||||
const nextEmail = form.email.trim();
|
||||
|
||||
settingsError.value = null;
|
||||
settingsStatus.value = null;
|
||||
|
||||
try {
|
||||
if (nextFirstname !== (user.firstname ?? '') || nextLastname !== (user.lastname ?? '')) {
|
||||
await userProfileStore.changeFullname(nextFirstname, nextLastname);
|
||||
}
|
||||
|
||||
if (nextAlias !== (user.alias ?? '')) {
|
||||
await userProfileStore.changeAlias(nextAlias || null);
|
||||
}
|
||||
|
||||
if (nextEmail !== (user.email ?? '')) {
|
||||
await userProfileStore.changeEmail(nextEmail);
|
||||
}
|
||||
|
||||
settingsStatus.value = t('userSettings.saved');
|
||||
syncFormFromUser(userProfileStore.user);
|
||||
} catch (error) {
|
||||
console.error('Failed to update user settings:', error);
|
||||
settingsError.value = t('userSettings.errors.saveFailed');
|
||||
}
|
||||
}
|
||||
|
||||
async function savePortrait(result) {
|
||||
isSavingPortrait.value = true;
|
||||
settingsError.value = null;
|
||||
settingsStatus.value = null;
|
||||
|
||||
try {
|
||||
await userProfileStore.changePortrait(result.file);
|
||||
isPortraitDialogOpen.value = false;
|
||||
settingsStatus.value = t('userSettings.portraitSaved');
|
||||
} catch (error) {
|
||||
console.error('Failed to update user portrait:', error);
|
||||
settingsError.value = t('userSettings.errors.portraitFailed');
|
||||
} finally {
|
||||
isSavingPortrait.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userProfileStore.user,
|
||||
syncFormFromUser,
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,6 +115,7 @@
|
||||
|
||||
<button
|
||||
class="primary-button"
|
||||
type="button"
|
||||
@click="isPortraitDialogOpen = true"
|
||||
>
|
||||
{{ t('userSettings.updatePortrait') }}
|
||||
@@ -62,20 +128,77 @@
|
||||
<span>{{ t('userSettings.accountDetailsDescription') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-row">
|
||||
<span>{{ t('userSettings.alias') }}</span>
|
||||
<strong>{{ alias }}</strong>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>{{ t('userSettings.fullName') }}</span>
|
||||
<strong>{{ fullname }}</strong>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>{{ t('userSettings.email') }}</span>
|
||||
<strong>{{ email }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="settingsError"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ settingsError }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="settingsStatus"
|
||||
class="page-message success"
|
||||
>
|
||||
{{ settingsStatus }}
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="form-stack"
|
||||
@submit.prevent="submitSettings"
|
||||
>
|
||||
<div class="details-grid">
|
||||
<label class="field">
|
||||
<span>{{ t('userSettings.firstname') }}</span>
|
||||
<input
|
||||
v-model="form.firstname"
|
||||
type="text"
|
||||
autocomplete="given-name"
|
||||
:disabled="userProfileStore.isUpdating"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ t('userSettings.lastname') }}</span>
|
||||
<input
|
||||
v-model="form.lastname"
|
||||
type="text"
|
||||
autocomplete="family-name"
|
||||
:disabled="userProfileStore.isUpdating"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ t('userSettings.alias') }}</span>
|
||||
<input
|
||||
v-model="form.alias"
|
||||
type="text"
|
||||
autocomplete="nickname"
|
||||
:placeholder="fullname"
|
||||
:disabled="userProfileStore.isUpdating"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ t('userSettings.email') }}</span>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
:disabled="userProfileStore.isUpdating"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="primary-button"
|
||||
type="submit"
|
||||
:disabled="!canSave"
|
||||
>
|
||||
{{ userProfileStore.isUpdating ? t('common.saving') : t('userSettings.saveDetails') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ImageCropperDialog
|
||||
@@ -84,6 +207,7 @@
|
||||
:confirm-label="t('userSettings.savePortrait')"
|
||||
:upload-label="t('userSettings.choosePortrait')"
|
||||
:is-saving="isSavingPortrait"
|
||||
:initial-url="userProfileStore.portraitUrl"
|
||||
@save="savePortrait"
|
||||
/>
|
||||
</section>
|
||||
@@ -107,8 +231,7 @@
|
||||
.page-header p,
|
||||
.panel-heading span,
|
||||
.hero-identity span,
|
||||
.hero-identity small,
|
||||
.detail-row span {
|
||||
.hero-identity small {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
@@ -128,8 +251,7 @@
|
||||
}
|
||||
|
||||
.hero-identity strong,
|
||||
.panel-heading strong,
|
||||
.detail-row strong {
|
||||
.panel-heading strong {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
@@ -149,10 +271,45 @@
|
||||
@apply grid gap-4 md:grid-cols-2;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
@apply flex flex-col gap-1 rounded-[1.25rem] border p-4;
|
||||
.form-stack {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.field {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.field span {
|
||||
@apply text-sm font-semibold;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.field input {
|
||||
@apply rounded-[1rem] border px-4 py-3 text-sm;
|
||||
background: #fffaf2;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.field input:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
@apply flex justify-end;
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply rounded-[1rem] border px-4 py-3 text-sm font-semibold;
|
||||
background: rgba(15, 118, 110, 0.08);
|
||||
border-color: rgba(15, 118, 110, 0.18);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.page-message.error {
|
||||
background: rgba(185, 28, 28, 0.08);
|
||||
border-color: rgba(185, 28, 28, 0.16);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@@ -160,4 +317,9 @@
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.primary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user