feat(cabana): add backend user avatars to My account and the top bar
- cabana.BackendUser is an attach.Owner (MorphName Backend\Models\User,
cabana.BackendUserMorphType) with one public attachOne avatar
(cabana.BackendUserAvatarField), as WinterCMS stores it
- POST /account/avatar stores one file_data image (jpg, jpeg, png, gif or
webp, image guard, at most cabana.AvatarMaxBytes = 2 MiB, under a
server-generated name) and replaces the previous avatar: old rows are
deleted in the transaction, their blobs after the commit
- DELETE /account/avatar removes the avatar and is idempotent
- GET /auth/me and the account routes return avatar {url, thumb_url}
(250x250 crop); cabana.BackendUserAvatar lets plugins read it
- swag annotations, regenerated admin.json and schema.d.ts; route
inventories, CSRF walk (29) and OpenAPI conformance learn both routes
- SPA: avatar upload and remove on My account, avatar image in the user
menu trigger and header; backend::lang.myaccount avatar strings
- rebuilt boardwalk dist; cabana README, users-and-permissions and
admin-spa docs
This commit is contained in:
@@ -11,13 +11,14 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { ChevronDown, LogOut, UserRound } from '@lucide/vue'
|
||||
import { t } from '../../app/i18n'
|
||||
import { currentUser, logout } from '../../state/useAuth'
|
||||
import { currentUser, logout, profileInitials } from '../../state/useAuth'
|
||||
|
||||
// User menu (design: Top header): a 44px button with the initials avatar,
|
||||
// the name above the role from /auth/me (both hidden at tablet width) and a
|
||||
// chevron; the 280px menu (Reka DropdownMenu, role menu) shows the name and
|
||||
// e-mail, a My account item that opens /account and a Wyloguj item. Logging out always ends on the login screen,
|
||||
// even when the logout call fails (T-10-23).
|
||||
// User menu (design: Top header): a 44px button with the avatar image (the
|
||||
// initials when the admin has none), the name above the role from /auth/me
|
||||
// (both hidden at tablet width) and a chevron; the 280px menu (Reka
|
||||
// DropdownMenu, role menu) shows the name and e-mail, a My account item that
|
||||
// opens /account and a Wyloguj item. Logging out always ends on the login
|
||||
// screen, even when the logout call fails (T-10-23).
|
||||
const router = useRouter()
|
||||
|
||||
const displayName = computed(() => {
|
||||
@@ -30,20 +31,10 @@ const displayName = computed(() => {
|
||||
})
|
||||
|
||||
/** First and last name initials, else the login's first letters. */
|
||||
const initials = computed(() => {
|
||||
const user = currentUser.value
|
||||
if (!user) {
|
||||
return ''
|
||||
}
|
||||
const parts = [user.first_name, user.last_name].map((part) => part.trim()).filter(Boolean)
|
||||
if (parts.length > 0) {
|
||||
return parts
|
||||
.slice(0, 2)
|
||||
.map((part) => part.charAt(0).toUpperCase())
|
||||
.join('')
|
||||
}
|
||||
return user.login.slice(0, 2).toUpperCase()
|
||||
})
|
||||
const initials = computed(() => profileInitials(currentUser.value))
|
||||
|
||||
/** The avatar thumbnail, else the image; '' shows the initials. */
|
||||
const avatarSrc = computed(() => currentUser.value?.avatar?.thumb_url || currentUser.value?.avatar?.url || '')
|
||||
|
||||
const roleName = computed(() => currentUser.value?.role?.name ?? '')
|
||||
|
||||
@@ -66,7 +57,15 @@ async function onLogout(): Promise<void> {
|
||||
:aria-label="`${t('backend::lang.nav.user_menu')}: ${displayName}`"
|
||||
class="flex h-input items-center gap-2.5 rounded-inner px-2 text-left transition-colors duration-150 ease-out hover:bg-hover"
|
||||
>
|
||||
<img
|
||||
v-if="avatarSrc"
|
||||
:src="avatarSrc"
|
||||
alt=""
|
||||
data-user-avatar
|
||||
class="size-[34px] shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="flex size-[34px] shrink-0 items-center justify-center rounded-full bg-accent text-[13px] font-bold text-on-accent"
|
||||
aria-hidden="true"
|
||||
data-user-initials
|
||||
@@ -86,7 +85,15 @@ async function onLogout(): Promise<void> {
|
||||
class="z-50 flex w-[280px] flex-col rounded-[14px] border border-border bg-surface p-2 text-text shadow-menu"
|
||||
>
|
||||
<div class="flex items-center gap-3 px-2 py-2">
|
||||
<img
|
||||
v-if="avatarSrc"
|
||||
:src="avatarSrc"
|
||||
alt=""
|
||||
data-user-avatar
|
||||
class="size-10 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-accent text-[14px] font-bold text-on-accent"
|
||||
aria-hidden="true"
|
||||
>{{ initials }}</span
|
||||
|
||||
Reference in New Issue
Block a user