Enhance user profile management and Gravatar integration
- Added email field to user profile API and frontend components for better user identification. - Implemented PATCH /map/api/me endpoint to update user email, enhancing user experience. - Introduced useGravatarUrl composable for generating Gravatar URLs based on user email. - Updated profile and layout components to display user avatars using Gravatar, improving visual consistency. - Enhanced development documentation to guide testing of navbar and profile features.
This commit is contained in:
@@ -16,9 +16,21 @@
|
||||
</template>
|
||||
<template v-else-if="me">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-14">
|
||||
<span class="text-2xl font-semibold">{{ (me.username || '?')[0].toUpperCase() }}</span>
|
||||
<div class="avatar">
|
||||
<div class="rounded-full w-14 h-14 overflow-hidden flex items-center justify-center">
|
||||
<img
|
||||
v-if="me.email && !gravatarError"
|
||||
:src="useGravatarUrl(me.email, 80)"
|
||||
alt=""
|
||||
class="w-full h-full object-cover"
|
||||
@error="gravatarError = true"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="bg-primary text-primary-content rounded-full w-14 h-14 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-2xl font-semibold">{{ (me.username || '?')[0].toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -33,6 +45,34 @@
|
||||
</span>
|
||||
<span v-if="!me.auths?.length" class="text-sm text-base-content/60">No roles</span>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/80 mt-1">
|
||||
Email: {{ me.email || 'Not set' }}
|
||||
</p>
|
||||
<div v-if="!emailEditing" class="flex items-center gap-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs"
|
||||
@click="startEditEmail"
|
||||
>
|
||||
{{ me.email ? 'Edit' : 'Set' }} email
|
||||
</button>
|
||||
</div>
|
||||
<form v-else class="flex flex-wrap items-center gap-2 mt-2" @submit.prevent="saveEmail">
|
||||
<input
|
||||
v-model="emailEdit"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
autocomplete="email"
|
||||
>
|
||||
<button type="submit" class="btn btn-primary btn-sm" :disabled="loadingEmail">
|
||||
{{ loadingEmail ? '…' : 'Save' }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm" :disabled="loadingEmail" @click="cancelEditEmail">
|
||||
Cancel
|
||||
</button>
|
||||
<p v-if="emailError" class="text-error text-sm w-full">{{ emailError }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,10 +144,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MeResponse } from '~/types/api'
|
||||
|
||||
const api = useMapApi()
|
||||
const toast = useToast()
|
||||
const initialLoad = ref(true)
|
||||
const me = ref<{ username?: string; auths?: string[] } | null>(null)
|
||||
const me = useState<MeResponse | null>('me', () => null)
|
||||
const tokens = ref<string[]>([])
|
||||
const uploadPrefix = ref('')
|
||||
const newPass = ref('')
|
||||
@@ -117,8 +159,41 @@ const passMsg = ref('')
|
||||
const passOk = ref(false)
|
||||
const tokenError = ref('')
|
||||
const copiedToken = ref<string | null>(null)
|
||||
const gravatarError = ref(false)
|
||||
const emailEditing = ref(false)
|
||||
const emailEdit = ref('')
|
||||
const loadingEmail = ref(false)
|
||||
const emailError = ref('')
|
||||
let copiedTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function startEditEmail() {
|
||||
emailError.value = ''
|
||||
emailEdit.value = me.value?.email ?? ''
|
||||
emailEditing.value = true
|
||||
}
|
||||
|
||||
function cancelEditEmail() {
|
||||
emailEditing.value = false
|
||||
emailError.value = ''
|
||||
}
|
||||
|
||||
async function saveEmail() {
|
||||
emailError.value = ''
|
||||
loadingEmail.value = true
|
||||
try {
|
||||
await api.meUpdate({ email: emailEdit.value.trim() || undefined })
|
||||
const data = await api.me()
|
||||
me.value = data
|
||||
emailEditing.value = false
|
||||
gravatarError.value = false
|
||||
toast.success('Email updated')
|
||||
} catch (e: unknown) {
|
||||
emailError.value = e instanceof Error ? e.message : 'Failed to update email'
|
||||
} finally {
|
||||
loadingEmail.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function uploadTokenDisplay(token: string): string {
|
||||
const base = (uploadPrefix.value ?? '').replace(/\/+$/, '')
|
||||
return base ? `${base}/client/${token}` : `client/${token}`
|
||||
@@ -142,7 +217,7 @@ async function copyToken(token: string) {
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await api.me()
|
||||
me.value = { username: data.username, auths: data.auths }
|
||||
me.value = data
|
||||
tokens.value = data.tokens ?? []
|
||||
uploadPrefix.value = data.prefix ?? ''
|
||||
} catch {
|
||||
@@ -159,9 +234,10 @@ async function generateToken() {
|
||||
loadingTokens.value = true
|
||||
try {
|
||||
await api.meTokens()
|
||||
const me = await api.me()
|
||||
tokens.value = me.tokens ?? []
|
||||
uploadPrefix.value = me.prefix ?? ''
|
||||
const data = await api.me()
|
||||
me.value = data
|
||||
tokens.value = data.tokens ?? []
|
||||
uploadPrefix.value = data.prefix ?? ''
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : ''
|
||||
tokenError.value = msg === 'Forbidden'
|
||||
|
||||
Reference in New Issue
Block a user