feat(SPA): profile page, display name and the new account menu
This commit is contained in:
1 parent
55525c06d6
commit
b0f7b7892b
29 files changed
+3166
-469
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user