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

@@ -67,8 +67,9 @@ if (!self.define) {
}); });
}; };
} }
define(['./workbox-47da91e0'], (function (workbox) { 'use strict'; define(['./workbox-9be7f7ba'], (function (workbox) { 'use strict';
importScripts("/sw-custom.js");
self.skipWaiting(); self.skipWaiting();
workbox.clientsClaim(); workbox.clientsClaim();
@@ -80,14 +81,8 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
workbox.precacheAndRoute([{ workbox.precacheAndRoute([{
"url": "registerSW.js", "url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.abqp38bc5fg"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({ workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({
"cacheName": "google-fonts-cache", "cacheName": "google-fonts-cache",
plugins: [new workbox.ExpirationPlugin({ plugins: [new workbox.ExpirationPlugin({

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -40,18 +40,32 @@ self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SHOW_NOTIFICATION') { if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
const { title, options } = event.data const { title, options } = event.data
// Préparer les options de notification
const notificationOptions = {
icon: '/icon-192x192.png',
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
data: {
link: options.link || options.data?.link || '/',
notificationId: options.data?.notificationId || options.notificationId
},
body: options.body || options.message || '',
...options
}
// Retirer vibrate si présent (iOS ne le supporte pas)
// Le client devrait déjà l'avoir retiré, mais on s'assure ici aussi
if (notificationOptions.vibrate) {
// Vérifier si on est sur iOS (approximatif via user agent du client)
// Note: dans le SW on n'a pas accès direct à navigator.userAgent
// mais on peut retirer vibrate de toute façon car iOS l'ignore
delete notificationOptions.vibrate
}
event.waitUntil( event.waitUntil(
self.registration.showNotification(title, { self.registration.showNotification(title, notificationOptions).catch(error => {
icon: '/icon-192x192.png', console.error('Error showing notification in service worker:', error)
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
vibrate: [200, 100, 200],
data: {
link: options.link || '/',
notificationId: options.data?.notificationId
},
...options
}) })
) )
} }
@@ -90,8 +104,15 @@ self.addEventListener('push', (event) => {
} }
} }
// Retirer vibrate pour compatibilité iOS
if (notificationData.vibrate) {
delete notificationData.vibrate
}
event.waitUntil( event.waitUntil(
self.registration.showNotification(notificationData.title, notificationData) self.registration.showNotification(notificationData.title, notificationData).catch(error => {
console.error('Error showing push notification:', error)
})
) )
}) })

View File

@@ -434,12 +434,24 @@ onMounted(async () => {
await authStore.fetchUnreadCount() await authStore.fetchUnreadCount()
// Démarrer le polling des notifications // Démarrer le polling des notifications
const notificationService = (await import('@/services/notificationService')).default try {
notificationService.startPolling() const notificationService = (await import('@/services/notificationService')).default
notificationService.setupServiceWorkerListener() notificationService.startPolling()
notificationService.setupServiceWorkerListener()
// Demander la permission pour les notifications push
await notificationService.requestNotificationPermission() // 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 // Vérifier si PWA est installée
@@ -458,7 +470,11 @@ onBeforeUnmount(async () => {
window.removeEventListener('resize', checkIfMobile) window.removeEventListener('resize', checkIfMobile)
// Arrêter le polling des notifications // Arrêter le polling des notifications
const notificationService = (await import('@/services/notificationService')).default try {
notificationService.stopPolling() 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> </script>

View File

@@ -37,6 +37,29 @@ app.use(Toast, toastOptions)
// Maintenant installer le router // Maintenant installer le router
app.use(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 // Attendre que le router soit prêt avant de monter l'app
router.isReady().then(() => { router.isReady().then(() => {
app.mount('#app') app.mount('#app')

View File

@@ -7,6 +7,47 @@ class NotificationService {
this.isPolling = false 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() { startPolling() {
if (this.isPolling) return if (this.isPolling) return
@@ -89,28 +130,74 @@ class NotificationService {
// Gestion des notifications push PWA // Gestion des notifications push PWA
async requestNotificationPermission() { async requestNotificationPermission() {
if (!('Notification' in window)) { if (!this.isNotificationSupported()) {
console.log('Ce navigateur ne supporte pas les notifications') console.log('Notifications not supported on this platform')
return false return false
} }
if (Notification.permission === 'granted') { if (Notification.permission === 'granted') {
console.log('Notification permission already granted')
return true return true
} }
if (Notification.permission !== 'denied') { if (Notification.permission === 'denied') {
const permission = await Notification.requestPermission() console.warn('Notification permission denied by user')
return permission === 'granted' 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 = {}) { 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() 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 // Toujours utiliser le service worker pour les notifications push
// Cela permet aux notifications de fonctionner même quand l'app est fermée // Cela permet aux notifications de fonctionner même quand l'app est fermée
@@ -118,74 +205,58 @@ class NotificationService {
try { try {
const registration = await navigator.serviceWorker.ready 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 // 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', type: 'SHOW_NOTIFICATION',
title, title,
options: { options: notificationOptions
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
}
}) })
// Aussi utiliser l'API directe comme fallback // Aussi utiliser l'API directe du service worker
await registration.showNotification(title, { await registration.showNotification(title, notificationOptions)
icon: '/icon-192x192.png',
badge: '/icon-96x96.png', console.log('Notification shown via service worker')
tag: 'lediscord-notification', return null
requireInteraction: false,
vibrate: [200, 100, 200],
data: {
link: options.link || '/',
notificationId: options.data?.notificationId
},
body: options.body || options.message || '',
...options
})
return
} catch (error) { } catch (error) {
console.error('Error showing notification via service worker:', 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) // Fallback: notification native du navigateur (seulement si le SW n'est pas disponible)
const notification = new Notification(title, { try {
icon: '/icon-192x192.png', const notification = new Notification(title, notificationOptions)
badge: '/icon-96x96.png',
tag: 'lediscord-notification',
requireInteraction: false,
body: options.body || options.message || '',
...options
})
notification.onclick = () => { notification.onclick = () => {
window.focus() window.focus()
notification.close() notification.close()
if (options.link) { if (options.link) {
// Utiliser le router si disponible // Utiliser le router si disponible
if (window.location.pathname !== options.link) { if (window.location.pathname !== options.link) {
window.location.href = 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 // É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') const token = localStorage.getItem('token')
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} // Log détaillé en développement
if (import.meta.env.DEV) {
// Log des requêtes en développement console.log(`📤 Requête ${config.method?.toUpperCase()} vers: ${config.url}`, {
if (import.meta.env.DEV) { hasToken: !!token,
console.log(`📤 Requête ${config.method?.toUpperCase()} vers: ${config.url}`) 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 return config
@@ -86,9 +94,23 @@ instance.interceptors.response.use(
statusText: error.response?.statusText, statusText: error.response?.statusText,
url: error.config?.url, url: error.config?.url,
method: error.config?.method, 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) { if (error.response?.status === 401) {
// Ne pas rediriger si on est déjà sur une page d'auth // Ne pas rediriger si on est déjà sur une page d'auth
const currentRoute = router.currentRoute.value 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 * 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) { export function formatRelativeDateInFrenchTimezone(date) {
if (!date) return '' 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 { useRouter } from 'vue-router'
import axios from '@/utils/axios' import axios from '@/utils/axios'
import { getMediaUrl } from '@/utils/axios' import { getMediaUrl } from '@/utils/axios'
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils' import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone, convertFrenchTimezoneToUTC } from '@/utils/dateUtils'
import { import {
Plus, Plus,
Calendar, Calendar,
@@ -467,7 +467,13 @@ async function quickParticipation(eventId, status) {
} }
async function createEvent() { 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 // 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)) { 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 creating.value = true
console.log('🔵 creating.value = true')
try { try {
const eventData = { const eventData = {
title: newEvent.value.title, title: newEvent.value.title,
@@ -487,7 +495,12 @@ async function createEvent() {
invited_user_ids: newEvent.value.is_private ? newEvent.value.invited_user_ids : null 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 // Refresh events list
await fetchEvents() await fetchEvents()
@@ -496,9 +509,18 @@ async function createEvent() {
resetForm() resetForm()
toast.success(newEvent.value.is_private ? 'Événement privé créé avec succès' : 'Événement créé avec succès') toast.success(newEvent.value.is_private ? 'Événement privé créé avec succès' : 'Événement créé avec succès')
} catch (error) { } 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') 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() { function resetForm() {

View File

@@ -284,7 +284,7 @@
</template> </template>
<script setup> <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 { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
@@ -320,6 +320,7 @@ const showImageUpload = ref(false)
const offset = ref(0) const offset = ref(0)
const hasMorePosts = ref(true) const hasMorePosts = ref(true)
const dateRefreshInterval = ref(null)
const newPost = ref({ const newPost = ref({
content: '', content: '',
@@ -327,7 +328,12 @@ const newPost = ref({
mentioned_user_ids: [] mentioned_user_ids: []
}) })
// Force refresh pour les dates relatives
const dateRefreshKey = ref(0)
function formatRelativeDate(date) { function formatRelativeDate(date) {
// Utiliser dateRefreshKey pour forcer le recalcul
dateRefreshKey.value
return formatRelativeDateInFrenchTimezone(date) return formatRelativeDateInFrenchTimezone(date)
} }
@@ -358,6 +364,14 @@ async function createPost() {
// Add new post to the beginning of the list // Add new post to the beginning of the list
posts.value.unshift(response.data) 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 // Reset form
newPost.value = { newPost.value = {
content: '', content: '',
@@ -471,13 +485,35 @@ async function fetchPosts() {
loading.value = true loading.value = true
try { try {
const response = await axios.get(`/api/posts?limit=10&offset=${offset.value}`) 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) { if (offset.value === 0) {
posts.value = response.data posts.value = postsData
} else { } else {
posts.value.push(...response.data) posts.value.push(...postsData)
} }
hasMorePosts.value = response.data.length === 10 hasMorePosts.value = response.data.length === 10
// Forcer le rafraîchissement des dates après le chargement
dateRefreshKey.value++
} catch (error) { } catch (error) {
toast.error('Erreur lors du chargement des publications') toast.error('Erreur lors du chargement des publications')
} }
@@ -549,5 +585,16 @@ onMounted(async () => {
await nextTick() await nextTick()
await scrollToPost(parseInt(route.query.highlight)) 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> </script>

View File

@@ -224,7 +224,7 @@ module.exports = defineConfig(({ command, mode }) => {
}, },
{ {
urlPattern: /^https?:\/\/.*\/uploads\/.*/i, urlPattern: /^https?:\/\/.*\/uploads\/.*/i,
handler: 'CacheFirst', handler: 'StaleWhileRevalidate',
options: { options: {
cacheName: 'uploads-cache', cacheName: 'uploads-cache',
expiration: { expiration: {