687 lines
23 KiB
Vue
687 lines
23 KiB
Vue
<template>
|
|
<div
|
|
class="relative p-4"
|
|
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
|
|
@mouseleave="showEditButtons = false"
|
|
>
|
|
<!-- Edit buttons with absolute positioning -->
|
|
<div
|
|
v-if="showEditButtons || isEditMode"
|
|
class="absolute right-4 top-4 flex gap-2"
|
|
>
|
|
<!-- Edit button with pencil icon -->
|
|
<button
|
|
v-if="!isEditMode"
|
|
:title="t('edit')"
|
|
class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
|
|
@click="toggleEditMode()"
|
|
>
|
|
<v-icon
|
|
:icon="mdiPencil"
|
|
large
|
|
/>
|
|
</button>
|
|
|
|
<!-- Save button -->
|
|
<button
|
|
v-if="isEditMode"
|
|
:disabled="isSaving || !canSave"
|
|
:title="t('save')"
|
|
class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
|
|
@click="saveChanges()"
|
|
>
|
|
<template v-if="isSaving">
|
|
<v-progress-circular
|
|
color="white"
|
|
indeterminate
|
|
size="20"
|
|
width="2"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<v-icon :icon="mdiCheck" />
|
|
</template>
|
|
</button>
|
|
|
|
<!-- Cancel button -->
|
|
<button
|
|
v-if="isEditMode"
|
|
:title="t('cancel')"
|
|
class="flex size-12 items-center justify-center rounded-full bg-red-500 shadow-lg"
|
|
@click="cancelEdit"
|
|
>
|
|
<v-icon
|
|
:icon="mdiClose"
|
|
large
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- MainPage -->
|
|
<div class="flex flex-col">
|
|
<h1 class="mb-4 flex justify-start text-center text-2xl font-bold">
|
|
{{ t('creator.sections.about.title') }}
|
|
</h1>
|
|
|
|
<div>
|
|
<!-- Description Section -->
|
|
<div>
|
|
<div v-if="!isEditMode">
|
|
<p
|
|
v-if="description"
|
|
class="mb-6 whitespace-pre-line text-justify text-lg"
|
|
>
|
|
{{ description }}
|
|
</p>
|
|
</div>
|
|
<v-textarea
|
|
v-if="isEditMode"
|
|
v-model="editableDescription"
|
|
:counter="2000"
|
|
:error-messages="descriptionError"
|
|
:label="t('creator.sections.about.description')"
|
|
:rules="[
|
|
v => !!v || t('creator.validation.descriptionRequired'),
|
|
v => v.length <= 2000 || t('creator.validation.descriptionTooLong'),
|
|
]"
|
|
auto-grow
|
|
class="w-full p-2 py-6"
|
|
rows="5"
|
|
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"
|
|
:error-messages="videoUrlError"
|
|
:label="t('creator.fields.videoUrl')"
|
|
class="w-full p-2"
|
|
type="text"
|
|
variant="outlined"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photos Section using Album component -->
|
|
<div>
|
|
<!-- Use AlbumView for display mode -->
|
|
<AlbumView
|
|
v-if="!isEditMode && hasImages"
|
|
:class="[
|
|
'content-section',
|
|
{
|
|
'rounded-b-xl': videoUrl && !isEditMode,
|
|
'rounded-xl': !videoUrl && !isEditMode,
|
|
},
|
|
]"
|
|
:images="thumbnailUrls"
|
|
@photo-click="handlePhotoClick"
|
|
/>
|
|
|
|
<AlbumViewer
|
|
v-model="showAlbumViewer"
|
|
:images="originalUrls"
|
|
:start-index="selectedPhotoIndex"
|
|
/>
|
|
|
|
<!-- Use AlbumEditor for edit mode -->
|
|
<AlbumEditor
|
|
v-if="isEditMode"
|
|
:images="photos"
|
|
@update:images="updateImages"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Contact Information Section -->
|
|
<div
|
|
v-if="phoneNumber || email"
|
|
class="contact-info mt-6"
|
|
>
|
|
<!-- Phone Number -->
|
|
<div
|
|
v-if="phoneNumber"
|
|
class="contact-capsule"
|
|
@click="callPhone"
|
|
>
|
|
<v-icon
|
|
:icon="mdiPhone"
|
|
class="contact-icon"
|
|
/>
|
|
<span class="contact-text">{{ phoneNumber }}</span>
|
|
</div>
|
|
|
|
<!-- Email -->
|
|
<div
|
|
v-if="email"
|
|
class="contact-capsule"
|
|
@click="sendEmail"
|
|
>
|
|
<v-icon
|
|
:icon="mdiEmail"
|
|
class="contact-icon"
|
|
/>
|
|
<span class="contact-text">{{ email }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref, 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 { buildEmbedUrl, extractVideoId, isValidYouTubeUrlOrId } from '@/utils/youtube';
|
|
import AlbumEditor from '@/views/creators/AlbumEditor.vue';
|
|
import AlbumView from '@/views/creators/AlbumView.vue';
|
|
import AlbumViewer from './AlbumViewer.vue';
|
|
import { useToast } from 'vue-toastification';
|
|
import { mdiCheck, mdiClose, mdiEmail, mdiPencil, mdiPhone } from '@mdi/js';
|
|
|
|
const { t } = useI18n();
|
|
const creatorProfileStore = useCreatorProfileStore();
|
|
const brandingStore = useBrandingStore();
|
|
const client = useClient();
|
|
const toast = useToast();
|
|
|
|
// Fetch album data
|
|
const isLoadingAlbum = ref(false);
|
|
const isLoading = ref(true);
|
|
const isSaving = ref(false);
|
|
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 photos = ref([]); //before was thumbnailUrls
|
|
const albumId = ref(null);
|
|
const originalPhotos = ref([]);
|
|
// Add these refs with your other refs
|
|
const showAlbumViewer = ref(false);
|
|
const selectedPhotoIndex = ref(0);
|
|
|
|
// Editable fields
|
|
const editableDescription = ref('');
|
|
const editableVideoUrl = ref('');
|
|
const videoUrlError = ref('');
|
|
const descriptionError = ref('');
|
|
|
|
function callPhone() {
|
|
if (phoneNumber.value) {
|
|
toast.info('Calling your contact');
|
|
// Remove formatting and create tel: link
|
|
const cleanPhone = phoneNumber.value.replace(/\D/g, '');
|
|
window.location.href = `tel:+1${cleanPhone}`;
|
|
}
|
|
}
|
|
|
|
function sendEmail() {
|
|
if (email.value) {
|
|
window.location.href = `mailto:${email.value}`;
|
|
}
|
|
}
|
|
|
|
// Computed property to check if we can save
|
|
const canSave = computed(() => {
|
|
if (isSaving.value == true) {
|
|
return false;
|
|
}
|
|
|
|
// Check if description is empty or only whitespace
|
|
if (!editableDescription.value || editableDescription.value.trim() === '') {
|
|
return false;
|
|
}
|
|
|
|
// Check if description is too long
|
|
if (editableDescription.value.length > 2000) {
|
|
return false;
|
|
}
|
|
|
|
// Check if video URL is invalid (if one is provided)
|
|
if (editableVideoUrl.value && !validateVideoUrl(editableVideoUrl.value)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const thumbnailUrls = computed(() => {
|
|
return photos.value.map(photo => photo.image.thumbnailUrl);
|
|
});
|
|
|
|
// Add this computed property to get the original image URLs
|
|
const originalUrls = computed(() => {
|
|
return photos.value.map(photo => photo.image.originalUrl);
|
|
});
|
|
|
|
// 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 photos.value.length > 0;
|
|
});
|
|
|
|
// 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;
|
|
videoUrlError.value = '';
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => ({
|
|
id: brandingStore.value?.id,
|
|
presentation: brandingStore.value?.presentation,
|
|
}),
|
|
async ({ id, presentation }, previousValue) => {
|
|
// Only proceed if we have both id and presentation, and the id has changed
|
|
if (id && presentation && id !== previousValue?.id) {
|
|
console.log('Watcher triggered: Loading data for creator ID:', id);
|
|
|
|
// Load presentation data
|
|
description.value = presentation.description || '';
|
|
videoUrl.value = presentation.videoUrl || '';
|
|
phoneNumber.value = presentation.phoneNumber || '';
|
|
email.value = presentation.email || '';
|
|
|
|
// Fetch album data
|
|
await fetchAlbumData();
|
|
}
|
|
},
|
|
{ immediate: true, deep: true }
|
|
);
|
|
|
|
async function fetchAlbumData() {
|
|
if (isLoadingAlbum.value) {
|
|
console.log('Album data already loading, skipping duplicate request');
|
|
return;
|
|
}
|
|
|
|
console.log('in fetchAlbumData()');
|
|
if (!brandingStore.value?.id) return;
|
|
|
|
isLoadingAlbum.value = true;
|
|
const creatorId = brandingStore.value.id;
|
|
|
|
try {
|
|
// Try to get the album
|
|
const response = await client.get(`/api/albums/${creatorId}`);
|
|
|
|
if (response.data && response.data.photos) {
|
|
// Store original photos for comparison
|
|
originalPhotos.value = response.data.photos;
|
|
// Extract photo URLs from the album photos
|
|
photos.value = response.data.photos.map(photo => ({
|
|
file: null,
|
|
image: photo,
|
|
isProcessing: false,
|
|
isUploading: false,
|
|
}));
|
|
albumId.value = creatorId;
|
|
} else {
|
|
// Initialize with an empty array instead of empty slots
|
|
console.log('WOW! You found how to get here! Take a look at the stack!');
|
|
photos.value = [];
|
|
originalPhotos.value = [];
|
|
}
|
|
} catch (error) {
|
|
photos.value = [];
|
|
originalPhotos.value = [];
|
|
} finally {
|
|
isLoadingAlbum.value = false;
|
|
}
|
|
}
|
|
|
|
// 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 || '';
|
|
});
|
|
|
|
// Update images from Album component
|
|
function updateImages(newImages) {
|
|
photos.value = newImages;
|
|
}
|
|
|
|
async function saveChanges() {
|
|
if (!brandingStore.value?.id) {
|
|
console.error("L'ID du créateur est manquant !");
|
|
return;
|
|
}
|
|
|
|
// Validate description is not empty
|
|
if (!editableDescription.value || editableDescription.value.trim() === '') {
|
|
descriptionError.value = t('creator.validation.descriptionRequired');
|
|
return;
|
|
}
|
|
|
|
// Validate description length
|
|
if (editableDescription.value.length > 2000) {
|
|
descriptionError.value = t('creator.validation.descriptionTooLong');
|
|
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/${brandingStore.value.id}/presentation-infos`,
|
|
{
|
|
description: editableDescription.value || '',
|
|
videoUrl: editableVideoUrl.value || null,
|
|
}
|
|
);
|
|
|
|
// Mettre à jour les valeurs locales pour refléter les changements
|
|
description.value = editableDescription.value;
|
|
videoUrl.value = extractVideoId(editableVideoUrl.value) || '';
|
|
|
|
// Check for deleted photos
|
|
const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl);
|
|
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
|
|
// If the photo URL is not in the current images array, it was deleted
|
|
return !photosOriginalUrls.includes(originalPhoto.originalUrl);
|
|
});
|
|
const newImages = photos.value.filter(
|
|
photo => photo && photo.image && photo.image.originalUrl.startsWith('data:')
|
|
);
|
|
|
|
console.log('originalPhotos', originalPhotos.value);
|
|
console.log('photos', photos.value);
|
|
console.log('deletedPhotos', deletedPhotos);
|
|
console.log('newImages', newImages);
|
|
|
|
// Save album photos if they've changed
|
|
if (photos.value.length > 0 || deletedPhotos.length > 0) {
|
|
console.log('We got pending changes');
|
|
|
|
// Create the Album if we do not have one yet
|
|
if (albumId.value == null) {
|
|
console.log('We do not have an album yet');
|
|
try {
|
|
await client.post('/api/albums', {
|
|
albumId: brandingStore.value.id,
|
|
title: `${brandingStore.value.name}'s Album`,
|
|
description: 'Photo album for the creator',
|
|
});
|
|
albumId.value = brandingStore.value.id;
|
|
} catch (error) {
|
|
// Album might already exist, which is fine
|
|
console.log("Couldn't create an Album", error);
|
|
}
|
|
}
|
|
|
|
// Delete removed photos
|
|
for (const photo of deletedPhotos) {
|
|
try {
|
|
await client.delete(`/api/albums/${albumId.value}/photos/${photo.id}`);
|
|
} catch (error) {
|
|
console.error('Error deleting photo:', error);
|
|
}
|
|
}
|
|
|
|
// Now add or update photos
|
|
for (let i = 0; i < newImages.length; i++) {
|
|
const imageData = newImages[i];
|
|
imageData.isUploading = true;
|
|
console.log('Image Data to be uploaded:', imageData);
|
|
// This is a new image that needs to be uploaded
|
|
const photoId = crypto.randomUUID();
|
|
|
|
// Convert data URL to file
|
|
const response = await fetch(imageData.image.originalUrl);
|
|
const blob = await response.blob();
|
|
const file = new File([blob], imageData.file.name, { type: imageData.file.type });
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
await client.post(`/api/albums/${albumId.value}/photos`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
params: {
|
|
photoId: photoId,
|
|
},
|
|
});
|
|
imageData.isUploading = false;
|
|
}
|
|
|
|
// Refresh album data after changes
|
|
await fetchAlbumData();
|
|
}
|
|
|
|
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;
|
|
|
|
// Désactiver le mode édition
|
|
isEditMode.value = false;
|
|
}
|
|
|
|
// Add this function to handle photo clicks
|
|
function handlePhotoClick(index) {
|
|
selectedPhotoIndex.value = index;
|
|
showAlbumViewer.value = true;
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.content-section {
|
|
@apply w-full overflow-hidden;
|
|
@apply cursor-pointer;
|
|
}
|
|
|
|
.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 gap-3;
|
|
}
|
|
|
|
.contact-capsule {
|
|
@apply flex items-center gap-2 px-2 py-1 bg-hSurface;
|
|
@apply rounded-xl cursor-pointer transition-all duration-200;
|
|
@apply hover:shadow-md min-w-fit;
|
|
@apply border border-hutopyPrimary;
|
|
}
|
|
|
|
.contact-capsule:hover {
|
|
@apply transform scale-105;
|
|
}
|
|
|
|
.contact-icon {
|
|
@apply text-hutopyPrimary;
|
|
@apply text-xl;
|
|
}
|
|
|
|
.contact-text {
|
|
@apply text-hOnSurface font-medium text-base;
|
|
}
|
|
|
|
/* Formatting styles for description */
|
|
.text-justify {
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Add some spacing between paragraphs */
|
|
.text-justify p {
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
|
|
<i18n>
|
|
{
|
|
"en": {
|
|
"edit": "Edit",
|
|
"save": "Save",
|
|
"cancel": "Cancel",
|
|
"creator": {
|
|
"sections": {
|
|
"about": {
|
|
"title": "About",
|
|
"description": "Description",
|
|
"contactInfo": "Contact Information",
|
|
"characters": "characters",
|
|
"formattingHint": "Tip: Use line breaks and emojis to make your description more engaging!"
|
|
},
|
|
"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",
|
|
"descriptionTooLong": "Description cannot exceed 2000 characters",
|
|
"descriptionRequired": "Description is required"
|
|
}
|
|
}
|
|
},
|
|
"fr": {
|
|
"edit": "Modifier",
|
|
"save": "Enregistrer",
|
|
"cancel": "Annuler",
|
|
"creator": {
|
|
"sections": {
|
|
"about": {
|
|
"title": "À propos",
|
|
"description": "Description",
|
|
"contactInfo": "Informations de contact",
|
|
"characters": "caractères",
|
|
"formattingHint": "Astuce : Utilisez des sauts de ligne et des émojis pour rendre votre description plus attrayante !"
|
|
},
|
|
"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",
|
|
"descriptionTooLong": "La description ne peut pas dépasser 2000 caractères",
|
|
"descriptionRequired": "La description est obligatoire"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</i18n>
|