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:
parent
5f47cbaa98
commit
a1dea851a2
3 changed files with 127 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue