refactor(library): LibraryManager module with hook, subcomponents, skeleton
- Add library-manager/ with useLibraryManager, Header, Toolbar, Error, Empty, Content, Stats, Skeleton
- Layout min-h-layout-page (no arbitrary h-[600px])
- Props tracksOverride, errorOverride, isLoadingOverride for Storybook
- Re-export from LibraryManager.tsx
- Stories: Default, Loading (Skeleton), Empty, Error
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 23:00:35 +00:00
|
|
|
/**
|
|
|
|
|
* LibraryManager — fetch, state, handlers.
|
|
|
|
|
*/
|
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { apiClient } from '@/services/api/client';
|
|
|
|
|
import type { Track as ApiTrack } from '@/features/tracks/types/track';
|
|
|
|
|
import type { Track as PlayerTrack } from '@/features/player/types';
|
|
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
import { logger } from '@/utils/logger';
|
|
|
|
|
import { parseApiError } from '@/utils/apiErrorHandler';
|
|
|
|
|
|
|
|
|
|
export interface UseLibraryManagerOverrides {
|
|
|
|
|
tracksOverride?: ApiTrack[] | null;
|
|
|
|
|
isLoadingOverride?: boolean;
|
|
|
|
|
errorOverride?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useLibraryManager(
|
|
|
|
|
onTrackSelect?: (track: ApiTrack) => void,
|
|
|
|
|
overrides?: UseLibraryManagerOverrides,
|
|
|
|
|
) {
|
|
|
|
|
const [tracks, setTracks] = useState<ApiTrack[]>([]);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
|
|
|
const [filterType, setFilterType] = useState<string>('all');
|
|
|
|
|
const [pagination, setPagination] = useState({
|
|
|
|
|
page: 1,
|
|
|
|
|
limit: 20,
|
|
|
|
|
total: 0,
|
|
|
|
|
});
|
|
|
|
|
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
|
|
|
|
const [playingTrackId, setPlayingTrackId] = useState<string | null>(null);
|
|
|
|
|
const { info: showInfo } = useToast();
|
|
|
|
|
|
|
|
|
|
const fetchTracks = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
const response = await apiClient.get<{
|
|
|
|
|
data: ApiTrack[];
|
|
|
|
|
total: number;
|
|
|
|
|
page: number;
|
|
|
|
|
limit: number;
|
|
|
|
|
}>('/tracks', {
|
|
|
|
|
params: {
|
|
|
|
|
page: pagination.page,
|
|
|
|
|
limit: pagination.limit,
|
|
|
|
|
search: searchQuery || undefined,
|
|
|
|
|
artist: filterType !== 'all' ? filterType : undefined,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const data = response.data;
|
|
|
|
|
setTracks(data.data ?? []);
|
|
|
|
|
setPagination((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
total: data.total ?? 0,
|
|
|
|
|
}));
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
const apiError = parseApiError(err);
|
|
|
|
|
setError(apiError.message);
|
|
|
|
|
logger.error('Error fetching tracks:', { message: apiError.message });
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [pagination.page, pagination.limit, searchQuery, filterType]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (overrides?.tracksOverride !== undefined) {
|
|
|
|
|
setTracks(overrides.tracksOverride ?? []);
|
|
|
|
|
setPagination((p) => ({ ...p, total: (overrides.tracksOverride ?? []).length }));
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
setError(overrides.errorOverride ?? null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (overrides?.errorOverride != null) {
|
|
|
|
|
setError(overrides.errorOverride);
|
|
|
|
|
setTracks([]);
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fetchTracks();
|
|
|
|
|
}, [fetchTracks, overrides?.tracksOverride, overrides?.errorOverride]);
|
|
|
|
|
|
|
|
|
|
const handleUploadComplete = useCallback(() => {
|
|
|
|
|
fetchTracks();
|
|
|
|
|
}, [fetchTracks]);
|
|
|
|
|
|
|
|
|
|
const handleEditTrack = useCallback(
|
|
|
|
|
(_track: PlayerTrack) => {
|
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
|
|
|
showInfo('Edit functionality coming soon — available in the next release');
|
refactor(library): LibraryManager module with hook, subcomponents, skeleton
- Add library-manager/ with useLibraryManager, Header, Toolbar, Error, Empty, Content, Stats, Skeleton
- Layout min-h-layout-page (no arbitrary h-[600px])
- Props tracksOverride, errorOverride, isLoadingOverride for Storybook
- Re-export from LibraryManager.tsx
- Stories: Default, Loading (Skeleton), Empty, Error
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 23:00:35 +00:00
|
|
|
},
|
|
|
|
|
[showInfo],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handlePlayTrack = useCallback(
|
|
|
|
|
(track: PlayerTrack) => {
|
|
|
|
|
setPlayingTrackId(track.id);
|
|
|
|
|
const originalTrack = tracks.find((t) => t.id === track.id);
|
|
|
|
|
if (originalTrack) {
|
|
|
|
|
onTrackSelect?.(originalTrack);
|
|
|
|
|
showInfo(`Now playing: ${track.title} by ${track.artist}`);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[tracks, onTrackSelect, showInfo],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapToPlayerTrack = useCallback((track: ApiTrack): PlayerTrack => {
|
|
|
|
|
const t = track as ApiTrack & {
|
|
|
|
|
album?: string;
|
|
|
|
|
cover_art_path?: string;
|
|
|
|
|
stream_manifest_url?: string;
|
|
|
|
|
genre?: string;
|
|
|
|
|
};
|
|
|
|
|
return {
|
|
|
|
|
id: track.id,
|
|
|
|
|
title: track.title,
|
|
|
|
|
artist: track.artist,
|
|
|
|
|
album: t.album,
|
|
|
|
|
duration: track.duration,
|
feat: frontend pages and feature modules polish
Update dashboard (stats, recent tracks/activity), discover, distribution,
education, feed, subscription, support, search, settings, live, cloud,
analytics, auth, chat, social, tracks, playlists, presence, upload,
and library manager. Consistent UI patterns and error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:21 +00:00
|
|
|
url: t.stream_manifest_url || `/api/v1/tracks/${track.id}/download`,
|
refactor(library): LibraryManager module with hook, subcomponents, skeleton
- Add library-manager/ with useLibraryManager, Header, Toolbar, Error, Empty, Content, Stats, Skeleton
- Layout min-h-layout-page (no arbitrary h-[600px])
- Props tracksOverride, errorOverride, isLoadingOverride for Storybook
- Re-export from LibraryManager.tsx
- Stories: Default, Loading (Skeleton), Empty, Error
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 23:00:35 +00:00
|
|
|
cover: t.cover_art_path,
|
|
|
|
|
genre: t.genre,
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const playerTracks = tracks.map(mapToPlayerTrack);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tracks,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
searchQuery,
|
|
|
|
|
setSearchQuery,
|
|
|
|
|
viewMode,
|
|
|
|
|
setViewMode,
|
|
|
|
|
filterType,
|
|
|
|
|
setFilterType,
|
|
|
|
|
pagination,
|
|
|
|
|
isUploadModalOpen,
|
|
|
|
|
setIsUploadModalOpen,
|
|
|
|
|
playingTrackId,
|
|
|
|
|
playerTracks,
|
|
|
|
|
handleUploadComplete,
|
|
|
|
|
handleEditTrack,
|
|
|
|
|
handlePlayTrack,
|
|
|
|
|
};
|
|
|
|
|
}
|