Add vue-advanced-cropper for improved image editing in Banner and Logo editors; update dependencies and styles

This commit is contained in:
2025-04-17 22:36:08 -04:00
parent c32bdc9577
commit 8850f55dbf
4 changed files with 1633 additions and 971 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"pinia": "^2.1.7",
"uuid": "^10.0.0",
"vue": "^3.4.15",
"vue-advanced-cropper": "^2.8.9",
"vue-i18n": "^9.14.0",
"vue-router": "^4.2.5",
"vue3-google-login": "^2.0.26",
@@ -33,6 +34,6 @@
"eslint-plugin-vue": "^9.22.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.2.11"
"vite": "^6.3.1"
}
}

View File

@@ -29,22 +29,27 @@
{{ errorMessage }}
</div>
<div class="cropper-container" v-if="showCropper">
<div class="cropper-wrapper">
<img
ref="cropperImage"
<div v-if="showCropper" class="cropper-wrapper">
<Cropper
ref="cropper"
:src="fileUrl"
alt="Image à recadrer"
:aspect-ratio="3"
:stencil-props="{
aspectRatio: 3,
class: 'banner-stencil'
}"
/>
</div>
</div>
<div class="image-preview-container" v-else>
<div v-else class="image-preview-container" @click="startEditing">
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu de la bannière"
class="preview-image"
/>
<div class="edit-overlay">
<span class="edit-text">Cliquez pour modifier</span>
</div>
</div>
</div>
@@ -63,10 +68,10 @@
</template>
<script setup>
import {ref, onBeforeUnmount} from 'vue'
import {ref} from 'vue'
import {useClient} from '@/plugins/api.js'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { Cropper } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
const props = defineProps({
creator: {
@@ -81,33 +86,12 @@ const selectedFile = ref(null)
const fileUrl = ref(props.creator?.images?.banner)
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
const errorMessage = ref('')
const cropperImage = ref(null)
const showCropper = ref(false)
const cropper = ref(null)
const TARGET_RATIO = 3 // 3:1 ratio
const TARGET_WIDTH = 960
const TARGET_HEIGHT = 320
const initCropper = () => {
if (cropper.value) {
cropper.value.destroy()
}
cropper.value = new Cropper(cropperImage.value, {
aspectRatio: TARGET_RATIO,
viewMode: 2,
responsive: true,
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
})
}
const triggerFileInput = () => {
fileInput.value.click()
}
@@ -120,10 +104,6 @@ const onFileSelected = (event) => {
reader.onload = (e) => {
fileUrl.value = e.target.result
showCropper.value = true
// Wait for the image to be loaded in the DOM
setTimeout(() => {
initCropper()
}, 0)
}
reader.readAsDataURL(file)
} else {
@@ -133,18 +113,27 @@ const onFileSelected = (event) => {
}
}
const startEditing = () => {
if (fileUrl.value && fileUrl.value !== fallbackUrl) {
// Create a temporary file from the current image URL
fetch(fileUrl.value)
.then(res => res.blob())
.then(blob => {
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' })
showCropper.value = true
})
.catch(error => {
console.error('Error loading image for editing:', error)
errorMessage.value = 'Une erreur est survenue lors du chargement de l\'image'
})
}
}
const applyCrop = () => {
if (!cropper.value) return
// Get the cropped canvas
const canvas = cropper.value.getCroppedCanvas({
width: TARGET_WIDTH,
height: TARGET_HEIGHT
})
// Convert canvas to blob
const canvas = cropper.value.getResult().canvas
canvas.toBlob((blob) => {
// Create a new file from the blob
const croppedFile = new File([blob], selectedFile.value.name, {
type: selectedFile.value.type
})
@@ -176,17 +165,17 @@ const publish = async () => {
}
const cancel = () => {
if (cropper.value) {
cropper.value.destroy()
showCropper.value = false
// Reset to original state if we were editing
if (props.creator?.images?.banner) {
fileUrl.value = props.creator.images.banner
selectedFile.value = null
} else {
fileUrl.value = fallbackUrl
selectedFile.value = null
}
emits('closeRequested')
}
onBeforeUnmount(() => {
if (cropper.value) {
cropper.value.destroy()
}
})
</script>
<style scoped>
@@ -202,6 +191,8 @@ onBeforeUnmount(() => {
@apply items-center;
@apply overflow-hidden;
@apply rounded-lg;
@apply relative;
@apply cursor-pointer;
}
.preview-image {
@@ -210,17 +201,41 @@ onBeforeUnmount(() => {
@apply object-cover;
}
.cropper-container {
@apply mb-5;
@apply w-full;
.edit-overlay {
@apply absolute;
@apply inset-0;
@apply flex;
@apply justify-center;
@apply items-center;
@apply justify-center;
@apply bg-black;
@apply bg-opacity-0;
@apply transition-opacity;
@apply duration-200;
}
.image-preview-container:hover .edit-overlay {
@apply bg-opacity-30;
}
.edit-text {
@apply text-white;
@apply font-medium;
@apply opacity-0;
@apply transition-opacity;
@apply duration-200;
}
.image-preview-container:hover .edit-text {
@apply opacity-100;
}
.cropper-wrapper {
@apply max-w-full;
@apply max-h-[500px];
@apply mb-5;
@apply w-full;
@apply h-[400px];
@apply flex;
@apply justify-center;
@apply items-center;
@apply overflow-hidden;
}
@@ -245,4 +260,13 @@ onBeforeUnmount(() => {
@apply text-center;
@apply font-medium;
}
:deep(.banner-stencil) {
@apply border-2;
@apply border-white;
}
:deep(.cropper) {
@apply max-h-full;
}
</style>

View File

@@ -29,23 +29,29 @@
{{ errorMessage }}
</div>
<div class="cropper-container" v-if="showCropper">
<div class="cropper-wrapper">
<img
ref="cropperImage"
<div v-if="showCropper" class="cropper-wrapper">
<Cropper
ref="cropper"
:src="fileUrl"
alt="Image à recadrer"
:aspect-ratio="1"
:stencil-component="CircleStencil"
:stencil-props="{
aspectRatio: 1,
class: 'circle-stencil'
}"
/>
</div>
</div>
<div class="image-preview-container" v-else>
<div v-else class="image-preview-container" @click="startEditing">
<div class="circular-preview">
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu du logo"
class="preview-image"
/>
<div class="edit-overlay">
<span class="edit-text">Cliquez pour modifier</span>
</div>
</div>
</div>
</div>
@@ -65,10 +71,10 @@
</template>
<script setup>
import {ref, onBeforeUnmount} from 'vue'
import {ref} from 'vue'
import {useClient} from '@/plugins/api.js'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { Cropper, CircleStencil } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
const props = defineProps({
creator: {
@@ -83,42 +89,12 @@ const selectedFile = ref(null)
const fileUrl = ref(props.creator.images.logo)
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png'
const errorMessage = ref('')
const cropperImage = ref(null)
const showCropper = ref(false)
const cropper = ref(null)
const TARGET_RATIO = 1 // 1:1 ratio for square logo
const TARGET_WIDTH = 200
const TARGET_HEIGHT = 200
const initCropper = () => {
if (cropper.value) {
cropper.value.destroy()
}
cropper.value = new Cropper(cropperImage.value, {
aspectRatio: TARGET_RATIO,
viewMode: 2,
responsive: true,
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
cropBoxResizable: true,
cropBoxMovable: true,
cropBoxWidth: 200,
cropBoxHeight: 200,
preview: '.circular-preview',
cropBoxResizable: true,
cropBoxMovable: true,
cropBoxWidth: 200,
cropBoxHeight: 200,
})
}
const triggerFileInput = () => {
fileInput.value.click()
}
@@ -131,10 +107,6 @@ const onFileSelected = (event) => {
reader.onload = (e) => {
fileUrl.value = e.target.result
showCropper.value = true
// Wait for the image to be loaded in the DOM
setTimeout(() => {
initCropper()
}, 0)
}
reader.readAsDataURL(file)
} else {
@@ -144,18 +116,27 @@ const onFileSelected = (event) => {
}
}
const startEditing = () => {
if (fileUrl.value && fileUrl.value !== fallbackUrl) {
// Create a temporary file from the current image URL
fetch(fileUrl.value)
.then(res => res.blob())
.then(blob => {
selectedFile.value = new File([blob], 'current-image.jpg', { type: 'image/jpeg' })
showCropper.value = true
})
.catch(error => {
console.error('Error loading image for editing:', error)
errorMessage.value = 'Une erreur est survenue lors du chargement de l\'image'
})
}
}
const applyCrop = () => {
if (!cropper.value) return
// Get the cropped canvas
const canvas = cropper.value.getCroppedCanvas({
width: TARGET_WIDTH,
height: TARGET_HEIGHT
})
// Convert canvas to blob
const canvas = cropper.value.getResult().canvas
canvas.toBlob((blob) => {
// Create a new file from the blob
const croppedFile = new File([blob], selectedFile.value.name, {
type: selectedFile.value.type
})
@@ -187,17 +168,17 @@ const publish = async () => {
}
const cancel = () => {
if (cropper.value) {
cropper.value.destroy()
showCropper.value = false
// Reset to original state if we were editing
if (props.creator.images.logo) {
fileUrl.value = props.creator.images.logo
selectedFile.value = null
} else {
fileUrl.value = fallbackUrl
selectedFile.value = null
}
emits('closeRequested')
}
onBeforeUnmount(() => {
if (cropper.value) {
cropper.value.destroy()
}
})
</script>
<style scoped>
@@ -220,6 +201,8 @@ onBeforeUnmount(() => {
@apply overflow-hidden;
@apply border-2;
@apply border-gray-200;
@apply relative;
@apply cursor-pointer;
}
.preview-image {
@@ -228,17 +211,41 @@ onBeforeUnmount(() => {
@apply object-cover;
}
.cropper-container {
@apply mb-5;
@apply w-full;
.edit-overlay {
@apply absolute;
@apply inset-0;
@apply flex;
@apply justify-center;
@apply items-center;
@apply justify-center;
@apply bg-black;
@apply bg-opacity-0;
@apply transition-opacity;
@apply duration-200;
}
.circular-preview:hover .edit-overlay {
@apply bg-opacity-30;
}
.edit-text {
@apply text-white;
@apply font-medium;
@apply opacity-0;
@apply transition-opacity;
@apply duration-200;
}
.circular-preview:hover .edit-text {
@apply opacity-100;
}
.cropper-wrapper {
@apply max-w-full;
@apply max-h-[500px];
@apply mb-5;
@apply w-full;
@apply h-[400px];
@apply flex;
@apply justify-center;
@apply items-center;
@apply overflow-hidden;
}
@@ -264,19 +271,18 @@ onBeforeUnmount(() => {
@apply font-medium;
}
/* Add circular crop box styles */
:deep(.cropper-view-box),
:deep(.cropper-face) {
border-radius: 50%;
:deep(.circle-stencil) {
@apply border-2;
@apply border-white;
@apply rounded-full;
}
:deep(.cropper-view-box) {
outline: 2px solid #fff;
outline-color: #fff;
:deep(.cropper) {
@apply max-h-full;
}
:deep(.cropper-face) {
background-color: inherit !important;
:deep(.cropper__stencil) {
@apply rounded-full;
}
</style>