Files
social-media/frontend/src/views/creators/AboutCreator.vue

492 lines
14 KiB
Vue

<template>
<div class="p-4 relative"
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
@mouseleave="showEditButtons = false">
<!-- Edit buttons with absolute positioning -->
<div v-if="showEditButtons || isEditMode"
class="absolute top-4 right-4 flex gap-2">
<!-- Edit button with pencil icon -->
<button
v-if="!isEditMode"
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
@click="toggleEditMode()"
:title="t('edit')"
>
<v-icon large>mdi-pencil</v-icon>
</button>
<!-- Save button -->
<button
v-if="isEditMode"
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
@click="saveChanges()"
:title="t('save')"
>
<v-icon large>mdi-check</v-icon>
</button>
<!-- Cancel button -->
<button
v-if="isEditMode"
class="w-12 h-12 bg-red-500 rounded-full flex items-center justify-center shadow-lg"
@click="cancelEdit"
:title="t('cancel')"
>
<v-icon large>mdi-close</v-icon>
</button>
</div>
<!-- MainPage -->
<div class="flex flex-col">
<h1 class="flex justify-start text-2xl font-bold text-center mb-4">
{{ t('creator.sections.about.title') }}
</h1>
<div>
<!-- Description Section -->
<div>
<div v-if="!isEditMode">
<p v-if="description" class="text-lg text-justify mb-6">
{{ description }}
</p>
</div>
<v-textarea v-if="isEditMode"
v-model="editableDescription"
class="w-full p-2 py-6"
:label="t('creator.sections.about.description')"
variant="outlined"></v-textarea>
</div>
<!-- Video Section -->
<div v-if="videoUrl || isEditMode"
:class="['content-section', {
'rounded-t-xl': hasImages || isEditMode,
'rounded-xl': !hasImages && !isEditMode
}]">
<div v-if="!isEditMode && videoUrl" class="video-container">
<iframe
:src="youtubeEmbedUrl"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
class="video-frame"
title="YouTube video player">
</iframe>
</div>
<div v-if="isEditMode">
<v-text-field
v-model="editableVideoUrl"
class="w-full p-2"
:label="t('creator.fields.videoUrl')"
type="text"
variant="outlined"
:error-messages="videoUrlError"
/>
</div>
</div>
<!-- Photos Section using CreatorAlbum component -->
<CreatorAlbum
v-if="hasImages || isEditMode"
:is-edit-mode="isEditMode"
:images="imageUrls"
@update:images="updateImages"
@update:isEditMode="isEditMode = $event"
:class="['content-section', {
'rounded-b-xl': videoUrl || isEditMode,
'rounded-xl': !videoUrl && !isEditMode
}]"
/>
<!-- Contact Information Section -->
<div v-if="phoneNumber || email" class="contact-info mt-6">
<!-- Phone Number -->
<div v-if="phoneNumber" class="contact-item">
{{ phoneNumber }}
</div>
<!-- Email -->
<div v-if="email" class="contact-item">
{{ email }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref, computed, watch} from "vue";
import {useClient} from "@/plugins/api.js";
import {useBrandingStore} from "@/stores/brandingStore.js";
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
import {useI18n} from 'vue-i18n';
import CreatorAlbum from './CreatorAlbum.vue';
import {buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId} from '@/utils/youtube';
const {t} = useI18n();
const creatorProfileStore = useCreatorProfileStore();
const brandingStore = useBrandingStore();
const client = useClient();
const isLoading = ref(true);
const isLoggedIn = true;
const isEditMode = ref(false);
const showEditButtons = ref(false);
// Variables réactives pour les données
const description = ref("");
const videoUrl = ref("");
const phoneNumber = ref("");
const email = ref("");
const imageUrls = ref([]);
const albumId = ref(null);
const originalPhotos = ref([]);
// Editable fields
const editableDescription = ref("");
const editableVideoUrl = ref("");
const editablePhoneNumber = ref("");
const editableEmail = ref("");
const videoUrlError = ref("");
// Computed property to check if there are images
const hasImages = computed(() => {
// Only consider it has images if there are actual image URLs (not empty strings)
return imageUrls.value.some(img => img && img.trim() !== "");
});
// Computed property for YouTube embed URL
const youtubeEmbedUrl = computed(() => {
if (!videoUrl.value) return "";
return buildEmbedUrl(videoUrl.value);
});
// Validate video URL
function validateVideoUrl(url) {
if (!url) {
videoUrlError.value = "";
return true;
}
if (!isValidYouTubeUrlOrId(url)) {
videoUrlError.value = t('creator.validation.invalidYoutubeUrl');
return false;
}
videoUrlError.value = "";
return true;
}
// Watch for changes in editableVideoUrl
watch(editableVideoUrl, (newValue) => {
validateVideoUrl(newValue);
});
// Activer/désactiver le mode édition
function toggleEditMode() {
isEditMode.value = !isEditMode.value;
if (isEditMode.value) {
// Charger les valeurs pour l'édition
editableDescription.value = description.value;
editableVideoUrl.value = videoUrl.value;
editablePhoneNumber.value = phoneNumber.value;
editableEmail.value = email.value;
videoUrlError.value = "";
}
}
// Fetch album data
async function fetchAlbumData() {
if (!creatorProfileStore.creator?.id) return;
albumId.value = creatorProfileStore.creator.id;
try {
// Try to get the album
const response = await client.get(`/api/albums/${albumId.value}`);
if (response.data && response.data.photos) {
// Store original photos for comparison
originalPhotos.value = response.data.photos;
// Extract photo URLs from the album photos
imageUrls.value = response.data.photos.map(photo => photo.photoUrl);
} else {
// Initialize with empty slots for adding new photos
imageUrls.value = Array(6).fill("");
originalPhotos.value = [];
}
} catch (error) {
// Album might not exist yet, which is fine
console.log("Album might not exist yet:", error);
// Initialize with empty slots for adding new photos
imageUrls.value = Array(6).fill("");
originalPhotos.value = [];
}
}
// Charger les données au montage
onMounted(async () => {
if (!brandingStore.value?.presentation) return;
description.value = brandingStore.value.presentation.description || "";
videoUrl.value = brandingStore.value.presentation.videoUrl || "";
phoneNumber.value = brandingStore.value.presentation.phoneNumber || "";
email.value = brandingStore.value.presentation.email || "";
// Fetch album data
await fetchAlbumData();
});
// Update images from CreatorAlbum component
function updateImages(newImages) {
imageUrls.value = newImages;
}
async function saveChanges() {
if (!creatorProfileStore.creator.id) {
console.error("L'ID du créateur est manquant !");
return;
}
// Validate video URL before saving
if (!validateVideoUrl(editableVideoUrl.value)) {
return;
}
try {
isLoading.value = true;
// Save presentation info
const presentationResponse = await client.post(
`/api/creators/${creatorProfileStore.creator.id}/presentation-infos`,
{
description: editableDescription.value || "",
videoUrl: editableVideoUrl.value || "",
phoneNumber: editablePhoneNumber.value || "",
email: editableEmail.value || ""
}
);
// Mettre à jour les valeurs locales pour refléter les changements
description.value = editableDescription.value;
videoUrl.value = extractVideoId(editableVideoUrl.value) || "";
phoneNumber.value = editablePhoneNumber.value;
email.value = editableEmail.value;
// Save album photos if they've changed
if (imageUrls.value.length > 0) {
// Create or update the album
const albumId = creatorProfileStore.creator.id;
try {
// Try to create the album first (it will fail if it already exists)
await client.post('/api/albums', {
albumId: albumId,
title: `${creatorProfileStore.creator.name}'s Album`,
description: "Photo album for the creator"
});
} catch (error) {
// Album might already exist, which is fine
console.log("Album might already exist:", error);
}
// Check for deleted photos
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
// If the photo URL is not in the current images array, it was deleted
return !imageUrls.value.includes(originalPhoto.photoUrl);
});
// Delete removed photos
for (const photo of deletedPhotos) {
try {
await client.delete(`/api/albums/${albumId}/photos/${photo.id}`);
} catch (error) {
console.error("Error deleting photo:", error);
}
}
// Now add or update photos
for (let i = 0; i < imageUrls.value.length; i++) {
const imageUrl = imageUrls.value[i];
if (imageUrl && imageUrl.startsWith('data:')) {
// This is a new image that needs to be uploaded
const photoId = crypto.randomUUID();
const formData = new FormData();
// Convert data URL to file
const response = await fetch(imageUrl);
const blob = await response.blob();
const file = new File([blob], `photo-${i}.jpg`, { type: 'image/jpeg' });
formData.append('file', file);
await client.post(`/api/albums/${albumId}/photos`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
params: {
photoId: photoId
}
});
}
}
// Refresh album data after changes
await fetchAlbumData();
}
console.log("Données sauvegardées :", presentationResponse.data);
isEditMode.value = false;
} catch (error) {
console.error("Erreur lors de la sauvegarde :", error);
} finally {
isLoading.value = false;
}
}
function cancelEdit() {
// Restaurer les valeurs d'origine
editableDescription.value = description.value;
editableVideoUrl.value = videoUrl.value;
editablePhoneNumber.value = phoneNumber.value;
editableEmail.value = email.value;
// Désactiver le mode édition
isEditMode.value = false;
}
</script>
<style scoped>
.content-section {
@apply w-full overflow-hidden;
}
.video-container {
position: relative;
width: 100%;
padding-top: 31.25%; /* Reduced from 56.25% to make it shorter while maintaining aspect ratio */
max-height: 40vh;
}
.video-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
/* Add responsive breakpoints */
@media (max-width: 640px) {
.video-container {
padding-top: 35%;
max-height: 35vh;
}
}
@media (min-width: 1024px) {
.video-container {
padding-top: 30%;
max-height: 38vh;
}
}
.contact-info {
@apply flex flex-col items-center;
}
.contact-item {
@apply text-lg text-center mb-2 font-semibold;
}
</style>
<i18n>
{
"en": {
"edit": "Edit",
"save": "Save",
"cancel": "Cancel",
"creator": {
"sections": {
"about": {
"title": "About",
"description": "Description",
"contactInfo": "Contact Information"
},
"photos": {
"title": "Photos",
"image": "Image"
}
},
"fields": {
"videoUrl": "Video URL",
"phoneNumber": "Phone Number",
"email": "Email"
},
"validation": {
"invalidYoutubeUrl": "Please enter a valid YouTube URL or video ID"
}
}
},
"fr": {
"edit": "Modifier",
"save": "Enregistrer",
"cancel": "Annuler",
"creator": {
"sections": {
"about": {
"title": "À propos",
"description": "Description",
"contactInfo": "Informations de contact"
},
"photos": {
"title": "Photos",
"image": "Image"
}
},
"fields": {
"videoUrl": "URL de la vidéo",
"phoneNumber": "Numéro de téléphone",
"email": "Email"
},
"validation": {
"invalidYoutubeUrl": "Veuillez entrer une URL YouTube ou un ID de vidéo valide"
}
}
},
"es": {
"edit": "Editar",
"save": "Guardar",
"cancel": "Cancelar",
"creator": {
"sections": {
"about": {
"title": "Acerca de",
"description": "Descripción",
"contactInfo": "Información de contacto"
},
"photos": {
"title": "Fotos",
"image": "Imagen"
}
},
"fields": {
"videoUrl": "URL del video",
"phoneNumber": "Número de teléfono",
"email": "Correo electrónico"
},
"validation": {
"invalidYoutubeUrl": "Por favor, introduce una URL de YouTube o un ID de video válido"
}
}
}
}
</i18n>