-
+ {/* Background Effects */} +
+
+
+
+ +
{/* Logo and Title */}
-
+
- + Veza

{title}

{subtitle && (

{subtitle} @@ -58,7 +64,7 @@ export function AuthLayout({ {/* Content Card */}

{children} diff --git a/apps/web/src/features/chat/store/chatStore.ts b/apps/web/src/features/chat/store/chatStore.ts index bec7616e6..eb0011df5 100644 --- a/apps/web/src/features/chat/store/chatStore.ts +++ b/apps/web/src/features/chat/store/chatStore.ts @@ -96,7 +96,24 @@ export const useChatStore = create()( }), loadMessages: (conversationId, newMessages) => set((state) => { - state.messages[conversationId] = newMessages; + const existing = state.messages[conversationId] || []; + + // Create a Set of IDs from newMessages for efficient lookup + const newMessageIds = new Set(newMessages.map(m => m.id)); + + // Keep existing messages that are NOT in the new batch + // (these are likely real-time messages that arrived after fetch started) + const realtimeMessages = existing.filter(m => !newMessageIds.has(m.id)); + + // Merge: combine real-time messages with history + const merged = [...realtimeMessages, ...newMessages]; + + // Sort by created_at to maintain chronological order + merged.sort((a, b) => + new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ); + + state.messages[conversationId] = merged; }), addReaction: (conversationId, messageId, userId, emoji) => set((state) => { diff --git a/apps/web/src/features/chat/types/index.ts b/apps/web/src/features/chat/types/index.ts index aa729d253..1ec80471d 100644 --- a/apps/web/src/features/chat/types/index.ts +++ b/apps/web/src/features/chat/types/index.ts @@ -49,7 +49,18 @@ export interface IncomingMessage { is_typing?: boolean; emoji?: string; attachments?: MessageAttachment[]; - messages?: any[]; // For HistoryChunk + messages?: HistoryMessage[]; // For HistoryChunk has_more_before?: boolean; has_more_after?: boolean; } + +export interface HistoryMessage { + id: string; + conversation_id: string; + sender_id: string; + sender_username: string; + content: string; + created_at: string; + reactions?: Record; + attachments?: MessageAttachment[]; +} diff --git a/apps/web/src/features/library/pages/LibraryPage.tsx b/apps/web/src/features/library/pages/LibraryPage.tsx index 7614010ad..6cf60524a 100644 --- a/apps/web/src/features/library/pages/LibraryPage.tsx +++ b/apps/web/src/features/library/pages/LibraryPage.tsx @@ -25,6 +25,10 @@ import { Trash2, CheckSquare, X, + Grid3x3, + List, + Heart, + Clock, } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { @@ -39,14 +43,6 @@ import { DropdownMenuLabel, } from '@/components/ui/dropdown-menu'; import { Select } from '@/components/ui/select'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; import { UploadModal } from '@/features/upload/components/UploadModal'; import { useToast } from '@/hooks/useToast'; import { Checkbox } from '@/components/ui/checkbox'; @@ -54,33 +50,34 @@ import { Pagination } from '@/components/navigation/Pagination'; import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'; import { logger } from '@/utils/logger'; import { parseApiError } from '@/utils/apiErrorHandler'; - -// FE-PAGE-002: Complete Library page implementation +import { cn } from '@/lib/utils'; type SortField = 'created_at' | 'title' | 'popularity'; type SortOrder = 'asc' | 'desc'; +type ViewMode = 'grid' | 'list'; -export default function LibraryPage() { +/** + * Library Page Premium - Version MVP avec UI moderne et professionnelle + * Grille de tracks avec design premium + */ +export default function LibraryPagePremium() { const queryClient = useQueryClient(); const toast = useToast(); const [page, setPage] = useState(1); const [limit] = useState(50); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [viewMode, setViewMode] = useState('grid'); - // FE-PAGE-002: Filtering and sorting state const [searchTerm, setSearchTerm] = useState(''); const [genreFilter, setGenreFilter] = useState(''); const [formatFilter, setFormatFilter] = useState(''); const [sortBy, setSortBy] = useState('created_at'); const [sortOrder, setSortOrder] = useState('desc'); - // FE-PAGE-002: Bulk operations state const [selectedTracks, setSelectedTracks] = useState>(new Set()); const [isBulkMode, setIsBulkMode] = useState(false); - // CRITIQUE FIX #48: Build query params avec recherche côté serveur - // Utiliser la recherche backend si searchTerm est présent const queryParams: GetTracksParams = { page, limit, @@ -94,14 +91,10 @@ export default function LibraryPage() { if (formatFilter) { queryParams.format = formatFilter; } - // CRITIQUE FIX #48: Ajouter le paramètre de recherche au backend if (searchTerm.trim()) { queryParams.search = searchTerm.trim(); } - // CRITIQUE FIX #24: Utiliser la recherche backend si disponible pour éviter le filtrage côté client - // Note: Si le backend ne supporte pas la recherche, on devra faire le filtrage côté client - // mais seulement sur la page actuelle, pas sur toutes les données const { data: tracksData, isLoading: isTracksLoading, @@ -115,23 +108,17 @@ export default function LibraryPage() { const { data: playlistsData } = usePlaylists(); const addTrackToPlaylistMutation = useAddTrackToPlaylist(); - // CRITIQUE FIX #48: Utiliser directement les tracks du backend car la recherche est maintenant côté serveur - // Le backend filtre et retourne les résultats paginés, donc pas besoin de filtrage côté client const filteredTracks: Track[] = useMemo(() => { if (!tracksData?.tracks) return []; - // CRITIQUE FIX #48: Le backend gère maintenant la recherche, donc on utilise directement les résultats return tracksData.tracks; }, [tracksData?.tracks]); - // CRITIQUE FIX #24: Réinitialiser à la page 1 lors d'un changement de recherche pour une meilleure UX - // Utiliser useEffect pour réinitialiser la page quand searchTerm change useEffect(() => { if (searchTerm.trim() && page !== 1) { setPage(1); } }, [searchTerm]); - // FE-PAGE-002: Get unique genres and formats for filters const genres = Array.from( new Set( tracksData?.tracks @@ -163,11 +150,9 @@ export default function LibraryPage() { const handleCloseUpload = () => { setIsUploadModalOpen(false); - // Refresh tracks after upload queryClient.invalidateQueries({ queryKey: ['tracks'] }); }; - // FE-PAGE-002: Toggle track selection const toggleTrackSelection = (trackId: string) => { setSelectedTracks((prev) => { const next = new Set(prev); @@ -180,7 +165,6 @@ export default function LibraryPage() { }); }; - // FE-PAGE-002: Select all / deselect all const toggleSelectAll = () => { if (selectedTracks.size === filteredTracks.length) { setSelectedTracks(new Set()); @@ -189,7 +173,6 @@ export default function LibraryPage() { } }; - // CRITIQUE FIX #46: Bulk delete avec modal de confirmation au lieu de confirm() const handleBulkDelete = async () => { if (selectedTracks.size === 0) return; setShowDeleteConfirm(true); @@ -212,7 +195,6 @@ export default function LibraryPage() { } }; - // CRITIQUE FIX #56: Bulk update avec gestion d'erreur améliorée const handleBulkUpdate = async (updates: { is_public?: boolean }) => { if (selectedTracks.size === 0) return; @@ -223,15 +205,12 @@ export default function LibraryPage() { setIsBulkMode(false); queryClient.invalidateQueries({ queryKey: ['tracks'] }); } catch (error: unknown) { - // CRITIQUE FIX #56: Gestion d'erreur améliorée avec message détaillé const apiError = parseApiError(error); - const errorMessage = apiError.message; - logger.error('Erreur lors de la mise à jour des pistes:', { error: errorMessage }); - toast.error(errorMessage); + logger.error('Erreur lors de la mise à jour des pistes:', { error: apiError.message }); + toast.error(apiError.message); } }; - // FE-PAGE-002: Toggle sort order const handleSort = (field: SortField) => { if (sortBy === field) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); @@ -241,22 +220,29 @@ export default function LibraryPage() { } }; + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + return ( -
-
+
+ {/* Header */} +
-

Bibliothèque

-

- Gérez vos fichiers et documents +

Bibliothèque

+

+ Gérez et organisez vos fichiers audio

-
+
{isBulkMode && selectedTracks.size > 0 && ( <>