fix
This commit is contained in:
@@ -28,56 +28,58 @@
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="px-6 py-6">
|
||||
<div v-if="isIOS" class="space-y-6">
|
||||
<div class="px-4 sm:px-6 py-4 sm:py-6">
|
||||
<!-- iOS Instructions -->
|
||||
<div v-if="instructionType === 'ios'" class="space-y-4 sm:space-y-6">
|
||||
<!-- Step 1 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Appuyez sur le bouton de partage</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
En bas de l'écran, dans la barre d'outils Safari
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Appuyez sur le bouton de partage</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
En bas de l'écran Safari, cherchez l'icône carrée avec une flèche vers le haut ⬆️
|
||||
</p>
|
||||
<div class="bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-200">
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<div class="w-12 h-12 bg-gray-200 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
</div>
|
||||
<ArrowDown class="w-5 h-5 text-gray-400" />
|
||||
<p class="text-xs text-gray-500">Bouton de partage</p>
|
||||
<p class="text-xs sm:text-sm text-blue-700">
|
||||
<strong>Astuce :</strong> Si vous ne voyez pas la barre, faites défiler vers le haut pour la faire apparaître
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Sélectionnez "Sur l'écran d'accueil"</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Faites défiler le menu de partage vers le haut
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Cherchez "Sur l'écran d'accueil"</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
Faites défiler le menu vers le bas jusqu'à trouver cette option
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-primary-50 to-purple-50 rounded-lg p-4 border-2 border-primary-200">
|
||||
<div class="bg-gradient-to-br from-primary-50 to-purple-50 rounded-lg p-3 sm:p-4 border-2 border-primary-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-primary-700 text-sm">Sur l'écran d'accueil</p>
|
||||
<p class="text-xs text-primary-600">Ajouter à l'écran d'accueil</p>
|
||||
<p class="font-semibold text-primary-700 text-xs sm:text-sm">Sur l'écran d'accueil</p>
|
||||
<p class="text-xs text-primary-600">L'icône ressemble à un + dans un carré</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,97 +87,138 @@
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Confirmez l'installation</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Appuyez sur "Ajouter" dans la popup de confirmation
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Appuyez sur "Ajouter"</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
En haut à droite de l'écran
|
||||
</p>
|
||||
<div class="bg-green-50 rounded-lg p-4 border-2 border-green-200">
|
||||
<div class="bg-green-50 rounded-lg p-3 sm:p-4 border-2 border-green-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<CheckCircle class="w-6 h-6 text-green-600" />
|
||||
<p class="text-sm text-green-700 font-medium">L'application apparaîtra sur votre écran d'accueil !</p>
|
||||
<CheckCircle class="w-5 h-5 sm:w-6 sm:h-6 text-green-600 flex-shrink-0" />
|
||||
<p class="text-xs sm:text-sm text-green-700 font-medium">L'application apparaîtra sur votre écran d'accueil !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note importante iOS -->
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 sm:p-4 mt-4">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-lg">⚠️</span>
|
||||
<div class="text-xs sm:text-sm text-amber-800">
|
||||
<p class="font-medium mb-1">Important pour iOS :</p>
|
||||
<p>Les notifications push ne fonctionnent que si l'app est installée sur l'écran d'accueil (iOS 16.4+)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Android -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Android Instructions -->
|
||||
<div v-else-if="instructionType === 'android'" class="space-y-4 sm:space-y-6">
|
||||
<!-- Step 1 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Appuyez sur le menu</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
En haut à droite de votre navigateur (⋮)
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Appuyez sur le menu ⋮</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
Les 3 points verticaux en haut à droite de Chrome
|
||||
</p>
|
||||
<div class="bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-200">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-12 h-12 bg-gray-200 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">"Installer l'application"</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
Ou "Ajouter à l'écran d'accueil"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Confirmez avec "Installer"</h4>
|
||||
<div class="bg-green-50 rounded-lg p-3 sm:p-4 border-2 border-green-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<CheckCircle class="w-5 h-5 sm:w-6 sm:h-6 text-green-600 flex-shrink-0" />
|
||||
<p class="text-xs sm:text-sm text-green-700 font-medium">L'app s'installera sur votre téléphone !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Instructions (Windows/Mac) -->
|
||||
<div v-else class="space-y-4 sm:space-y-6">
|
||||
<!-- Step 1 -->
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Cherchez l'icône d'installation</h4>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3">
|
||||
Dans la barre d'adresse de Chrome/Edge, cherchez l'icône 📥 ou ➕
|
||||
</p>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-xs sm:text-sm text-blue-700">
|
||||
<strong>Chrome :</strong> Icône avec un écran et une flèche à droite de la barre d'adresse
|
||||
</p>
|
||||
<p class="text-xs sm:text-sm text-blue-700 mt-1">
|
||||
<strong>Edge :</strong> "Installer LeDiscord" dans le menu ⋯
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex items-start space-x-3 sm:space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm sm:text-lg shadow-lg">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Sélectionnez "Ajouter à l'écran d'accueil"</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Ou "Installer l'application" selon votre navigateur
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-primary-50 to-purple-50 rounded-lg p-4 border-2 border-primary-200">
|
||||
<div class="flex-1 pt-0.5 sm:pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-1 sm:mb-2 text-sm sm:text-base">Cliquez sur "Installer"</h4>
|
||||
<div class="bg-green-50 rounded-lg p-3 sm:p-4 border-2 border-green-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-primary-700 text-sm">Ajouter à l'écran d'accueil</p>
|
||||
</div>
|
||||
<CheckCircle class="w-5 h-5 sm:w-6 sm:h-6 text-green-600 flex-shrink-0" />
|
||||
<p class="text-xs sm:text-sm text-green-700 font-medium">L'app s'ouvrira dans sa propre fenêtre !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 pt-1">
|
||||
<h4 class="font-semibold text-gray-900 mb-2">Confirmez l'installation</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Appuyez sur "Ajouter" ou "Installer" dans la popup
|
||||
</p>
|
||||
<div class="bg-green-50 rounded-lg p-4 border-2 border-green-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<CheckCircle class="w-6 h-6 text-green-600" />
|
||||
<p class="text-sm text-green-700 font-medium">L'application sera installée !</p>
|
||||
</div>
|
||||
|
||||
<!-- Note si pas dispo -->
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 sm:p-4 mt-4">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-lg">💡</span>
|
||||
<div class="text-xs sm:text-sm text-amber-800">
|
||||
<p>Si vous ne voyez pas l'option d'installation, rafraîchissez la page ou essayez avec Chrome/Edge.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,11 +274,31 @@ const props = defineProps({
|
||||
isIOS: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isAndroid: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isWindows: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMac: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// Déterminer quel type d'instructions afficher
|
||||
const instructionType = computed(() => {
|
||||
if (props.isIOS) return 'ios'
|
||||
if (props.isAndroid) return 'android'
|
||||
if (props.isWindows || props.isMac) return 'desktop'
|
||||
return 'android' // Par défaut
|
||||
})
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -123,23 +123,28 @@ instance.interceptors.response.use(
|
||||
}
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
// Ne pas déconnecter automatiquement sur les requêtes POST/PUT avec FormData
|
||||
// car les erreurs peuvent être dues à des problèmes réseau temporaires sur mobile
|
||||
const isFormDataUpload = error.config?.data instanceof FormData ||
|
||||
(error.config?.method === 'POST' || error.config?.method === 'PUT')
|
||||
const currentRoute = router.currentRoute.value
|
||||
const errorDetail = error.response?.data?.detail || ''
|
||||
const errorDetailLower = errorDetail.toLowerCase()
|
||||
|
||||
// Vérifier si c'est une vraie erreur d'authentification
|
||||
const isRealAuthError = errorDetailLower.includes('credential') ||
|
||||
errorDetailLower.includes('token') ||
|
||||
errorDetailLower.includes('not authenticated') ||
|
||||
errorDetailLower.includes('could not validate') ||
|
||||
errorDetailLower.includes('expired')
|
||||
|
||||
console.warn(`🔒 401 reçu - Auth error: ${isRealAuthError}, Detail: ${errorDetail}`)
|
||||
|
||||
// Ne pas rediriger si on est déjà sur une page d'auth
|
||||
const currentRoute = router.currentRoute.value
|
||||
if (!currentRoute.path.includes('/login') && !currentRoute.path.includes('/register')) {
|
||||
// Pour les uploads, ne déconnecter que si c'est vraiment une erreur d'authentification
|
||||
// (pas juste une erreur réseau qui se manifeste comme 401)
|
||||
if (!isFormDataUpload || error.response?.data?.detail?.includes('credentials') || error.response?.data?.detail?.includes('token')) {
|
||||
if (isRealAuthError) {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
toast.error('Session expirée, veuillez vous reconnecter')
|
||||
} else {
|
||||
// Pour les uploads, juste afficher une erreur sans déconnecter
|
||||
toast.error('Erreur lors de l\'upload. Veuillez réessayer.')
|
||||
// Erreur 401 non liée à l'auth (rare mais possible)
|
||||
toast.error('Erreur d\'autorisation')
|
||||
}
|
||||
}
|
||||
} else if (error.response?.status === 403) {
|
||||
@@ -210,27 +215,145 @@ export function getAppUrl() {
|
||||
return import.meta.env.VITE_APP_URL || window.location.origin
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour upload de FormData via fetch natif
|
||||
// (contourne les problèmes d'axios avec FormData sur certains navigateurs/mobiles)
|
||||
export async function uploadFormData(endpoint, formData) {
|
||||
// Fonction utilitaire pour les requêtes POST JSON via fetch natif
|
||||
// (contourne les problèmes d'axios sur certains navigateurs mobiles/PWA)
|
||||
export async function postJson(endpoint, data = {}) {
|
||||
const token = localStorage.getItem('token')
|
||||
const apiUrl = getApiUrl()
|
||||
|
||||
const response = await fetch(`${apiUrl}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
// Ne PAS mettre Content-Type, fetch le gère automatiquement avec FormData
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
console.log(`📤 POST JSON vers: ${apiUrl}${endpoint}`)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
console.log(`📥 POST response status: ${response.status}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const error = new Error(errorData.detail || 'Erreur lors de l\'upload')
|
||||
error.response = { status: response.status, data: errorData }
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Erreur inconnue' }))
|
||||
console.error('❌ POST error:', errorData)
|
||||
|
||||
const toast = useToast()
|
||||
if (response.status === 401) {
|
||||
const errorDetail = (errorData.detail || '').toLowerCase()
|
||||
const isRealAuthError = errorDetail.includes('credential') ||
|
||||
errorDetail.includes('token') ||
|
||||
errorDetail.includes('not authenticated') ||
|
||||
errorDetail.includes('could not validate') ||
|
||||
errorDetail.includes('expired')
|
||||
if (isRealAuthError) {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
toast.error('Session expirée, veuillez vous reconnecter')
|
||||
} else {
|
||||
toast.error('Erreur d\'autorisation')
|
||||
}
|
||||
} else if (response.status === 403) {
|
||||
toast.error('Accès non autorisé')
|
||||
} else if (response.status === 422) {
|
||||
toast.error('Données invalides')
|
||||
} else {
|
||||
toast.error(errorData.detail || 'Erreur serveur')
|
||||
}
|
||||
|
||||
const error = new Error(errorData.detail || 'Erreur')
|
||||
error.response = { status: response.status, data: errorData }
|
||||
throw error
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
console.error('❌ Network error:', error)
|
||||
const toast = useToast()
|
||||
toast.error('Erreur de connexion')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour upload de FormData via fetch natif
|
||||
// (contourne les problèmes d'axios avec FormData sur certains navigateurs/mobiles)
|
||||
export async function uploadFormData(endpoint, formData, options = {}) {
|
||||
const token = localStorage.getItem('token')
|
||||
const apiUrl = getApiUrl()
|
||||
const toast = useToast()
|
||||
|
||||
// Timeout par défaut de 5 minutes pour les uploads
|
||||
const timeout = options.timeout || 300000
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
console.log(`📤 Upload FormData vers: ${apiUrl}${endpoint}`)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
// Ne PAS mettre Content-Type, fetch le gère automatiquement avec FormData
|
||||
},
|
||||
body: formData,
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
console.log(`📥 Upload response status: ${response.status}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Erreur inconnue' }))
|
||||
console.error('❌ Upload error:', errorData)
|
||||
|
||||
// Gestion des erreurs d'authentification
|
||||
if (response.status === 401) {
|
||||
// Vérifier si c'est vraiment une erreur d'auth ou juste un problème réseau
|
||||
const isAuthError = errorData.detail?.toLowerCase().includes('credential') ||
|
||||
errorData.detail?.toLowerCase().includes('token') ||
|
||||
errorData.detail?.toLowerCase().includes('not authenticated')
|
||||
if (isAuthError) {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
toast.error('Session expirée, veuillez vous reconnecter')
|
||||
} else {
|
||||
toast.error('Erreur d\'authentification lors de l\'upload')
|
||||
}
|
||||
} else if (response.status === 413) {
|
||||
toast.error('Fichier trop volumineux')
|
||||
} else if (response.status === 422) {
|
||||
toast.error('Données invalides: ' + (errorData.detail || 'Vérifiez le formulaire'))
|
||||
} else {
|
||||
toast.error(errorData.detail || 'Erreur lors de l\'upload')
|
||||
}
|
||||
|
||||
const error = new Error(errorData.detail || 'Erreur lors de l\'upload')
|
||||
error.response = { status: response.status, data: errorData }
|
||||
throw error
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('✅ Upload réussi')
|
||||
return data
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
console.error('❌ Upload timeout')
|
||||
toast.error('Délai d\'attente dépassé. Le fichier est peut-être trop volumineux.')
|
||||
throw new Error('Timeout lors de l\'upload')
|
||||
}
|
||||
|
||||
// Erreur réseau
|
||||
if (!error.response) {
|
||||
console.error('❌ Network error during upload:', error)
|
||||
toast.error('Erreur de connexion lors de l\'upload')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
@@ -950,8 +950,9 @@
|
||||
<img
|
||||
:src="getMediaUrl(selectedTicket.screenshot_path)"
|
||||
:alt="'Screenshot du ticket ' + selectedTicket.title"
|
||||
class="max-w-full h-auto rounded-lg shadow-sm ticket-screenshot cursor-pointer"
|
||||
class="w-full sm:max-w-2xl h-auto rounded-lg shadow-sm ticket-screenshot cursor-pointer"
|
||||
@click="openImageModal(selectedTicket.screenshot_path)"
|
||||
@error="(e) => console.error('Image load error:', e.target.src)"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">Cliquez sur l'image pour l'agrandir</p>
|
||||
</div>
|
||||
@@ -1009,7 +1010,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="showImageModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-90 z-60 flex items-center justify-center p-4"
|
||||
class="fixed inset-0 bg-black bg-opacity-90 z-[70] flex items-center justify-center p-4"
|
||||
@click="showImageModal = false"
|
||||
>
|
||||
<div class="relative max-w-4xl max-h-[90vh]">
|
||||
@@ -1849,7 +1850,11 @@ function openImageModal(imageUrl) {
|
||||
|
||||
function getMediaUrl(path) {
|
||||
if (!path) return ''
|
||||
return path.startsWith('http') ? path : `${import.meta.env.VITE_API_URL || 'http://localhost:8002'}${path}`
|
||||
if (path.startsWith('http')) return path
|
||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8002'
|
||||
// Ensure path starts with /
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
return `${baseUrl}${normalizedPath}`
|
||||
}
|
||||
|
||||
// Nouvelles fonctions pour les filtres et actions rapides
|
||||
|
||||
@@ -475,6 +475,17 @@
|
||||
<X class="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
<!-- Download Button -->
|
||||
<a
|
||||
:href="getMediaUrl(selectedMedia.file_path)"
|
||||
:download="selectedMedia.caption || 'media'"
|
||||
target="_blank"
|
||||
class="absolute top-6 right-20 z-10 bg-black bg-opacity-70 text-white rounded-full w-12 h-12 flex items-center justify-center hover:bg-opacity-90 transition-colors shadow-lg"
|
||||
title="Télécharger"
|
||||
>
|
||||
<Download class="w-6 h-6" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
@@ -582,7 +593,8 @@ import {
|
||||
Eye,
|
||||
Heart,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Download
|
||||
} from 'lucide-vue-next'
|
||||
import LoadingLogo from '@/components/LoadingLogo.vue'
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from '@/utils/axios'
|
||||
import axios, { postJson } from '@/utils/axios'
|
||||
import { getMediaUrl, uploadFormData } from '@/utils/axios'
|
||||
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
@@ -630,8 +630,7 @@ async function createAlbum() {
|
||||
}
|
||||
|
||||
uploadStatus.value = 'Création de l\'album...'
|
||||
const albumResponse = await axios.post('/api/albums', albumData)
|
||||
const album = albumResponse.data
|
||||
const album = await postJson('/api/albums', albumData)
|
||||
|
||||
// Upload media files in batches for better performance
|
||||
const batchSize = 5 // Upload 5 files at a time
|
||||
|
||||
@@ -221,23 +221,25 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label">Date et heure</label>
|
||||
<label class="label text-sm">Date et heure</label>
|
||||
<input
|
||||
v-model="newEvent.date"
|
||||
type="datetime-local"
|
||||
required
|
||||
class="input"
|
||||
class="input text-sm w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label">Date de fin (optionnel)</label>
|
||||
<label class="label text-sm">Date de fin (optionnel)</label>
|
||||
<input
|
||||
v-model="newEvent.end_date"
|
||||
type="datetime-local"
|
||||
class="input"
|
||||
class="input text-sm w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,7 +333,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from '@/utils/axios'
|
||||
import axios, { postJson } from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone, convertFrenchTimezoneToUTC } from '@/utils/dateUtils'
|
||||
import {
|
||||
@@ -496,11 +498,11 @@ async function createEvent() {
|
||||
}
|
||||
|
||||
console.log('🔵 eventData préparé:', eventData)
|
||||
console.log('🔵 Envoi de la requête axios.post...')
|
||||
console.log('🔵 Envoi de la requête postJson...')
|
||||
|
||||
const response = await axios.post('/api/events', eventData)
|
||||
const data = await postJson('/api/events', eventData)
|
||||
|
||||
console.log('✅ Réponse reçue:', response)
|
||||
console.log('✅ Réponse reçue:', data)
|
||||
|
||||
// Refresh events list
|
||||
await fetchEvents()
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<img
|
||||
:src="getMediaUrl(ticket.screenshot_path)"
|
||||
:alt="ticket.title"
|
||||
class="max-w-md rounded-lg border border-gray-200 cursor-pointer hover:opacity-90 transition-opacity"
|
||||
class="w-full sm:max-w-md rounded-lg border border-gray-200 cursor-pointer hover:opacity-90 transition-opacity"
|
||||
@click="viewScreenshot(ticket.screenshot_path)"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -288,7 +288,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import axios, { postJson } from '@/utils/axios'
|
||||
import { getMediaUrl, uploadFormData } from '@/utils/axios'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
@@ -355,14 +355,14 @@ async function createPost() {
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/posts', {
|
||||
const data = await postJson('/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)
|
||||
posts.value.unshift(data)
|
||||
|
||||
// Forcer le rafraîchissement de la date immédiatement
|
||||
dateRefreshKey.value++
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="username" class="label text-sm sm:text-base">Nom d'utilisateur</label>
|
||||
<label for="username" class="label text-sm sm:text-base">
|
||||
Identifiant unique
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">(non modifiable)</span>
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
@@ -76,22 +79,26 @@
|
||||
required
|
||||
minlength="3"
|
||||
class="input text-sm sm:text-base"
|
||||
placeholder="nom_utilisateur"
|
||||
placeholder="mon_pseudo"
|
||||
@blur="touchedFields.username = true"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="full_name" class="label text-sm sm:text-base">Nom complet</label>
|
||||
<label for="full_name" class="label text-sm sm:text-base">
|
||||
Pseudo affiché
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">(modifiable plus tard)</span>
|
||||
</label>
|
||||
<input
|
||||
id="full_name"
|
||||
v-model="form.full_name"
|
||||
type="text"
|
||||
required
|
||||
class="input text-sm sm:text-base"
|
||||
placeholder="Prénom Nom"
|
||||
placeholder="Ton surnom / prénom"
|
||||
@blur="touchedFields.full_name = true"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">C'est ce qui sera affiché aux autres membres</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div>
|
||||
@@ -337,6 +344,9 @@
|
||||
<PWAInstallTutorial
|
||||
:show="showPWAInstructions"
|
||||
:is-ios="isIOS"
|
||||
:is-android="isAndroid"
|
||||
:is-windows="isWindows"
|
||||
:is-mac="isMac"
|
||||
@close="showPWAInstructions = false"
|
||||
/>
|
||||
</div>
|
||||
@@ -451,6 +461,9 @@ const isMobile = ref(false)
|
||||
const showPWAInstructions = ref(false)
|
||||
|
||||
const isIOS = computed(() => /iPhone|iPad|iPod/i.test(navigator.userAgent))
|
||||
const isAndroid = computed(() => /Android/i.test(navigator.userAgent))
|
||||
const isWindows = computed(() => /Windows/i.test(navigator.userAgent))
|
||||
const isMac = computed(() => /Macintosh|Mac OS/i.test(navigator.userAgent) && !isIOS.value)
|
||||
|
||||
function checkIfMobile() {
|
||||
isMobile.value = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) ||
|
||||
@@ -476,8 +489,8 @@ async function handleInstallApp() {
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'installation:', error)
|
||||
}
|
||||
} else if (isMobile.value) {
|
||||
// Sur mobile sans beforeinstallprompt, afficher les instructions
|
||||
} else {
|
||||
// Si pas de beforeinstallprompt, afficher les instructions (mobile ou desktop)
|
||||
showPWAInstructions.value = true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user