library: move filters to sidebar

- Restructured layout to use flex with sidebar and main content
- Moved filters (search, genre, format, sort) to Sidebar component
- Sidebar positioned on left, collapsible, open by default
- Main content area now uses flex-1 for better space utilization
- Filters organized vertically with labels for better UX
- Task 7.4.1.2 complete
This commit is contained in:
senke 2026-01-16 00:30:04 +01:00
parent 5f47cbaa98
commit a1dea851a2
3 changed files with 127 additions and 80 deletions

View file

@ -2691,11 +2691,30 @@ Critical path dependencies:
- **Reusability**: Can be used for filters, additional content, or any sidebar-worthy information
- **Rollback**: Delete component
- [ ] **Action 7.4.1.2**: Move filters to sidebar or collapsible section
- **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx:317-383` - Move to sidebar
- **Dependencies**: Action 7.4.1.1 complete
- [x] **Action 7.4.1.2**: Move filters to sidebar or collapsible section
- **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx:397-496` - Move to sidebar
- **Dependencies**: Action 7.4.1.1 complete
- **Risk**: MEDIUM (layout change)
- **Validation**: Filters in sidebar, main area for content
- **Validation**: ✅ Filters moved to sidebar:
- **Layout restructure**: Changed from vertical stack to flex layout with sidebar and main content
- **Sidebar implementation**:
- Filters moved to Sidebar component on the left
- Sidebar includes: search input, genre filter, format filter, sort dropdown
- Collapsible functionality (open by default)
- Title "Filtres" with Filter icon
- Width: w-64 (256px)
- **Main content**:
- Tracks display now in flex-1 container (takes remaining space)
- Filters no longer take full width
- More space for track grid/list
- **User experience**:
- Filters accessible but don't dominate the page
- Sidebar can be collapsed to maximize content area
- Better use of horizontal space
- **Styling**:
- Filters organized vertically in sidebar with labels
- Improved spacing and hierarchy
- Maintains all filter functionality
- **Rollback**: Restore full-width filters
- [ ] **Action 7.4.1.3**: Make sidebar collapsible

View file

@ -136,10 +136,10 @@ export function Sidebar({
<aside
className={cn(
'relative flex flex-col transition-all duration-300 ease-in-out',
'flex flex-col transition-all duration-300 ease-in-out',
position === 'left' ? 'border-r' : 'border-l',
'border-white/10 bg-kodo-ink/40 backdrop-blur-md',
isCollapsed ? (position === 'left' ? '-ml-64' : '-mr-64') : width,
'border-white/10 bg-kodo-ink/40 backdrop-blur-md rounded-xl',
isCollapsed ? 'w-0 overflow-hidden' : width,
className,
)}
>

View file

@ -50,6 +50,7 @@ import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
import { Pagination } from '@/components/navigation/Pagination';
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
import { LoadingState } from '@/components/ui/LoadingState';
import { Sidebar } from '@/components/ui/Sidebar';
import { logger } from '@/utils/logger';
import { parseApiError } from '@/utils/apiErrorHandler';
import { cn } from '@/lib/utils';
@ -83,6 +84,7 @@ export default function LibraryPagePremium() {
const [mutationError, setMutationError] = useState<Error | null>(null);
const [retryCount, setRetryCount] = useState(0);
const lastMutationRef = useRef<(() => Promise<void>) | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(true);
const queryParams: GetTracksParams = {
limit,
@ -394,84 +396,108 @@ export default function LibraryPagePremium() {
</div>
</div>
{/* Filters */}
<Card>
<CardContent className="p-4 space-y-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-kodo-secondary" />
<Input
placeholder="Rechercher dans la bibliothèque..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-kodo-secondary" />
<Select
options={[
{ value: '', label: 'Tous les genres' },
...genres.map((genre) => ({ value: genre, label: genre })),
]}
value={genreFilter}
onChange={(value) =>
setGenreFilter(Array.isArray(value) ? value[0] : value)
}
placeholder="Tous les genres"
className="w-[180px]"
{/* Layout with Sidebar and Main Content */}
<div className="flex gap-6">
{/* Filters Sidebar */}
<Sidebar
position="left"
width="w-64"
open={sidebarOpen}
onOpenChange={setSidebarOpen}
collapsible
title="Filtres"
icon={<Filter className="w-4 h-4" />}
className="h-fit"
>
<div className="space-y-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-kodo-secondary" />
<Input
placeholder="Rechercher dans la bibliothèque..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select
options={[
{ value: '', label: 'Tous les formats' },
...formats.map((format) => ({ value: format, label: format })),
]}
value={formatFilter}
onChange={(value) =>
setFormatFilter(Array.isArray(value) ? value[0] : value)
}
placeholder="Tous les formats"
className="w-[180px]"
/>
<div className="flex items-center gap-2 ml-auto">
<span className="text-sm text-kodo-secondary">Trier par:</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<ArrowUpDown className="mr-2 h-4 w-4" />
{sortBy === 'created_at'
? 'Date'
: sortBy === 'title'
? 'Titre'
: 'Popularité'}
{sortOrder === 'asc' ? ' ↑' : ' ↓'}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Trier par</DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleSort('created_at')}>
Date{' '}
{sortBy === 'created_at' &&
(sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSort('title')}>
Titre{' '}
{sortBy === 'title' && (sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSort('popularity')}>
Popularité{' '}
{sortBy === 'popularity' &&
(sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="space-y-3">
<div className="space-y-2">
<label className="text-xs font-medium text-kodo-secondary uppercase tracking-wider">
Genre
</label>
<Select
options={[
{ value: '', label: 'Tous les genres' },
...genres.map((genre) => ({ value: genre, label: genre })),
]}
value={genreFilter}
onChange={(value) =>
setGenreFilter(Array.isArray(value) ? value[0] : value)
}
placeholder="Tous les genres"
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-kodo-secondary uppercase tracking-wider">
Format
</label>
<Select
options={[
{ value: '', label: 'Tous les formats' },
...formats.map((format) => ({ value: format, label: format })),
]}
value={formatFilter}
onChange={(value) =>
setFormatFilter(Array.isArray(value) ? value[0] : value)
}
placeholder="Tous les formats"
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-kodo-secondary uppercase tracking-wider">
Trier par
</label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between">
<span className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
{sortBy === 'created_at'
? 'Date'
: sortBy === 'title'
? 'Titre'
: 'Popularité'}
{sortOrder === 'asc' ? ' ↑' : ' ↓'}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Trier par</DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleSort('created_at')}>
Date{' '}
{sortBy === 'created_at' &&
(sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSort('title')}>
Titre{' '}
{sortBy === 'title' && (sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSort('popularity')}>
Popularité{' '}
{sortBy === 'popularity' &&
(sortOrder === 'asc' ? '↑' : '↓')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</CardContent>
</Card>
</Sidebar>
{/* Tracks Display */}
{/* Main Content */}
<div className="flex-1 min-w-0">
{/* Tracks Display */}
{isTracksLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array(8)
@ -752,6 +778,8 @@ export default function LibraryPagePremium() {
confirmLabel="Supprimer"
variant="destructive"
/>
</div>
</div>
</div>
);
}