UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

301 lines (298 loc) 10.4 kB
'use client'; import { useState, useCallback, useMemo } from 'react'; const DEFAULT_OPTIONS = { multiSort: false, maxSortLevels: 3, stable: true, caseSensitive: false, nullsFirst: false }; function useSortableData(data, options = {}) { const finalOptions = { ...DEFAULT_OPTIONS, ...options }; const [sortConfigs, setSortConfigs] = useState(finalOptions.initialSort ? [finalOptions.initialSort] : []); // Get the primary sort config const sortConfig = sortConfigs[0] || { key: null, direction: null }; // Default sort function const defaultSort = useCallback((a, b, key, direction) => { const aValue = finalOptions.getValue ? finalOptions.getValue(a, key) : a[key]; const bValue = finalOptions.getValue ? finalOptions.getValue(b, key) : b[key]; // Handle null/undefined values if (aValue == null && bValue == null) return 0; if (aValue == null) return finalOptions.nullsFirst ? -1 : 1; if (bValue == null) return finalOptions.nullsFirst ? 1 : -1; // String comparison if (typeof aValue === 'string' && typeof bValue === 'string') { const comparison = finalOptions.caseSensitive ? aValue.localeCompare(bValue) : aValue.toLowerCase().localeCompare(bValue.toLowerCase()); return direction === 'asc' ? comparison : -comparison; } // Number/Date comparison if (aValue < bValue) return direction === 'asc' ? -1 : 1; if (aValue > bValue) return direction === 'asc' ? 1 : -1; return 0; }, [finalOptions]); // Sort data based on configurations const sortedData = useMemo(() => { if (!sortConfigs || (sortConfigs?.length || 0) === 0) return data; const sortFn = finalOptions.customSort || defaultSort; return [...data].sort((a, b) => { for (const config of sortConfigs) { if (config.key === null) continue; const result = sortFn(a, b, config.key, config.direction); if (result !== 0) return result; } return 0; }); }, [data, sortConfigs, defaultSort, finalOptions.customSort]); // Sort function const sort = useCallback((key, direction, append = false) => { setSortConfigs(prevConfigs => { // If not appending, replace all configs if (!finalOptions.multiSort || !append) { const newDirection = direction || (prevConfigs[0]?.key === key && prevConfigs[0]?.direction === 'asc' ? 'desc' : 'asc'); return [{ key, direction: newDirection }]; } // Multi-sort logic const existingIndex = prevConfigs.findIndex(config => config.key === key); if (existingIndex >= 0) { // Toggle direction or remove if already desc const currentDirection = prevConfigs[existingIndex].direction; if (currentDirection === 'desc') { // Remove this sort level return prevConfigs.filter((_, index) => index !== existingIndex); } else { // Change to desc const newConfigs = [...prevConfigs]; if (newConfigs) { newConfigs[existingIndex] = { ...newConfigs?.[existingIndex], direction: 'desc' }; } return newConfigs; } } else { // Add new sort level if ((prevConfigs?.length || 0) >= finalOptions.maxSortLevels) { // Remove oldest if at max levels return [...prevConfigs.slice(1), { key, direction: direction || 'asc' }]; } else { return [...prevConfigs, { key, direction: direction || 'asc' }]; } } }); }, [finalOptions.multiSort, finalOptions.maxSortLevels]); // Clear all sorting const clearSort = useCallback(() => { setSortConfigs([]); }, []); // Toggle sort direction const toggleSort = useCallback(key => { const existingConfig = sortConfigs.find(config => config.key === key); if (existingConfig) { sort(key, existingConfig.direction === 'asc' ? 'desc' : 'asc', true); } else { sort(key, 'asc', true); } }, [sortConfigs, sort]); // Check if column is sorted const isSorted = useCallback(key => { return sortConfigs.some(config => config.key === key); }, [sortConfigs]); // Get sort direction for column const getSortDirection = useCallback(key => { const config = sortConfigs.find(config => config.key === key); return config?.direction || null; }, [sortConfigs]); // Get sort priority for column const getSortPriority = useCallback(key => { const index = sortConfigs.findIndex(config => config.key === key); return index >= 0 ? index + 1 : null; }, [sortConfigs]); return { data: sortedData, sortConfig, sortConfigs, sort, clearSort, toggleSort, isSorted, getSortDirection, getSortPriority }; } // Hook for searchable and sortable data function useSearchableSortableData(data, searchFields, options = {}) { const { searchQuery = '', searchDebounce = 300, caseSensitive = false, fuzzy = false, ...sortOptions } = options; const [debouncedQuery, setDebouncedQuery] = useState(searchQuery); // Debounce search query useMemo(() => { if (searchDebounce > 0) { const timer = setTimeout(() => { setDebouncedQuery(searchQuery); }, searchDebounce); return () => clearTimeout(timer); } else { setDebouncedQuery(searchQuery); } }, [searchQuery, searchDebounce]); // Filter data based on search query const filteredData = useMemo(() => { if (!debouncedQuery.trim()) return data; const query = caseSensitive ? debouncedQuery : debouncedQuery.toLowerCase(); return data?.filter(item => { return searchFields.some(field => { const value = String(item[field] || '').trim(); const normalizedValue = caseSensitive ? value : value.toLowerCase(); if (fuzzy) { // Simple fuzzy search return normalizedValue.includes(query); } else { return normalizedValue.includes(query); } }); }); }, [data, debouncedQuery, searchFields, caseSensitive, fuzzy]); // Apply sorting to filtered data const sortableResult = useSortableData(filteredData, sortOptions); return { ...sortableResult, filteredData, searchQuery: debouncedQuery, totalCount: data?.length || 0, filteredCount: filteredData?.length || 0 }; } // Hook for paginated sortable data function usePaginatedSortableData(data, itemsPerPage = 10, options = {}) { const { initialPage = 1, onPageChange, ...sortOptions } = options; const [currentPage, setCurrentPage] = useState(initialPage); const sortableResult = useSortableData(data, sortOptions); // Calculate pagination const totalPages = Math.ceil((sortableResult.data?.length || 0) / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedData = sortableResult.data?.slice(startIndex, endIndex); const goToPage = useCallback(page => { const validPage = Math.max(1, Math.min(page, totalPages)); setCurrentPage(validPage); onPageChange?.(validPage); }, [totalPages, onPageChange]); const nextPage = useCallback(() => { if (currentPage < totalPages) { goToPage(currentPage + 1); } }, [currentPage, totalPages, goToPage]); const previousPage = useCallback(() => { if (currentPage > 1) { goToPage(currentPage - 1); } }, [currentPage, goToPage]); return { ...sortableResult, data: paginatedData, currentPage, totalPages, itemsPerPage, totalItems: sortableResult.data?.length || 0, goToPage, nextPage, previousPage, hasNextPage: currentPage < totalPages, hasPreviousPage: currentPage > 1 }; } // Utility functions for sorting const sortUtils = { // Natural sort for strings with numbers naturalSort: (a, b, direction = 'asc') => { const regex = /(\d+|[^\d]+)/g; const aParts = a.match(regex) || []; const bParts = b.match(regex) || []; for (let i = 0; i < Math.max(aParts?.length || 0, bParts?.length || 0); i++) { const aPart = aParts[i] || ''; const bPart = bParts[i] || ''; const aIsNumber = /^\d+$/.test(aPart); const bIsNumber = /^\d+$/.test(bPart); if (aIsNumber && bIsNumber) { const diff = parseInt(aPart) - parseInt(bPart); if (diff !== 0) return direction === 'asc' ? diff : -diff; } else { const comparison = aPart.localeCompare(bPart); if (comparison !== 0) return direction === 'asc' ? comparison : -comparison; } } return 0; }, // Sort by multiple criteria with weights weightedSort: (items, criteria) => { return [...items].sort((a, b) => { let scoreA = 0; const scoreB = 0; for (const criterion of criteria) { const aValue = a[criterion.key]; const bValue = b?.[criterion.key]; const direction = criterion.direction || 'asc'; const multiplier = direction === 'asc' ? 1 : -1; if (typeof aValue === 'number' && typeof bValue === 'number') { scoreA += (aValue - bValue) * criterion.weight * multiplier; } else if (typeof aValue === 'string' && typeof bValue === 'string') { scoreA += aValue.localeCompare(bValue) * criterion.weight * multiplier; } } return scoreA - scoreB; }); }, // Group sort (sort within groups) groupSort: (items, groupBy, sortBy, direction = 'asc') => { const groups = new Map(); // Group items items.forEach(item => { const groupKey = item?.[groupBy]; if (!groups.has(groupKey)) { groups.set(groupKey, []); } groups.get(groupKey).push(item); }); // Sort within each group groups.forEach(group => { group.sort((a, b) => { const aValue = a[sortBy]; const bValue = b?.[sortBy]; if (aValue != null && bValue != null) { if (aValue < bValue) return direction === 'asc' ? -1 : 1; if (aValue > bValue) return direction === 'asc' ? 1 : -1; } return 0; }); }); // Flatten groups back into array return Array.from(groups.values()).flat(); } }; export { sortUtils, usePaginatedSortableData, useSearchableSortableData, useSortableData }; //# sourceMappingURL=useSortableData.js.map