Fix image handling for both the creator's banner and logo

This commit is contained in:
2025-04-17 05:17:25 -04:00
parent 685a758f53
commit c32bdc9577
3 changed files with 419 additions and 63 deletions

View File

@@ -42,7 +42,7 @@
button.secondary {
@apply btn;
@apply bg-hSecondary text-hOnSecondary;
@apply hover:bg-hSecondary;
@apply hover:brightness-125;
}
div.dialog {

View File

@@ -1,23 +1,51 @@
<template>
<div class="card">
<div class="card-title">
Bannière
Choisissez votre Bannière
</div>
<div class="card-content">
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu de la bannière"
class="mb-5 transition duration-200 ease-in-out transform w-[1024px] h-[256px]"
/>
<p class="card-text">
La bannière doit avoir un ratio de 3:1. Les dimensions cibles sont 960 x 320.
</p>
<div class="file-input-container">
<input
type="file"
ref="fileInput"
accept="image/*"
class="hidden"
@change="onFileSelected"
/>
<button
class="choose-file-button"
@click="triggerFileInput"
>
Choisir une image...
</button>
</div>
<v-file-input
v-model="selectedFile"
accept="image/*"
label="Votre bannière"
variant="outlined"
@change="onFileSelected"
></v-file-input>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div class="cropper-container" v-if="showCropper">
<div class="cropper-wrapper">
<img
ref="cropperImage"
:src="fileUrl"
alt="Image à recadrer"
/>
</div>
</div>
<div class="image-preview-container" v-else>
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu de la bannière"
class="preview-image"
/>
</div>
</div>
<div class="card-actions">
@@ -26,17 +54,19 @@
Annuler
</button>
<button class="primary"
@click="publish">
Enregistrer
@click="showCropper ? applyCrop() : publish()"
:disabled="!selectedFile">
{{ showCropper ? 'Appliquer' : 'Enregistrer' }}
</button>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {ref, onBeforeUnmount} from 'vue'
import {useClient} from '@/plugins/api.js'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
const props = defineProps({
creator: {
@@ -46,24 +76,88 @@ const props = defineProps({
const emits = defineEmits(['closeRequested'])
const selectedFile = ref({})
const fileInput = ref(null)
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 onFileSelected = () => {
if (selectedFile.value) {
const reader = new FileReader()
reader.onload = (event) => {
fileUrl.value = event.target.result
}
reader.readAsDataURL(selectedFile.value)
} else {
fileUrl.value = 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()
}
const onFileSelected = (event) => {
const file = event.target.files[0]
if (file) {
selectedFile.value = file
const reader = new FileReader()
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 {
selectedFile.value = null
fileUrl.value = null
showCropper.value = false
}
}
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
canvas.toBlob((blob) => {
// Create a new file from the blob
const croppedFile = new File([blob], selectedFile.value.name, {
type: selectedFile.value.type
})
selectedFile.value = croppedFile
fileUrl.value = canvas.toDataURL()
showCropper.value = false
}, selectedFile.value.type)
}
const client = useClient()
const publish = async () => {
if (!selectedFile.value) return
try {
const formData = new FormData()
formData.append('file', selectedFile.value)
@@ -77,10 +171,78 @@ const publish = async () => {
emits('closeRequested')
} catch (error) {
console.error(error)
errorMessage.value = 'Une erreur est survenue lors de l\'envoi de l\'image'
}
}
const cancel = () => {
if (cropper.value) {
cropper.value.destroy()
}
emits('closeRequested')
}
onBeforeUnmount(() => {
if (cropper.value) {
cropper.value.destroy()
}
})
</script>
<style scoped>
.card-text {
@apply font-sans text-lg;
}
.image-preview-container {
@apply mb-5;
@apply w-full;
@apply flex;
@apply justify-center;
@apply items-center;
@apply overflow-hidden;
@apply rounded-lg;
}
.preview-image {
@apply w-[960px];
@apply h-[320px];
@apply object-cover;
}
.cropper-container {
@apply mb-5;
@apply w-full;
@apply flex;
@apply justify-center;
@apply items-center;
}
.cropper-wrapper {
@apply max-w-full;
@apply max-h-[500px];
@apply overflow-hidden;
}
.file-input-container {
@apply flex;
@apply justify-center;
@apply items-center;
@apply w-full;
}
.choose-file-button {
@apply px-4;
@apply py-2;
@apply primary;
@apply rounded-lg;
@apply cursor-pointer;
}
.error-message {
@apply text-red-500;
@apply mt-2;
@apply text-center;
@apply font-medium;
}
</style>

View File

@@ -1,24 +1,53 @@
<template>
<div class="card">
<div class="card-title">
Logo
Choisissez votre Logo
</div>
<div class="card-content flex flex-col items-center">
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu du logo"
class="mb-5 transition duration-200 ease-in-out transform w-[200px] h-[200px]"
/>
<div class="card-content">
<p class="card-text">
Le logo doit être carré. Les dimensions recommandées sont 200 x 200 pixels.
</p>
<div class="file-input-container">
<input
type="file"
ref="fileInput"
accept="image/*"
class="hidden"
@change="onFileSelected"
/>
<button
class="choose-file-button"
@click="triggerFileInput"
>
Choisir une image...
</button>
</div>
<v-file-input
v-model="selectedFile"
accept="image/*"
class="w-full"
label="Votre logo"
variant="outlined"
@change="onFileSelected"
></v-file-input>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div class="cropper-container" v-if="showCropper">
<div class="cropper-wrapper">
<img
ref="cropperImage"
:src="fileUrl"
alt="Image à recadrer"
/>
</div>
</div>
<div class="image-preview-container" v-else>
<div class="circular-preview">
<img
:src="fileUrl || fallbackUrl"
alt="Aperçu du logo"
class="preview-image"
/>
</div>
</div>
</div>
<div class="card-actions">
@@ -27,18 +56,19 @@
Annuler
</button>
<button class="primary"
@click="publish">
Enregistrer
@click="showCropper ? applyCrop() : publish()"
:disabled="!selectedFile">
{{ showCropper ? 'Appliquer' : 'Enregistrer' }}
</button>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {ref, onBeforeUnmount} from 'vue'
import {useClient} from '@/plugins/api.js'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
const props = defineProps({
creator: {
@@ -48,41 +78,205 @@ const props = defineProps({
const emits = defineEmits(['closeRequested'])
const selectedFile = ref("")
const fileInput = ref(null)
const selectedFile = ref(null)
const fileUrl = ref(props.creator.images.logo)
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png' // Chemin de votre image de secours
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png'
const errorMessage = ref('')
const cropperImage = ref(null)
const showCropper = ref(false)
const cropper = ref(null)
const onFileSelected = () => {
if (selectedFile.value) {
const reader = new FileReader()
reader.onload = (event) => {
fileUrl.value = event.target.result
}
reader.readAsDataURL(selectedFile.value)
} else {
fileUrl.value = 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()
}
const onFileSelected = (event) => {
const file = event.target.files[0]
if (file) {
selectedFile.value = file
const reader = new FileReader()
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 {
selectedFile.value = null
fileUrl.value = null
showCropper.value = false
}
}
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
canvas.toBlob((blob) => {
// Create a new file from the blob
const croppedFile = new File([blob], selectedFile.value.name, {
type: selectedFile.value.type
})
selectedFile.value = croppedFile
fileUrl.value = canvas.toDataURL()
showCropper.value = false
}, selectedFile.value.type)
}
const client = useClient()
const publish = async () => {
if (!selectedFile.value) return
try {
const formData = new FormData();
const formData = new FormData()
formData.append('file', selectedFile.value)
const response = await client.post(
`/api/creators/${props.creator.id}/logo`,
formData)
formData
)
props.creator.images.logo = `${response.data.blobUrl}?t=${Date.now()}`;
emits('closeRequested');
props.creator.images.logo = `${response.data.blobUrl}?t=${Date.now()}`
emits('closeRequested')
} catch (error) {
console.error(error)
errorMessage.value = 'Une erreur est survenue lors de l\'envoi de l\'image'
}
}
const cancel = () => {
if (cropper.value) {
cropper.value.destroy()
}
emits('closeRequested')
}
onBeforeUnmount(() => {
if (cropper.value) {
cropper.value.destroy()
}
})
</script>
<style scoped>
.card-text {
@apply font-sans text-lg;
}
.image-preview-container {
@apply mb-5;
@apply w-full;
@apply flex;
@apply justify-center;
@apply items-center;
}
.circular-preview {
@apply w-[200px];
@apply h-[200px];
@apply rounded-full;
@apply overflow-hidden;
@apply border-2;
@apply border-gray-200;
}
.preview-image {
@apply w-full;
@apply h-full;
@apply object-cover;
}
.cropper-container {
@apply mb-5;
@apply w-full;
@apply flex;
@apply justify-center;
@apply items-center;
}
.cropper-wrapper {
@apply max-w-full;
@apply max-h-[500px];
@apply overflow-hidden;
}
.file-input-container {
@apply flex;
@apply justify-center;
@apply items-center;
@apply w-full;
}
.choose-file-button {
@apply px-4;
@apply py-2;
@apply primary;
@apply rounded-lg;
@apply cursor-pointer;
}
.error-message {
@apply text-red-500;
@apply mt-2;
@apply text-center;
@apply font-medium;
}
/* Add circular crop box styles */
:deep(.cropper-view-box),
:deep(.cropper-face) {
border-radius: 50%;
}
:deep(.cropper-view-box) {
outline: 2px solid #fff;
outline-color: #fff;
}
:deep(.cropper-face) {
background-color: inherit !important;
}
</style>