feat(SPA): profile page, display name and the new account menu

This commit is contained in:
fsylvain committed 2026-09-29 10:13:09 +02:00
1 parent 55525c06d6
commit b0f7b7892b
29 files changed
+3166 -469

No files matched your search

+64 -31
View File
@@ -1,43 +1,65 @@
<script setup lang="ts">
import { computed } from 'vue'
import {
useColorMode,
type ColorModePreference,
} from '@/composables/useColorMode'
import { computed, ref } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import { colorModeOptions, useAccountMenu } from '@/composables/useAccountMenu'
const DESKTOP_QUERY = '(min-width: 64rem)'
const props = defineProps<{
username: string
displayName?: string
email?: string
collapsed: boolean
}>()
const emit = defineEmits<{
logout: []
'open-sheet': []
}>()
const colorMode = useColorMode()
const { colorMode, name, initials, onProfile, goToProfile } = useAccountMenu(() => props)
const isDesktop = useMediaQuery(DESKTOP_QUERY)
const open = ref(false)
const initials = computed(() => {
const parts = props.username.split(/[^a-zA-Z0-9]+/).filter(Boolean)
if (parts.length >= 2) {
return (
(parts[0] ?? '').charAt(0) + (parts[1] ?? '').charAt(0)
).toUpperCase()
}
return (parts[0] ?? '').slice(0, 2).toUpperCase()
})
const chipActive = computed(() => open.value || onProfile.value)
const triggerAriaLabel = computed(() =>
props.collapsed ? `${props.username} — account menu` : undefined,
props.collapsed ? `${name.value} — account menu` : undefined,
)
const colorModeOptions: { label: string; value: ColorModePreference }[] = [
{ label: 'System', value: 'system' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
]
const chipClass = computed(() => [
'h-10 gap-2.5 px-1.5 font-normal',
props.collapsed ? 'justify-center' : 'justify-start',
chipActive.value
? 'bg-secondary-200 text-primary-900 hover:bg-secondary-200 hover:text-primary-900 dark:bg-secondary-300 dark:hover:bg-secondary-300'
: 'text-highlighted',
])
function onUpdateOpen(value: boolean) {
if (isDesktop.value) {
open.value = value
return
}
if (value) {
emit('open-sheet')
}
}
const items = computed(() => [
[{ type: 'label' as const, label: props.username }],
[
{
type: 'label' as const,
label: name.value,
slot: 'account-header' as const,
class: 'mb-1 border-b border-default px-2 pt-2 pb-2.5',
},
{
label: 'Profile',
icon: 'i-lucide-circle-user',
class: onProfile.value ? 'bg-elevated' : undefined,
onSelect: goToProfile,
},
],
colorModeOptions.map((option) => ({
type: 'checkbox' as const,
label: option.label,
@@ -51,6 +73,7 @@ const items = computed(() => [
[
{
label: 'Log out',
icon: 'i-lucide-log-out',
onSelect: () => emit('logout'),
},
],
@@ -58,17 +81,21 @@ const items = computed(() => [
</script>
<template>
<UDropdownMenu :items="items" :content="{ side: 'top', align: 'start' }">
<UDropdownMenu
:open="open"
:items="items"
:content="{ side: 'top', align: 'start' }"
:ui="{ content: 'w-56 p-1' }"
@update:open="onUpdateOpen"
>
<UButton
color="neutral"
variant="ghost"
block
:class="[
'h-10 gap-2.5 px-1.5 font-normal text-highlighted',
collapsed ? 'justify-center' : 'justify-start',
]"
title="Account"
:class="chipClass"
:trailing-icon="collapsed ? undefined : 'i-lucide-chevrons-up-down'"
:ui="{ trailingIcon: 'ms-auto size-3.5 text-default' }"
:ui="{ trailingIcon: 'ms-auto size-3.5 text-current' }"
:aria-label="triggerAriaLabel"
>
<template #leading>
@@ -78,12 +105,18 @@ const items = computed(() => [
size="sm"
:ui="{
root: 'size-6.5 bg-secondary-300',
fallback:
'text-[0.6875rem] font-semibold text-primary-900',
fallback: 'text-[0.6875rem] font-semibold text-primary-900',
}"
/>
</template>
<span v-if="!collapsed" class="truncate">{{ username }}</span>
<span v-if="!collapsed" class="truncate font-medium">{{ name }}</span>
</UButton>
<template #account-header>
<span class="flex min-w-0 flex-col gap-0.5">
<span class="truncate text-sm font-semibold text-highlighted">{{ name }}</span>
<span class="truncate text-xs font-normal text-muted">{{ email }}</span>
</span>
</template>
</UDropdownMenu>
</template>