diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js
index 7ea82f661..8a6ca380e 100644
--- a/backend/src/db/factories.js
+++ b/backend/src/db/factories.js
@@ -49,8 +49,9 @@ Factory.define('badge')
Factory.define('image')
.attr('url', faker.image.unsplash.imageUrl)
- .attr('aspectRatio', 1)
+ .attr('aspectRatio', 1.3333333333333333)
.attr('alt', faker.lorem.sentence)
+ .attr('type', 'image/jpeg')
.after((buildObject, options) => {
const { url: imageUrl } = buildObject
if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl)
diff --git a/backend/src/models/Image.js b/backend/src/models/Image.js
index 19824b493..b46342c18 100644
--- a/backend/src/models/Image.js
+++ b/backend/src/models/Image.js
@@ -3,5 +3,6 @@ export default {
alt: { type: 'string' },
sensitive: { type: 'boolean', default: false },
aspectRatio: { type: 'float', default: 1.0 },
+ type: { type: 'string' },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
}
diff --git a/backend/src/schema/resolvers/images.js b/backend/src/schema/resolvers/images.js
index 8b3f4a3e8..111f84888 100644
--- a/backend/src/schema/resolvers/images.js
+++ b/backend/src/schema/resolvers/images.js
@@ -2,7 +2,7 @@ import Resolver from './helpers/Resolver'
export default {
Image: {
...Resolver('Image', {
- undefinedToNull: ['sensitive', 'alt', 'aspectRatio'],
+ undefinedToNull: ['sensitive', 'alt', 'aspectRatio', 'type'],
}),
},
}
diff --git a/backend/src/schema/resolvers/images/images.js b/backend/src/schema/resolvers/images/images.js
index 9b57579c4..656ae114a 100644
--- a/backend/src/schema/resolvers/images/images.js
+++ b/backend/src/schema/resolvers/images/images.js
@@ -53,8 +53,8 @@ export async function mergeImage(resource, relationshipType, imageInput, opts =
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
if (existingImage && upload) deleteImageFile(existingImage, deleteCallback)
const url = await uploadImageFile(upload, uploadCallback)
- const { alt, sensitive, aspectRatio } = imageInput
- const image = { alt, sensitive, aspectRatio, url }
+ const { alt, sensitive, aspectRatio, type } = imageInput
+ const image = { alt, sensitive, aspectRatio, url, type }
txResult = await transaction.run(
`
MATCH (resource {id: $resource.id})
diff --git a/backend/src/schema/types/type/Image.gql b/backend/src/schema/types/type/Image.gql
index 41cc11eef..f171a4b77 100644
--- a/backend/src/schema/types/type/Image.gql
+++ b/backend/src/schema/types/type/Image.gql
@@ -8,6 +8,7 @@ type Image {
alt: String,
sensitive: Boolean,
aspectRatio: Float,
+ type: String,
}
input ImageInput {
@@ -15,4 +16,5 @@ input ImageInput {
upload: Upload,
sensitive: Boolean,
aspectRatio: Float,
+ type: String,
}
diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js
index eb4e9cd5a..cce187a63 100644
--- a/webapp/components/ContributionForm/ContributionForm.spec.js
+++ b/webapp/components/ContributionForm/ContributionForm.spec.js
@@ -151,6 +151,7 @@ describe('ContributionForm.vue', () => {
aspectRatio: null,
sensitive: false,
upload: imageUpload,
+ type: null,
}
const spy = jest
.spyOn(FileReader.prototype, 'readAsDataURL')
diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue
index 42ed2799e..d2cb419a4 100644
--- a/webapp/components/ContributionForm/ContributionForm.vue
+++ b/webapp/components/ContributionForm/ContributionForm.vue
@@ -19,6 +19,7 @@
:class="[formData.imageBlurred && '--blur-image']"
@addHeroImage="addHeroImage"
@addImageAspectRatio="addImageAspectRatio"
+ @addImageType="addImageType"
/>
@@ -84,7 +85,11 @@ export default {
},
data() {
const { title, content, image } = this.contribution
- const { sensitive: imageBlurred = false, aspectRatio: imageAspectRatio = null } = image || {}
+ const {
+ sensitive: imageBlurred = false,
+ aspectRatio: imageAspectRatio = null,
+ type: imageType = null,
+ } = image || {}
return {
links,
@@ -93,6 +98,7 @@ export default {
content: content || '',
image: image || null,
imageAspectRatio,
+ imageType,
imageBlurred,
},
formSchema: {
@@ -125,6 +131,7 @@ export default {
if (this.imageUpload) {
image.upload = this.imageUpload
image.aspectRatio = this.formData.imageAspectRatio
+ image.type = this.formData.imageType
}
}
this.loading = true
@@ -173,6 +180,9 @@ export default {
addImageAspectRatio(aspectRatio) {
this.formData.imageAspectRatio = aspectRatio
},
+ addImageType(imageType) {
+ this.formData.imageType = imageType
+ },
},
apollo: {
User: {
diff --git a/webapp/components/ImageUploader/ImageUploader.spec.js b/webapp/components/ImageUploader/ImageUploader.spec.js
index 537febac3..200369436 100644
--- a/webapp/components/ImageUploader/ImageUploader.spec.js
+++ b/webapp/components/ImageUploader/ImageUploader.spec.js
@@ -25,19 +25,11 @@ describe('ImageUploader.vue', () => {
describe('handles errors', () => {
beforeEach(() => jest.useFakeTimers())
- const message = 'File upload failed'
- const fileError = { status: 'error' }
- const unSupportedFileMessage =
- 'Please upload an image of file format : JPG , JPEG , PNG or GIF'
-
- it('shows an error toaster when verror is called', () => {
- wrapper.vm.onDropzoneError(fileError, message)
- expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
- })
+ const unSupportedFileMessage = 'message'
it('shows an error toaster when unSupported file is uploaded', () => {
- wrapper.vm.onUnSupportedFormat(fileError.status, unSupportedFileMessage)
- expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, unSupportedFileMessage)
+ wrapper.vm.onUnSupportedFormat(unSupportedFileMessage)
+ expect(mocks.$toast.error).toHaveBeenCalledWith(unSupportedFileMessage)
})
})
})
diff --git a/webapp/components/ImageUploader/ImageUploader.vue b/webapp/components/ImageUploader/ImageUploader.vue
index d4f402e02..d885782f3 100644
--- a/webapp/components/ImageUploader/ImageUploader.vue
+++ b/webapp/components/ImageUploader/ImageUploader.vue
@@ -1,17 +1,21 @@
-
+
+
+ {{ $t('contribution.teaserImage.supportedFormats') }}
+
+
+
-
- {{ $t('contribution.teaserImage.supportedFormats') }}
-
-
+
+
+
+ {{ $t('contribution.teaserImage.cropImage') }}
+
+
@@ -48,6 +54,8 @@ import Cropper from 'cropperjs'
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
import 'cropperjs/dist/cropper.css'
+const minAspectRatio = 0.3
+
export default {
components: {
LoadingSpinner,
@@ -70,50 +78,64 @@ export default {
cropper: null,
file: null,
showCropper: false,
+ imageCanBeCropped: false,
isLoadingImage: false,
}
},
methods: {
- onDropzoneError(file, message) {
- this.$toast.error(file.status, message)
+ onUnSupportedFormat(message) {
+ this.$toast.error(message)
},
-
- onUnSupportedFormat(status, message) {
- this.$toast.error(status, message)
+ addImageProcess(src) {
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.onload = () => resolve(img)
+ img.onerror = reject
+ img.src = src
+ })
},
- initCropper(file) {
+ async fileAdded(file) {
const supportedFormats = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif']
-
if (supportedFormats.indexOf(file.type) < 0) {
- this.onUnSupportedFormat(
- 'error',
- this.$t('contribution.teaserImage.errors.unSupported-file-format'),
- )
- return
+ this.onUnSupportedFormat(this.$t('contribution.teaserImage.errors.unSupported-file-format'))
+ this.$nextTick((this.isLoadingImage = false))
+ return null
}
- this.showCropper = true
+ const imageURL = URL.createObjectURL(file)
+ const image = await this.addImageProcess(imageURL)
+ const aspectRatio = image.width / image.height
+ if (aspectRatio < minAspectRatio) {
+ this.aspectRatioError()
+ return null
+ }
+ this.saveImage(aspectRatio, file, file.type)
this.file = file
-
+ if (this.file.type === 'image/jpeg') this.imageCanBeCropped = true
+ this.$nextTick((this.isLoadingImage = false))
+ },
+ initCropper() {
+ this.showCropper = true
const imageElement = document.querySelector('#cropping-image')
- imageElement.src = URL.createObjectURL(file)
+ imageElement.src = URL.createObjectURL(this.file)
this.cropper = new Cropper(imageElement, { zoomable: false, autoCropArea: 0.9 })
},
cropImage() {
this.isLoadingImage = true
-
- const onCropComplete = (aspectRatio, imageFile) => {
- this.$emit('addImageAspectRatio', aspectRatio)
- this.$emit('addHeroImage', imageFile)
+ const onCropComplete = (aspectRatio, imageFile, imageType) => {
+ this.saveImage(aspectRatio, imageFile, imageType)
this.$nextTick((this.isLoadingImage = false))
this.closeCropper()
}
-
if (this.file.type === 'image/jpeg') {
const canvas = this.cropper.getCroppedCanvas()
canvas.toBlob((blob) => {
const imageAspectRatio = canvas.width / canvas.height
+ if (imageAspectRatio < minAspectRatio) {
+ this.aspectRatioError()
+ return
+ }
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
- onCropComplete(imageAspectRatio, croppedImageFile)
+ onCropComplete(imageAspectRatio, croppedImageFile, 'image/jpeg')
}, 'image/jpeg')
} else {
// TODO: use cropped file instead of original file
@@ -125,9 +147,18 @@ export default {
this.showCropper = false
this.cropper.destroy()
},
+ aspectRatioError() {
+ this.$toast.error(this.$t('contribution.teaserImage.errors.aspect-ratio-too-small'))
+ },
+ saveImage(aspectRatio = 1.0, file, fileType) {
+ this.$emit('addImageAspectRatio', aspectRatio)
+ this.$emit('addHeroImage', file)
+ this.$emit('addImageType', fileType)
+ },
deleteImage() {
this.$emit('addHeroImage', null)
this.$emit('addImageAspectRatio', null)
+ this.$emit('addImageType', null)
},
},
}
@@ -136,7 +167,6 @@ export default {
.image-uploader {
position: relative;
min-height: $size-image-uploader-min-height;
- cursor: pointer;
.image + & {
position: absolute;
@@ -181,6 +211,14 @@ export default {
}
}
+ .delete-image-button {
+ position: absolute;
+ top: $space-small;
+ right: $space-small;
+ z-index: $z-index-surface;
+ cursor: pointer;
+ }
+
.dz-message {
position: absolute;
display: flex;
@@ -189,6 +227,7 @@ export default {
width: 100%;
height: 100%;
z-index: $z-index-surface;
+ cursor: pointer;
&:hover {
> .base-icon {
diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js
index 2626581e1..e4c31bd4b 100644
--- a/webapp/graphql/Fragments.js
+++ b/webapp/graphql/Fragments.js
@@ -51,6 +51,7 @@ export const postFragment = gql`
url
sensitive
aspectRatio
+ type
}
author {
...user
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index a603e58fb..4d34757d8 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -213,8 +213,10 @@
"newPost": "Erstelle einen neuen Beitrag",
"success": "Gespeichert!",
"teaserImage": {
+ "cropImage": "Bild zuschneiden",
"cropperConfirm": "Bestätigen",
"errors": {
+ "aspect-ratio-too-small": "Dieses Bild ist zu hoch.",
"unSupported-file-format": "Bitte lade ein Bild in den folgenden Formaten hoch: JPG, JPEG, PNG or GIF!"
},
"supportedFormats": "Füge ein Bild im Dateiformat JPG, PNG oder GIF ein"
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index 28ee013af..afb767c14 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -213,8 +213,10 @@
"newPost": "Create a new Post",
"success": "Saved!",
"teaserImage": {
+ "cropImage": "Crop image",
"cropperConfirm": "Confirm",
"errors": {
+ "aspect-ratio-too-small": "This image is too high.",
"unSupported-file-format": "Please upload an image of file format: JPG, JPEG, PNG or GIF!"
},
"supportedFormats": "Insert a picture of file format JPG, PNG or GIF"
diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue
index a2147db41..f4ff56264 100644
--- a/webapp/pages/post/_id/_slug/index.vue
+++ b/webapp/pages/post/_id/_slug/index.vue
@@ -176,13 +176,13 @@ export default {
/* Return false when image property is not present or is not a number
so no unnecessary css variables are set.
*/
- if (!this.post.image || typeof this.post.image.aspectRatio !== 'number') return false
+ if (!this.post.image || typeof this.post.image.aspectRatio !== 'number') return false
/* Return the aspect ratio as a css variable. Later to be used when calculating
the height with respect to the width.
*/
return {
- '--hero-image-aspect-ratio': 1 / this.post.image.aspectRatio,
+ '--hero-image-aspect-ratio': 1.0 / this.post.image.aspectRatio,
}
},
},
@@ -258,8 +258,8 @@ export default {
hero image aspect ratio) before the hero image loads so
the autoscroll works correctly when following a comment link.
*/
- padding-top: calc(var(--hero-image-aspect-ratio) * 100%);
+ padding-top: calc(var(--hero-image-aspect-ratio) * (100% + 48px));
/* Letting the image fill the container, since the container
is the one determining height
*/