initial commit - LeDiscord plateforme des copains

This commit is contained in:
EvanChal
2025-08-21 00:28:21 +02:00
commit b7a84a53aa
93 changed files with 16247 additions and 0 deletions

View 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>