371 lines
12 KiB
Vue
Executable File
371 lines
12 KiB
Vue
Executable File
<template>
|
|
<div class="max-w-6xl 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 du vlog..." />
|
|
</div>
|
|
|
|
<!-- Vlog not found -->
|
|
<div v-else-if="!vlog" class="text-center py-12">
|
|
<h1 class="text-2xl font-bold text-gray-900 mb-4">Vlog non trouvé</h1>
|
|
<p class="text-gray-600 mb-6">Le vlog que vous recherchez n'existe pas ou a été supprimé.</p>
|
|
<router-link to="/vlogs" class="btn-primary">
|
|
Retour aux vlogs
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Vlog details -->
|
|
<div v-else>
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<router-link to="/vlogs" class="text-primary-600 hover:text-primary-700 flex items-center">
|
|
<ArrowLeft class="w-4 h-4 mr-2" />
|
|
Retour aux vlogs
|
|
</router-link>
|
|
|
|
<div v-if="canEdit" class="flex space-x-2">
|
|
<button
|
|
@click="showEditModal = true"
|
|
class="btn-secondary"
|
|
>
|
|
<Edit class="w-4 h-4 mr-2" />
|
|
Modifier
|
|
</button>
|
|
<button
|
|
@click="deleteVlog"
|
|
class="btn-secondary text-accent-600 hover:text-accent-700 hover:border-accent-300"
|
|
>
|
|
<Trash2 class="w-4 h-4 mr-2" />
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">{{ vlog.title }}</h1>
|
|
|
|
<div class="flex items-center space-x-6 text-gray-600 mb-6">
|
|
<div class="flex items-center">
|
|
<img
|
|
v-if="vlog.author_avatar"
|
|
:src="getMediaUrl(vlog.author_avatar)"
|
|
:alt="vlog.author_name"
|
|
class="w-8 h-8 rounded-full object-cover mr-3"
|
|
>
|
|
<div v-else class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center mr-3">
|
|
<User class="w-4 h-4 text-primary-600" />
|
|
</div>
|
|
<span>Par {{ vlog.author_name }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<Calendar class="w-4 h-4 mr-2" />
|
|
<span>{{ formatDate(vlog.created_at) }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<Eye class="w-4 h-4 mr-2" />
|
|
<span>{{ vlog.views_count }} vue{{ vlog.views_count > 1 ? 's' : '' }}</span>
|
|
</div>
|
|
|
|
<div v-if="vlog.duration" class="flex items-center">
|
|
<Clock class="w-4 h-4 mr-2" />
|
|
<span>{{ formatDuration(vlog.duration) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Player -->
|
|
<div class="card p-6 mb-8">
|
|
<VideoPlayer
|
|
:src="vlog.video_url"
|
|
:poster="vlog.thumbnail_url"
|
|
:title="vlog.title"
|
|
:description="vlog.description"
|
|
:duration="vlog.duration"
|
|
:views-count="vlog.views_count"
|
|
:likes-count="vlog.likes_count"
|
|
:comments-count="vlog.comments?.length || 0"
|
|
:is-liked="vlog.is_liked"
|
|
@like="toggleLike"
|
|
@toggle-comments="showComments = !showComments"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div v-if="vlog.description" class="card p-6 mb-8">
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Description</h2>
|
|
<p class="text-gray-700 whitespace-pre-wrap">{{ vlog.description }}</p>
|
|
</div>
|
|
|
|
<!-- Comments Section -->
|
|
<div class="card p-6 mb-8">
|
|
<VlogComments
|
|
:vlog-id="vlog.id"
|
|
:comments="vlog.comments || []"
|
|
:comment-users="users"
|
|
@comment-added="onCommentAdded"
|
|
@comment-deleted="onCommentDeleted"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Related Vlogs -->
|
|
<div class="card p-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Autres vlogs de {{ vlog.author_name }}</h2>
|
|
|
|
<div v-if="relatedVlogs.length === 0" class="text-center py-8 text-gray-500">
|
|
Aucun autre vlog de cet auteur
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<router-link
|
|
v-for="relatedVlog in relatedVlogs"
|
|
:key="relatedVlog.id"
|
|
:to="`/vlogs/${relatedVlog.id}`"
|
|
class="block hover:shadow-md transition-shadow rounded-lg overflow-hidden"
|
|
>
|
|
<div class="aspect-video bg-gray-100 relative overflow-hidden">
|
|
<img
|
|
v-if="relatedVlog.thumbnail_url"
|
|
:src="getMediaUrl(relatedVlog.thumbnail_url)"
|
|
:alt="relatedVlog.title"
|
|
class="w-full h-full object-cover"
|
|
>
|
|
<div v-else class="w-full h-full flex items-center justify-center">
|
|
<Film class="w-12 h-12 text-gray-400" />
|
|
</div>
|
|
|
|
<!-- Play Button Overlay -->
|
|
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
<Play class="w-8 h-8 text-white" />
|
|
</div>
|
|
|
|
<!-- Duration Badge -->
|
|
<div v-if="relatedVlog.duration" class="absolute bottom-2 right-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded">
|
|
{{ formatDuration(relatedVlog.duration) }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3">
|
|
<h3 class="font-medium text-gray-900 line-clamp-2">{{ relatedVlog.title }}</h3>
|
|
<p class="text-sm text-gray-600 mt-1">{{ formatRelativeDate(relatedVlog.created_at) }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ relatedVlog.views_count }} vue{{ relatedVlog.views_count > 1 ? 's' : '' }}</p>
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Vlog 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 le vlog</h2>
|
|
|
|
<form @submit.prevent="updateVlog" 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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } 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,
|
|
User,
|
|
Calendar,
|
|
Eye,
|
|
Clock,
|
|
Edit,
|
|
Trash2,
|
|
Film,
|
|
Play
|
|
} from 'lucide-vue-next'
|
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
import VlogComments from '@/components/VlogComments.vue'
|
|
import LoadingLogo from '@/components/LoadingLogo.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
const toast = useToast()
|
|
|
|
const vlog = ref(null)
|
|
const relatedVlogs = ref([])
|
|
const users = ref([])
|
|
const loading = ref(true)
|
|
const updating = ref(false)
|
|
const showEditModal = ref(false)
|
|
const showComments = ref(true)
|
|
|
|
const editForm = ref({
|
|
title: '',
|
|
description: ''
|
|
})
|
|
|
|
const canEdit = computed(() =>
|
|
vlog.value && (vlog.value.author_id === authStore.user?.id || authStore.user?.is_admin)
|
|
)
|
|
|
|
function formatDate(date) {
|
|
return format(new Date(date), 'dd MMMM yyyy', { locale: fr })
|
|
}
|
|
|
|
function formatRelativeDate(date) {
|
|
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
if (!seconds) return '--:--'
|
|
const minutes = Math.floor(seconds / 60)
|
|
const remainingSeconds = seconds % 60
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
async function toggleLike() {
|
|
try {
|
|
const response = await axios.post(`/api/vlogs/${vlog.value.id}/like`)
|
|
// Refresh vlog data to get updated like count
|
|
await fetchVlog()
|
|
toast.success(response.data.message)
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la mise à jour du like')
|
|
}
|
|
}
|
|
|
|
function onCommentAdded(comment) {
|
|
// Add the new comment to the vlog
|
|
if (!vlog.value.comments) {
|
|
vlog.value.comments = []
|
|
}
|
|
vlog.value.comments.unshift(comment)
|
|
}
|
|
|
|
function onCommentDeleted(commentId) {
|
|
// Remove the deleted comment from the vlog
|
|
if (vlog.value.comments) {
|
|
const index = vlog.value.comments.findIndex(c => c.id === commentId)
|
|
if (index > -1) {
|
|
vlog.value.comments.splice(index, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fetchUsers() {
|
|
try {
|
|
const response = await axios.get('/api/users')
|
|
users.value = response.data
|
|
} catch (error) {
|
|
console.error('Error fetching users:', error)
|
|
}
|
|
}
|
|
|
|
async function fetchVlog() {
|
|
try {
|
|
const response = await axios.get(`/api/vlogs/${route.params.id}`)
|
|
vlog.value = response.data
|
|
|
|
// Initialize edit form
|
|
editForm.value = {
|
|
title: vlog.value.title,
|
|
description: vlog.value.description || ''
|
|
}
|
|
|
|
// Fetch related vlogs from same author
|
|
const relatedResponse = await axios.get(`/api/vlogs?limit=6&offset=0`)
|
|
relatedVlogs.value = relatedResponse.data
|
|
.filter(v => v.id !== vlog.value.id && v.author_id === vlog.value.author_id)
|
|
.slice(0, 3)
|
|
} catch (error) {
|
|
toast.error('Erreur lors du chargement du vlog')
|
|
console.error('Error fetching vlog:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function updateVlog() {
|
|
updating.value = true
|
|
try {
|
|
const response = await axios.put(`/api/vlogs/${vlog.value.id}`, editForm.value)
|
|
vlog.value = response.data
|
|
showEditModal.value = false
|
|
toast.success('Vlog mis à jour')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la mise à jour')
|
|
}
|
|
updating.value = false
|
|
}
|
|
|
|
async function deleteVlog() {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer ce vlog ?')) return
|
|
|
|
try {
|
|
await axios.delete(`/api/vlogs/${vlog.value.id}`)
|
|
toast.success('Vlog supprimé')
|
|
router.push('/vlogs')
|
|
} catch (error) {
|
|
toast.error('Erreur lors de la suppression')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchVlog()
|
|
fetchUsers()
|
|
})
|
|
</script>
|