feat: Update banner system components

This commit is contained in:
2025-04-22 15:36:38 -04:00
parent aa59d293cf
commit 3bb52e7555
3 changed files with 100 additions and 90 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>