initial commit - LeDiscord plateforme des copains
This commit is contained in:
370
frontend/src/views/VlogDetail.vue
Normal file
370
frontend/src/views/VlogDetail.vue
Normal file
@@ -0,0 +1,370 @@
|
||||
<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">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p class="mt-4 text-gray-600">Chargement du vlog...</p>
|
||||
</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="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'
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user