Support use of initials, add tests, move component

- if there is no user.avatar, we show a user's initials - up to 3
characters unless there is no name or the name is 'Anonymous'. This is
to support users who on the old alpha were allowed to be anonymous (do
we still want to support this?)

- Add test cases for ☝️
- Refactor to not use any styleguide components and move UserAvatar to
generic directory
This commit is contained in:
mattwr18 2020-01-08 14:09:25 +01:00
parent 068e2b4417
commit 905f34c827
7 changed files with 141 additions and 97 deletions

View File

@ -11,7 +11,7 @@
"
@click.prevent="toggleMenu"
>
<user-avatar :image="user && user.avatar" />
<user-avatar :user="user" />
<base-icon class="dropdown-arrow" name="angle-down" />
</a>
</template>
@ -49,7 +49,7 @@
<script>
import { mapGetters } from 'vuex'
import Dropdown from '~/components/Dropdown'
import UserAvatar from '~/components/UserAvatar/UserAvatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default {
components: {

View File

@ -9,7 +9,7 @@
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
<div @mouseover="showPopover ? openMenu(true) : () => {}" @mouseleave="closeMenu(true)">
<user-avatar v-if="showAvatar" class="avatar" :image="user && user.avatar" />
<user-avatar v-if="showAvatar" class="avatar" :user="user" />
<div>
<ds-text class="userinfo">
<b>{{ userSlug }}</b>
@ -89,7 +89,7 @@ import { mapGetters } from 'vuex'
import HcRelativeDateTime from '~/components/RelativeDateTime'
import HcFollowButton from '~/components/FollowButton'
import HcBadges from '~/components/Badges'
import UserAvatar from '~/components/UserAvatar/UserAvatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import Dropdown from '~/components/Dropdown'
export default {

View File

@ -1,68 +0,0 @@
import { mount } from '@vue/test-utils'
import Avatar from './Avatar.vue'
const localVue = global.localVue
describe('Avatar.vue', () => {
let propsData = {}
const Wrapper = () => {
return mount(Avatar, { propsData, localVue })
}
it('renders no image', () => {
expect(
Wrapper()
.find('img')
.exists(),
).toBe(false)
})
// this is testing the style guide
it('renders an icon', () => {
expect(
Wrapper()
.find('.ds-icon')
.exists(),
).toBe(true)
})
describe('given a user', () => {
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: '/avatar.jpg',
},
}
})
it('adds a prefix to load the image from the uploads service', () => {
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg')
})
})
})
})

View File

@ -0,0 +1,98 @@
import { mount } from '@vue/test-utils'
import UserAvatar from './UserAvatar.vue'
import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon'
const localVue = global.localVue
describe('UserAvatar.vue', () => {
let propsData, wrapper
beforeEach(() => {
propsData = {}
wrapper = Wrapper()
})
const Wrapper = () => {
return mount(UserAvatar, { propsData, localVue })
}
it('renders no image', () => {
expect(wrapper.find('img').exists()).toBe(false)
})
// this is testing the style guide
it('renders an icon', () => {
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
describe('given a user', () => {
describe('with no image', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Matt Rider',
},
}
wrapper = Wrapper()
})
describe('no user name', () => {
it('renders an icon', () => {
propsData = { user: { name: null } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
describe("user name is 'Anonymous'", () => {
it('renders an icon', () => {
propsData = { user: { name: 'Anonymous' } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
it('displays user initials', () => {
expect(wrapper.find('.no-image').text()).toEqual('MR')
})
it('displays no more than 3 initials', () => {
propsData = { user: { name: 'Ana Paula Nunes Marques' } }
wrapper = Wrapper()
expect(wrapper.find('.no-image').text()).toEqual('APN')
})
})
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: '/avatar.jpg',
},
}
wrapper = Wrapper()
})
it('adds a prefix to load the image from the uploads service', () => {
expect(wrapper.find('img').attributes('src')).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
wrapper = Wrapper()
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(wrapper.find('img').attributes('src')).toBe(
'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
)
})
})
})
})

View File

@ -1,16 +1,16 @@
<template>
<div :class="[`size-${this.size}`, 'user-avatar']">
<ds-flex v-if="!hasImage || error" style="height: 100%">
<ds-flex-item centered>
<template v-if="isAnonymus">
<span v-if="!hasImage || error" class="no-image">
<span class="flex-item">
<template v-if="isAnonymous">
<base-icon name="eye-slash" />
</template>
<template v-else>
{{ userInitials }}
</template>
</ds-flex-item>
</ds-flex>
<img v-if="image && !error" :src="image | proxyApiUrl" @error="onError" />
</span>
</span>
<img v-if="user && user.avatar && !error" :src="user.avatar | proxyApiUrl" @error="onError" />
</div>
</template>
@ -18,11 +18,6 @@
export default {
name: 'UserAvatar',
props: {
name: { type: String, default: 'Anonymus' },
/**
* The size used for the avatar.
* @options small|base|large
*/
size: {
type: String,
default: 'base',
@ -30,7 +25,7 @@ export default {
return value.match(/(small|base|large|x-large)/)
},
},
image: { type: String, default: null },
user: { type: Object, default: null },
},
data() {
return {
@ -38,11 +33,19 @@ export default {
}
},
computed: {
isAnonymus() {
return !this.name || this.name.toLowerCase() === 'anonymus'
isAnonymous() {
return !this.user || !this.user.name || this.user.name.toLowerCase() === 'anonymous'
},
hasImage() {
return Boolean(this.image) && !this.error
return Boolean(this.user && this.user.avatar) && !this.error
},
userInitials() {
const { name } = this.user || 'Anonymous'
const namesArray = name.split(/[ -]/)
let initials = ''
for (var i = 0; i < namesArray.length; i++) initials += namesArray[i].charAt(0)
if (initials.length > 3 && /[A-Z]/.test(initials)) initials = initials.replace(/[a-z]+/g, '')
return initials.substr(0, 3).toUpperCase()
},
},
methods: {
@ -56,7 +59,6 @@ export default {
.user-avatar {
img {
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
object-fit: cover;
@ -84,5 +86,21 @@ export default {
width: $size-avatar-x-large;
height: $size-avatar-x-large;
}
.no-image {
height: 100%;
display: flex;
flex-wrap: wrap;
border-radius: 50%;
background-color: $background-color-secondary;
color: $text-color-primary-inverse;
}
.no-image .flex-item {
box-sizing: border-box;
padding: 0;
margin: 0 auto;
align-self: center;
display: table;
}
}
</style>

View File

@ -11,13 +11,9 @@
style="position: relative; height: auto;"
>
<hc-upload v-if="myProfile" :user="user">
<user-avatar
:image="user && user.avatar"
class="profile-avatar"
size="x-large"
></user-avatar>
<user-avatar :user="user" class="profile-avatar" size="x-large"></user-avatar>
</hc-upload>
<user-avatar v-else :image="user && user.avatar" class="profile-avatar" size="x-large" />
<user-avatar v-else :user="user" class="profile-avatar" size="x-large" />
<!-- Menu -->
<client-only>
<content-menu
@ -283,7 +279,7 @@ import HcBadges from '~/components/Badges.vue'
import HcEmpty from '~/components/Empty/Empty'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcUpload from '~/components/Upload'
import UserAvatar from '~/components/UserAvatar/UserAvatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { profilePagePosts } from '~/graphql/PostQuery'

View File

@ -33,7 +33,7 @@
params: { id: scope.row.id, slug: scope.row.slug },
}"
>
<user-avatar :image="scope.row && scope.row.avatar" size="small" />
<user-avatar :user="scope.row" size="small" />
</nuxt-link>
</template>
<template slot="name" slot-scope="scope">
@ -79,7 +79,7 @@
<script>
import { BlockedUsers, Unblock } from '~/graphql/settings/BlockedUsers'
import UserAvatar from '~/components/UserAvatar/UserAvatar.vue'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default {
components: {