initial commit - LeDiscord plateforme des copains
This commit is contained in:
521
frontend/src/views/Posts.vue
Normal file
521
frontend/src/views/Posts.vue
Normal file
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Publications</h1>
|
||||
<p class="text-gray-600 mt-1">Partagez vos moments avec le groupe</p>
|
||||
</div>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="btn-primary"
|
||||
>
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
Nouvelle publication
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create Post Form -->
|
||||
<div class="card p-6 mb-8">
|
||||
<div class="flex items-start space-x-3">
|
||||
<img
|
||||
v-if="user?.avatar_url"
|
||||
:src="getMediaUrl(user.avatar_url)"
|
||||
:alt="user?.full_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">
|
||||
<MentionInput
|
||||
v-model="newPost.content"
|
||||
:users="users"
|
||||
:rows="3"
|
||||
placeholder="Quoi de neuf ? Mentionnez des amis avec @..."
|
||||
@mentions-changed="handleMentionsChanged"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="showImageUpload = !showImageUpload"
|
||||
class="text-gray-500 hover:text-primary-600 transition-colors"
|
||||
title="Ajouter une image"
|
||||
>
|
||||
<Image class="w-5 h-5" />
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ newPost.content.length }}/5000</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="createPost"
|
||||
:disabled="!newPost.content.trim() || creating"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ creating ? 'Publication...' : 'Publier' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Image upload -->
|
||||
<div v-if="showImageUpload" class="mt-3">
|
||||
<input
|
||||
ref="imageInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleImageChange"
|
||||
>
|
||||
<button
|
||||
@click="$refs.imageInput.click()"
|
||||
class="btn-secondary text-sm"
|
||||
>
|
||||
<Upload class="w-4 h-4 mr-2" />
|
||||
Sélectionner une image
|
||||
</button>
|
||||
<div v-if="newPost.image_url" class="mt-2 relative inline-block">
|
||||
<img
|
||||
:src="getMediaUrl(newPost.image_url)"
|
||||
:alt="newPost.content"
|
||||
class="max-w-xs max-h-32 rounded-lg object-cover"
|
||||
>
|
||||
<button
|
||||
@click="removeImage"
|
||||
class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts List -->
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
class="card p-6"
|
||||
>
|
||||
<!-- Post Header -->
|
||||
<div class="flex items-start space-x-3 mb-4">
|
||||
<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">
|
||||
<router-link
|
||||
:to="`/profile/${post.author_id}`"
|
||||
class="font-medium text-gray-900 hover:text-primary-600 transition-colors"
|
||||
>
|
||||
{{ post.author_name }}
|
||||
</router-link>
|
||||
<span class="text-sm text-gray-500">{{ formatRelativeDate(post.created_at) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Mentions -->
|
||||
<div v-if="post.mentioned_users && post.mentioned_users.length > 0" class="mt-1">
|
||||
<span class="text-xs text-gray-500">Mentionne : </span>
|
||||
<span
|
||||
v-for="mentionedUser in post.mentioned_users"
|
||||
:key="mentionedUser.id"
|
||||
class="text-xs text-primary-600 hover:underline cursor-pointer"
|
||||
>
|
||||
@{{ mentionedUser.username }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete button for author or admin -->
|
||||
<button
|
||||
v-if="canDeletePost(post)"
|
||||
@click="deletePost(post.id)"
|
||||
class="text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Post Content -->
|
||||
<div class="mb-4">
|
||||
<Mentions :content="post.content" :mentions="post.mentioned_users" />
|
||||
|
||||
<!-- 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-96 object-cover"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Post Actions -->
|
||||
<div class="flex items-center space-x-6 text-sm text-gray-500">
|
||||
<button
|
||||
@click="togglePostLike(post)"
|
||||
class="flex items-center space-x-2 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-2 hover:text-primary-600 transition-colors"
|
||||
>
|
||||
<MessageCircle class="w-4 h-4" />
|
||||
<span>{{ post.comments_count || 0 }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div v-if="post.showComments" class="mt-4 pt-4 border-t border-gray-100">
|
||||
<!-- Add Comment -->
|
||||
<div class="flex items-start space-x-3 mb-4">
|
||||
<img
|
||||
v-if="user?.avatar_url"
|
||||
:src="getMediaUrl(user.avatar_url)"
|
||||
:alt="user?.full_name"
|
||||
class="w-8 h-8 rounded-full object-cover"
|
||||
>
|
||||
<div v-else class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center">
|
||||
<User class="w-4 h-4 text-primary-600" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<MentionInput
|
||||
v-model="post.newComment"
|
||||
:users="users"
|
||||
:rows="2"
|
||||
placeholder="Ajouter un commentaire... (utilisez @username pour mentionner)"
|
||||
@mentions-changed="(mentions) => handleCommentMentionsChanged(post.id, mentions)"
|
||||
/>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-xs text-gray-500">{{ (post.newComment || '').length }}/500</span>
|
||||
<button
|
||||
@click="addComment(post)"
|
||||
:disabled="!post.newComment?.trim()"
|
||||
class="btn-primary text-sm px-3 py-1"
|
||||
>
|
||||
Commenter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments List -->
|
||||
<div v-if="post.comments && post.comments.length > 0" class="space-y-3">
|
||||
<div
|
||||
v-for="comment in post.comments"
|
||||
:key="comment.id"
|
||||
class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<img
|
||||
v-if="comment.author_avatar"
|
||||
:src="getMediaUrl(comment.author_avatar)"
|
||||
:alt="comment.author_name"
|
||||
class="w-6 h-6 rounded-full object-cover"
|
||||
>
|
||||
<div v-else class="w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User class="w-3 h-3 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<router-link
|
||||
:to="`/profile/${comment.author_id}`"
|
||||
class="font-medium text-sm text-gray-900 hover:text-primary-600 transition-colors"
|
||||
>
|
||||
{{ comment.author_name }}
|
||||
</router-link>
|
||||
<span class="text-xs text-gray-500">{{ formatRelativeDate(comment.created_at) }}</span>
|
||||
</div>
|
||||
<Mentions :content="comment.content" :mentions="comment.mentioned_users || []" class="text-sm text-gray-700 mt-1" />
|
||||
</div>
|
||||
|
||||
<!-- Delete comment button -->
|
||||
<button
|
||||
v-if="canDeleteComment(comment)"
|
||||
@click="deleteComment(post.id, comment.id)"
|
||||
class="text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load more -->
|
||||
<div v-if="hasMorePosts" class="text-center mt-8">
|
||||
<button
|
||||
@click="loadMorePosts"
|
||||
:disabled="loading"
|
||||
class="btn-secondary"
|
||||
>
|
||||
{{ loading ? 'Chargement...' : 'Charger plus' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="posts.length === 0 && !loading" class="text-center py-12">
|
||||
<MessageSquare class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucune publication</h3>
|
||||
<p class="text-gray-600">Soyez le premier à partager quelque chose !</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import {
|
||||
Plus,
|
||||
User,
|
||||
Image,
|
||||
Upload,
|
||||
X,
|
||||
Heart,
|
||||
MessageCircle,
|
||||
Trash2,
|
||||
MessageSquare
|
||||
} from 'lucide-vue-next'
|
||||
import Mentions from '@/components/Mentions.vue'
|
||||
import MentionInput from '@/components/MentionInput.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const posts = ref([])
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const showCreateModal = ref(false)
|
||||
const showImageUpload = ref(false)
|
||||
|
||||
const offset = ref(0)
|
||||
const hasMorePosts = ref(true)
|
||||
|
||||
const newPost = ref({
|
||||
content: '',
|
||||
image_url: '',
|
||||
mentioned_user_ids: []
|
||||
})
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
}
|
||||
|
||||
function handleMentionsChanged(mentions) {
|
||||
newPost.value.mentioned_user_ids = mentions.map(m => m.id)
|
||||
}
|
||||
|
||||
function handleCommentMentionsChanged(postId, mentions) {
|
||||
const post = posts.value.find(p => p.id === postId)
|
||||
if (post) {
|
||||
post.commentMentions = mentions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function createPost() {
|
||||
if (!newPost.value.content.trim()) return
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/posts', {
|
||||
content: newPost.value.content,
|
||||
image_url: newPost.value.image_url,
|
||||
mentioned_user_ids: newPost.value.mentioned_user_ids
|
||||
})
|
||||
|
||||
// Add new post to the beginning of the list
|
||||
posts.value.unshift(response.data)
|
||||
|
||||
// Reset form
|
||||
newPost.value = {
|
||||
content: '',
|
||||
image_url: '',
|
||||
mentioned_user_ids: []
|
||||
}
|
||||
showImageUpload.value = false
|
||||
|
||||
toast.success('Publication créée avec succès')
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de la création de la publication')
|
||||
}
|
||||
creating.value = false
|
||||
}
|
||||
|
||||
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
|
||||
toast.success(post.is_liked ? 'Post liké' : 'Like retiré')
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de la mise à jour du like')
|
||||
}
|
||||
}
|
||||
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addComment(post) {
|
||||
if (!post.newComment?.trim()) return
|
||||
|
||||
try {
|
||||
const response = await axios.post(`/api/posts/${post.id}/comment`, {
|
||||
content: post.newComment.trim()
|
||||
})
|
||||
|
||||
// Add new comment to the post
|
||||
if (!post.comments) post.comments = []
|
||||
post.comments.push(response.data.comment)
|
||||
post.comments_count = (post.comments_count || 0) + 1
|
||||
|
||||
// Reset comment input
|
||||
post.newComment = ''
|
||||
|
||||
toast.success('Commentaire ajouté')
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de l\'ajout du commentaire')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteComment(postId, commentId) {
|
||||
try {
|
||||
await axios.delete(`/api/posts/${postId}/comment/${commentId}`)
|
||||
|
||||
const post = posts.value.find(p => p.id === postId)
|
||||
if (post && post.comments) {
|
||||
post.comments = post.comments.filter(c => c.id !== commentId)
|
||||
post.comments_count = Math.max(0, (post.comments_count || 1) - 1)
|
||||
}
|
||||
|
||||
toast.success('Commentaire supprimé')
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de la suppression du commentaire')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImageChange(event) {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Veuillez sélectionner une image')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('L\'image est trop volumineuse (max 5MB)')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await axios.post('/api/posts/upload-image', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
||||
newPost.value.image_url = response.data.image_url
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de l\'upload de l\'image')
|
||||
}
|
||||
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
newPost.value.image_url = ''
|
||||
}
|
||||
|
||||
async function fetchPosts() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get(`/api/posts?limit=10&offset=${offset.value}`)
|
||||
if (offset.value === 0) {
|
||||
posts.value = response.data
|
||||
} else {
|
||||
posts.value.push(...response.data)
|
||||
}
|
||||
|
||||
hasMorePosts.value = response.data.length === 10
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors du chargement des publications')
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
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 loadMorePosts() {
|
||||
offset.value += 10
|
||||
await fetchPosts()
|
||||
}
|
||||
|
||||
async function deletePost(postId) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer cette publication ?')) return
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/posts/${postId}`)
|
||||
posts.value = posts.value.filter(p => p.id !== postId)
|
||||
toast.success('Publication supprimée')
|
||||
} catch (error) {
|
||||
toast.error('Erreur lors de la suppression')
|
||||
}
|
||||
}
|
||||
|
||||
function canDeletePost(post) {
|
||||
return user.value && (post.author_id === user.value.id || user.value.is_admin)
|
||||
}
|
||||
|
||||
function canDeleteComment(comment) {
|
||||
return user.value && (comment.author_id === user.value.id || user.value.is_admin)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPosts()
|
||||
fetchUsers()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user