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();
workbox.clientsClaim();
@@ -80,14 +81,8 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.abqp38bc5fg"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({
"cacheName": "google-fonts-cache",
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') {
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(
self.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
},
...options
self.registration.showNotification(title, notificationOptions).catch(error => {
console.error('Error showing notification in service worker:', error)
})
)
}
@@ -90,8 +104,15 @@ self.addEventListener('push', (event) => {
}
}
// Retirer vibrate pour compatibilité iOS
if (notificationData.vibrate) {
delete notificationData.vibrate
}
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()
// Démarrer le polling des notifications
const notificationService = (await import('@/services/notificationService')).default
notificationService.startPolling()
notificationService.setupServiceWorkerListener()
try {
const notificationService = (await import('@/services/notificationService')).default
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
@@ -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()
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
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>

View File

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