working version
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5173
|
||||
|
||||
# Run the application
|
||||
CMD ["npm", "run", "dev"]
|
||||
30
frontend/Dockerfile.dev
Executable file
30
frontend/Dockerfile.dev
Executable file
@@ -0,0 +1,30 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
# Métadonnées
|
||||
LABEL maintainer="LeDiscord Team"
|
||||
LABEL version="1.0"
|
||||
LABEL description="LeDiscord Frontend - Environnement Development"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Variables d'environnement pour le développement
|
||||
ENV NODE_ENV=development
|
||||
ENV VITE_ENVIRONMENT=development
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Copy development environment file
|
||||
COPY .env.development .env
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5173
|
||||
|
||||
# Run the application in development mode
|
||||
CMD ["npm", "run", "dev"]
|
||||
50
frontend/Dockerfile.prod
Executable file
50
frontend/Dockerfile.prod
Executable file
@@ -0,0 +1,50 @@
|
||||
# Multi-stage build pour la production
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# Métadonnées
|
||||
LABEL maintainer="LeDiscord Team"
|
||||
LABEL version="1.0"
|
||||
LABEL description="LeDiscord Frontend - Build Production"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Variables d'environnement pour la production
|
||||
ENV NODE_ENV=production
|
||||
ENV VITE_ENVIRONMENT=production
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Copy production environment file
|
||||
COPY .env.production .env
|
||||
|
||||
# Debug: Vérifier le contenu du fichier .env
|
||||
RUN echo "=== Contenu du fichier .env ===" && cat .env
|
||||
RUN echo "=== Variables d'environnement ===" && env | grep VITE
|
||||
|
||||
# Charger les variables d'environnement depuis le fichier .env (en filtrant les commentaires)
|
||||
RUN export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) && echo "=== Variables après export ===" && env | grep VITE
|
||||
|
||||
# Build the application avec les variables d'environnement chargées
|
||||
RUN export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) && npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx-frontend.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
0
frontend/index.html
Normal file → Executable file
0
frontend/index.html
Normal file → Executable file
64
frontend/nginx-frontend.conf
Executable file
64
frontend/nginx-frontend.conf
Executable file
@@ -0,0 +1,64 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Static files with long cache
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Vue Router - SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Health check
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
}
|
||||
3396
frontend/package-lock.json
generated
Executable file
3396
frontend/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
13
frontend/package.json
Normal file → Executable file
13
frontend/package.json
Normal file → Executable file
@@ -8,21 +8,22 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.8",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.2",
|
||||
"@vueuse/core": "^10.6.1",
|
||||
"axios": "^1.6.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"lucide-vue-next": "^0.294.0",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"video.js": "^8.6.1"
|
||||
"pinia": "^2.1.7",
|
||||
"video.js": "^8.6.1",
|
||||
"vue": "^3.3.8",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-toastification": "^2.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"terser": "^5.43.1",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
0
frontend/postcss.config.js
Normal file → Executable file
0
frontend/postcss.config.js
Normal file → Executable file
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/logo_lediscord.png
Normal file
BIN
frontend/public/logo_lediscord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
4
frontend/src/App.vue
Normal file → Executable file
4
frontend/src/App.vue
Normal file → Executable 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>
|
||||
62
frontend/src/components/EnvironmentDebug.vue
Executable file
62
frontend/src/components/EnvironmentDebug.vue
Executable 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>
|
||||
50
frontend/src/components/LoadingLogo.vue
Normal file
50
frontend/src/components/LoadingLogo.vue
Normal 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
0
frontend/src/components/MentionInput.vue
Normal file → Executable file
2
frontend/src/components/Mentions.vue
Normal file → Executable file
2
frontend/src/components/Mentions.vue
Normal file → Executable 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
10
frontend/src/components/TicketFloatingButton.vue
Normal file → Executable 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
0
frontend/src/components/UserAvatar.vue
Normal file → Executable file
0
frontend/src/components/VideoPlayer.vue
Normal file → Executable file
0
frontend/src/components/VideoPlayer.vue
Normal file → Executable file
26
frontend/src/components/VlogComments.vue
Normal file → Executable file
26
frontend/src/components/VlogComments.vue
Normal file → Executable 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
12
frontend/src/layouts/AuthLayout.vue
Normal file → Executable 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
7
frontend/src/layouts/DefaultLayout.vue
Normal file → Executable 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
21
frontend/src/main.js
Normal file → Executable 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
17
frontend/src/router/index.js
Normal file → Executable 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
0
frontend/src/stores/auth.js
Normal file → Executable file
0
frontend/src/style.css
Normal file → Executable file
0
frontend/src/style.css
Normal file → Executable file
99
frontend/src/utils/axios.js
Normal file → Executable file
99
frontend/src/utils/axios.js
Normal file → Executable 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
16
frontend/src/views/Admin.vue
Normal file → Executable 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
12
frontend/src/views/AlbumDetail.vue
Normal file → Executable 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
2
frontend/src/views/Albums.vue
Normal file → Executable file
@@ -783,4 +783,4 @@ onMounted(() => {
|
||||
fetchUsers()
|
||||
fetchUploadLimits()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
20
frontend/src/views/EventDetail.vue
Normal file → Executable file
20
frontend/src/views/EventDetail.vue
Normal file → Executable 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
16
frontend/src/views/Events.vue
Normal file → Executable 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
0
frontend/src/views/Home.vue
Normal file → Executable file
10
frontend/src/views/Information.vue
Normal file → Executable file
10
frontend/src/views/Information.vue
Normal file → Executable 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
0
frontend/src/views/Login.vue
Normal file → Executable file
10
frontend/src/views/MyTickets.vue
Normal file → Executable file
10
frontend/src/views/MyTickets.vue
Normal file → Executable 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
0
frontend/src/views/Posts.vue
Normal file → Executable file
0
frontend/src/views/Profile.vue
Normal file → Executable file
0
frontend/src/views/Profile.vue
Normal file → Executable file
0
frontend/src/views/Register.vue
Normal file → Executable file
0
frontend/src/views/Register.vue
Normal file → Executable file
10
frontend/src/views/Stats.vue
Normal file → Executable file
10
frontend/src/views/Stats.vue
Normal file → Executable 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
12
frontend/src/views/UserProfile.vue
Normal file → Executable 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
12
frontend/src/views/VlogDetail.vue
Normal file → Executable 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
2
frontend/src/views/Vlogs.vue
Normal file → Executable 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"
|
||||
>
|
||||
|
||||
0
frontend/tailwind.config.js
Normal file → Executable file
0
frontend/tailwind.config.js
Normal file → Executable file
222
frontend/test-env.js
Executable file
222
frontend/test-env.js
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script de test des environnements Frontend LeDiscord
|
||||
* Vérifie la cohérence des configurations et évite les problèmes de mixed content
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Couleurs pour la console
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function testEnvironment(envName) {
|
||||
log(`\n🔍 Test de l'environnement: ${envName.toUpperCase()}`, 'cyan')
|
||||
|
||||
const envFile = path.join(__dirname, `env.${envName}`)
|
||||
|
||||
if (!fs.existsSync(envFile)) {
|
||||
log(`❌ Fichier ${envFile} non trouvé`, 'red')
|
||||
return false
|
||||
}
|
||||
|
||||
log(`✅ Fichier ${envFile} trouvé`, 'green')
|
||||
|
||||
// Lire le fichier d'environnement
|
||||
const envContent = fs.readFileSync(envFile, 'utf8')
|
||||
const envVars = {}
|
||||
|
||||
// Parser les variables d'environnement
|
||||
envContent.split('\n').forEach(line => {
|
||||
line = line.trim()
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
if (key && valueParts.length > 0) {
|
||||
envVars[key] = valueParts.join('=')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Vérifier les variables requises
|
||||
const requiredVars = ['VITE_API_URL', 'VITE_APP_URL', 'VITE_UPLOAD_URL', 'VITE_ENVIRONMENT']
|
||||
let allValid = true
|
||||
|
||||
requiredVars.forEach(varName => {
|
||||
if (!envVars[varName]) {
|
||||
log(`❌ Variable manquante: ${varName}`, 'red')
|
||||
allValid = false
|
||||
} else {
|
||||
log(`✅ ${varName}: ${envVars[varName]}`, 'green')
|
||||
}
|
||||
})
|
||||
|
||||
// Vérifier la cohérence des protocoles
|
||||
if (envVars.VITE_API_URL && envVars.VITE_APP_URL) {
|
||||
const apiProtocol = envVars.VITE_API_URL.split('://')[0]
|
||||
const appProtocol = envVars.VITE_APP_URL.split('://')[0]
|
||||
|
||||
if (apiProtocol !== appProtocol) {
|
||||
log(`⚠️ Protocoles différents: API=${apiProtocol}, APP=${appProtocol}`, 'yellow')
|
||||
log(` Cela peut causer des problèmes de mixed content!`, 'yellow')
|
||||
allValid = false
|
||||
} else {
|
||||
log(`✅ Protocoles cohérents: ${apiProtocol}`, 'green')
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier la cohérence des domaines
|
||||
if (envVars.VITE_API_URL && envVars.VITE_APP_URL) {
|
||||
const apiDomain = envVars.VITE_API_URL.split('://')[1]?.split('/')[0]
|
||||
const appDomain = envVars.VITE_APP_URL.split('://')[1]?.split('/')[0]
|
||||
|
||||
if (apiDomain && appDomain) {
|
||||
if (envName === 'local') {
|
||||
// En local, les domaines peuvent être différents (localhost:8000 vs localhost:5173)
|
||||
log(`✅ Domaines locaux: API=${apiDomain}, APP=${appDomain}`, 'green')
|
||||
} else if (apiDomain.includes('dev.') && appDomain.includes('dev.')) {
|
||||
log(`✅ Domaines de développement cohérents: ${apiDomain}`, 'green')
|
||||
} else if (!apiDomain.includes('dev.') && !appDomain.includes('dev.')) {
|
||||
log(`✅ Domaines de production cohérents: ${apiDomain}`, 'green')
|
||||
} else {
|
||||
log(`⚠️ Domaines incohérents: API=${apiDomain}, APP=${appDomain}`, 'yellow')
|
||||
allValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier l'environnement spécifié
|
||||
if (envVars.VITE_ENVIRONMENT && envVars.VITE_ENVIRONMENT !== envName) {
|
||||
log(`⚠️ Environnement spécifié (${envVars.VITE_ENVIRONMENT}) ne correspond pas au nom du fichier (${envName})`, 'yellow')
|
||||
allValid = false
|
||||
}
|
||||
|
||||
return allValid
|
||||
}
|
||||
|
||||
function testDockerfiles() {
|
||||
log(`\n🐳 Test des Dockerfiles`, 'cyan')
|
||||
|
||||
const dockerfiles = ['Dockerfile.local', 'Dockerfile.dev', 'Dockerfile.prod']
|
||||
let allValid = true
|
||||
|
||||
dockerfiles.forEach(dockerfile => {
|
||||
const dockerfilePath = path.join(__dirname, dockerfile)
|
||||
if (fs.existsSync(dockerfilePath)) {
|
||||
log(`✅ ${dockerfile} trouvé`, 'green')
|
||||
|
||||
// Vérifier que le Dockerfile copie le bon fichier d'environnement
|
||||
const content = fs.readFileSync(dockerfilePath, 'utf8')
|
||||
const envName = dockerfile.replace('Dockerfile.', '')
|
||||
|
||||
if (content.includes(`env.${envName}`)) {
|
||||
log(`✅ ${dockerfile} copie le bon fichier env.${envName}`, 'green')
|
||||
} else {
|
||||
log(`❌ ${dockerfile} ne copie pas env.${envName}`, 'red')
|
||||
allValid = false
|
||||
}
|
||||
} else {
|
||||
log(`❌ ${dockerfile} manquant`, 'red')
|
||||
allValid = false
|
||||
}
|
||||
})
|
||||
|
||||
return allValid
|
||||
}
|
||||
|
||||
function testViteConfig() {
|
||||
log(`\n⚙️ Test de la configuration Vite`, 'cyan')
|
||||
|
||||
const viteConfigPath = path.join(__dirname, 'vite.config.js')
|
||||
|
||||
if (!fs.existsSync(viteConfigPath)) {
|
||||
log(`❌ vite.config.js non trouvé`, 'red')
|
||||
return false
|
||||
}
|
||||
|
||||
log(`✅ vite.config.js trouvé`, 'green')
|
||||
|
||||
// Vérifier que la configuration gère les environnements
|
||||
const content = fs.readFileSync(viteConfigPath, 'utf8')
|
||||
|
||||
if (content.includes('getEnvironmentConfig')) {
|
||||
log(`✅ Configuration par environnement détectée`, 'green')
|
||||
} else {
|
||||
log(`⚠️ Configuration par environnement non détectée`, 'yellow')
|
||||
}
|
||||
|
||||
if (content.includes('proxy')) {
|
||||
log(`✅ Configuration proxy détectée`, 'green')
|
||||
} else {
|
||||
log(`⚠️ Configuration proxy non détectée`, 'yellow')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
log(`🚀 Test des environnements Frontend LeDiscord`, 'bright')
|
||||
log(`📁 Répertoire: ${__dirname}`, 'blue')
|
||||
|
||||
let allTestsPassed = true
|
||||
|
||||
if (args.length > 0) {
|
||||
// Test d'un environnement spécifique
|
||||
const envName = args[0]
|
||||
if (['local', 'development', 'production'].includes(envName)) {
|
||||
allTestsPassed = testEnvironment(envName) && allTestsPassed
|
||||
} else {
|
||||
log(`❌ Environnement invalide: ${envName}`, 'red')
|
||||
log(` Environnements valides: local, development, production`, 'yellow')
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
// Test de tous les environnements
|
||||
log(`\n🌍 Test de tous les environnements`, 'cyan')
|
||||
|
||||
const environments = ['local', 'development', 'production']
|
||||
environments.forEach(env => {
|
||||
allTestsPassed = testEnvironment(env) && allTestsPassed
|
||||
})
|
||||
}
|
||||
|
||||
// Tests généraux
|
||||
allTestsPassed = testDockerfiles() && allTestsPassed
|
||||
allTestsPassed = testViteConfig() && allTestsPassed
|
||||
|
||||
// Résumé
|
||||
log(`\n📊 Résumé des tests`, 'cyan')
|
||||
if (allTestsPassed) {
|
||||
log(`✅ Tous les tests sont passés avec succès!`, 'green')
|
||||
log(`🎉 Votre configuration frontend est prête pour la production!`, 'green')
|
||||
} else {
|
||||
log(`❌ Certains tests ont échoué`, 'red')
|
||||
log(`🔧 Veuillez corriger les problèmes avant de continuer`, 'yellow')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log(`\n💡 Conseils pour éviter les problèmes de mixed content:`, 'cyan')
|
||||
log(` - Assurez-vous que toutes les URLs d'un même environnement utilisent le même protocole`, 'blue')
|
||||
log(` - En local: utilisez HTTP (http://localhost:*)`, 'blue')
|
||||
log(` - En développement/production: utilisez HTTPS (https://*.lediscord.com)`, 'blue')
|
||||
log(` - Vérifiez que VITE_API_URL et VITE_APP_URL sont cohérents`, 'blue')
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
139
frontend/vite.config.js
Normal file → Executable file
139
frontend/vite.config.js
Normal file → Executable file
@@ -2,25 +2,130 @@ const { defineConfig } = require('vite')
|
||||
const vue = require('@vitejs/plugin-vue')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://backend:8000',
|
||||
changeOrigin: true
|
||||
// Configuration par environnement
|
||||
const getEnvironmentConfig = (mode) => {
|
||||
const configs = {
|
||||
local: {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
allowedHosts: ['localhost', '127.0.0.1'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'http://backend:8000',
|
||||
changeOrigin: true
|
||||
define: {
|
||||
__ENVIRONMENT__: '"local"'
|
||||
}
|
||||
},
|
||||
development: {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
allowedHosts: ['dev.lediscord.com', 'localhost'],
|
||||
// Pas de proxy en développement car l'API est externe (https://api-dev.lediscord.com)
|
||||
// Le proxy n'est nécessaire que pour l'environnement local
|
||||
},
|
||||
define: {
|
||||
__ENVIRONMENT__: '"development"'
|
||||
}
|
||||
},
|
||||
production: {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
allowedHosts: ['lediscord.com', 'www.lediscord.com'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://api.lediscord.com',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'https://api.lediscord.com',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
__ENVIRONMENT__: '"production"'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs[mode] || configs.local
|
||||
}
|
||||
|
||||
module.exports = defineConfig(({ command, mode }) => {
|
||||
// Détecter l'environnement
|
||||
const env = process.env.NODE_ENV || mode || 'local'
|
||||
const envConfig = getEnvironmentConfig(env)
|
||||
|
||||
console.log(`🚀 Configuration Vite pour l'environnement: ${env.toUpperCase()}`)
|
||||
console.log(`🔧 Variables d'environnement:`, {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
VITE_ENVIRONMENT: process.env.VITE_ENVIRONMENT,
|
||||
VITE_API_URL: process.env.VITE_API_URL,
|
||||
VITE_APP_URL: process.env.VITE_APP_URL
|
||||
})
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
|
||||
// Configuration du serveur selon l'environnement
|
||||
server: envConfig.server,
|
||||
|
||||
// Configuration pour la production
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['vue', 'vue-router', 'pinia'],
|
||||
utils: ['axios', 'date-fns']
|
||||
}
|
||||
}
|
||||
},
|
||||
// Optimisations de production
|
||||
minify: env === 'production' ? 'terser' : false,
|
||||
sourcemap: env !== 'production',
|
||||
// Variables d'environnement
|
||||
define: envConfig.define
|
||||
},
|
||||
|
||||
// Configuration des variables d'environnement
|
||||
define: {
|
||||
...envConfig.define,
|
||||
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
|
||||
// Forcer les variables d'environnement
|
||||
'import.meta.env.VITE_ENVIRONMENT': JSON.stringify(process.env.VITE_ENVIRONMENT || env),
|
||||
'import.meta.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL),
|
||||
'import.meta.env.VITE_APP_URL': JSON.stringify(process.env.VITE_APP_URL)
|
||||
},
|
||||
|
||||
// Optimisations selon l'environnement
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'vue-router', 'pinia', 'axios']
|
||||
},
|
||||
|
||||
// Configuration des assets
|
||||
assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg', '**/*.mp4', '**/*.webm'],
|
||||
|
||||
// Configuration du mode
|
||||
mode: env === 'production' ? 'production' : 'development'
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user