diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index e607a5dd8..4a98e6868 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -7331,8 +7331,12 @@ "description": "Add reusable filter and sort components", "owner": "frontend", "estimated_hours": 6, - "status": "todo", - "files_involved": [], + "status": "completed", + "files_involved": [ + "apps/web/src/components/filters/Sort.tsx", + "apps/web/src/components/filters/FilterBar.tsx", + "apps/web/src/components/filters/index.ts" + ], "implementation_steps": [ { "step": 1, @@ -7352,7 +7356,9 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completed_at": "2025-12-25T12:00:00.000Z", + "implementation_notes": "Created reusable filter and sort UI components. Sort component provides generic sorting functionality with field selection and order toggle (asc/desc), with optional localStorage persistence. FilterBar component combines Filters and Sort in a collapsible bar for better UX. Components are exported from filters/index.ts for easy import. Existing Filters component already provides comprehensive filtering capabilities (select, checkbox, range, date). All components follow consistent design patterns and are fully reusable across the application." }, { "id": "FE-COMP-008", diff --git a/apps/web/src/components/filters/FilterBar.tsx b/apps/web/src/components/filters/FilterBar.tsx new file mode 100644 index 000000000..385297b42 --- /dev/null +++ b/apps/web/src/components/filters/FilterBar.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; +import { Filter, X } from 'lucide-react'; +import { Filters, type FilterOption, type FiltersProps } from './Filters'; +import { Sort, type SortProps } from './Sort'; + +/** + * FE-COMP-007: FilterBar component combining filters and sort in a collapsible bar + */ + +export interface FilterBarProps { + filters?: FiltersProps; + sort?: SortProps; + className?: string; + collapsible?: boolean; + defaultOpen?: boolean; +} + +/** + * FilterBar component that combines Filters and Sort components + */ +export function FilterBar({ + filters, + sort, + className, + collapsible = true, + defaultOpen = false, +}: FilterBarProps) { + const [isOpen, setIsOpen] = useState(!collapsible || defaultOpen); + + const hasFilters = filters && filters.filters.length > 0; + const hasSort = sort !== undefined; + + if (!hasFilters && !hasSort) { + return null; + } + + return ( + + + {collapsible && ( +
+
+ + Filtres et tri +
+ +
+ )} + + {(!collapsible || isOpen) && ( +
+ {/* Filters */} + {hasFilters && ( +
+ +
+ )} + + {/* Sort */} + {hasSort && ( +
+ Trier par: + +
+ )} +
+ )} +
+
+ ); +} + diff --git a/apps/web/src/components/filters/Sort.tsx b/apps/web/src/components/filters/Sort.tsx new file mode 100644 index 000000000..252d05e61 --- /dev/null +++ b/apps/web/src/components/filters/Sort.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Select } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; + +/** + * FE-COMP-007: Reusable Sort component for consistent sorting UI across all pages + */ + +export interface SortOption { + value: string; + label: string; + icon?: React.ElementType; +} + +export interface SortProps { + sortBy: string; + sortOrder: 'asc' | 'desc'; + onSortChange: (sortBy: string, sortOrder: 'asc' | 'desc') => void; + options: SortOption[]; + className?: string; + showOrderToggle?: boolean; + persistPreference?: boolean; + storageKey?: string; +} + +/** + * Generic Sort component for sorting lists + */ +export function Sort({ + sortBy, + sortOrder, + onSortChange, + options, + className, + showOrderToggle = true, + persistPreference = false, + storageKey = 'sortOptions', +}: SortProps) { + const [localSortBy, setLocalSortBy] = useState(sortBy); + const [localSortOrder, setLocalSortOrder] = useState<'asc' | 'desc'>(sortOrder); + + // Load preference from localStorage on mount if persistPreference is enabled + useEffect(() => { + if (persistPreference && typeof window !== 'undefined') { + const stored = localStorage.getItem(storageKey); + if (stored) { + try { + const parsed = JSON.parse(stored) as { sortBy: string; sortOrder: 'asc' | 'desc' }; + if (parsed.sortBy && parsed.sortOrder) { + setLocalSortBy(parsed.sortBy); + setLocalSortOrder(parsed.sortOrder); + onSortChange(parsed.sortBy, parsed.sortOrder); + } + } catch (error) { + console.error('Error parsing stored sort options:', error); + } + } + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Update local state when props change + useEffect(() => { + setLocalSortBy(sortBy); + setLocalSortOrder(sortOrder); + }, [sortBy, sortOrder]); + + // Persist preference to localStorage + useEffect(() => { + if (persistPreference && typeof window !== 'undefined') { + localStorage.setItem( + storageKey, + JSON.stringify({ sortBy: localSortBy, sortOrder: localSortOrder }), + ); + } + }, [localSortBy, localSortOrder, persistPreference, storageKey]); + + const handleSortByChange = useCallback( + (value: string | string[]) => { + const newSortBy = Array.isArray(value) ? value[0] : value; + setLocalSortBy(newSortBy); + onSortChange(newSortBy, localSortOrder); + }, + [localSortOrder, onSortChange], + ); + + const handleOrderToggle = useCallback(() => { + const newOrder = localSortOrder === 'asc' ? 'desc' : 'asc'; + setLocalSortOrder(newOrder); + onSortChange(localSortBy, newOrder); + }, [localSortBy, localSortOrder, onSortChange]); + + const selectedOption = options.find((opt) => opt.value === localSortBy); + const Icon = selectedOption?.icon || ArrowUpDown; + + return ( +
+ {/* Sort field selector */} +
+ +