working version

This commit is contained in:
root
2025-08-27 18:34:38 +02:00
parent b7a84a53aa
commit dfaae262c7
153 changed files with 19389 additions and 788 deletions

4
frontend/src/App.vue Normal file → Executable file
View File

@@ -2,6 +2,7 @@
<div id="app">
<component :is="layout">
<router-view />
<EnvironmentDebug />
</component>
</div>
</template>
@@ -11,10 +12,11 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import AuthLayout from '@/layouts/AuthLayout.vue'
import EnvironmentDebug from '@/components/EnvironmentDebug.vue'
const route = useRoute()
const layout = computed(() => {
return route.meta.layout === 'auth' ? AuthLayout : DefaultLayout
})
</script>
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div v-if="showDebug" class="fixed bottom-4 right-4 bg-black bg-opacity-75 text-white p-3 rounded-lg text-xs font-mono z-50 max-w-xs">
<div class="flex items-center space-x-2 mb-2">
<span class="w-2 h-2 rounded-full" :class="environmentColor"></span>
<span class="font-bold">{{ environment.toUpperCase() }}</span>
</div>
<div class="space-y-1 text-gray-300">
<div>API: {{ apiUrl }}</div>
<div>App: {{ appUrl }}</div>
<div>Build: {{ buildTime }}</div>
<div>Router: {{ routerStatus }}</div>
<div class="text-yellow-400">VITE_API_URL: {{ viteApiUrl }}</div>
<div class="text-yellow-400">NODE_ENV: {{ nodeEnv }}</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
const routerStatus = ref('Initializing...')
// Utiliser directement les variables d'environnement Vite
const environment = computed(() => import.meta.env.VITE_ENVIRONMENT || 'local')
const apiUrl = computed(() => import.meta.env.VITE_API_URL || 'Non défini')
const appUrl = computed(() => import.meta.env.VITE_APP_URL || 'Non défini')
const viteApiUrl = computed(() => import.meta.env.VITE_API_URL || 'Non défini')
const nodeEnv = computed(() => import.meta.env.NODE_ENV || 'Non défini')
const buildTime = computed(() => new Date().toLocaleTimeString())
const environmentColor = computed(() => {
switch (environment.value) {
case 'local': return 'bg-green-500'
case 'development': return 'bg-yellow-500'
case 'production': return 'bg-red-500'
default: return 'bg-gray-500'
}
})
const showDebug = computed(() => {
// Toujours afficher en développement, conditionnellement en production
return environment.value !== 'production' || import.meta.env.DEV
})
onMounted(() => {
// Attendre un peu que le router soit initialisé
setTimeout(() => {
routerStatus.value = 'Ready'
}, 300)
// Debug des variables d'environnement
console.log('🔍 EnvironmentDebug - Variables d\'environnement:')
console.log(' VITE_ENVIRONMENT:', import.meta.env.VITE_ENVIRONMENT)
console.log(' VITE_API_URL:', import.meta.env.VITE_API_URL)
console.log(' VITE_APP_URL:', import.meta.env.VITE_APP_URL)
console.log(' NODE_ENV:', import.meta.env.NODE_ENV)
})
</script>
<style scoped>
/* Styles spécifiques au composant */
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="text-center">
<img
v-if="variant === 'pulse'"
src="/logo_lediscord.png"
alt="LeDiscord Logo"
:class="[
'mx-auto animate-pulse',
size === 'small' ? 'h-8 w-auto' :
size === 'medium' ? 'h-12 w-auto' :
'h-16 w-auto'
]"
>
<img
v-else-if="variant === 'spinner'"
src="/logo_lediscord.png"
alt="LeDiscord Logo"
:class="[
'mx-auto animate-spin',
size === 'small' ? 'h-5 w-auto' :
size === 'medium' ? 'h-6 w-auto' :
'h-8 w-auto'
]"
>
<p v-if="showText" class="text-sm text-gray-600 mt-2">{{ text || 'Chargement...' }}</p>
</div>
</template>
<script setup>
defineProps({
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
variant: {
type: String,
default: 'pulse',
validator: (value) => ['pulse', 'spinner'].includes(value)
},
text: {
type: String,
default: ''
},
showText: {
type: Boolean,
default: true
}
})
</script>

0
frontend/src/components/MentionInput.vue Normal file → Executable file
View File

2
frontend/src/components/Mentions.vue Normal file → Executable file
View File

@@ -76,4 +76,4 @@ const parsedContent = computed(() => {
color: #7c3aed;
filter: drop-shadow(0 0 4px rgba(139, 92, 246, 0.5));
}
</style>
</style>

10
frontend/src/components/TicketFloatingButton.vue Normal file → Executable file
View File

@@ -116,10 +116,7 @@
class="flex-1 btn-primary"
:disabled="submitting"
>
<svg v-if="submitting" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<LoadingLogo v-if="submitting" variant="spinner" size="small" :showText="false" />
{{ submitting ? 'Envoi...' : 'Envoyer le ticket' }}
</button>
</div>
@@ -130,10 +127,13 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
import axios from '@/utils/axios'
import { useAuthStore } from '@/stores/auth'
import { Plus, X, Send } from 'lucide-vue-next'
import LoadingLogo from '@/components/LoadingLogo.vue'
const toast = useToast()
const router = useRouter()

0
frontend/src/components/UserAvatar.vue Normal file → Executable file
View File

0
frontend/src/components/VideoPlayer.vue Normal file → Executable file
View File

26
frontend/src/components/VlogComments.vue Normal file → Executable file
View File

@@ -100,6 +100,7 @@ import MentionInput from '@/components/MentionInput.vue'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale'
import { getMediaUrl } from '@/utils/axios'
import axios from '@/utils/axios'
const props = defineProps({
vlogId: {
@@ -152,18 +153,10 @@ async function submitComment() {
submitting.value = true
try {
const response = await fetch(`/api/vlogs/${props.vlogId}/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
},
body: JSON.stringify({ content: newComment.value.trim() })
})
const response = await axios.post(`/api/vlogs/${props.vlogId}/comment`, { content: newComment.value.trim() })
if (response.ok) {
const result = await response.json()
emit('comment-added', result.comment)
if (response.data) {
emit('comment-added', response.data.comment)
newComment.value = ''
showCommentForm.value = false
toast.success('Commentaire ajouté')
@@ -182,14 +175,9 @@ async function deleteComment(commentId) {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce commentaire ?')) return
try {
const response = await fetch(`/api/vlogs/${props.vlogId}/comment/${commentId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
const response = await axios.delete(`/api/vlogs/${props.vlogId}/comment/${commentId}`)
if (response.ok) {
if (response.data) {
emit('comment-deleted', commentId)
toast.success('Commentaire supprimé')
} else {
@@ -200,4 +188,4 @@ async function deleteComment(commentId) {
console.error('Error deleting comment:', error)
}
}
</script>
</script>

12
frontend/src/layouts/AuthLayout.vue Normal file → Executable file
View File

@@ -1,14 +1,22 @@
<template>
<div class="min-h-screen bg-gradient-discord flex items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold bg-gradient-primary bg-clip-text text-transparent mb-2">LeDiscord</h1>
<div class="text-center mb-4">
<h1 class="text-4xl font-bold bg-gradient-primary bg-clip-text text-transparent mb-0">LeDiscord</h1>
<p class="text-secondary-600">Notre espace privé</p>
</div>
<div class="bg-white rounded-2xl shadow-xl p-8">
<slot />
</div>
<div class="text-center mt-4">
<img
src="/logo_lediscord.png"
alt="LeDiscord Logo"
class="mx-auto h-48 w-auto mb-0 drop-shadow-lg"
>
</div>
</div>
</div>
</template>

7
frontend/src/layouts/DefaultLayout.vue Normal file → Executable file
View File

@@ -7,6 +7,11 @@
<div class="flex">
<!-- Logo -->
<router-link to="/" class="flex items-center">
<img
src="/logo_lediscord.png"
alt="LeDiscord Logo"
class="h-8 w-auto mr-2"
>
<span class="text-2xl font-bold bg-gradient-primary bg-clip-text text-transparent">LeDiscord</span>
</router-link>
@@ -281,4 +286,4 @@ onMounted(async () => {
await fetchNotifications()
await authStore.fetchUnreadCount()
})
</script>
</script>

21
frontend/src/main.js Normal file → Executable file
View File

@@ -1,4 +1,4 @@
import { createApp } from 'vue'
import { createApp, nextTick } from 'vue'
import { createPinia } from 'pinia'
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
@@ -7,7 +7,10 @@ import App from './App.vue'
import router from './router'
import './style.css'
// Créer l'application
const app = createApp(App)
// Créer et configurer Pinia
const pinia = createPinia()
// Toast configuration
@@ -26,8 +29,20 @@ const toastOptions = {
rtl: false
}
// Installer les plugins dans l'ordre correct
// IMPORTANT: Pinia doit être installé AVANT le router
app.use(pinia)
app.use(router)
app.use(Toast, toastOptions)
app.mount('#app')
// Maintenant installer le router
app.use(router)
// Attendre que le router soit prêt avant de monter l'app
router.isReady().then(() => {
app.mount('#app')
console.log('🚀 Application montée avec succès')
}).catch((error) => {
console.error('❌ Erreur lors du montage de l\'application:', error)
// Fallback: monter l'app même en cas d'erreur
app.mount('#app')
})

17
frontend/src/router/index.js Normal file → Executable file
View File

@@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
// Views
import Home from '@/views/Home.vue'
@@ -125,19 +124,11 @@ const router = createRouter({
routes
})
// Navigation guard
// Navigation guard simplifié - la logique d'authentification sera gérée dans les composants
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else if (to.meta.requiresAdmin && !authStore.isAdmin) {
next('/')
} else if ((to.name === 'Login' || to.name === 'Register') && authStore.isAuthenticated) {
next('/')
} else {
next()
}
// Pour l'instant, on laisse passer toutes les routes
// La logique d'authentification sera gérée dans les composants individuels
next()
})
export default router

0
frontend/src/stores/auth.js Normal file → Executable file
View File

0
frontend/src/style.css Normal file → Executable file
View File

99
frontend/src/utils/axios.js Normal file → Executable file
View File

@@ -2,14 +2,51 @@ import axios from 'axios'
import { useToast } from 'vue-toastification'
import router from '@/router'
// Configuration de l'URL de base selon l'environnement
const getBaseURL = () => {
// Récupérer l'environnement depuis les variables Vite
const environment = import.meta.env.VITE_ENVIRONMENT || 'local'
// Log de debug pour l'environnement
console.log(`🌍 Frontend - Environnement détecté: ${environment}`)
console.log(`🔗 API URL: ${import.meta.env.VITE_API_URL}`)
console.log(`🔧 VITE_ENVIRONMENT: ${import.meta.env.VITE_ENVIRONMENT}`)
console.log(`🔧 NODE_ENV: ${import.meta.env.NODE_ENV}`)
// Utiliser directement la variable d'environnement VITE_API_URL
// qui est déjà configurée correctement pour chaque environnement
const apiUrl = import.meta.env.VITE_API_URL
if (!apiUrl) {
console.warn('⚠️ VITE_API_URL non définie, utilisation de la valeur par défaut')
// Valeurs par défaut selon l'environnement
switch (environment) {
case 'production':
return 'https://api.lediscord.com'
case 'development':
return 'https://api-dev.lediscord.com' // API externe HTTPS en développement
case 'local':
default:
return 'http://localhost:8000'
}
}
console.log(`🎯 URL finale utilisée: ${apiUrl}`)
return apiUrl
}
// Configuration de l'instance axios
const instance = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000',
baseURL: getBaseURL(),
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
// Log de la configuration
console.log(`🚀 Axios configuré avec l'URL de base: ${getBaseURL()}`)
// Request interceptor
instance.interceptors.request.use(
config => {
@@ -17,19 +54,41 @@ instance.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Log des requêtes en développement
if (import.meta.env.DEV) {
console.log(`📤 Requête ${config.method?.toUpperCase()} vers: ${config.url}`)
}
return config
},
error => {
console.error('❌ Erreur dans l\'intercepteur de requête:', error)
return Promise.reject(error)
}
)
// Response interceptor
instance.interceptors.response.use(
response => response,
response => {
// Log des réponses en développement
if (import.meta.env.DEV) {
console.log(`📥 Réponse ${response.status} de: ${response.config.url}`)
}
return response
},
error => {
const toast = useToast()
// Log détaillé des erreurs
console.error('❌ Erreur API:', {
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method,
data: error.response?.data
})
if (error.response?.status === 401) {
// Ne pas rediriger si on est déjà sur une page d'auth
const currentRoute = router.currentRoute.value
@@ -42,6 +101,10 @@ instance.interceptors.response.use(
toast.error('Accès non autorisé')
} else if (error.response?.status === 500) {
toast.error('Erreur serveur, veuillez réessayer plus tard')
} else if (error.code === 'ECONNABORTED') {
toast.error('Délai d\'attente dépassé, veuillez réessayer')
} else if (!error.response) {
toast.error('Erreur de connexion, vérifiez votre connexion internet')
}
return Promise.reject(error)
@@ -56,7 +119,7 @@ export function getMediaUrl(path) {
if (typeof path !== 'string') return path
if (path.startsWith('http')) return path
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000'
const baseUrl = getBaseURL()
// Déjà un chemin uploads complet
if (path.startsWith('/uploads/')) {
@@ -71,3 +134,33 @@ export function getMediaUrl(path) {
// Fallback
return `${baseUrl}/uploads/${path}`
}
// Fonction utilitaire pour obtenir l'environnement actuel
export function getCurrentEnvironment() {
return import.meta.env.VITE_ENVIRONMENT || 'local'
}
// Fonction utilitaire pour vérifier si on est en production
export function isProduction() {
return getCurrentEnvironment() === 'production'
}
// Fonction utilitaire pour vérifier si on est en développement
export function isDevelopment() {
return getCurrentEnvironment() === 'development'
}
// Fonction utilitaire pour vérifier si on est en local
export function isLocal() {
return getCurrentEnvironment() === 'local'
}
// Fonction utilitaire pour obtenir l'URL de l'API
export function getApiUrl() {
return import.meta.env.VITE_API_URL || getBaseURL()
}
// Fonction utilitaire pour obtenir l'URL de l'application
export function getAppUrl() {
return import.meta.env.VITE_APP_URL || window.location.origin
}

16
frontend/src/views/Admin.vue Normal file → Executable file
View File

@@ -4,8 +4,7 @@
<!-- 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 dashboard...</p>
<LoadingLogo size="large" text="Chargement du dashboard..." />
</div>
<!-- Admin content -->
@@ -579,16 +578,14 @@
<!-- Loading State -->
<div v-if="ticketsLoading" class="text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p class="text-gray-600">Chargement des tickets...</p>
<LoadingLogo size="large" text="Chargement des tickets..." />
</div>
<!-- Tickets Grid -->
<div v-else-if="filteredTickets.length > 0" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<div v-if="!ticketsLoading && filteredTickets.length > 0" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<div
v-for="ticket in filteredTickets"
:key="ticket.id"
v-if="ticket && ticket.id"
class="bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-lg transition-all duration-200 cursor-pointer group ticket-card"
@click="showTicketDetails(ticket)"
>
@@ -674,7 +671,7 @@
</div>
<!-- No Tickets -->
<div v-else class="text-center py-12">
<div v-if="!ticketsLoading && filteredTickets.length === 0" class="text-center py-12">
<div class="w-24 h-24 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<svg class="w-12 h-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -1244,6 +1241,7 @@ import {
} from 'lucide-vue-next'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale'
import LoadingLogo from '@/components/LoadingLogo.vue'
const authStore = useAuthStore()
const toast = useToast()
@@ -1852,7 +1850,7 @@ function openImageModal(imageUrl) {
function getMediaUrl(path) {
if (!path) return ''
return path.startsWith('http') ? path : `http://localhost:8000${path}`
return path.startsWith('http') ? path : `${import.meta.env.VITE_API_URL || 'http://localhost:8002'}${path}`
}
// Nouvelles fonctions pour les filtres et actions rapides
@@ -2126,4 +2124,4 @@ watch(showTicketEditModal, (newValue) => {
grid-template-columns: 1fr;
}
}
</style>
</style>

12
frontend/src/views/AlbumDetail.vue Normal file → Executable file
View File

@@ -1,10 +1,9 @@
<template>
<div class="max-w-7xl 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 de l'album...</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement de l'album..." />
</div>
<!-- Album not found -->
<div v-else-if="!album" class="text-center py-12">
@@ -586,6 +585,7 @@ import {
ChevronLeft,
ChevronRight
} from 'lucide-vue-next'
import LoadingLogo from '@/components/LoadingLogo.vue'
const route = useRoute()
const router = useRouter()
@@ -859,4 +859,4 @@ onUnmounted(() => {
// Clean up event listeners
document.removeEventListener('keydown', handleKeyboardNavigation)
})
</script>
</script>

2
frontend/src/views/Albums.vue Normal file → Executable file
View File

@@ -783,4 +783,4 @@ onMounted(() => {
fetchUsers()
fetchUploadLimits()
})
</script>
</script>

20
frontend/src/views/EventDetail.vue Normal file → Executable file
View File

@@ -1,10 +1,9 @@
<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 de l'événement...</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement de l'événement..." />
</div>
<!-- Event not found -->
<div v-else-if="!event" class="text-center py-12">
@@ -46,7 +45,15 @@
<div class="flex items-start space-x-6">
<!-- Cover Image -->
<div class="w-64 h-48 bg-gradient-to-br from-primary-400 to-primary-600 rounded-xl flex items-center justify-center">
<Calendar v-if="!event.cover_image" class="w-16 h-16 text-white" />
<img
v-if="!event.cover_image && event.creator_avatar"
:src="getMediaUrl(event.creator_avatar)"
:alt="event.creator_name"
class="w-16 h-16 rounded-full object-cover"
>
<div v-else-if="!event.cover_image" 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>
<img
v-else
:src="getMediaUrl(event.cover_image)"
@@ -366,6 +373,7 @@ import {
Trash2,
Image
} from 'lucide-vue-next'
import LoadingLogo from '@/components/LoadingLogo.vue'
const route = useRoute()
const router = useRouter()

16
frontend/src/views/Events.vue Normal file → Executable file
View File

@@ -54,7 +54,15 @@
<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">
<Calendar class="w-16 h-16 text-gray-400" />
<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 -->
@@ -480,6 +488,11 @@ 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 {
@@ -488,6 +501,7 @@ async function fetchEvents() {
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

0
frontend/src/views/Home.vue Normal file → Executable file
View File

10
frontend/src/views/Information.vue Normal file → Executable file
View File

@@ -34,11 +34,10 @@
</button>
</div>
<!-- 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 mb-4"></div>
<p class="text-gray-600">Chargement des informations...</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement des informations..." />
</div>
<!-- No Information -->
<div v-else-if="filteredInformations.length === 0" class="text-center py-12">
@@ -107,6 +106,7 @@ import { useToast } from 'vue-toastification'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale'
import axios from '@/utils/axios'
import LoadingLogo from '@/components/LoadingLogo.vue'
const toast = useToast()

0
frontend/src/views/Login.vue Normal file → Executable file
View File

10
frontend/src/views/MyTickets.vue Normal file → Executable file
View File

@@ -54,11 +54,10 @@
</button>
</div>
<!-- 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 mb-4"></div>
<p class="text-gray-600">Chargement de vos tickets...</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement de vos tickets..." />
</div>
<!-- No Tickets -->
<div v-else-if="filteredTickets.length === 0" class="text-center py-12">
@@ -318,6 +317,7 @@ import { fr } from 'date-fns/locale'
import { Save } from 'lucide-vue-next'
import axios from '@/utils/axios'
import { getMediaUrl } from '@/utils/axios'
import LoadingLogo from '@/components/LoadingLogo.vue'
const toast = useToast()

0
frontend/src/views/Posts.vue Normal file → Executable file
View File

0
frontend/src/views/Profile.vue Normal file → Executable file
View File

0
frontend/src/views/Register.vue Normal file → Executable file
View File

10
frontend/src/views/Stats.vue Normal file → Executable file
View File

@@ -2,11 +2,10 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold text-gray-900 mb-8">Statistiques du groupe</h1>
<!-- 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 des statistiques...</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement des statistiques..." />
</div>
<!-- Stats content -->
<div v-else>
@@ -252,6 +251,7 @@ import {
AtSign,
Eye
} from 'lucide-vue-next'
import LoadingLogo from '@/components/LoadingLogo.vue'
const toast = useToast()

12
frontend/src/views/UserProfile.vue Normal file → Executable file
View File

@@ -1,10 +1,9 @@
<template>
<div class="max-w-4xl 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 profil...</p>
</div>
<!-- Loading state -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement du profil..." />
</div>
<!-- Profile not found -->
<div v-else-if="!profileUser" class="text-center py-12">
@@ -118,7 +117,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useToast } from 'vue-toastification'
@@ -127,6 +126,7 @@ import { getMediaUrl } from '@/utils/axios'
import { format, formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale'
import { User, ArrowLeft, ArrowRight, MessageSquare, Video, Image, Calendar, Activity } from 'lucide-vue-next'
import LoadingLogo from '@/components/LoadingLogo.vue'
const route = useRoute()
const router = useRouter()

12
frontend/src/views/VlogDetail.vue Normal file → Executable file
View File

@@ -1,10 +1,9 @@
<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>
<!-- Loading state -->
<div v-if="loading" class="text-center py-12">
<LoadingLogo size="large" text="Chargement du vlog..." />
</div>
<!-- Vlog not found -->
<div v-else-if="!vlog" class="text-center py-12">
@@ -49,7 +48,7 @@
<div class="flex items-center">
<img
v-if="vlog.author_avatar"
:src="vlog.author_avatar"
:src="getMediaUrl(vlog.author_avatar)"
:alt="vlog.author_name"
class="w-8 h-8 rounded-full object-cover mr-3"
>
@@ -238,6 +237,7 @@ import {
} from 'lucide-vue-next'
import VideoPlayer from '@/components/VideoPlayer.vue'
import VlogComments from '@/components/VlogComments.vue'
import LoadingLogo from '@/components/LoadingLogo.vue'
const route = useRoute()
const router = useRouter()

2
frontend/src/views/Vlogs.vue Normal file → Executable file
View File

@@ -58,7 +58,7 @@
<div class="flex items-center space-x-2 text-sm text-gray-600 mb-3">
<img
v-if="vlog.author_avatar"
:src="vlog.author_avatar"
:src="getMediaUrl(vlog.author_avatar)"
:alt="vlog.author_name"
class="w-5 h-5 rounded-full object-cover"
>