862 lines
30 KiB
Vue
862 lines
30 KiB
Vue
<template>
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Loading state -->
|
|
<div v-if="loading" class="text-center py-12">
|
|
<LoadingLogo size="large" text="Chargement de l'album..." />
|
|
</div>
|
|
|
|
<!-- Album not found -->
|
|
<div v-else-if="!album" class="text-center py-12">
|
|
<h1 class="text-2xl font-bold text-gray-900 mb-4">Album non trouvé</h1>
|
|
<p class="text-gray-600 mb-6">L'album que vous recherchez n'existe pas ou a été supprimé.</p>
|
|
<router-link to="/albums" class="btn-primary">
|
|
Retour aux albums
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Album details -->
|
|
<div v-else>
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4 space-y-4 sm:space-y-0">
|
|
<router-link to="/albums" class="text-primary-600 hover:text-primary-700 flex items-center">
|
|
<ArrowLeft class="w-4 h-4 mr-2" />
|
|
Retour aux albums
|
|
</router-link>
|
|
|
|
<div v-if="canEdit" class="flex flex-wrap gap-2 w-full sm:w-auto">
|
|
<button
|
|
@click="showEditModal = true"
|
|
class="flex-1 sm:flex-none btn-secondary justify-center"
|
|
>
|
|
<Edit class="w-4 h-4 mr-2" />
|
|
Modifier
|
|
</button>
|
|
<button
|
|
@click="showUploadModal = true"
|
|
class="flex-1 sm:flex-none btn-primary justify-center"
|
|
>
|
|
<Upload class="w-4 h-4 mr-2" />
|
|
Ajouter
|
|
</button>
|
|
<button
|
|
@click="deleteAlbum"
|
|
class="flex-1 sm:flex-none btn-secondary text-accent-600 hover:text-accent-700 hover:border-accent-300 justify-center"
|
|
>
|
|
<Trash2 class="w-4 h-4 mr-2" />
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col md:flex-row items-start space-y-6 md:space-y-0 md:space-x-6">
|
|
<!-- Cover Image -->
|
|
<div class="w-full md:w-64 h-48 bg-gradient-to-br from-primary-400 to-primary-600 rounded-xl flex items-center justify-center flex-shrink-0">
|
|
<Image v-if="!album.cover_image" class="w-16 h-16 text-white" />
|
|
<img
|
|
v-else
|
|
:src="getMediaUrl(album.cover_image)"
|
|
:alt="album.title"
|
|
class="w-full h-full object-cover rounded-xl"
|
|
>
|
|
</div>
|
|
|
|
<!-- Album Info -->
|
|
<div class="flex-1">
|
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">{{ album.title }}</h1>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-3">
|
|
<div v-if="album.description" class="text-gray-700">
|
|
{{ album.description }}
|
|
</div>
|
|
|
|
<div class="flex items-center text-gray-600">
|
|
<User class="w-5 h-5 mr-3" />
|
|
<span>Créé par {{ album.creator_name }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center text-gray-600">
|
|
<Calendar class="w-5 h-5 mr-3" />
|
|
<span>{{ formatDate(album.created_at) }}</span>
|
|
</div>
|
|
|
|
<div v-if="album.event_title" class="flex items-center text-primary-600">
|
|
<Calendar class="w-5 h-5 mr-3" />
|
|
<router-link :to="`/events/${album.event_id}`" class="hover:underline">
|
|
{{ album.event_title }}
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<h3 class="font-semibold text-gray-900 mb-3">Statistiques</h3>
|
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-primary-600">{{ album.media_count || 0 }}</div>
|
|
<div class="text-gray-600">Médias</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-success-600">{{ formatBytes(totalSize) }}</div>
|
|
<div class="text-gray-600">Taille totale</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Media Section -->
|
|
<div v-if="album.top_media && album.top_media.length > 0" class="card p-6 mb-8">
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Top Media</h2>
|
|
<p class="text-gray-600 mb-4">Les médias les plus appréciés de cet album</p>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
<div
|
|
v-for="media in album.top_media"
|
|
:key="media.id"
|
|
class="group relative aspect-square bg-gray-100 rounded-lg overflow-hidden cursor-pointer"
|
|
@click="openMediaViewer(media)"
|
|
>
|
|
<img
|
|
v-if="media.media_type === 'image'"
|
|
:src="getMediaUrl(media.thumbnail_path) || getMediaUrl(media.file_path)"
|
|
:alt="media.caption || 'Image'"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
>
|
|
<video
|
|
v-else
|
|
:src="getMediaUrl(media.file_path)"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
/>
|
|
|
|
<!-- Media Type Badge -->
|
|
<div class="absolute top-1 left-1 bg-black bg-opacity-70 text-white text-xs px-1 py-0.5 rounded">
|
|
{{ media.media_type === 'image' ? '📷' : '🎥' }}
|
|
</div>
|
|
|
|
<!-- Likes Badge -->
|
|
<div class="absolute top-1 right-1 bg-primary-600 text-white text-xs px-1 py-0.5 rounded flex items-center">
|
|
<Heart class="w-2 h-2 mr-1" />
|
|
{{ media.likes_count }}
|
|
</div>
|
|
|
|
<!-- Caption -->
|
|
<div v-if="media.caption" class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-70 text-white text-xs p-1 truncate">
|
|
{{ media.caption }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Media Gallery -->
|
|
<div class="card p-6 mb-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900">Galerie</h2>
|
|
<div class="flex items-center space-x-2">
|
|
<button
|
|
@click="viewMode = 'grid'"
|
|
class="p-2 rounded-lg transition-colors"
|
|
:class="viewMode === 'grid' ? 'bg-primary-100 text-primary-600' : 'text-gray-400 hover:text-gray-600'"
|
|
>
|
|
<Grid class="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
@click="viewMode = 'list'"
|
|
class="p-2 rounded-lg transition-colors"
|
|
:class="viewMode === 'list' ? 'bg-primary-100 text-primary-600' : 'text-gray-400 hover:text-gray-600'"
|
|
>
|
|
<List class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="album.media.length === 0" class="text-center py-12 text-gray-500">
|
|
<Image class="w-16 h-16 mx-auto mb-4 text-gray-300" />
|
|
<h3 class="text-lg font-medium mb-2">Aucun média</h3>
|
|
<p>Cet album ne contient pas encore de photos ou vidéos</p>
|
|
</div>
|
|
|
|
<!-- Grid View -->
|
|
<div v-else-if="viewMode === 'grid'" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
<div
|
|
v-for="media in album.media"
|
|
:key="media.id"
|
|
class="group relative aspect-square bg-gray-100 rounded-lg overflow-hidden cursor-pointer"
|
|
@click="openMediaViewer(media)"
|
|
>
|
|
<img
|
|
v-if="media.media_type === 'image'"
|
|
:src="getMediaUrl(media.thumbnail_path) || getMediaUrl(media.file_path)"
|
|
:alt="media.caption || 'Image'"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
>
|
|
<video
|
|
v-else
|
|
:src="getMediaUrl(media.file_path)"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
/>
|
|
|
|
<!-- Media Type Badge -->
|
|
<div class="absolute top-2 left-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded">
|
|
{{ media.media_type === 'image' ? '📷' : '🎥' }}
|
|
</div>
|
|
|
|
<!-- Caption -->
|
|
<div v-if="media.caption" class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-70 text-white text-xs p-2">
|
|
{{ media.caption }}
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div v-if="canEdit" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
@click.stop="deleteMedia(media.id)"
|
|
class="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600"
|
|
>
|
|
<X class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Like Button -->
|
|
<div class="absolute bottom-2 right-2 opacity-100 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
@click.stop="toggleMediaLike(media)"
|
|
class="flex items-center space-x-2 px-3 py-2 rounded-full text-sm transition-colors shadow-lg"
|
|
:class="media.is_liked ? 'bg-red-500 text-white hover:bg-red-600' : 'bg-black bg-opacity-80 text-white hover:bg-opacity-90'"
|
|
>
|
|
<Heart :class="media.is_liked ? 'fill-current' : ''" class="w-4 h-4" />
|
|
<span class="font-medium">{{ media.likes_count }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List View -->
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="media in album.media"
|
|
:key="media.id"
|
|
class="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div class="w-16 h-16 bg-gray-200 rounded overflow-hidden">
|
|
<img
|
|
v-if="media.media_type === 'image'"
|
|
:src="getMediaUrl(media.thumbnail_path) || getMediaUrl(media.file_path)"
|
|
:alt="media.caption || 'Image'"
|
|
class="w-full h-full object-cover"
|
|
>
|
|
<video
|
|
v-else
|
|
:src="getMediaUrl(media.file_path)"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex-1">
|
|
<p class="font-medium text-gray-900">{{ media.caption || 'Sans titre' }}</p>
|
|
<p class="text-sm text-gray-600">{{ formatBytes(media.file_size) }} • {{ media.media_type === 'image' ? 'Image' : 'Vidéo' }}</p>
|
|
<p class="text-xs text-gray-500">{{ formatDate(media.created_at) }}</p>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<button
|
|
@click="openMediaViewer(media)"
|
|
class="p-2 text-gray-400 hover:text-primary-600 transition-colors"
|
|
>
|
|
<Eye class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="toggleMediaLike(media)"
|
|
class="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
|
:class="{ 'text-red-600': media.is_liked }"
|
|
>
|
|
<Heart :class="media.is_liked ? 'fill-current' : ''" class="w-5 h-5" />
|
|
<span class="ml-2 text-sm font-medium">{{ media.likes_count }}</span>
|
|
</button>
|
|
|
|
<button
|
|
v-if="canEdit"
|
|
@click="deleteMedia(media.id)"
|
|
class="p-2 text-gray-400 hover:text-accent-600 transition-colors"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Album Modal -->
|
|
<transition
|
|
enter-active-class="transition ease-out duration-200"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition ease-in duration-150"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="showEditModal"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
|
>
|
|
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
|
<h2 class="text-xl font-semibold mb-4">Modifier l'album</h2>
|
|
|
|
<form @submit.prevent="updateAlbum" class="space-y-4">
|
|
<div>
|
|
<label class="label">Titre</label>
|
|
<input
|
|
v-model="editForm.title"
|
|
type="text"
|
|
required
|
|
class="input"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="label">Description</label>
|
|
<textarea
|
|
v-model="editForm.description"
|
|
rows="3"
|
|
class="input"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
@click="showEditModal = false"
|
|
class="flex-1 btn-secondary"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
:disabled="updating"
|
|
class="flex-1 btn-primary"
|
|
>
|
|
{{ updating ? 'Mise à jour...' : 'Mettre à jour' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<!-- Upload Media Modal -->
|
|
<transition
|
|
enter-active-class="transition ease-out duration-200"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition ease-in duration-150"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="showUploadModal"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
|
|
>
|
|
<div class="bg-white rounded-xl max-w-2xl w-full p-6 max-h-[90vh] overflow-y-auto">
|
|
<h2 class="text-xl font-semibold mb-4">Ajouter des médias</h2>
|
|
|
|
<form @submit.prevent="uploadMedia" class="space-y-4">
|
|
<div>
|
|
<label class="label">Photos et vidéos</label>
|
|
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
|
<input
|
|
ref="mediaInput"
|
|
type="file"
|
|
accept="image/*,video/*"
|
|
multiple
|
|
class="hidden"
|
|
@change="handleMediaChange"
|
|
>
|
|
|
|
<div v-if="newMedia.length === 0" class="space-y-2">
|
|
<Upload class="w-12 h-12 text-gray-400 mx-auto" />
|
|
<p class="text-gray-600">Glissez-déposez ou cliquez pour sélectionner</p>
|
|
<p class="text-sm text-gray-500">Images et vidéos (max 100MB par fichier)</p>
|
|
<button
|
|
type="button"
|
|
@click="$refs.mediaInput.click()"
|
|
class="btn-secondary"
|
|
>
|
|
Sélectionner des fichiers
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="space-y-3">
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
<div
|
|
v-for="(media, index) in newMedia"
|
|
:key="index"
|
|
class="relative aspect-square bg-gray-100 rounded overflow-hidden"
|
|
>
|
|
<img
|
|
v-if="media.type === 'image'"
|
|
:src="media.preview"
|
|
:alt="media.name"
|
|
class="w-full h-full object-cover"
|
|
>
|
|
<video
|
|
v-else
|
|
:src="media.preview"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
|
|
<button
|
|
@click="removeMedia(index)"
|
|
class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600"
|
|
>
|
|
<X class="w-4 h-4" />
|
|
</button>
|
|
|
|
<div class="absolute bottom-1 left-1 bg-black bg-opacity-70 text-white text-xs px-1 py-0.5 rounded">
|
|
{{ media.type === 'image' ? '📷' : '🎥' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
@click="$refs.mediaInput.click()"
|
|
class="btn-secondary text-sm"
|
|
>
|
|
Ajouter plus de fichiers
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
@click="showUploadModal = false"
|
|
class="flex-1 btn-secondary"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
:disabled="uploading || newMedia.length === 0"
|
|
class="flex-1 btn-primary"
|
|
>
|
|
{{ uploading ? 'Upload...' : 'Ajouter les médias' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<!-- Media Viewer Modal -->
|
|
<transition
|
|
enter-active-class="transition ease-out duration-200"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition ease-in duration-150"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="selectedMedia"
|
|
class="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
|
|
>
|
|
<div class="relative max-w-7xl max-h-[95vh] w-full h-full">
|
|
<!-- Close Button -->
|
|
<button
|
|
@click="closeMediaViewer"
|
|
class="absolute top-6 right-6 z-10 bg-black bg-opacity-70 text-white rounded-full w-12 h-12 flex items-center justify-center hover:bg-opacity-90 transition-colors shadow-lg"
|
|
>
|
|
<X class="w-6 h-6" />
|
|
</button>
|
|
|
|
|
|
|
|
<!-- Navigation Buttons -->
|
|
<button
|
|
v-if="album.media.length > 1"
|
|
@click="previousMedia"
|
|
class="absolute left-6 top-1/2 transform -translate-y-1/2 z-10 bg-black bg-opacity-70 text-white rounded-full w-12 h-12 flex items-center justify-center hover:bg-opacity-90 transition-colors shadow-lg"
|
|
>
|
|
<ChevronLeft class="w-6 h-6" />
|
|
</button>
|
|
|
|
<button
|
|
v-if="album.media.length > 1"
|
|
@click="nextMedia"
|
|
class="absolute right-6 top-1/2 transform -translate-y-1/2 z-10 bg-black bg-opacity-70 text-white rounded-full w-12 h-12 flex items-center justify-center hover:bg-opacity-90 transition-colors shadow-lg"
|
|
>
|
|
<ChevronRight class="w-6 h-6" />
|
|
</button>
|
|
|
|
<!-- Position Indicator -->
|
|
<div v-if="album.media.length > 1" class="absolute top-6 left-1/2 transform -translate-x-1/2 z-10 bg-black bg-opacity-70 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm">
|
|
{{ getCurrentMediaIndex() + 1 }} / {{ album.media.length }}
|
|
</div>
|
|
|
|
<!-- Media Content -->
|
|
<div class="flex flex-col items-center h-full">
|
|
<!-- Image or Video -->
|
|
<div class="w-full h-full flex items-center justify-center">
|
|
<img
|
|
v-if="selectedMedia.media_type === 'image'"
|
|
:src="getMediaUrl(selectedMedia.thumbnail_path) || getMediaUrl(selectedMedia.file_path)"
|
|
:alt="selectedMedia.caption || 'Image'"
|
|
class="w-auto h-auto max-w-none max-h-none object-contain rounded-lg shadow-2xl"
|
|
:style="getOptimalMediaSize()"
|
|
>
|
|
<video
|
|
v-else
|
|
:src="getMediaUrl(selectedMedia.file_path)"
|
|
controls
|
|
class="w-auto h-auto max-w-none max-h-none rounded-lg shadow-2xl"
|
|
:style="getOptimalMediaSize()"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Media Info -->
|
|
<div class="mt-6 text-center text-white bg-black bg-opacity-50 rounded-xl p-6 backdrop-blur-sm">
|
|
<h3 v-if="selectedMedia.caption" class="text-xl font-semibold mb-3 text-white">
|
|
{{ selectedMedia.caption }}
|
|
</h3>
|
|
<div class="flex items-center justify-center space-x-6 text-sm text-gray-200 mb-4">
|
|
<span class="flex items-center space-x-2">
|
|
<span class="w-2 h-2 bg-blue-400 rounded-full"></span>
|
|
<span>{{ formatBytes(selectedMedia.file_size) }}</span>
|
|
</span>
|
|
<span class="flex items-center space-x-2">
|
|
<span class="w-2 h-2 bg-green-400 rounded-full"></span>
|
|
<span>{{ selectedMedia.media_type === 'image' ? '📷 Image' : '🎥 Vidéo' }}</span>
|
|
</span>
|
|
<span class="flex items-center space-x-2">
|
|
<span class="w-2 h-2 bg-purple-400 rounded-full"></span>
|
|
<span>{{ formatDate(selectedMedia.created_at) }}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Like Button in Viewer -->
|
|
<div class="flex items-center justify-center">
|
|
<button
|
|
@click="toggleMediaLikeFromViewer(selectedMedia)"
|
|
class="flex items-center space-x-3 px-6 py-3 rounded-full transition-all duration-300 transform hover:scale-105"
|
|
:class="selectedMedia.is_liked ? 'bg-red-500 text-white shadow-lg hover:bg-red-600' : 'bg-white bg-opacity-20 text-white hover:bg-opacity-40 hover:shadow-lg'"
|
|
>
|
|
<Heart :class="selectedMedia.is_liked ? 'fill-current' : ''" class="w-6 h-6" />
|
|
<span class="font-medium text-lg">{{ selectedMedia.likes_count }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useToast } from 'vue-toastification'
|
|
import axios from '@/utils/axios'
|
|
import { getMediaUrl } from '@/utils/axios'
|
|
import { format, formatDistanceToNow } from 'date-fns'
|
|
import { fr } from 'date-fns/locale'
|
|
import {
|
|
ArrowLeft,
|
|
Image,
|
|
User,
|
|
Calendar,
|
|
Edit,
|
|
Upload,
|
|
Trash2,
|
|
Grid,
|
|
List,
|
|
X,
|
|
Eye,
|
|
Heart,
|
|
ChevronLeft,
|
|
ChevronRight
|
|
} from 'lucide-vue-next'
|
|
import LoadingLogo from '@/components/LoadingLogo.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
const toast = useToast()
|
|
|
|
const album = ref(null)
|
|
const loading = ref(true)
|
|
const updating = ref(false)
|
|
const uploading = ref(false)
|
|
const showEditModal = ref(false)
|
|
const showUploadModal = ref(false)
|
|
const viewMode = ref('grid')
|
|
const selectedMedia = ref(null)
|
|
|
|
|
|
const editForm = ref({
|
|
title: '',
|
|
description: ''
|
|
})
|
|
|
|
const newMedia = ref([])
|
|
|
|
const canEdit = computed(() =>
|
|
album.value && (album.value.creator_id === authStore.user?.id || authStore.user?.is_admin)
|
|
)
|
|
|
|
const totalSize = computed(() =>
|
|
album.value?.media?.reduce((sum, media) => sum + (media.file_size || 0), 0) || 0
|
|
)
|
|
|
|
function formatDate(date) {
|
|
return format(new Date(date), 'dd MMMM yyyy', { locale: fr })
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B'
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
}
|
|
|
|
async function fetchAlbum() {
|
|
try {
|
|
const response = await axios.get(`/api/albums/${route.params.id}`)
|
|
album.value = response.data
|
|
|
|
// Initialize edit form
|
|
editForm.value = {
|
|
title: album.value.title,
|
|
description: album.value.description || ''
|
|
}
|
|
} catch (error) {
|
|
toast.error('Erreur lors du chargement de l\'album')
|
|
console.error('Error fetching album:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function updateAlbum() {
|
|
updating.value = true
|
|
try {
|
|
const response = await axios.put(`/api/albums/${album.value.id}`, editForm.value)
|
|
album.value = response.data
|
|
showEditModal.value = false
|
|
toast.success('Album mis à jour')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la mise à jour')
|
|
}
|
|
updating.value = false
|
|
}
|
|
|
|
async function deleteAlbum() {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer cet album ?')) return
|
|
|
|
try {
|
|
await axios.delete(`/api/albums/${album.value.id}`)
|
|
toast.success('Album supprimé')
|
|
router.push('/albums')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la suppression')
|
|
}
|
|
}
|
|
|
|
async function handleMediaChange(event) {
|
|
const files = Array.from(event.target.files)
|
|
|
|
for (const file of files) {
|
|
if (!file.type.startsWith('image/') && !file.type.startsWith('video/')) {
|
|
toast.error(`${file.name} n'est pas un fichier image ou vidéo valide`)
|
|
continue
|
|
}
|
|
|
|
if (file.size > 100 * 1024 * 1024) {
|
|
toast.error(`${file.name} est trop volumineux (max 100MB)`)
|
|
continue
|
|
}
|
|
|
|
const media = {
|
|
file: file,
|
|
name: file.name,
|
|
type: file.type.startsWith('image/') ? 'image' : 'video',
|
|
preview: URL.createObjectURL(file)
|
|
}
|
|
|
|
newMedia.value.push(media)
|
|
}
|
|
|
|
event.target.value = ''
|
|
}
|
|
|
|
function removeMedia(index) {
|
|
const media = newMedia.value[index]
|
|
if (media.preview && media.preview.startsWith('blob:')) {
|
|
URL.revokeObjectURL(media.preview)
|
|
}
|
|
newMedia.value.splice(index, 1)
|
|
}
|
|
|
|
async function uploadMedia() {
|
|
if (newMedia.value.length === 0) return
|
|
|
|
uploading.value = true
|
|
try {
|
|
const formData = new FormData()
|
|
newMedia.value.forEach(media => {
|
|
formData.append('files', media.file)
|
|
})
|
|
|
|
await axios.post(`/api/albums/${album.value.id}/media`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
})
|
|
|
|
// Refresh album data
|
|
await fetchAlbum()
|
|
|
|
showUploadModal.value = false
|
|
newMedia.value.forEach(media => {
|
|
if (media.preview && media.preview.startsWith('blob:')) {
|
|
URL.revokeObjectURL(media.preview)
|
|
}
|
|
})
|
|
newMedia.value = []
|
|
|
|
toast.success('Médias ajoutés avec succès')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de l\'upload')
|
|
}
|
|
uploading.value = false
|
|
}
|
|
|
|
async function deleteMedia(mediaId) {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer ce média ?')) return
|
|
|
|
try {
|
|
await axios.delete(`/api/albums/${album.value.id}/media/${mediaId}`)
|
|
await fetchAlbum()
|
|
toast.success('Média supprimé')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la suppression')
|
|
}
|
|
}
|
|
|
|
async function toggleMediaLike(media) {
|
|
try {
|
|
const response = await axios.post(`/api/albums/${album.value.id}/media/${media.id}/like`)
|
|
media.is_liked = response.data.is_liked
|
|
media.likes_count = response.data.likes_count
|
|
toast.success('Like mis à jour')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la mise à jour du like')
|
|
}
|
|
}
|
|
|
|
async function toggleMediaLikeFromViewer(media) {
|
|
try {
|
|
const response = await axios.post(`/api/albums/${album.value.id}/media/${media.id}/like`)
|
|
media.is_liked = response.data.is_liked
|
|
media.likes_count = response.data.likes_count
|
|
toast.success('Like mis à jour')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la mise à jour du like')
|
|
}
|
|
}
|
|
|
|
function openMediaViewer(media) {
|
|
selectedMedia.value = media
|
|
|
|
// Add keyboard event listeners
|
|
document.addEventListener('keydown', handleKeyboardNavigation)
|
|
}
|
|
|
|
function closeMediaViewer() {
|
|
selectedMedia.value = null
|
|
|
|
// Remove keyboard event listeners
|
|
document.removeEventListener('keydown', handleKeyboardNavigation)
|
|
}
|
|
|
|
function handleKeyboardNavigation(event) {
|
|
if (!selectedMedia.value) return
|
|
|
|
switch (event.key) {
|
|
case 'Escape':
|
|
closeMediaViewer()
|
|
break
|
|
case 'ArrowLeft':
|
|
if (album.value.media.length > 1) {
|
|
previousMedia()
|
|
}
|
|
break
|
|
case 'ArrowRight':
|
|
if (album.value.media.length > 1) {
|
|
nextMedia()
|
|
}
|
|
break
|
|
|
|
}
|
|
}
|
|
|
|
function previousMedia() {
|
|
const currentIndex = album.value.media.findIndex(media => media.id === selectedMedia.value.id)
|
|
if (currentIndex > 0) {
|
|
selectedMedia.value = album.value.media[currentIndex - 1]
|
|
}
|
|
}
|
|
|
|
function nextMedia() {
|
|
const currentIndex = album.value.media.findIndex(media => media.id === selectedMedia.value.id)
|
|
if (currentIndex < album.value.media.length - 1) {
|
|
selectedMedia.value = album.value.media[currentIndex + 1]
|
|
}
|
|
}
|
|
|
|
function getCurrentMediaIndex() {
|
|
if (!selectedMedia.value || !album.value) return 0
|
|
return album.value.media.findIndex(media => media.id === selectedMedia.value.id)
|
|
}
|
|
|
|
function getOptimalMediaSize() {
|
|
if (!selectedMedia.value) return {}
|
|
|
|
// Utilisation MAXIMALE de l'espace disponible
|
|
if (selectedMedia.value.media_type === 'image') {
|
|
// Images : 100% de l'écran avec juste une petite marge
|
|
return {
|
|
'max-width': '98vw',
|
|
'max-height': '96vh',
|
|
'width': 'auto',
|
|
'height': 'auto',
|
|
'object-fit': 'contain'
|
|
}
|
|
}
|
|
|
|
// Vidéos : presque plein écran
|
|
return {
|
|
'max-width': '96vw',
|
|
'max-height': '94vh',
|
|
'width': 'auto',
|
|
'height': 'auto'
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchAlbum()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// Clean up event listeners
|
|
document.removeEventListener('keydown', handleKeyboardNavigation)
|
|
})
|
|
</script> |