305 lines
11 KiB
Vue
305 lines
11 KiB
Vue
<template>
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Welcome Section -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">
|
|
Salut {{ user?.full_name }} ! 👋
|
|
</h1>
|
|
<p class="text-gray-600">Voici ce qui se passe dans le groupe</p>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
<div class="card p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-600">Prochain événement</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ nextEvent?.title || 'Aucun' }}</p>
|
|
<p v-if="nextEvent" class="text-sm text-gray-500 mt-1">
|
|
{{ formatDate(nextEvent.date) }}
|
|
</p>
|
|
</div>
|
|
<Calendar class="w-8 h-8 text-primary-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-600">Taux de présence</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ Math.round(user?.attendance_rate || 0) }}%</p>
|
|
</div>
|
|
<TrendingUp class="w-8 h-8 text-green-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-600">Nouveaux posts</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ recentPosts }}</p>
|
|
<p class="text-sm text-gray-500 mt-1">Cette semaine</p>
|
|
</div>
|
|
<MessageSquare class="w-8 h-8 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-600">Membres actifs</p>
|
|
<p class="text-2xl font-bold text-gray-900">{{ activeMembers }}</p>
|
|
</div>
|
|
<Users class="w-8 h-8 text-purple-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Recent Posts -->
|
|
<div class="lg:col-span-2">
|
|
<div class="card">
|
|
<div class="p-6 border-b border-gray-100">
|
|
<h2 class="text-lg font-semibold text-gray-900">Publications récentes</h2>
|
|
</div>
|
|
<div class="divide-y divide-gray-100">
|
|
<div v-if="posts.length === 0" class="p-6 text-center text-gray-500">
|
|
Aucune publication récente
|
|
</div>
|
|
<div
|
|
v-for="post in posts"
|
|
:key="post.id"
|
|
class="p-6 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div class="flex items-start space-x-3">
|
|
<img
|
|
v-if="post.author_avatar"
|
|
:src="getMediaUrl(post.author_avatar)"
|
|
:alt="post.author_name"
|
|
class="w-10 h-10 rounded-full object-cover"
|
|
>
|
|
<div v-else class="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center">
|
|
<User class="w-5 h-5 text-primary-600" />
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-2">
|
|
<p class="font-medium text-gray-900">{{ post.author_name }}</p>
|
|
<span class="text-xs text-gray-500">{{ formatRelativeDate(post.created_at) }}</span>
|
|
</div>
|
|
<div class="mt-1 text-gray-700">
|
|
<Mentions :content="post.content" :mentions="post.mentioned_users || []" />
|
|
</div>
|
|
|
|
<!-- Post Image -->
|
|
<img
|
|
v-if="post.image_url"
|
|
:src="getMediaUrl(post.image_url)"
|
|
:alt="post.content"
|
|
class="mt-3 rounded-lg max-w-full max-h-48 object-cover"
|
|
>
|
|
|
|
<!-- Post Actions -->
|
|
<div class="flex items-center space-x-4 mt-3 text-sm text-gray-500">
|
|
<button
|
|
@click="togglePostLike(post)"
|
|
class="flex items-center space-x-1 hover:text-primary-600 transition-colors"
|
|
:class="{ 'text-primary-600': post.is_liked }"
|
|
>
|
|
<Heart :class="post.is_liked ? 'fill-current' : ''" class="w-4 h-4" />
|
|
<span>{{ post.likes_count || 0 }}</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="toggleComments(post.id)"
|
|
class="flex items-center space-x-1 hover:text-primary-600 transition-colors"
|
|
>
|
|
<MessageCircle class="w-4 h-4" />
|
|
<span>{{ post.comments_count || 0 }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<router-link
|
|
to="/posts"
|
|
class="block p-4 text-center text-primary-600 hover:bg-gray-50 font-medium"
|
|
>
|
|
Voir toutes les publications →
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upcoming Events -->
|
|
<div>
|
|
<div class="card">
|
|
<div class="p-6 border-b border-gray-100">
|
|
<h2 class="text-lg font-semibold text-gray-900">Événements à venir</h2>
|
|
</div>
|
|
<div class="divide-y divide-gray-100">
|
|
<div v-if="upcomingEvents.length === 0" class="p-6 text-center text-gray-500">
|
|
Aucun événement prévu
|
|
</div>
|
|
<router-link
|
|
v-for="event in upcomingEvents"
|
|
:key="event.id"
|
|
:to="`/events/${event.id}`"
|
|
class="block p-4 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<h3 class="font-medium text-gray-900">{{ event.title }}</h3>
|
|
<p class="text-sm text-gray-600 mt-1">{{ formatDate(event.date) }}</p>
|
|
<p v-if="event.location" class="text-sm text-gray-500 mt-1">
|
|
📍 {{ event.location }}
|
|
</p>
|
|
<div class="mt-3 flex items-center space-x-4 text-xs">
|
|
<span class="text-green-600">
|
|
✓ {{ event.present_count }} présents
|
|
</span>
|
|
<span class="text-yellow-600">
|
|
? {{ event.maybe_count }} peut-être
|
|
</span>
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
<router-link
|
|
to="/events"
|
|
class="block p-4 text-center text-primary-600 hover:bg-gray-50 font-medium"
|
|
>
|
|
Voir tous les événements →
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Recent Vlogs -->
|
|
<div class="card mt-6">
|
|
<div class="p-6 border-b border-gray-100">
|
|
<h2 class="text-lg font-semibold text-gray-900">Derniers vlogs</h2>
|
|
</div>
|
|
<div class="divide-y divide-gray-100">
|
|
<div v-if="recentVlogs.length === 0" class="p-6 text-center text-gray-500">
|
|
Aucun vlog récent
|
|
</div>
|
|
<router-link
|
|
v-for="vlog in recentVlogs"
|
|
:key="vlog.id"
|
|
:to="`/vlogs/${vlog.id}`"
|
|
class="block p-4 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div class="aspect-video bg-gray-100 rounded-lg mb-3 relative overflow-hidden">
|
|
<img
|
|
v-if="vlog.thumbnail_url"
|
|
:src="getMediaUrl(vlog.thumbnail_url)"
|
|
:alt="vlog.title"
|
|
class="w-full h-full object-cover"
|
|
>
|
|
<div v-else class="w-full h-full flex items-center justify-center">
|
|
<Film class="w-8 h-8 text-gray-400" />
|
|
</div>
|
|
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
<Play class="w-12 h-12 text-white" />
|
|
</div>
|
|
</div>
|
|
<h3 class="font-medium text-gray-900">{{ vlog.title }}</h3>
|
|
<p class="text-sm text-gray-600 mt-1">Par {{ vlog.author_name }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ vlog.views_count }} vues</p>
|
|
</router-link>
|
|
</div>
|
|
<router-link
|
|
to="/vlogs"
|
|
class="block p-4 text-center text-primary-600 hover:bg-gray-50 font-medium"
|
|
>
|
|
Voir tous les vlogs →
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import axios from '@/utils/axios'
|
|
import { getMediaUrl } from '@/utils/axios'
|
|
import { format, formatDistanceToNow } from 'date-fns'
|
|
import { fr } from 'date-fns/locale'
|
|
import {
|
|
Calendar,
|
|
TrendingUp,
|
|
MessageSquare,
|
|
Users,
|
|
User,
|
|
Film,
|
|
Play,
|
|
Heart,
|
|
MessageCircle
|
|
} from 'lucide-vue-next'
|
|
import Mentions from '@/components/Mentions.vue'
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
const posts = ref([])
|
|
const upcomingEvents = ref([])
|
|
const recentVlogs = ref([])
|
|
const stats = ref({})
|
|
|
|
const user = computed(() => authStore.user)
|
|
const nextEvent = computed(() => upcomingEvents.value[0])
|
|
const recentPosts = computed(() => stats.value.recent_posts || 0)
|
|
const activeMembers = computed(() => stats.value.total_users || 0)
|
|
|
|
function formatDate(date) {
|
|
return format(new Date(date), 'EEEE d MMMM à HH:mm', { locale: fr })
|
|
}
|
|
|
|
function formatRelativeDate(date) {
|
|
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
|
}
|
|
|
|
async function fetchDashboardData() {
|
|
try {
|
|
// Fetch recent posts
|
|
const postsResponse = await axios.get('/api/posts?limit=5')
|
|
posts.value = postsResponse.data
|
|
|
|
// Fetch upcoming events
|
|
const eventsResponse = await axios.get('/api/events?upcoming=true')
|
|
upcomingEvents.value = eventsResponse.data.slice(0, 3)
|
|
|
|
// Fetch recent vlogs
|
|
const vlogsResponse = await axios.get('/api/vlogs?limit=2')
|
|
recentVlogs.value = vlogsResponse.data
|
|
|
|
// Fetch stats
|
|
const statsResponse = await axios.get('/api/stats/overview')
|
|
stats.value = statsResponse.data
|
|
} catch (error) {
|
|
console.error('Error fetching dashboard data:', error)
|
|
}
|
|
}
|
|
|
|
async function togglePostLike(post) {
|
|
try {
|
|
const response = await axios.post(`/api/posts/${post.id}/like`)
|
|
post.is_liked = response.data.is_liked
|
|
post.likes_count = response.data.likes_count
|
|
} catch (error) {
|
|
console.error('Error toggling like:', error)
|
|
}
|
|
}
|
|
|
|
function toggleComments(postId) {
|
|
const post = posts.value.find(p => p.id === postId)
|
|
if (post) {
|
|
post.showComments = !post.showComments
|
|
if (post.showComments && !post.comments) {
|
|
post.comments = []
|
|
post.newComment = ''
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchDashboardData()
|
|
})
|
|
</script>
|