feat: Update banner system components
This commit is contained in:
@@ -2,13 +2,13 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<!-- Banner Container with mouse events -->
|
<!-- Banner Container with mouse events -->
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative rounded-b-2xl overflow-y-auto"
|
||||||
@mouseenter="showTint = isCurrentCreator"
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
@mouseleave="showTint = false"
|
@mouseleave="showTint = false"
|
||||||
@click="isCurrentCreator && openBannerEditor()"
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="w-full aspect-[3/1] banner object-cover"
|
class="w-full aspect-[4/1] banner object-cover"
|
||||||
:src="brandingStore.value?.images?.banner ?? '/images/placeholders/banner.png'"
|
:src="brandingStore.value?.images?.banner ?? '/images/placeholders/banner.png'"
|
||||||
:alt="t('alt')"
|
:alt="t('alt')"
|
||||||
>
|
>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Top-right Icon -->
|
<!-- Top-right Icon -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-4 right-4 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
class="absolute top-4 right-4 w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
|
||||||
>
|
>
|
||||||
<v-icon large>mdi-pencil</v-icon>
|
<v-icon large>mdi-pencil</v-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,11 +28,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||||
<template #default="{ close }">
|
<BannerEditor
|
||||||
<banner-editor :creator="brandingStore.value"
|
:creator="brandingStore.value"
|
||||||
@closeRequested="() => isDialogOpen = false"
|
@closeRequested="() => isDialogOpen = false"
|
||||||
></banner-editor>
|
/>
|
||||||
</template>
|
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,9 +61,6 @@ const isCurrentCreator = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.banner {
|
|
||||||
@apply border-b border-solid border-hTertiary;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
|
||||||
import {useBrandingStore} from '@/stores/brandingStore.js';
|
import {useBrandingStore} from '@/stores/brandingStore.js';
|
||||||
import DonationButtonBanner from '@/views/creators/DonationButtonBanner.vue';
|
import DonationButton from '@/views/creators/DonationButton.vue';
|
||||||
import CreatorLogo from "@/views/creators/CreatorLogo.vue";
|
import CreatorLogo from "@/views/creators/CreatorLogo.vue";
|
||||||
import NameTitle from "@/views/creators/NameTitle.vue";
|
import NameTitle from "@/views/creators/NameTitle.vue";
|
||||||
import Linkedin from "@/views/svg/Linkedin.vue";
|
import Linkedin from "@/views/svg/Linkedin.vue";
|
||||||
@@ -17,63 +16,36 @@ import { useI18n } from 'vue-i18n'
|
|||||||
const brandingStore = useBrandingStore();
|
const brandingStore = useBrandingStore();
|
||||||
const baseURL = window.location.origin;
|
const baseURL = window.location.origin;
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
// Gèrer le breakpoint du block information.
|
|
||||||
// Définir un point de rupture pour "moyen" (correspondant à md: de Tailwind)
|
|
||||||
const mediumBreakpoint = 768;
|
|
||||||
// Référence réactive pour indiquer si nous sommes sur un écran moyen ou grand
|
|
||||||
const isMediumOrLarger = ref(window.innerWidth >= mediumBreakpoint);
|
|
||||||
// Fonction pour mettre à jour isMediumOrLarger en fonction de la largeur de l'écran
|
|
||||||
const updateScreenSize = () => {
|
|
||||||
isMediumOrLarger.value = window.innerWidth >= mediumBreakpoint;
|
|
||||||
};
|
|
||||||
// Ajouter un écouteur d'événements pour redimensionner la fenêtre
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', updateScreenSize);
|
|
||||||
// Appeler updateScreenSize une fois au montage pour initialiser la valeur
|
|
||||||
updateScreenSize();
|
|
||||||
});
|
|
||||||
// Supprimer l'écouteur d'événements lors du démontage du composant
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', updateScreenSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-column w-full">
|
<div class="flex flex-column w-full">
|
||||||
<!-- Container principal avec le profil -->
|
<!-- Container principal avec le profil -->
|
||||||
<div class="relative w-full shadow-xl">
|
<div class="relative w-full">
|
||||||
<div class="shadow-2xl bg-hPrimary text-hOnPrimary"
|
<div class="bg-hPrimary text-hOnPrimary relative">
|
||||||
:style="{
|
|
||||||
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.3)',
|
|
||||||
}">
|
|
||||||
|
|
||||||
<!-- Desktop version (visible only on écrans moyens et grands) -->
|
<!-- Portrait that overlaps both sections -->
|
||||||
<div v-if="isMediumOrLarger" class="flex flex-row py-4 pr-4 items-center social-info">
|
<div class="absolute left-4 -bottom-2 z-10">
|
||||||
<name-title></name-title>
|
|
||||||
<creator-logo/>
|
<creator-logo/>
|
||||||
<donation-button-banner
|
|
||||||
v-if="brandingStore.value?.acceptDonation"
|
|
||||||
:creator-id="brandingStore.value?.id"
|
|
||||||
:creator-name="brandingStore.value?.name"
|
|
||||||
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
|
||||||
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
|
|
||||||
></donation-button-banner>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile version (visible only on les petits écrans) -->
|
<!-- Desktop version (visible only on écrans moyens et grands) -->
|
||||||
<div v-else class="flex flex-row py-4 pr-4 items-center social-info">
|
<div class="social-info">
|
||||||
<div><creator-logo /></div>
|
<div class="w-36">
|
||||||
<div class="name-button">
|
</div>
|
||||||
|
<div class="flex-grow flex flex-row">
|
||||||
|
<div class="flex-grow">
|
||||||
<name-title></name-title>
|
<name-title></name-title>
|
||||||
<donation-button-banner
|
</div>
|
||||||
|
<div class="hidden sm:flex pr-6">
|
||||||
|
<DonationButton
|
||||||
v-if="brandingStore.value?.acceptDonation"
|
v-if="brandingStore.value?.acceptDonation"
|
||||||
:creator-id="brandingStore.value?.id"
|
:creator-id="brandingStore.value?.id"
|
||||||
:creator-name="brandingStore.value?.name"
|
:creator-name="brandingStore.value?.name"
|
||||||
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
||||||
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
|
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
|
||||||
></donation-button-banner>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,7 +54,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- Section pour les icônes de réseaux sociaux -->
|
<!-- Section pour les icônes de réseaux sociaux -->
|
||||||
<div
|
<div
|
||||||
class="rounded-b-2xl h-12 flex w-full items-center justify-center bg-hSecondary text-hOnSecondary"
|
class="h-12 flex w-full items-center justify-center bg-hSecondary text-hOnSecondary relative"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-10">
|
<div class="flex flex-row gap-10">
|
||||||
|
|
||||||
@@ -157,14 +129,10 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.social-info {
|
.social-info {
|
||||||
@apply flex justify-center items-center w-full;
|
@apply flex flex-row;
|
||||||
|
@apply py-4 w-full;
|
||||||
|
@apply justify-center ;
|
||||||
@apply max-h-52;
|
@apply max-h-52;
|
||||||
@apply px-1;
|
|
||||||
@apply gap-8;
|
|
||||||
|
|
||||||
.name-button {
|
|
||||||
@apply flex flex-col gap-y-4;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@
|
|||||||
<Cropper
|
<Cropper
|
||||||
ref="cropper"
|
ref="cropper"
|
||||||
:src="fileUrl"
|
:src="fileUrl"
|
||||||
:aspect-ratio="3"
|
:aspect-ratio="4"
|
||||||
:stencil-props="{
|
:stencil-props="{
|
||||||
aspectRatio: 3,
|
aspectRatio: 4,
|
||||||
class: 'banner-stencil'
|
class: 'banner-stencil'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -55,13 +55,20 @@
|
|||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="secondary"
|
<button class="secondary"
|
||||||
@click="cancel">
|
@click="cancel"
|
||||||
|
:disabled="isUploading">
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary"
|
<button class="primary"
|
||||||
@click="showCropper ? applyCrop() : publish()"
|
@click="showCropper ? applyCrop() : publish()"
|
||||||
:disabled="!selectedFile">
|
:disabled="!selectedFile || isUploading">
|
||||||
|
<template v-if="isUploading">
|
||||||
|
<span class="loading-spinner"></span>
|
||||||
|
{{ t('uploading') }} ({{ uploadProgress }}%)
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
{{ showCropper ? t('apply') : t('save') }}
|
{{ showCropper ? t('apply') : t('save') }}
|
||||||
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,9 +96,8 @@ const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const showCropper = ref(false)
|
const showCropper = ref(false)
|
||||||
const cropper = ref(null)
|
const cropper = ref(null)
|
||||||
|
const isUploading = ref(false)
|
||||||
const TARGET_WIDTH = 960
|
const uploadProgress = ref(0)
|
||||||
const TARGET_HEIGHT = 320
|
|
||||||
|
|
||||||
// Get translations for this component
|
// Get translations for this component
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -151,15 +157,22 @@ const applyCrop = () => {
|
|||||||
|
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
if (!selectedFile.value) return
|
if (!selectedFile.value || isUploading.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
isUploading.value = true
|
||||||
|
uploadProgress.value = 0
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', selectedFile.value)
|
formData.append('file', selectedFile.value)
|
||||||
|
|
||||||
const response = await client.post(
|
const response = await client.post(
|
||||||
`/api/creators/${props.creator.id}/banner`,
|
`/api/creators/${props.creator.id}/banner`,
|
||||||
formData
|
formData,
|
||||||
|
{
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
props.creator.images.banner = `${response.data.blobUrl}?t=${Date.now()}`
|
props.creator.images.banner = `${response.data.blobUrl}?t=${Date.now()}`
|
||||||
@@ -167,6 +180,9 @@ const publish = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
errorMessage.value = t('errors.imageUpload')
|
errorMessage.value = t('errors.imageUpload')
|
||||||
|
} finally {
|
||||||
|
isUploading.value = false
|
||||||
|
uploadProgress.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +218,8 @@ const cancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-image {
|
.preview-image {
|
||||||
@apply w-[960px];
|
@apply w-full;
|
||||||
@apply h-[320px];
|
@apply aspect-[4/1];
|
||||||
@apply object-cover;
|
@apply object-cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,21 +291,51 @@ const cancel = () => {
|
|||||||
:deep(.cropper) {
|
:deep(.cropper) {
|
||||||
@apply max-h-full;
|
@apply max-h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
@apply inline-block;
|
||||||
|
@apply w-4;
|
||||||
|
@apply h-4;
|
||||||
|
@apply mr-2;
|
||||||
|
@apply border-2;
|
||||||
|
@apply border-white;
|
||||||
|
@apply border-t-transparent;
|
||||||
|
@apply rounded-full;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
{
|
||||||
"en": {
|
"en": {
|
||||||
|
"title": "Banner Editor",
|
||||||
|
"description": "Upload or edit your profile banner image. The recommended size is 1024x256 pixels (4:1 ratio).",
|
||||||
"chooseImage": "Choose an image",
|
"chooseImage": "Choose an image",
|
||||||
"clickToEdit": "Click to edit"
|
"clickToEdit": "Click to edit",
|
||||||
|
"uploading": "Uploading"
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
|
"title": "Éditeur de bannière",
|
||||||
|
"description": "Téléchargez ou modifiez votre image de bannière de profil. La taille recommandée est de 1024x256 pixels (ratio 4:1).",
|
||||||
"chooseImage": "Choisir une image",
|
"chooseImage": "Choisir une image",
|
||||||
"clickToEdit": "Cliquez pour modifier"
|
"clickToEdit": "Cliquez pour modifier",
|
||||||
|
"uploading": "Téléchargement"
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
|
"title": "Editor de banner",
|
||||||
|
"description": "Sube o edita tu imagen de banner de perfil. El tamaño recomendado es de 1024x256 píxeles (ratio 4:1).",
|
||||||
"chooseImage": "Elegir una imagen",
|
"chooseImage": "Elegir una imagen",
|
||||||
"clickToEdit": "Haga clic para editar"
|
"clickToEdit": "Haga clic para editar",
|
||||||
|
"uploading": "Subiendo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
Reference in New Issue
Block a user