fix(date): correction on date utils
Some checks failed
Deploy to Development / build-and-deploy (push) Has been cancelled
Deploy to Production / build-and-deploy (push) Successful in 1m51s

This commit is contained in:
EvanChal
2026-01-26 22:43:35 +01:00
parent d63f2f9f51
commit 08810440e0
13 changed files with 4981 additions and 105 deletions

View File

@@ -434,12 +434,24 @@ onMounted(async () => {
await authStore.fetchUnreadCount()
// Démarrer le polling des notifications
const notificationService = (await import('@/services/notificationService')).default
notificationService.startPolling()
notificationService.setupServiceWorkerListener()
// Demander la permission pour les notifications push
await notificationService.requestNotificationPermission()
try {
const notificationService = (await import('@/services/notificationService')).default
notificationService.startPolling()
notificationService.setupServiceWorkerListener()
// Demander la permission pour les notifications push
// Sur iOS, attendre un peu pour s'assurer que la PWA est bien détectée
if (typeof notificationService.isIOS === 'function' && notificationService.isIOS()) {
// Attendre que la page soit complètement chargée
setTimeout(async () => {
await notificationService.requestNotificationPermission()
}, 1000)
} else {
await notificationService.requestNotificationPermission()
}
} catch (error) {
console.error('Erreur lors du chargement du service de notifications:', error)
}
}
// Vérifier si PWA est installée
@@ -458,7 +470,11 @@ onBeforeUnmount(async () => {
window.removeEventListener('resize', checkIfMobile)
// Arrêter le polling des notifications
const notificationService = (await import('@/services/notificationService')).default
notificationService.stopPolling()
try {
const notificationService = (await import('@/services/notificationService')).default
notificationService.stopPolling()
} catch (error) {
console.error('Erreur lors de l\'arrêt du service de notifications:', error)
}
})
</script>

View File

@@ -37,6 +37,29 @@ app.use(Toast, toastOptions)
// Maintenant installer le router
app.use(router)
// Handler d'erreur global pour capturer les erreurs non catchées
window.addEventListener('error', (event) => {
console.error('❌ Erreur JavaScript globale:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
stack: event.error?.stack
})
})
// Handler pour les promesses rejetées non catchées
window.addEventListener('unhandledrejection', (event) => {
console.error('❌ Promesse rejetée non catchée:', {
reason: event.reason,
promise: event.promise,
stack: event.reason?.stack
})
// Empêcher le message d'erreur par défaut dans la console
event.preventDefault()
})
// Attendre que le router soit prêt avant de monter l'app
router.isReady().then(() => {
app.mount('#app')

View File

@@ -7,6 +7,47 @@ class NotificationService {
this.isPolling = false
}
// Détecter iOS
isIOS() {
return /iPhone|iPad|iPod/i.test(navigator.userAgent)
}
// Vérifier si la PWA est installée (nécessaire pour iOS)
isPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true ||
document.referrer.includes('android-app://')
}
// Vérifier si les notifications sont supportées sur cette plateforme
isNotificationSupported() {
if (!('Notification' in window)) {
console.warn('Notifications API not supported')
return false
}
// Sur iOS, les notifications push ne fonctionnent que si la PWA est installée (iOS 16.4+)
if (this.isIOS()) {
if (!this.isPWAInstalled()) {
console.warn('iOS: Notifications push require PWA to be installed (added to home screen)')
return false
}
// Vérifier la version iOS (approximatif via user agent)
const iosVersion = navigator.userAgent.match(/OS (\d+)_(\d+)/)
if (iosVersion) {
const major = parseInt(iosVersion[1], 10)
const minor = parseInt(iosVersion[2], 10)
if (major < 16 || (major === 16 && minor < 4)) {
console.warn('iOS: Push notifications require iOS 16.4 or later')
return false
}
}
}
return true
}
startPolling() {
if (this.isPolling) return
@@ -89,28 +130,74 @@ class NotificationService {
// Gestion des notifications push PWA
async requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Ce navigateur ne supporte pas les notifications')
if (!this.isNotificationSupported()) {
console.log('Notifications not supported on this platform')
return false
}
if (Notification.permission === 'granted') {
console.log('Notification permission already granted')
return true
}
if (Notification.permission !== 'denied') {
const permission = await Notification.requestPermission()
return permission === 'granted'
if (Notification.permission === 'denied') {
console.warn('Notification permission denied by user')
return false
}
return false
// Sur iOS, s'assurer que la PWA est installée avant de demander
if (this.isIOS() && !this.isPWAInstalled()) {
console.warn('iOS: Cannot request notification permission - PWA must be installed first')
return false
}
try {
const permission = await Notification.requestPermission()
const granted = permission === 'granted'
if (granted) {
console.log('Notification permission granted')
} else {
console.warn('Notification permission denied:', permission)
}
return granted
} catch (error) {
console.error('Error requesting notification permission:', error)
return false
}
}
async showPushNotification(title, options = {}) {
if (!('Notification' in window)) return
if (!this.isNotificationSupported()) {
console.warn('Cannot show notification - not supported on this platform')
return null
}
const hasPermission = await this.requestNotificationPermission()
if (!hasPermission) return
if (!hasPermission) {
console.warn('Cannot show notification - permission not granted')
return null
}
// Préparer les options de notification (iOS ne supporte pas vibrate)
const notificationOptions = {
body: options.body || options.message || '',
icon: '/icon-192x192.png',
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
data: {
link: options.link || '/',
notificationId: options.data?.notificationId
},
...options
}
// Retirer vibrate sur iOS (non supporté)
if (!this.isIOS()) {
notificationOptions.vibrate = [200, 100, 200]
}
// Toujours utiliser le service worker pour les notifications push
// Cela permet aux notifications de fonctionner même quand l'app est fermée
@@ -118,74 +205,58 @@ class NotificationService {
try {
const registration = await navigator.serviceWorker.ready
if (!registration.active) {
console.warn('Service worker not active, using fallback')
throw new Error('Service worker not active')
}
// Envoyer un message au service worker pour afficher la notification
// Cela permet de gérer les clics correctement
registration.active?.postMessage({
registration.active.postMessage({
type: 'SHOW_NOTIFICATION',
title,
options: {
body: options.body || options.message || '',
icon: '/icon-192x192.png',
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
vibrate: [200, 100, 200],
data: {
link: options.link || '/',
notificationId: options.data?.notificationId
},
...options
}
options: notificationOptions
})
// Aussi utiliser l'API directe comme fallback
await registration.showNotification(title, {
icon: '/icon-192x192.png',
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
vibrate: [200, 100, 200],
data: {
link: options.link || '/',
notificationId: options.data?.notificationId
},
body: options.body || options.message || '',
...options
})
return
// Aussi utiliser l'API directe du service worker
await registration.showNotification(title, notificationOptions)
console.log('Notification shown via service worker')
return null
} catch (error) {
console.error('Error showing notification via service worker:', error)
// Continuer avec le fallback
}
}
// Fallback: notification native du navigateur (seulement si le SW n'est pas disponible)
const notification = new Notification(title, {
icon: '/icon-192x192.png',
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
body: options.body || options.message || '',
...options
})
try {
const notification = new Notification(title, notificationOptions)
notification.onclick = () => {
window.focus()
notification.close()
if (options.link) {
// Utiliser le router si disponible
if (window.location.pathname !== options.link) {
window.location.href = options.link
notification.onclick = () => {
window.focus()
notification.close()
if (options.link) {
// Utiliser le router si disponible
if (window.location.pathname !== options.link) {
window.location.href = options.link
}
}
}
// Fermer automatiquement après 5 secondes (sauf sur iOS où c'est géré par le système)
if (!this.isIOS()) {
setTimeout(() => {
notification.close()
}, 5000)
}
console.log('Notification shown via native API')
return notification
} catch (error) {
console.error('Error showing notification:', error)
return null
}
// Fermer automatiquement après 5 secondes
setTimeout(() => {
notification.close()
}, 5000)
return notification
}
// Écouter les messages du service worker pour les notifications push

View File

@@ -53,11 +53,19 @@ instance.interceptors.request.use(
const token = localStorage.getItem('token')
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}`)
// Log détaillé en développement
if (import.meta.env.DEV) {
console.log(`📤 Requête ${config.method?.toUpperCase()} vers: ${config.url}`, {
hasToken: !!token,
tokenLength: token.length,
tokenPreview: token.substring(0, 20) + '...'
})
}
} else {
// Log si pas de token
if (import.meta.env.DEV) {
console.warn(`⚠️ Requête ${config.method?.toUpperCase()} vers: ${config.url} - Pas de token`)
}
}
return config
@@ -86,9 +94,23 @@ instance.interceptors.response.use(
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method,
data: error.response?.data
data: error.response?.data,
headers: error.response?.headers,
requestHeaders: error.config?.headers
})
// Log supplémentaire pour les erreurs 401/403
if (error.response?.status === 401 || error.response?.status === 403) {
const token = localStorage.getItem('token')
console.error('🔍 Diagnostic erreur auth:', {
hasToken: !!token,
tokenLength: token?.length,
tokenPreview: token ? token.substring(0, 20) + '...' : null,
url: error.config?.url,
method: error.config?.method
})
}
if (error.response?.status === 401) {
// Ne pas rediriger si on est déjà sur une page d'auth
const currentRoute = router.currentRoute.value

View File

@@ -25,11 +25,23 @@ export function formatDateInFrenchTimezone(date, formatStr = 'dd MMM à HH:mm')
/**
* Formate une date relative dans le fuseau horaire français
* Note: On s'assure que la date est correctement parsée comme UTC
*/
export function formatRelativeDateInFrenchTimezone(date) {
if (!date) return ''
const frenchDate = toFrenchTimezone(date)
return formatDistanceToNow(frenchDate, { addSuffix: true, locale: fr })
// Convertir la date en objet Date si ce n'est pas déjà le cas
let dateObj = date instanceof Date ? date : new Date(date)
// Si la date est une string sans "Z" à la fin, elle est interprétée comme locale
// On doit s'assurer qu'elle est traitée comme UTC
if (typeof date === 'string' && !date.endsWith('Z') && !date.includes('+') && !date.includes('-', 10)) {
// Si c'est une date ISO sans timezone, on l'interprète comme UTC
dateObj = new Date(date + 'Z')
}
// Calculer la distance depuis maintenant
return formatDistanceToNow(dateObj, { addSuffix: true, locale: fr })
}
/**

View File

@@ -333,7 +333,7 @@ import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
import axios from '@/utils/axios'
import { getMediaUrl } from '@/utils/axios'
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils'
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone, convertFrenchTimezoneToUTC } from '@/utils/dateUtils'
import {
Plus,
Calendar,
@@ -467,7 +467,13 @@ async function quickParticipation(eventId, status) {
}
async function createEvent() {
if (!newEvent.value.title || !newEvent.value.date) return
console.log('🔵 createEvent appelée')
console.log('🔵 newEvent.value:', newEvent.value)
if (!newEvent.value.title || !newEvent.value.date) {
console.warn('⚠️ Validation échouée: titre ou date manquant')
return
}
// Vérifier que des invités sont sélectionnés pour les événements privés
if (newEvent.value.is_private && (!newEvent.value.invited_user_ids || newEvent.value.invited_user_ids.length === 0)) {
@@ -476,6 +482,8 @@ async function createEvent() {
}
creating.value = true
console.log('🔵 creating.value = true')
try {
const eventData = {
title: newEvent.value.title,
@@ -487,7 +495,12 @@ async function createEvent() {
invited_user_ids: newEvent.value.is_private ? newEvent.value.invited_user_ids : null
}
await axios.post('/api/events', eventData)
console.log('🔵 eventData préparé:', eventData)
console.log('🔵 Envoi de la requête axios.post...')
const response = await axios.post('/api/events', eventData)
console.log('✅ Réponse reçue:', response)
// Refresh events list
await fetchEvents()
@@ -496,9 +509,18 @@ async function createEvent() {
resetForm()
toast.success(newEvent.value.is_private ? 'Événement privé créé avec succès' : 'Événement créé avec succès')
} catch (error) {
console.error('❌ Erreur dans createEvent:', error)
console.error('❌ Détails de l\'erreur:', {
message: error.message,
response: error.response,
request: error.request,
config: error.config
})
toast.error('Erreur lors de la création de l\'événement')
} finally {
creating.value = false
console.log('🔵 creating.value = false')
}
creating.value = false
}
function resetForm() {

View File

@@ -284,7 +284,7 @@
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
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'
@@ -320,6 +320,7 @@ const showImageUpload = ref(false)
const offset = ref(0)
const hasMorePosts = ref(true)
const dateRefreshInterval = ref(null)
const newPost = ref({
content: '',
@@ -327,7 +328,12 @@ const newPost = ref({
mentioned_user_ids: []
})
// Force refresh pour les dates relatives
const dateRefreshKey = ref(0)
function formatRelativeDate(date) {
// Utiliser dateRefreshKey pour forcer le recalcul
dateRefreshKey.value
return formatRelativeDateInFrenchTimezone(date)
}
@@ -358,6 +364,14 @@ async function createPost() {
// Add new post to the beginning of the list
posts.value.unshift(response.data)
// Forcer le rafraîchissement de la date immédiatement
dateRefreshKey.value++
// Rafraîchir à nouveau après 1 seconde pour s'assurer que ça s'affiche correctement
setTimeout(() => {
dateRefreshKey.value++
}, 1000)
// Reset form
newPost.value = {
content: '',
@@ -471,13 +485,35 @@ async function fetchPosts() {
loading.value = true
try {
const response = await axios.get(`/api/posts?limit=10&offset=${offset.value}`)
// S'assurer que les dates sont correctement parsées comme UTC
const postsData = response.data.map(post => {
// Si la date created_at est une string sans timezone, l'interpréter comme UTC
if (post.created_at && typeof post.created_at === 'string' && !post.created_at.endsWith('Z') && !post.created_at.includes('+') && !post.created_at.includes('-', 10)) {
post.created_at = post.created_at + 'Z'
}
// Même chose pour les commentaires
if (post.comments) {
post.comments = post.comments.map(comment => {
if (comment.created_at && typeof comment.created_at === 'string' && !comment.created_at.endsWith('Z') && !comment.created_at.includes('+') && !comment.created_at.includes('-', 10)) {
comment.created_at = comment.created_at + 'Z'
}
return comment
})
}
return post
})
if (offset.value === 0) {
posts.value = response.data
posts.value = postsData
} else {
posts.value.push(...response.data)
posts.value.push(...postsData)
}
hasMorePosts.value = response.data.length === 10
// Forcer le rafraîchissement des dates après le chargement
dateRefreshKey.value++
} catch (error) {
toast.error('Erreur lors du chargement des publications')
}
@@ -549,5 +585,16 @@ onMounted(async () => {
await nextTick()
await scrollToPost(parseInt(route.query.highlight))
}
// Rafraîchir les dates relatives toutes les 30 secondes
dateRefreshInterval.value = setInterval(() => {
dateRefreshKey.value++
}, 30000)
})
onBeforeUnmount(() => {
if (dateRefreshInterval.value) {
clearInterval(dateRefreshInterval.value)
}
})
</script>