fix(pwa+timezone)
This commit is contained in:
97
frontend/public/sw-custom.js
Normal file
97
frontend/public/sw-custom.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// Service Worker personnalisé pour gérer les notifications push
|
||||
// Ce fichier sera fusionné avec le service worker généré par vite-plugin-pwa
|
||||
|
||||
// Écouter les événements de notification
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('Notification clicked:', event.notification)
|
||||
|
||||
event.notification.close()
|
||||
|
||||
// Récupérer le lien depuis les données de la notification
|
||||
const link = event.notification.data?.link || event.notification.data?.url || '/'
|
||||
|
||||
// Ouvrir ou focus la fenêtre/clients
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
||||
// Si une fenêtre est déjà ouverte, la focus
|
||||
for (let i = 0; i < clientList.length; i++) {
|
||||
const client = clientList[i]
|
||||
if (client.url && 'focus' in client) {
|
||||
// Naviguer vers le lien si nécessaire
|
||||
if (link && !client.url.includes(link.split('/')[1])) {
|
||||
return client.navigate(link).then(() => client.focus())
|
||||
}
|
||||
return client.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Sinon, ouvrir une nouvelle fenêtre
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(link || '/')
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// Écouter les messages du client pour afficher des notifications
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('Service Worker received message:', event.data)
|
||||
|
||||
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
|
||||
const { title, options } = event.data
|
||||
|
||||
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
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Écouter les push events (pour les vraies push notifications depuis le serveur)
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('Push event received:', event)
|
||||
|
||||
let notificationData = {
|
||||
title: 'LeDiscord',
|
||||
body: 'Vous avez une nouvelle notification',
|
||||
icon: '/icon-192x192.png',
|
||||
badge: '/icon-96x96.png',
|
||||
tag: 'lediscord-notification',
|
||||
data: {
|
||||
link: '/'
|
||||
}
|
||||
}
|
||||
|
||||
// Si des données sont envoyées avec le push
|
||||
if (event.data) {
|
||||
try {
|
||||
const data = event.data.json()
|
||||
notificationData = {
|
||||
...notificationData,
|
||||
title: data.title || notificationData.title,
|
||||
body: data.body || data.message || notificationData.body,
|
||||
data: {
|
||||
link: data.link || '/',
|
||||
notificationId: data.notificationId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing push data:', e)
|
||||
}
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(notificationData.title, notificationData)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -8,15 +8,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
import EnvironmentDebug from '@/components/EnvironmentDebug.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const layout = computed(() => {
|
||||
return route.meta.layout === 'auth' ? AuthLayout : DefaultLayout
|
||||
})
|
||||
|
||||
// Restaurer la session au démarrage de l'app
|
||||
onMounted(async () => {
|
||||
if (authStore.token && !authStore.user) {
|
||||
await authStore.fetchCurrentUser()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
243
frontend/src/components/PWAInstallTutorial.vue
Normal file
243
frontend/src/components/PWAInstallTutorial.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 p-4"
|
||||
@click.self="close"
|
||||
>
|
||||
<div
|
||||
class="bg-white rounded-2xl shadow-2xl max-w-md w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between rounded-t-2xl">
|
||||
<h3 class="text-xl font-bold text-gray-900">Installer l'application</h3>
|
||||
<button
|
||||
@click="close"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="px-6 py-6">
|
||||
<div v-if="isIOS" class="space-y-6">
|
||||
<!-- Step 1 -->
|
||||
<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">
|
||||
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
|
||||
</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" />
|
||||
</svg>
|
||||
</div>
|
||||
<ArrowDown class="w-5 h-5 text-gray-400" />
|
||||
<p class="text-xs text-gray-500">Bouton de partage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<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">
|
||||
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
|
||||
</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 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">Sur l'écran d'accueil</p>
|
||||
<p class="text-xs text-primary-600">Ajouter à l'écran d'accueil</p>
|
||||
</div>
|
||||
</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" dans la popup de confirmation
|
||||
</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 apparaîtra sur votre écran d'accueil !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Android -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Step 1 -->
|
||||
<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">
|
||||
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 (⋮)
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<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">
|
||||
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 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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benefits -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<p class="text-sm font-semibold text-gray-900 mb-3">Une fois installée, vous pourrez :</p>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<CheckCircle class="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span>Accéder rapidement à l'app depuis l'écran d'accueil</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<CheckCircle class="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span>Recevoir des notifications push</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<CheckCircle class="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span>Utiliser l'app hors ligne (contenu mis en cache)</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<CheckCircle class="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span>Rester connecté automatiquement</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-6 py-4 rounded-b-2xl">
|
||||
<button
|
||||
@click="close"
|
||||
class="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-3 px-4 rounded-lg transition-colors shadow-lg hover:shadow-xl"
|
||||
>
|
||||
J'ai compris
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { X, ArrowDown, CheckCircle } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isIOS: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -97,8 +97,7 @@ import { useToast } from 'vue-toastification'
|
||||
import { MessageSquare, User } from 'lucide-vue-next'
|
||||
import Mentions from '@/components/Mentions.vue'
|
||||
import MentionInput from '@/components/MentionInput.vue'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import axios from '@/utils/axios'
|
||||
|
||||
@@ -130,7 +129,7 @@ const commentMentions = ref([])
|
||||
const currentUser = computed(() => authStore.user)
|
||||
|
||||
function formatDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function getAvatarUrl(avatarUrl) {
|
||||
|
||||
@@ -204,6 +204,13 @@
|
||||
<!-- Ticket Floating Button -->
|
||||
<TicketFloatingButton />
|
||||
|
||||
<!-- PWA Install Tutorial -->
|
||||
<PWAInstallTutorial
|
||||
:show="showPWAInstructions"
|
||||
:is-ios="isIOS"
|
||||
@close="showPWAInstructions = false"
|
||||
/>
|
||||
|
||||
<!-- Notifications Panel -->
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
@@ -270,8 +277,7 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import {
|
||||
Home,
|
||||
@@ -288,6 +294,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import TicketFloatingButton from '@/components/TicketFloatingButton.vue'
|
||||
import PWAInstallTutorial from '@/components/PWAInstallTutorial.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
@@ -308,13 +315,16 @@ const deferredPrompt = ref(null)
|
||||
const isPWAInstalled = ref(false)
|
||||
const canInstall = ref(false)
|
||||
const isMobile = ref(false)
|
||||
const showPWAInstructions = ref(false)
|
||||
|
||||
const isIOS = computed(() => /iPhone|iPad|iPod/i.test(navigator.userAgent))
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const notifications = computed(() => authStore.notifications)
|
||||
const unreadNotifications = computed(() => authStore.unreadCount)
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'dd MMM à HH:mm', { locale: fr })
|
||||
return formatDateInFrenchTimezone(date, 'dd MMM à HH:mm')
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
@@ -336,11 +346,18 @@ async function handleNotificationClick(notification) {
|
||||
await authStore.markNotificationRead(notification.id)
|
||||
}
|
||||
|
||||
if (notification.link) {
|
||||
router.push(notification.link)
|
||||
}
|
||||
|
||||
showNotifications.value = false
|
||||
|
||||
if (notification.link) {
|
||||
// Gérer les liens de posts différemment car il n'y a pas de route /posts/:id
|
||||
if (notification.link.startsWith('/posts/')) {
|
||||
const postId = notification.link.split('/posts/')[1]
|
||||
// Naviguer vers /posts et passer l'ID en query pour scroll
|
||||
router.push({ path: '/posts', query: { highlight: postId } })
|
||||
} else {
|
||||
router.push(notification.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PWA Installation logic
|
||||
@@ -398,20 +415,19 @@ async function handleInstallApp() {
|
||||
console.error('Erreur lors de l\'installation:', error)
|
||||
}
|
||||
} else if (isMobile.value) {
|
||||
// Sur mobile sans beforeinstallprompt, afficher les instructions
|
||||
// Sur mobile sans beforeinstallprompt, afficher le tutoriel
|
||||
showUserMenu.value = false
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
if (isIOS) {
|
||||
alert('Sur iOS :\n1. Appuyez sur le bouton de partage (□↑) en bas de l\'écran\n2. Faites défiler et sélectionnez "Sur l\'écran d\'accueil"\n3. Appuyez sur "Ajouter"')
|
||||
} else {
|
||||
alert('Sur Android :\n1. Appuyez sur le menu (⋮) en haut à droite\n2. Sélectionnez "Ajouter à l\'écran d\'accueil" ou "Installer l\'application"\n3. Confirmez l\'installation')
|
||||
}
|
||||
showPWAInstructions.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
checkIfMobile()
|
||||
await authStore.fetchCurrentUser()
|
||||
|
||||
// Restaurer la session si un token existe
|
||||
if (authStore.token) {
|
||||
await authStore.fetchCurrentUser()
|
||||
}
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
await fetchNotifications()
|
||||
|
||||
@@ -51,11 +51,25 @@ class NotificationService {
|
||||
.slice(0, result.newCount - result.previousCount)
|
||||
|
||||
if (newUnreadNotifications.length > 0) {
|
||||
// Ne pas afficher de notification si l'app est en focus
|
||||
// (pour éviter les doublons avec les notifications dans l'app)
|
||||
if (document.hasFocus()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Afficher une notification push pour la plus récente
|
||||
const latestNotification = newUnreadNotifications[0]
|
||||
|
||||
// Gérer les liens de posts différemment
|
||||
let link = latestNotification.link || '/'
|
||||
if (link.startsWith('/posts/')) {
|
||||
const postId = link.split('/posts/')[1]
|
||||
link = `/posts?highlight=${postId}`
|
||||
}
|
||||
|
||||
await this.showPushNotification(latestNotification.title, {
|
||||
body: latestNotification.message,
|
||||
link: latestNotification.link || '/',
|
||||
link: link,
|
||||
data: { notificationId: latestNotification.id }
|
||||
})
|
||||
}
|
||||
@@ -98,16 +112,44 @@ class NotificationService {
|
||||
const hasPermission = await this.requestNotificationPermission()
|
||||
if (!hasPermission) return
|
||||
|
||||
// Si on est dans un service worker, utiliser la notification API du SW
|
||||
// Toujours utiliser le service worker pour les notifications push
|
||||
// Cela permet aux notifications de fonctionner même quand l'app est fermée
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
|
||||
// Envoyer un message au service worker pour afficher la notification
|
||||
// Cela permet de gérer les clics correctement
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
@@ -116,12 +158,13 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: notification native du navigateur
|
||||
// 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
|
||||
})
|
||||
|
||||
@@ -130,7 +173,10 @@ class NotificationService {
|
||||
notification.close()
|
||||
|
||||
if (options.link) {
|
||||
window.location.href = options.link
|
||||
// Utiliser le router si disponible
|
||||
if (window.location.pathname !== options.link) {
|
||||
window.location.href = options.link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('token'))
|
||||
const toast = useToast()
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const isAdmin = computed(() => user.value?.is_admin || false)
|
||||
|
||||
if (token.value) {
|
||||
|
||||
66
frontend/src/utils/dateUtils.js
Normal file
66
frontend/src/utils/dateUtils.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'
|
||||
|
||||
// Fuseau horaire français
|
||||
const FRENCH_TIMEZONE = 'Europe/Paris'
|
||||
|
||||
/**
|
||||
* Convertit une date UTC en date du fuseau horaire français
|
||||
*/
|
||||
function toFrenchTimezone(date) {
|
||||
if (!date) return null
|
||||
const dateObj = date instanceof Date ? date : new Date(date)
|
||||
return utcToZonedTime(dateObj, FRENCH_TIMEZONE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date dans le fuseau horaire français
|
||||
*/
|
||||
export function formatDateInFrenchTimezone(date, formatStr = 'dd MMM à HH:mm') {
|
||||
if (!date) return ''
|
||||
const frenchDate = toFrenchTimezone(date)
|
||||
return format(frenchDate, formatStr, { locale: fr })
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date relative dans le fuseau horaire français
|
||||
*/
|
||||
export function formatRelativeDateInFrenchTimezone(date) {
|
||||
if (!date) return ''
|
||||
const frenchDate = toFrenchTimezone(date)
|
||||
return formatDistanceToNow(frenchDate, { addSuffix: true, locale: fr })
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date complète dans le fuseau horaire français
|
||||
*/
|
||||
export function formatFullDateInFrenchTimezone(date) {
|
||||
return formatDateInFrenchTimezone(date, 'EEEE d MMMM yyyy à HH:mm')
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date courte dans le fuseau horaire français
|
||||
*/
|
||||
export function formatShortDateInFrenchTimezone(date) {
|
||||
return formatDateInFrenchTimezone(date, 'dd/MM/yyyy')
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date pour un input datetime-local dans le fuseau horaire français
|
||||
*/
|
||||
export function formatDateForInputInFrenchTimezone(date) {
|
||||
if (!date) return ''
|
||||
const frenchDate = toFrenchTimezone(date)
|
||||
return format(frenchDate, "yyyy-MM-dd'T'HH:mm", { locale: fr })
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une date du fuseau horaire français vers UTC pour l'envoyer au backend
|
||||
*/
|
||||
export function convertFrenchTimezoneToUTC(date) {
|
||||
if (!date) return null
|
||||
const dateObj = date instanceof Date ? date : new Date(date)
|
||||
return zonedTimeToUtc(dateObj, FRENCH_TIMEZONE)
|
||||
}
|
||||
|
||||
@@ -1239,8 +1239,7 @@ import {
|
||||
TestTube,
|
||||
Plus
|
||||
} from 'lucide-vue-next'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import LoadingLogo from '@/components/LoadingLogo.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -1710,7 +1709,7 @@ function getPriorityBadgeClass(priority) {
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
// Gestion des tickets
|
||||
|
||||
@@ -567,8 +567,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDateInFrenchTimezone, formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Image,
|
||||
@@ -618,7 +617,7 @@ const totalSize = computed(() =>
|
||||
)
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'dd MMMM yyyy', { locale: fr })
|
||||
return formatDateInFrenchTimezone(date, 'dd MMMM yyyy')
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
@@ -335,8 +335,7 @@ import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { formatDistanceToNow, format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
Plus,
|
||||
Image,
|
||||
@@ -384,11 +383,11 @@ const uploadSuccess = ref([])
|
||||
const isDragOver = ref(false)
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'dd/MM/yyyy', { locale: fr })
|
||||
return formatShortDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
|
||||
@@ -342,8 +342,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatFullDateInFrenchTimezone, formatRelativeDateInFrenchTimezone, formatDateForInputInFrenchTimezone, convertFrenchTimezoneToUTC } from '@/utils/dateUtils'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
@@ -379,11 +378,11 @@ const canEdit = computed(() =>
|
||||
)
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'EEEE d MMMM yyyy à HH:mm', { locale: fr })
|
||||
return formatFullDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function getParticipationClass(status) {
|
||||
@@ -444,7 +443,7 @@ async function fetchEvent() {
|
||||
editForm.value = {
|
||||
title: event.value.title,
|
||||
description: event.value.description || '',
|
||||
date: format(new Date(event.value.date), "yyyy-MM-dd'T'HH:mm", { locale: fr }),
|
||||
date: formatDateForInputInFrenchTimezone(event.value.date),
|
||||
location: event.value.location || ''
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -470,7 +469,7 @@ async function updateEvent() {
|
||||
try {
|
||||
const response = await axios.put(`/api/events/${event.value.id}`, {
|
||||
...editForm.value,
|
||||
date: new Date(editForm.value.date).toISOString()
|
||||
date: convertFrenchTimezoneToUTC(new Date(editForm.value.date)).toISOString()
|
||||
})
|
||||
event.value = response.data
|
||||
showEditModal.value = false
|
||||
|
||||
@@ -333,8 +333,7 @@ import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { formatDistanceToNow, format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone, formatShortDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
Plus,
|
||||
Calendar,
|
||||
@@ -380,11 +379,11 @@ const filteredEvents = computed(() => {
|
||||
})
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'dd/MM/yyyy', { locale: fr })
|
||||
return formatShortDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function openEvent(event) {
|
||||
@@ -481,9 +480,9 @@ async function createEvent() {
|
||||
const eventData = {
|
||||
title: newEvent.value.title,
|
||||
description: newEvent.value.description,
|
||||
date: new Date(newEvent.value.date).toISOString(),
|
||||
date: convertFrenchTimezoneToUTC(new Date(newEvent.value.date)).toISOString(),
|
||||
location: newEvent.value.location,
|
||||
end_date: newEvent.value.end_date ? new Date(newEvent.value.end_date).toISOString() : null,
|
||||
end_date: newEvent.value.end_date ? convertFrenchTimezoneToUTC(new Date(newEvent.value.end_date)).toISOString() : null,
|
||||
is_private: newEvent.value.is_private,
|
||||
invited_user_ids: newEvent.value.is_private ? newEvent.value.invited_user_ids : null
|
||||
}
|
||||
|
||||
@@ -220,8 +220,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatFullDateInFrenchTimezone, formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
@@ -248,11 +247,11 @@ const recentPosts = computed(() => stats.value.recent_posts || 0)
|
||||
const activeMembers = computed(() => stats.value.total_users || 0)
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'EEEE d MMMM à HH:mm', { locale: fr })
|
||||
return formatFullDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
async function fetchDashboardData() {
|
||||
|
||||
@@ -103,8 +103,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import axios from '@/utils/axios'
|
||||
import LoadingLogo from '@/components/LoadingLogo.vue'
|
||||
|
||||
@@ -154,7 +153,7 @@ function getCategoryBadgeClass(category) {
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
async function fetchInformations() {
|
||||
|
||||
@@ -306,8 +306,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import { Save } from 'lucide-vue-next'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
@@ -419,7 +418,7 @@ function getPriorityBadgeClass(priority) {
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function resetTicketForm() {
|
||||
|
||||
@@ -99,7 +99,9 @@
|
||||
<div
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
class="card p-4 sm:p-6"
|
||||
:id="`post-${post.id}`"
|
||||
class="card p-4 sm:p-6 transition-all duration-300"
|
||||
:class="{ 'ring-2 ring-primary-500 bg-primary-50': route.query.highlight == post.id }"
|
||||
>
|
||||
<!-- Post Header -->
|
||||
<div class="flex items-start space-x-3 mb-4">
|
||||
@@ -282,13 +284,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, 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 { getMediaUrl } from '@/utils/axios'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
Plus,
|
||||
User,
|
||||
@@ -305,6 +307,8 @@ import MentionInput from '@/components/MentionInput.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const posts = ref([])
|
||||
@@ -324,7 +328,7 @@ const newPost = ref({
|
||||
})
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function handleMentionsChanged(mentions) {
|
||||
@@ -514,8 +518,36 @@ function canDeleteComment(comment) {
|
||||
return user.value && (comment.author_id === user.value.id || user.value.is_admin)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPosts()
|
||||
fetchUsers()
|
||||
// Fonction pour scroller vers un post spécifique
|
||||
async function scrollToPost(postId) {
|
||||
await nextTick()
|
||||
const postElement = document.getElementById(`post-${postId}`)
|
||||
if (postElement) {
|
||||
postElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// Retirer le highlight après 3 secondes
|
||||
setTimeout(() => {
|
||||
if (route.query.highlight) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// Watcher pour le highlight dans la query
|
||||
watch(() => route.query.highlight, async (postId) => {
|
||||
if (postId && posts.value.length > 0) {
|
||||
await scrollToPost(parseInt(postId))
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchPosts()
|
||||
await fetchUsers()
|
||||
|
||||
// Si on a un highlight dans la query, scroller vers le post
|
||||
if (route.query.highlight) {
|
||||
await nextTick()
|
||||
await scrollToPost(parseInt(route.query.highlight))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -159,8 +159,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import { User, Camera } from 'lucide-vue-next'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -179,7 +178,7 @@ const form = ref({
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return ''
|
||||
return format(new Date(date), 'MMMM yyyy', { locale: fr })
|
||||
return formatDateInFrenchTimezone(date, 'MMMM yyyy')
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
|
||||
@@ -466,13 +466,9 @@ async function handleInstallApp() {
|
||||
console.error('Erreur lors de l\'installation:', error)
|
||||
}
|
||||
} else if (isMobile.value) {
|
||||
// Sur mobile sans beforeinstallprompt, afficher les instructions
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
if (isIOS) {
|
||||
alert('Sur iOS :\n1. Appuyez sur le bouton de partage (□↑) en bas de l\'écran\n2. Faites défiler et sélectionnez "Sur l\'écran d\'accueil"\n3. Appuyez sur "Ajouter"')
|
||||
} else {
|
||||
alert('Sur Android :\n1. Appuyez sur le menu (⋮) en haut à droite\n2. Sélectionnez "Ajouter à l\'écran d\'accueil" ou "Installer l\'application"\n3. Confirmez l\'installation')
|
||||
}
|
||||
// Sur mobile sans beforeinstallprompt, on ne peut pas installer directement
|
||||
// L'utilisateur devra utiliser le menu une fois connecté
|
||||
console.log('Installation PWA non disponible sur ce navigateur mobile')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDateInFrenchTimezone, formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import { User, ArrowLeft, ArrowRight, MessageSquare, Video, Image, Calendar, Activity } from 'lucide-vue-next'
|
||||
import LoadingLogo from '@/components/LoadingLogo.vue'
|
||||
|
||||
@@ -140,12 +139,12 @@ const recentActivity = ref([])
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return ''
|
||||
return format(new Date(date), 'MMMM yyyy', { locale: fr })
|
||||
return formatDateInFrenchTimezone(date, 'MMMM yyyy')
|
||||
}
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
if (!date) return ''
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function getActivityIcon(type) {
|
||||
|
||||
@@ -228,8 +228,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatDateInFrenchTimezone, formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
@@ -269,11 +268,11 @@ const canEdit = computed(() =>
|
||||
)
|
||||
|
||||
function formatDate(date) {
|
||||
return format(new Date(date), 'dd MMMM yyyy', { locale: fr })
|
||||
return formatDateInFrenchTimezone(date, 'dd MMMM yyyy')
|
||||
}
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
|
||||
@@ -268,8 +268,7 @@ import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from '@/utils/axios'
|
||||
import { getMediaUrl } from '@/utils/axios'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { formatRelativeDateInFrenchTimezone } from '@/utils/dateUtils'
|
||||
import {
|
||||
Plus,
|
||||
Film,
|
||||
@@ -312,7 +311,7 @@ function formatDuration(seconds) {
|
||||
}
|
||||
|
||||
function formatRelativeDate(date) {
|
||||
return formatDistanceToNow(new Date(date), { addSuffix: true, locale: fr })
|
||||
return formatRelativeDateInFrenchTimezone(date)
|
||||
}
|
||||
|
||||
function openVlog(vlog) {
|
||||
|
||||
@@ -179,6 +179,8 @@ module.exports = defineConfig(({ command, mode }) => {
|
||||
navigateFallback: null,
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
// Intégrer le service worker personnalisé
|
||||
importScripts: ['/sw-custom.js'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||
|
||||
Reference in New Issue
Block a user