mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-01-14 17:04:38 +00:00
Made profile header component that supports uploading and displaying a header image and added it to the profile page. Added locales in all languages for a succesful or failed upload toast. Included test for the new userProfileHeader component. Updated the gql model to include the profileHeader.
This commit is contained in:
parent
a551959b79
commit
f62efe39fa
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="profile-header">
|
||||
<vue-dropzone
|
||||
v-if="user && this.editable"
|
||||
id="profileHeaderDropzone"
|
||||
:key="profileHeaderUrl"
|
||||
ref="el"
|
||||
:use-custom-slot="true"
|
||||
:options="dropzoneOptions"
|
||||
@vdropzone-error="verror"
|
||||
>
|
||||
<div class="dz-message" @mouseover="hover = true" @mouseleave="hover = false">
|
||||
<img
|
||||
v-if="user && user.profileHeader"
|
||||
:src="user.profileHeader | proxyApiUrl"
|
||||
@error="$event.target.style.display = 'none'"
|
||||
class="profile-header-image"
|
||||
:alt="imageAlt"
|
||||
/>
|
||||
<div class="profileHeader-attachments-upload-area">
|
||||
<div class="profileHeader-drag-marker">
|
||||
<base-icon v-if="hover" name="image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</vue-dropzone>
|
||||
<div v-else>
|
||||
<img
|
||||
v-if="user && user.profileHeader"
|
||||
:src="user.profileHeader | proxyApiUrl"
|
||||
@error="$event.target.style.display = 'none'"
|
||||
class="profile-header-image"
|
||||
:alt="imageAlt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
name: 'UserProfileHeader',
|
||||
components: {
|
||||
vueDropzone,
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
url: this.vddrop,
|
||||
maxFilesize: 5.0,
|
||||
previewTemplate: this.template(),
|
||||
},
|
||||
error: false,
|
||||
hover: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageAlt() {
|
||||
if (!this.user && !this.user.name) return 'Profile header image'
|
||||
return 'Profile header image of ' + this.user.name
|
||||
},
|
||||
profileHeaderUrl() {
|
||||
const { profileHeader } = this.user
|
||||
return profileHeader && profileHeader.url
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
error() {
|
||||
const that = this
|
||||
setTimeout(function () {
|
||||
that.error = false
|
||||
}, 2000)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
template() {
|
||||
return `<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-image">
|
||||
<div data-dz-thumbnail-bg></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
vddrop(file) {
|
||||
const profileHeaderUpload = file[0]
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
profileHeader: {
|
||||
upload: profileHeaderUpload,
|
||||
},
|
||||
id: this.user.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('user.profileHeader.submitted'))
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
},
|
||||
verror(file, message) {
|
||||
if (file.status === 'error') {
|
||||
this.error = true
|
||||
this.$toast.error(file.status, message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-header {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: DarkGrey; /* Fallback color */
|
||||
}
|
||||
|
||||
.profile-header-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#profileHeaderDropzone {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dz-message {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#profileHeaderDropzone .dz-preview {
|
||||
transition: all 0.2s ease-out;
|
||||
width: 160px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#profileHeaderDropzone .dz-preview .dz-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#profileHeaderDropzone .dz-preview .dz-image > div {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#profileHeaderDropzone .dz-preview .dz-image > img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profileHeader-attachments-upload-area {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profileHeader-attachments-upload-button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.profileHeader-drag-marker {
|
||||
position: relative;
|
||||
width: 122px;
|
||||
height: 122px;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: hsl(0, 0%, 25%);
|
||||
transition: all 0.2s ease-out;
|
||||
font-size: 60px;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
opacity: 0.1;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
border: 20px solid rgba(255, 255, 255, 0.4);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border-radius: 100%;
|
||||
border: 1px dashed hsl(0, 0%, 25%);
|
||||
}
|
||||
.profileHeader-attachments-upload-area:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,217 @@
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import UserProfileHeader from './UserProfileHeader.vue'
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import Vue from 'vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserProfileHeader.vue', () => {
|
||||
let propsData, wrapper, mocks
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(UserProfileHeader, { propsData, localVue })
|
||||
}
|
||||
|
||||
it('renders no image', () => {
|
||||
expect(wrapper.contains('img')).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no dropzone', () => {
|
||||
expect(wrapper.contains(vueDropzone)).toBe(false)
|
||||
})
|
||||
|
||||
describe('given a user', () => {
|
||||
describe('with no header image', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Matt Rider',
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders no img tag', () => {
|
||||
expect(wrapper.contains('img')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a header image', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Matt Rider',
|
||||
profileHeader: {
|
||||
url: 'https://source.unsplash.com/640x480',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders an image', () => {
|
||||
expect(wrapper.contains('img')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a relative avatar url', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
profileHeader: {
|
||||
url: '/profileHeader.jpg',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('adds a prefix to load the image from the uploads service', () => {
|
||||
expect(wrapper.find('img').attributes('src')).toBe('/api/profileHeader.jpg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an absolute avatar url', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
profileHeader: {
|
||||
url: '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',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on his own userpage', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
editable: true,
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
profileHeader: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('a dropzone is present', () => {
|
||||
expect(wrapper.contains(vueDropzone)).toBe(true)
|
||||
})
|
||||
|
||||
describe('uploading and image', () => {
|
||||
beforeAll(() => {
|
||||
propsData = {
|
||||
user: {
|
||||
profileHeader: { url: '/api/profileHeader.jpg' },
|
||||
},
|
||||
}
|
||||
mocks = {
|
||||
$apollo: {
|
||||
mutate: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
id: 'upload1',
|
||||
profileHeader: { url: '/upload/profileHeader.jpg' },
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockRejectedValue({
|
||||
message: 'File upload unsuccessful!',
|
||||
}),
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$t: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
wrapper = shallowMount(UserProfileHeader, { localVue, propsData, mocks })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('sends a UpdateUser mutation when vddrop is called', () => {
|
||||
wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }])
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
const message = 'File upload failed'
|
||||
const fileError = { status: 'error' }
|
||||
|
||||
it('defaults to error false', () => {
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
|
||||
it('shows an error toaster when verror is called', () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
|
||||
})
|
||||
|
||||
it('changes error status from false to true to false', async () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
await Vue.nextTick()
|
||||
expect(wrapper.vm.error).toEqual(true)
|
||||
jest.runAllTimers()
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
|
||||
it('shows an error toaster when the apollo mutation rejects', async () => {
|
||||
// calls vddrop twice because of how mockResolvedValueOnce works in jest
|
||||
// the first time the mock function is called it will resolve, calling it a
|
||||
// second time will cause it to fail(with this implementation)
|
||||
// https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue
|
||||
await wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }])
|
||||
await wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }])
|
||||
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("on a different user's userpage", () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
editable: false,
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
profileHeader: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('no dropzone is present', () => {
|
||||
expect(wrapper.contains(vueDropzone)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -20,6 +20,9 @@ export default (i18n) => {
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
about
|
||||
profileHeader {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
createdAt
|
||||
followedByCurrentUser
|
||||
@ -226,6 +229,7 @@ export const updateUserMutation = () => {
|
||||
$showShoutsPublicly: Boolean
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$avatar: ImageInput
|
||||
$profileHeader: ImageInput
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
@ -237,6 +241,7 @@ export const updateUserMutation = () => {
|
||||
showShoutsPublicly: $showShoutsPublicly
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
avatar: $avatar
|
||||
profileHeader: $profileHeader
|
||||
) {
|
||||
id
|
||||
slug
|
||||
@ -250,6 +255,9 @@ export const updateUserMutation = () => {
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
profileHeader {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -830,6 +830,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Erfolgreich hochgeladen!"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Erfolgreich hochgeladen!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -830,6 +830,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Upload successful!"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Upload successful!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -827,6 +827,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Carga con éxito"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Carga con éxito!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -791,6 +791,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Téléchargement réussi"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Téléchargement réussi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -734,6 +734,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": ""
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Caricamento eseguito correttamente!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,5 +172,10 @@
|
||||
"taxident": "Identificatienummer voor de belasting over de toegevoegde waarde overeenkomstig § 27 a Wet op de belasting over de toegevoegde waarde (Duitsland).",
|
||||
"termsAc": "Gebruiksvoorwaarden",
|
||||
"tribunal": "registerrechtbank"
|
||||
},
|
||||
"user": {
|
||||
"profileHeader": {
|
||||
"submitted": "Uploaden geslaagd!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,6 +363,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Przesłano pomyślnie"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Przesłano pomyślnie"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -722,6 +722,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Carregado com sucesso!"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Carregado com sucesso!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -823,6 +823,9 @@
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Успешная загрузка!"
|
||||
},
|
||||
"profileHeader": {
|
||||
"submitted": "Успешная загрузка!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space />
|
||||
<ds-flex v-if="user" :width="{ base: '100%' }" gutter="base">
|
||||
<ds-flex-item width="100%">
|
||||
<base-card class="profile-header">
|
||||
<user-profile-header :user="user" :editable="myProfile"></user-profile-header>
|
||||
</base-card>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
|
||||
<base-card
|
||||
:class="{ 'disabled-content': user.disabled }"
|
||||
@ -242,6 +246,7 @@ import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
import UserProfileHeader from '~/components/_new/generic/UserProfileHeader/UserProfileHeader'
|
||||
|
||||
const tabToFilterMapping = ({ tab, id }) => {
|
||||
return {
|
||||
@ -264,6 +269,7 @@ export default {
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
FollowList,
|
||||
UserProfileHeader,
|
||||
},
|
||||
transition: {
|
||||
name: 'slide-up',
|
||||
@ -543,4 +549,9 @@ export default {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
padding: 0px; /* Overwrite default card padding to 0. */
|
||||
height: 250px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user