diff --git a/apps/web/src/features/chat/components/ChatMessages.tsx b/apps/web/src/features/chat/components/ChatMessages.tsx index 8c0f24eee..6f95eb8b3 100644 --- a/apps/web/src/features/chat/components/ChatMessages.tsx +++ b/apps/web/src/features/chat/components/ChatMessages.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useMemo } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { useChatStore } from '../store/chatStore'; import { useUser } from '@/features/auth/hooks/useUser'; import { Card, CardContent } from '@/components/ui/card'; @@ -13,6 +13,34 @@ import { MessageSquare, } from 'lucide-react'; +function formatMessageDate(dateStr: string): string { + const date = new Date(dateStr); + const now = new Date(); + const diffDays = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) + ); + + if (diffDays === 0) return 'Today'; + if (diffDays === 1) return 'Yesterday'; + + return date.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + }); +} + +function DateSeparator({ date }: { date: string }) { + const formatted = formatMessageDate(date); + return ( +
+
+ {formatted} +
+
+ ); +} + export function ChatMessages() { const { currentConversationId, conversations, messages, typingUsers } = useChatStore(); const { data: user } = useUser(); @@ -80,71 +108,81 @@ export function ChatMessages() {

Aucun message dans cette conversation

) : ( - conversationMessages.map((message) => { + conversationMessages.map((message, index) => { const isOwn = message.sender_id === user?.id; + const prevMessage = index > 0 ? conversationMessages[index - 1] : null; + const showDateSeparator = + !prevMessage || + new Date(message.created_at).toDateString() !== + new Date(prevMessage.created_at).toDateString(); + return ( -
-
-
- - {isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`} - - - {new Date(message.created_at).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} - -
- - -

+ + {showDateSeparator && ( + + )} +

+
+
+ + {isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`} + + + {new Date(message.created_at).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + +
+ + +

- {/* Réactions */} - {message.reactions && Object.keys(message.reactions).length > 0 && ( -

- {Object.entries(message.reactions).map(([emoji, userIds]) => ( - - ))} -
- )} -
-
+ {/* Réactions */} + {message.reactions && Object.keys(message.reactions).length > 0 && ( +
+ {Object.entries(message.reactions).map(([emoji, userIds]) => ( + + ))} +
+ )} + + - {/* Actions sur le message */} -
- - - - + {/* Actions sur le message */} +
+ + + + +
-
+ ); }) )} diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx index f2092edd4..70e908e31 100644 --- a/apps/web/src/features/settings/pages/SettingsPage.tsx +++ b/apps/web/src/features/settings/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { useUser } from '@/features/auth/hooks/useUser'; import { usersApi } from '@/services/api/users'; import { UserSettings } from '../types/settings'; @@ -11,6 +11,7 @@ import toast from '@/utils/toast'; import { settingsSchema } from '../schemas/settingsSchema'; import { ErrorDisplay } from '@/components/ui/ErrorDisplay'; import { Save, Shield, Sliders } from 'lucide-react'; +import { useUnsavedChanges, useFormDirtyState } from '@/hooks/useUnsavedChanges'; function SettingsPageSkeleton() { return ( @@ -72,6 +73,13 @@ export function SettingsPage() { const [mutationError, setMutationError] = useState(null); const [retryCount, setRetryCount] = useState(0); const lastMutationRef = useRef<(() => Promise) | null>(null); + const { isDirty, markDirty, markClean } = useFormDirtyState(); + useUnsavedChanges(isDirty); + + const handleSettingsChange = useCallback((newSettings: UserSettings) => { + setSettings(newSettings); + markDirty(); + }, [markDirty]); const loadSettings = async () => { if (!user?.id) { @@ -107,6 +115,7 @@ export function SettingsPage() { setMutationError(null); setRetryCount(0); lastMutationRef.current = null; + markClean(); }; lastMutationRef.current = performMutation; @@ -166,7 +175,7 @@ export function SettingsPage() {

Global Preferences

- +
diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts index caa50ed9d..d25cc2260 100644 --- a/apps/web/src/hooks/index.ts +++ b/apps/web/src/hooks/index.ts @@ -22,6 +22,7 @@ export { useKeyboardNavigation } from './useKeyboardNavigation'; export { useGlobalKeyboardShortcuts } from './useGlobalKeyboardShortcuts'; export { usePreload, usePreloadRoute } from './usePreload'; export { useCopyToClipboard } from './useCopyToClipboard'; +export { useUnsavedChanges, useFormDirtyState } from './useUnsavedChanges'; // Hook types export type { diff --git a/apps/web/src/hooks/useUnsavedChanges.ts b/apps/web/src/hooks/useUnsavedChanges.ts new file mode 100644 index 000000000..60ca0406e --- /dev/null +++ b/apps/web/src/hooks/useUnsavedChanges.ts @@ -0,0 +1,33 @@ +import { useEffect, useCallback, useState } from 'react'; + +/** + * Hook that warns users when they try to navigate away with unsaved changes. + * Shows a browser confirmation dialog on page unload and can be used + * with React Router's navigation blocking. + */ +export function useUnsavedChanges(hasChanges: boolean) { + useEffect(() => { + if (!hasChanges) return; + + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = ''; + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [hasChanges]); +} + +/** + * Hook to track form dirty state. + * Returns isDirty and methods to mark dirty/clean. + */ +export function useFormDirtyState() { + const [isDirty, setIsDirty] = useState(false); + + const markDirty = useCallback(() => setIsDirty(true), []); + const markClean = useCallback(() => setIsDirty(false), []); + + return { isDirty, markDirty, markClean }; +}