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:
2026-03-01 16:48:56 +03:00
parent db0b48774a
commit 6a6977ddff
14 changed files with 311 additions and 28 deletions

View File

@@ -19,30 +19,39 @@
</NuxtLink>
<div
v-if="me"
class="hidden md:flex items-center gap-2 shrink-0"
class="hidden md:flex items-center gap-2 shrink-0 min-h-9"
>
<div
class="tooltip tooltip-bottom shrink-0"
:data-tip="live ? 'Connected to live updates' : 'Disconnected'"
>
<div class="flex items-center shrink-0 min-h-9">
<span
class="inline-flex items-center gap-1.5 text-xs text-base-content/70"
class="inline-flex items-center gap-1.5 text-xs text-base-content/70 leading-none"
:class="live ? 'text-success' : 'text-base-content/50'"
>
<span
class="size-2 rounded-full"
class="size-2 rounded-full shrink-0"
:class="live ? 'bg-success animate-pulse' : 'bg-base-content/40'"
aria-hidden="true"
/>
{{ live ? 'Live' : 'Offline' }}
</span>
</div>
<div class="dropdown dropdown-end">
<div class="dropdown dropdown-end flex items-center">
<details ref="userDropdownRef" class="dropdown group">
<summary class="btn btn-ghost btn-sm gap-2 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-8">
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
<summary class="btn btn-ghost btn-sm gap-2 flex items-center min-h-9 h-9 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
<div class="avatar">
<div class="rounded-full w-8 h-8 overflow-hidden flex items-center justify-center">
<img
v-if="me.email && !gravatarErrorDesktop"
:src="useGravatarUrl(me.email, 32)"
alt=""
class="w-full h-full object-cover"
@error="gravatarErrorDesktop = true"
>
<div
v-else
class="bg-primary text-primary-content rounded-full w-8 h-8 flex items-center justify-center"
>
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
</div>
</div>
</div>
<span class="max-w-[8rem] truncate font-medium">{{ me.username }}</span>
@@ -119,9 +128,21 @@
<aside class="bg-base-200/95 backdrop-blur-xl min-h-full w-72 p-4 flex flex-col">
<!-- Mobile: user + live when logged in -->
<div v-if="!isLogin && me" class="flex items-center gap-3 pb-4 mb-2 border-b border-base-300/50">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-10">
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
<div class="avatar">
<div class="rounded-full w-10 h-10 overflow-hidden flex items-center justify-center">
<img
v-if="me.email && !gravatarErrorDrawer"
:src="useGravatarUrl(me.email, 40)"
alt=""
class="w-full h-full object-cover"
@error="gravatarErrorDrawer = true"
>
<div
v-else
class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center"
>
<span class="text-sm font-medium">{{ (me.username || '?')[0].toUpperCase() }}</span>
</div>
</div>
</div>
<div class="flex-1 min-w-0">
@@ -185,6 +206,8 @@
</template>
<script setup lang="ts">
import type { MeResponse } from '~/types/api'
const route = useRoute()
const router = useRouter()
const THEME_KEY = 'hnh-map-theme'
@@ -202,7 +225,9 @@ function getInitialDark(): boolean {
const title = ref('HnH Map')
const dark = ref(false)
const live = ref(false)
const me = ref<{ username?: string; auths?: string[] } | null>(null)
const me = useState<MeResponse | null>('me', () => null)
const gravatarErrorDesktop = ref(false)
const gravatarErrorDrawer = ref(false)
const userDropdownRef = ref<HTMLDetailsElement | null>(null)
const drawerCheckboxRef = ref<HTMLInputElement | null>(null)
@@ -248,6 +273,14 @@ watch(
{ immediate: true }
)
watch(
() => me.value?.email,
() => {
gravatarErrorDesktop.value = false
gravatarErrorDrawer.value = false
}
)
function onThemeToggle() {
dark.value = !dark.value
applyTheme()