Files
LeDiscord/frontend/src/views/Events.vue
2026-01-25 18:08:38 +01:00

469 lines
15 KiB
Vue

<template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900">Événements</h1>
<p class="text-sm sm:text-base text-gray-600 mt-1">Organisez et participez aux événements du groupe</p>
</div>
<button
@click="showCreateModal = true"
class="w-full sm:w-auto btn-primary justify-center"
>
<Plus class="w-4 h-4 mr-2" />
Nouvel événement
</button>
</div>
<!-- Tabs -->
<div class="border-b border-gray-200 mb-8">
<nav class="-mb-px flex space-x-8">
<button
@click="activeTab = 'upcoming'"
class="py-2 px-1 border-b-2 font-medium text-sm transition-colors"
:class="activeTab === 'upcoming'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
>
À venir
</button>
<button
@click="activeTab = 'past'"
class="py-2 px-1 border-b-2 font-medium text-sm transition-colors"
:class="activeTab === 'past'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
>
Passés
</button>
<button
@click="activeTab = 'all'"
class="py-2 px-1 border-b-2 font-medium text-sm transition-colors"
:class="activeTab === 'all'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
>
Tous
</button>
</nav>
</div>
<!-- Events Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="event in filteredEvents" :key="event.id" class="card hover:shadow-lg transition-shadow cursor-pointer" @click="openEvent(event)">
<div class="aspect-video bg-gray-100 relative overflow-hidden">
<img v-if="event.cover_image" :src="getMediaUrl(event.cover_image)" :alt="event.title" class="w-full h-full object-cover">
<div v-else class="w-full h-full flex items-center justify-center">
<img
v-if="event.creator_avatar"
:src="getMediaUrl(event.creator_avatar)"
:alt="event.creator_name"
class="w-16 h-16 rounded-full object-cover"
>
<div v-else class="w-16 h-16 rounded-full bg-primary-100 flex items-center justify-center">
<User class="w-8 h-8 text-primary-600" />
</div>
</div>
<!-- Date Badge -->
<div class="absolute top-2 left-2 bg-black bg-opacity-70 text-white text-xs px-2 py-1 rounded">
{{ formatDate(event.date) }}
</div>
<!-- Participation Badge -->
<div class="absolute top-2 right-2">
<span
v-if="getUserParticipation(event)"
class="px-2 py-1 rounded-full text-xs font-medium text-white"
:class="getParticipationBadgeClass(getUserParticipation(event))"
>
{{ getParticipationText(getUserParticipation(event)) }}
</span>
</div>
</div>
<div class="p-4">
<h3 class="font-semibold text-gray-900 mb-2 line-clamp-2">{{ event.title }}</h3>
<!-- Description with mentions -->
<div v-if="event.description" class="mb-3">
<Mentions :content="event.description" :mentions="getMentionsFromContent(event.description)" class="text-sm text-gray-600 line-clamp-2" />
</div>
<div class="flex items-center text-gray-600 mb-3">
<MapPin class="w-4 h-4 mr-2" />
<span class="text-sm">{{ event.location || 'Lieu non spécifié' }}</span>
</div>
<div class="flex items-center text-gray-600 mb-4">
<User class="w-4 h-4 mr-2" />
<router-link
:to="`/profile/${event.creator_id}`"
class="text-sm text-gray-900 hover:text-primary-600 transition-colors cursor-pointer"
>
{{ event.creator_name }}
</router-link>
</div>
<!-- Quick Participation Buttons -->
<div class="flex gap-2 mb-3">
<button
@click.stop="quickParticipation(event.id, 'present')"
class="flex-1 py-2 px-3 rounded text-xs font-medium transition-colors"
:class="getUserParticipation(event) === 'present' ? 'bg-success-100 text-success-700' : 'bg-gray-100 text-gray-700 hover:bg-success-50'"
>
Présent
</button>
<button
@click.stop="quickParticipation(event.id, 'maybe')"
class="flex-1 py-2 px-3 rounded text-xs font-medium transition-colors"
:class="getUserParticipation(event) === 'maybe' ? 'bg-warning-100 text-warning-700' : 'bg-gray-100 text-gray-700 hover:bg-warning-50'"
>
? Peut-être
</button>
<button
@click.stop="quickParticipation(event.id, 'absent')"
class="flex-1 py-2 px-3 rounded text-xs font-medium transition-colors"
:class="getUserParticipation(event) === 'absent' ? 'bg-accent-100 text-accent-700' : 'bg-gray-100 text-gray-700 hover:bg-accent-50'"
>
Absent
</button>
</div>
<!-- Participation Stats -->
<div class="flex items-center justify-between text-xs text-gray-500">
<span>{{ event.present_count || 0 }} présents</span>
<span>{{ event.maybe_count || 0 }} peut-être</span>
<span>{{ event.absent_count || 0 }} absents</span>
</div>
</div>
</div>
</div>
<!-- Load more -->
<div v-if="hasMoreEvents" class="text-center mt-8">
<button
@click="loadMoreEvents"
:disabled="loading"
class="btn-secondary"
>
{{ loading ? 'Chargement...' : 'Charger plus' }}
</button>
</div>
<!-- Empty state -->
<div v-if="filteredEvents.length === 0 && !loading" class="text-center py-12">
<Calendar class="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">
{{ activeTab === 'upcoming' ? 'Aucun événement à venir' :
activeTab === 'past' ? 'Aucun événement passé' : 'Aucun événement' }}
</h3>
<p class="text-gray-600">
{{ activeTab === 'upcoming' ? 'Créez le premier événement pour commencer !' :
activeTab === 'past' ? 'Les événements passés apparaîtront ici' :
'Créez le premier événement pour commencer !' }}
</p>
</div>
<!-- Create Event 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="showCreateModal"
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">Créer un nouvel événement</h2>
<form @submit.prevent="createEvent" class="space-y-4">
<div>
<label class="label">Titre</label>
<input
v-model="newEvent.title"
type="text"
required
class="input"
placeholder="Titre de l'événement..."
>
</div>
<div>
<label class="label">Description (optionnel)</label>
<MentionInput
v-model="newEvent.description"
:users="users"
:rows="3"
placeholder="Décrivez votre événement... (utilisez @username pour mentionner)"
@mentions-changed="handleEventMentionsChanged"
/>
</div>
<div>
<label class="label">Lieu</label>
<input
v-model="newEvent.location"
type="text"
class="input"
placeholder="Adresse ou lieu de l'événement..."
>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label">Date et heure</label>
<input
v-model="newEvent.date"
type="datetime-local"
required
class="input"
>
</div>
<div>
<label class="label">Date de fin (optionnel)</label>
<input
v-model="newEvent.end_date"
type="datetime-local"
class="input"
>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="showCreateModal = false"
class="flex-1 btn-secondary"
>
Annuler
</button>
<button
type="submit"
:disabled="creating"
class="flex-1 btn-primary"
>
{{ creating ? 'Création...' : 'Créer l\'événement' }}
</button>
</div>
</form>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
import axios from '@/utils/axios'
import { getMediaUrl } from '@/utils/axios'
import { formatDistanceToNow, format } from 'date-fns'
import { fr } from 'date-fns/locale'
import {
Plus,
Calendar,
User,
MapPin
} from 'lucide-vue-next'
import Mentions from '@/components/Mentions.vue'
import MentionInput from '@/components/MentionInput.vue'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const toast = useToast()
const authStore = useAuthStore()
const events = ref([])
const users = ref([])
const loading = ref(false)
const creating = ref(false)
const showCreateModal = ref(false)
const hasMoreEvents = ref(true)
const offset = ref(0)
const activeTab = ref('upcoming')
const newEvent = ref({
title: '',
description: '',
date: '',
location: '',
end_date: null
})
const eventMentions = ref([])
const filteredEvents = computed(() => {
if (activeTab.value === 'upcoming') {
return events.value.filter(event => new Date(event.date) >= new Date())
} else if (activeTab.value === 'past') {
return events.value.filter(event => new Date(event.date) < new Date())
}
return events.value
})
function formatRelativeDate(date) {
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
}
function formatDate(date) {
return format(new Date(date), 'dd/MM/yyyy', { locale: fr })
}
function openEvent(event) {
router.push(`/events/${event.id}`)
}
function handleEventMentionsChanged(mentions) {
eventMentions.value = mentions
}
async function fetchUsers() {
try {
const response = await axios.get('/api/users')
users.value = response.data
} catch (error) {
console.error('Error fetching users:', error)
}
}
function getMentionsFromContent(content) {
if (!content) return []
const mentions = []
const mentionRegex = /@(\w+)/g
let match
while ((match = mentionRegex.exec(content)) !== null) {
const username = match[1]
const user = users.value.find(u => u.username === username)
if (user) {
mentions.push({
id: user.id,
username: user.username,
full_name: user.full_name
})
}
}
return mentions
}
function getUserParticipation(event) {
const participation = event.participations?.find(p => p.user_id === authStore.user?.id)
return participation?.status || null
}
function getParticipationBadgeClass(status) {
switch (status) {
case 'present': return 'bg-success-600'
case 'maybe': return 'bg-warning-600'
case 'absent': return 'bg-accent-600'
default: return 'bg-gray-600'
}
}
function getParticipationText(status) {
switch (status) {
case 'present': return 'Présent'
case 'maybe': return 'Peut-être'
case 'absent': return 'Absent'
default: return 'En attente'
}
}
async function quickParticipation(eventId, status) {
try {
const response = await axios.put(`/api/events/${eventId}/participation`, {
status: status
})
// Update the event in the list
const eventIndex = events.value.findIndex(e => e.id === eventId)
if (eventIndex !== -1) {
events.value[eventIndex] = response.data
}
toast.success(`Participation mise à jour : ${getParticipationText(status)}`)
} catch (error) {
toast.error('Erreur lors de la mise à jour de la participation')
}
}
async function createEvent() {
if (!newEvent.value.title || !newEvent.value.date) return
creating.value = true
try {
const eventData = {
title: newEvent.value.title,
description: newEvent.value.description,
date: new Date(newEvent.value.date).toISOString(),
location: newEvent.value.location,
end_date: newEvent.value.end_date ? new Date(newEvent.value.end_date).toISOString() : null
}
await axios.post('/api/events', eventData)
// Refresh events list
await fetchEvents()
showCreateModal.value = false
resetForm()
toast.success('Événement créé avec succès')
} catch (error) {
toast.error('Erreur lors de la création de l\'événement')
}
creating.value = false
}
function resetForm() {
newEvent.value = {
title: '',
description: '',
date: '',
location: '',
end_date: null
}
}
async function fetchEvents() {
loading.value = true
try {
const response = await axios.get(`/api/events?limit=12&offset=${offset.value}`)
console.log('Events response:', response.data)
if (response.data && response.data.length > 0) {
console.log('First event:', response.data[0])
console.log('Creator avatar:', response.data[0].creator_avatar)
}
if (offset.value === 0) {
events.value = response.data
} else {
events.value.push(...response.data)
}
hasMoreEvents.value = response.data.length === 12
} catch (error) {
console.error('Error fetching events:', error)
toast.error('Erreur lors du chargement des événements')
}
loading.value = false
}
async function loadMoreEvents() {
offset.value += 12
await fetchEvents()
}
onMounted(() => {
fetchEvents()
fetchUsers()
})
</script>