mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 10:51:12 -05:00
feat: Extract moderation filter and sort logic
This commit is contained in:
@@ -8,3 +8,12 @@
|
|||||||
export { useEntityCache } from './useEntityCache';
|
export { useEntityCache } from './useEntityCache';
|
||||||
export { useProfileCache } from './useProfileCache';
|
export { useProfileCache } from './useProfileCache';
|
||||||
export type { CachedProfile } from './useProfileCache';
|
export type { CachedProfile } from './useProfileCache';
|
||||||
|
|
||||||
|
export { useModerationFilters } from './useModerationFilters';
|
||||||
|
export type { ModerationFilters, ModerationFiltersConfig } from './useModerationFilters';
|
||||||
|
|
||||||
|
export { useModerationSort } from './useModerationSort';
|
||||||
|
export type { ModerationSort, ModerationSortConfig } from './useModerationSort';
|
||||||
|
|
||||||
|
export { usePagination } from './usePagination';
|
||||||
|
export type { PaginationState, PaginationConfig } from './usePagination';
|
||||||
|
|||||||
191
src/hooks/moderation/useModerationFilters.ts
Normal file
191
src/hooks/moderation/useModerationFilters.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Moderation Queue Filters Hook
|
||||||
|
*
|
||||||
|
* Manages filter state for the moderation queue, including:
|
||||||
|
* - Entity type filtering (all, reviews, submissions, photos)
|
||||||
|
* - Status filtering (pending, approved, rejected, etc.)
|
||||||
|
* - Tab management (main queue vs archive)
|
||||||
|
* - Filter persistence and clearing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
|
import type { EntityFilter, StatusFilter, QueueTab } from '@/types/moderation';
|
||||||
|
|
||||||
|
export interface ModerationFiltersConfig {
|
||||||
|
/** Initial entity filter */
|
||||||
|
initialEntityFilter?: EntityFilter;
|
||||||
|
|
||||||
|
/** Initial status filter */
|
||||||
|
initialStatusFilter?: StatusFilter;
|
||||||
|
|
||||||
|
/** Initial active tab */
|
||||||
|
initialTab?: QueueTab;
|
||||||
|
|
||||||
|
/** Debounce delay for filter changes (ms) */
|
||||||
|
debounceDelay?: number;
|
||||||
|
|
||||||
|
/** Whether to persist filters to localStorage */
|
||||||
|
persist?: boolean;
|
||||||
|
|
||||||
|
/** localStorage key prefix for persistence */
|
||||||
|
storageKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModerationFilters {
|
||||||
|
/** Current entity type filter */
|
||||||
|
entityFilter: EntityFilter;
|
||||||
|
|
||||||
|
/** Current status filter */
|
||||||
|
statusFilter: StatusFilter;
|
||||||
|
|
||||||
|
/** Current active tab */
|
||||||
|
activeTab: QueueTab;
|
||||||
|
|
||||||
|
/** Debounced entity filter (for API calls) */
|
||||||
|
debouncedEntityFilter: EntityFilter;
|
||||||
|
|
||||||
|
/** Debounced status filter (for API calls) */
|
||||||
|
debouncedStatusFilter: StatusFilter;
|
||||||
|
|
||||||
|
/** Set entity filter */
|
||||||
|
setEntityFilter: (filter: EntityFilter) => void;
|
||||||
|
|
||||||
|
/** Set status filter */
|
||||||
|
setStatusFilter: (filter: StatusFilter) => void;
|
||||||
|
|
||||||
|
/** Set active tab */
|
||||||
|
setActiveTab: (tab: QueueTab) => void;
|
||||||
|
|
||||||
|
/** Reset all filters to defaults */
|
||||||
|
clearFilters: () => void;
|
||||||
|
|
||||||
|
/** Check if any non-default filters are active */
|
||||||
|
hasActiveFilters: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing moderation queue filters
|
||||||
|
*
|
||||||
|
* @param config - Configuration options
|
||||||
|
* @returns Filter state and actions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const filters = useModerationFilters({
|
||||||
|
* persist: true,
|
||||||
|
* debounceDelay: 300
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Use in component
|
||||||
|
* <Select value={filters.entityFilter} onValueChange={filters.setEntityFilter}>
|
||||||
|
* ...
|
||||||
|
* </Select>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useModerationFilters(config: ModerationFiltersConfig = {}): ModerationFilters {
|
||||||
|
const {
|
||||||
|
initialEntityFilter = 'all',
|
||||||
|
initialStatusFilter = 'pending',
|
||||||
|
initialTab = 'mainQueue',
|
||||||
|
debounceDelay = 300,
|
||||||
|
persist = true,
|
||||||
|
storageKey = 'moderationQueue_filters',
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Load persisted filters on mount
|
||||||
|
const loadPersistedFilters = useCallback(() => {
|
||||||
|
if (!persist) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (saved) {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load persisted filters:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [persist, storageKey]);
|
||||||
|
|
||||||
|
const persisted = loadPersistedFilters();
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
const [entityFilter, setEntityFilterState] = useState<EntityFilter>(
|
||||||
|
persisted?.entityFilter || initialEntityFilter
|
||||||
|
);
|
||||||
|
const [statusFilter, setStatusFilterState] = useState<StatusFilter>(
|
||||||
|
persisted?.statusFilter || initialStatusFilter
|
||||||
|
);
|
||||||
|
const [activeTab, setActiveTabState] = useState<QueueTab>(
|
||||||
|
persisted?.activeTab || initialTab
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounced filters for API calls
|
||||||
|
const debouncedEntityFilter = useDebounce(entityFilter, debounceDelay);
|
||||||
|
const debouncedStatusFilter = useDebounce(statusFilter, debounceDelay);
|
||||||
|
|
||||||
|
// Persist filters to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (persist) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
storageKey,
|
||||||
|
JSON.stringify({
|
||||||
|
entityFilter,
|
||||||
|
statusFilter,
|
||||||
|
activeTab,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist filters:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [entityFilter, statusFilter, activeTab, persist, storageKey]);
|
||||||
|
|
||||||
|
// Set entity filter with logging
|
||||||
|
const setEntityFilter = useCallback((filter: EntityFilter) => {
|
||||||
|
console.log('🔍 Entity filter changed:', filter);
|
||||||
|
setEntityFilterState(filter);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set status filter with logging
|
||||||
|
const setStatusFilter = useCallback((filter: StatusFilter) => {
|
||||||
|
console.log('🔍 Status filter changed:', filter);
|
||||||
|
setStatusFilterState(filter);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set active tab with logging
|
||||||
|
const setActiveTab = useCallback((tab: QueueTab) => {
|
||||||
|
console.log('🔍 Tab changed:', tab);
|
||||||
|
setActiveTabState(tab);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
const clearFilters = useCallback(() => {
|
||||||
|
console.log('🔍 Filters cleared');
|
||||||
|
setEntityFilterState(initialEntityFilter);
|
||||||
|
setStatusFilterState(initialStatusFilter);
|
||||||
|
setActiveTabState(initialTab);
|
||||||
|
}, [initialEntityFilter, initialStatusFilter, initialTab]);
|
||||||
|
|
||||||
|
// Check if non-default filters are active
|
||||||
|
const hasActiveFilters =
|
||||||
|
entityFilter !== initialEntityFilter ||
|
||||||
|
statusFilter !== initialStatusFilter ||
|
||||||
|
activeTab !== initialTab;
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityFilter,
|
||||||
|
statusFilter,
|
||||||
|
activeTab,
|
||||||
|
debouncedEntityFilter,
|
||||||
|
debouncedStatusFilter,
|
||||||
|
setEntityFilter,
|
||||||
|
setStatusFilter,
|
||||||
|
setActiveTab,
|
||||||
|
clearFilters,
|
||||||
|
hasActiveFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
145
src/hooks/moderation/useModerationSort.ts
Normal file
145
src/hooks/moderation/useModerationSort.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Moderation Queue Sort Hook
|
||||||
|
*
|
||||||
|
* Manages sort configuration for the moderation queue with persistence.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import type { SortConfig, SortField, SortDirection } from '@/types/moderation';
|
||||||
|
import {
|
||||||
|
getDefaultSortConfig,
|
||||||
|
loadSortConfig,
|
||||||
|
saveSortConfig,
|
||||||
|
toggleSortDirection as toggleDirection,
|
||||||
|
isDefaultSortConfig,
|
||||||
|
} from '@/lib/moderation/sorting';
|
||||||
|
|
||||||
|
export interface ModerationSortConfig {
|
||||||
|
/** Initial sort configuration */
|
||||||
|
initialConfig?: SortConfig;
|
||||||
|
|
||||||
|
/** Whether to persist sort config */
|
||||||
|
persist?: boolean;
|
||||||
|
|
||||||
|
/** localStorage key for persistence */
|
||||||
|
storageKey?: string;
|
||||||
|
|
||||||
|
/** Callback when sort config changes */
|
||||||
|
onChange?: (config: SortConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModerationSort {
|
||||||
|
/** Current sort configuration */
|
||||||
|
config: SortConfig;
|
||||||
|
|
||||||
|
/** Sort field */
|
||||||
|
field: SortField;
|
||||||
|
|
||||||
|
/** Sort direction */
|
||||||
|
direction: SortDirection;
|
||||||
|
|
||||||
|
/** Set sort field */
|
||||||
|
setField: (field: SortField) => void;
|
||||||
|
|
||||||
|
/** Set sort direction */
|
||||||
|
setDirection: (direction: SortDirection) => void;
|
||||||
|
|
||||||
|
/** Toggle sort direction */
|
||||||
|
toggleDirection: () => void;
|
||||||
|
|
||||||
|
/** Set both field and direction */
|
||||||
|
setConfig: (config: SortConfig) => void;
|
||||||
|
|
||||||
|
/** Reset to default */
|
||||||
|
reset: () => void;
|
||||||
|
|
||||||
|
/** Check if using default config */
|
||||||
|
isDefault: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing moderation queue sort configuration
|
||||||
|
*
|
||||||
|
* @param config - Configuration options
|
||||||
|
* @returns Sort state and actions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const sort = useModerationSort({
|
||||||
|
* persist: true,
|
||||||
|
* onChange: (config) => fetchItems(config)
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Use in component
|
||||||
|
* <Select value={sort.field} onValueChange={sort.setField}>
|
||||||
|
* <SelectItem value="created_at">Date</SelectItem>
|
||||||
|
* <SelectItem value="username">User</SelectItem>
|
||||||
|
* </Select>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useModerationSort(config: ModerationSortConfig = {}): ModerationSort {
|
||||||
|
const {
|
||||||
|
initialConfig,
|
||||||
|
persist = true,
|
||||||
|
storageKey = 'moderationQueue_sortConfig',
|
||||||
|
onChange,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Load persisted or use initial/default config
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
|
||||||
|
if (initialConfig) return initialConfig;
|
||||||
|
if (persist) return loadSortConfig(storageKey);
|
||||||
|
return getDefaultSortConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (persist) {
|
||||||
|
saveSortConfig(sortConfig, storageKey);
|
||||||
|
}
|
||||||
|
onChange?.(sortConfig);
|
||||||
|
}, [sortConfig, persist, storageKey, onChange]);
|
||||||
|
|
||||||
|
// Set sort field (keep direction)
|
||||||
|
const setField = useCallback((field: SortField) => {
|
||||||
|
setSortConfig((prev) => ({ ...prev, field }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set sort direction (keep field)
|
||||||
|
const setDirection = useCallback((direction: SortDirection) => {
|
||||||
|
setSortConfig((prev) => ({ ...prev, direction }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle sort direction
|
||||||
|
const toggleSortDirection = useCallback(() => {
|
||||||
|
setSortConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
direction: toggleDirection(prev.direction),
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set entire config
|
||||||
|
const setConfig = useCallback((newConfig: SortConfig) => {
|
||||||
|
setSortConfig(newConfig);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset to default
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setSortConfig(getDefaultSortConfig());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check if using default config
|
||||||
|
const isDefault = isDefaultSortConfig(sortConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: sortConfig,
|
||||||
|
field: sortConfig.field,
|
||||||
|
direction: sortConfig.direction,
|
||||||
|
setField,
|
||||||
|
setDirection,
|
||||||
|
toggleDirection: toggleSortDirection,
|
||||||
|
setConfig,
|
||||||
|
reset,
|
||||||
|
isDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
253
src/hooks/moderation/usePagination.ts
Normal file
253
src/hooks/moderation/usePagination.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* Pagination Hook
|
||||||
|
*
|
||||||
|
* Manages pagination state and actions for the moderation queue.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
export interface PaginationConfig {
|
||||||
|
/** Initial page number (1-indexed) */
|
||||||
|
initialPage?: number;
|
||||||
|
|
||||||
|
/** Initial page size */
|
||||||
|
initialPageSize?: number;
|
||||||
|
|
||||||
|
/** Whether to persist pagination state */
|
||||||
|
persist?: boolean;
|
||||||
|
|
||||||
|
/** localStorage key for persistence */
|
||||||
|
storageKey?: string;
|
||||||
|
|
||||||
|
/** Callback when page changes */
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
|
|
||||||
|
/** Callback when page size changes */
|
||||||
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationState {
|
||||||
|
/** Current page (1-indexed) */
|
||||||
|
currentPage: number;
|
||||||
|
|
||||||
|
/** Items per page */
|
||||||
|
pageSize: number;
|
||||||
|
|
||||||
|
/** Total number of items */
|
||||||
|
totalCount: number;
|
||||||
|
|
||||||
|
/** Total number of pages */
|
||||||
|
totalPages: number;
|
||||||
|
|
||||||
|
/** Start index for current page (0-indexed) */
|
||||||
|
startIndex: number;
|
||||||
|
|
||||||
|
/** End index for current page (0-indexed) */
|
||||||
|
endIndex: number;
|
||||||
|
|
||||||
|
/** Whether there is a previous page */
|
||||||
|
hasPrevPage: boolean;
|
||||||
|
|
||||||
|
/** Whether there is a next page */
|
||||||
|
hasNextPage: boolean;
|
||||||
|
|
||||||
|
/** Set current page */
|
||||||
|
setCurrentPage: (page: number) => void;
|
||||||
|
|
||||||
|
/** Set page size */
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
|
||||||
|
/** Set total count */
|
||||||
|
setTotalCount: (count: number) => void;
|
||||||
|
|
||||||
|
/** Go to next page */
|
||||||
|
nextPage: () => void;
|
||||||
|
|
||||||
|
/** Go to previous page */
|
||||||
|
prevPage: () => void;
|
||||||
|
|
||||||
|
/** Go to first page */
|
||||||
|
firstPage: () => void;
|
||||||
|
|
||||||
|
/** Go to last page */
|
||||||
|
lastPage: () => void;
|
||||||
|
|
||||||
|
/** Reset pagination */
|
||||||
|
reset: () => void;
|
||||||
|
|
||||||
|
/** Get page range for display */
|
||||||
|
getPageRange: (maxPages?: number) => number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing pagination state
|
||||||
|
*
|
||||||
|
* @param config - Configuration options
|
||||||
|
* @returns Pagination state and actions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const pagination = usePagination({
|
||||||
|
* initialPageSize: 25,
|
||||||
|
* persist: true,
|
||||||
|
* onPageChange: (page) => fetchData(page)
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Set total count from API
|
||||||
|
* pagination.setTotalCount(response.count);
|
||||||
|
*
|
||||||
|
* // Use in query
|
||||||
|
* const { startIndex, endIndex } = pagination;
|
||||||
|
* query.range(startIndex, endIndex);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function usePagination(config: PaginationConfig = {}): PaginationState {
|
||||||
|
const {
|
||||||
|
initialPage = 1,
|
||||||
|
initialPageSize = 25,
|
||||||
|
persist = false,
|
||||||
|
storageKey = 'pagination_state',
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Load persisted state
|
||||||
|
const loadPersistedState = useCallback(() => {
|
||||||
|
if (!persist) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (saved) {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load pagination state:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [persist, storageKey]);
|
||||||
|
|
||||||
|
const persisted = loadPersistedState();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [currentPage, setCurrentPageState] = useState<number>(
|
||||||
|
persisted?.currentPage || initialPage
|
||||||
|
);
|
||||||
|
const [pageSize, setPageSizeState] = useState<number>(
|
||||||
|
persisted?.pageSize || initialPageSize
|
||||||
|
);
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
|
|
||||||
|
// Computed values
|
||||||
|
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
|
||||||
|
const startIndex = useMemo(() => (currentPage - 1) * pageSize, [currentPage, pageSize]);
|
||||||
|
const endIndex = useMemo(() => startIndex + pageSize - 1, [startIndex, pageSize]);
|
||||||
|
const hasPrevPage = currentPage > 1;
|
||||||
|
const hasNextPage = currentPage < totalPages;
|
||||||
|
|
||||||
|
// Persist state
|
||||||
|
useEffect(() => {
|
||||||
|
if (persist) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
storageKey,
|
||||||
|
JSON.stringify({
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist pagination state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentPage, pageSize, persist, storageKey]);
|
||||||
|
|
||||||
|
// Set current page with bounds checking
|
||||||
|
const setCurrentPage = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
const boundedPage = Math.max(1, Math.min(page, totalPages || 1));
|
||||||
|
setCurrentPageState(boundedPage);
|
||||||
|
onPageChange?.(boundedPage);
|
||||||
|
},
|
||||||
|
[totalPages, onPageChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set page size and reset to first page
|
||||||
|
const setPageSize = useCallback(
|
||||||
|
(size: number) => {
|
||||||
|
setPageSizeState(size);
|
||||||
|
setCurrentPageState(1);
|
||||||
|
onPageSizeChange?.(size);
|
||||||
|
},
|
||||||
|
[onPageSizeChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigation actions
|
||||||
|
const nextPage = useCallback(() => {
|
||||||
|
if (hasNextPage) {
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
}, [currentPage, hasNextPage, setCurrentPage]);
|
||||||
|
|
||||||
|
const prevPage = useCallback(() => {
|
||||||
|
if (hasPrevPage) {
|
||||||
|
setCurrentPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
}, [currentPage, hasPrevPage, setCurrentPage]);
|
||||||
|
|
||||||
|
const firstPage = useCallback(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [setCurrentPage]);
|
||||||
|
|
||||||
|
const lastPage = useCallback(() => {
|
||||||
|
setCurrentPage(totalPages);
|
||||||
|
}, [totalPages, setCurrentPage]);
|
||||||
|
|
||||||
|
// Reset pagination
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setCurrentPageState(initialPage);
|
||||||
|
setPageSizeState(initialPageSize);
|
||||||
|
setTotalCount(0);
|
||||||
|
}, [initialPage, initialPageSize]);
|
||||||
|
|
||||||
|
// Get page range for pagination controls
|
||||||
|
const getPageRange = useCallback(
|
||||||
|
(maxPages: number = 5): number[] => {
|
||||||
|
if (totalPages <= maxPages) {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const half = Math.floor(maxPages / 2);
|
||||||
|
let start = Math.max(1, currentPage - half);
|
||||||
|
let end = Math.min(totalPages, start + maxPages - 1);
|
||||||
|
|
||||||
|
// Adjust start if we're near the end
|
||||||
|
if (end - start < maxPages - 1) {
|
||||||
|
start = Math.max(1, end - maxPages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||||
|
},
|
||||||
|
[currentPage, totalPages]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalCount,
|
||||||
|
totalPages,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
hasPrevPage,
|
||||||
|
hasNextPage,
|
||||||
|
setCurrentPage,
|
||||||
|
setPageSize,
|
||||||
|
setTotalCount,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
firstPage,
|
||||||
|
lastPage,
|
||||||
|
reset,
|
||||||
|
getPageRange,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -42,3 +42,16 @@ export type {
|
|||||||
ModerationConfig,
|
ModerationConfig,
|
||||||
DeleteSubmissionConfig,
|
DeleteSubmissionConfig,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
|
// Sorting utilities
|
||||||
|
export {
|
||||||
|
sortModerationItems,
|
||||||
|
getDefaultSortConfig,
|
||||||
|
loadSortConfig,
|
||||||
|
saveSortConfig,
|
||||||
|
toggleSortDirection,
|
||||||
|
getSortFieldLabel,
|
||||||
|
isDefaultSortConfig,
|
||||||
|
} from './sorting';
|
||||||
|
|
||||||
|
export type { SortConfig, SortField, SortDirection } from '@/types/moderation';
|
||||||
|
|||||||
151
src/lib/moderation/sorting.ts
Normal file
151
src/lib/moderation/sorting.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Moderation Queue Sorting Utilities
|
||||||
|
*
|
||||||
|
* Provides sorting functions and utilities for moderation queue items.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ModerationItem, SortField, SortDirection, SortConfig } from '@/types/moderation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort moderation items based on configuration
|
||||||
|
*
|
||||||
|
* @param items - Array of moderation items to sort
|
||||||
|
* @param config - Sort configuration
|
||||||
|
* @returns Sorted array of items
|
||||||
|
*/
|
||||||
|
export function sortModerationItems(
|
||||||
|
items: ModerationItem[],
|
||||||
|
config: SortConfig
|
||||||
|
): ModerationItem[] {
|
||||||
|
const { field, direction } = config;
|
||||||
|
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'created_at':
|
||||||
|
comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'username':
|
||||||
|
const usernameA = a.user_profile?.username || a.user_profile?.display_name || '';
|
||||||
|
const usernameB = b.user_profile?.username || b.user_profile?.display_name || '';
|
||||||
|
comparison = usernameA.localeCompare(usernameB);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'submission_type':
|
||||||
|
comparison = (a.submission_type || '').localeCompare(b.submission_type || '');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
comparison = a.status.localeCompare(b.status);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'escalated':
|
||||||
|
const escalatedA = a.escalated ? 1 : 0;
|
||||||
|
const escalatedB = b.escalated ? 1 : 0;
|
||||||
|
comparison = escalatedB - escalatedA; // Escalated items first
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
comparison = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return direction === 'asc' ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default sort configuration
|
||||||
|
*
|
||||||
|
* @returns Default sort config
|
||||||
|
*/
|
||||||
|
export function getDefaultSortConfig(): SortConfig {
|
||||||
|
return {
|
||||||
|
field: 'created_at',
|
||||||
|
direction: 'asc',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load sort configuration from localStorage
|
||||||
|
*
|
||||||
|
* @param key - localStorage key
|
||||||
|
* @returns Saved sort config or default
|
||||||
|
*/
|
||||||
|
export function loadSortConfig(key: string = 'moderationQueue_sortConfig'): SortConfig {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(key);
|
||||||
|
if (saved) {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load sort config:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultSortConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save sort configuration to localStorage
|
||||||
|
*
|
||||||
|
* @param config - Sort configuration to save
|
||||||
|
* @param key - localStorage key
|
||||||
|
*/
|
||||||
|
export function saveSortConfig(
|
||||||
|
config: SortConfig,
|
||||||
|
key: string = 'moderationQueue_sortConfig'
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(config));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save sort config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle sort direction
|
||||||
|
*
|
||||||
|
* @param currentDirection - Current sort direction
|
||||||
|
* @returns Toggled direction
|
||||||
|
*/
|
||||||
|
export function toggleSortDirection(currentDirection: SortDirection): SortDirection {
|
||||||
|
return currentDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable label for sort field
|
||||||
|
*
|
||||||
|
* @param field - Sort field
|
||||||
|
* @returns Human-readable label
|
||||||
|
*/
|
||||||
|
export function getSortFieldLabel(field: SortField): string {
|
||||||
|
switch (field) {
|
||||||
|
case 'created_at':
|
||||||
|
return 'Date Created';
|
||||||
|
case 'username':
|
||||||
|
return 'Submitter';
|
||||||
|
case 'submission_type':
|
||||||
|
return 'Type';
|
||||||
|
case 'status':
|
||||||
|
return 'Status';
|
||||||
|
case 'escalated':
|
||||||
|
return 'Escalated';
|
||||||
|
default:
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if sort config is default
|
||||||
|
*
|
||||||
|
* @param config - Sort configuration to check
|
||||||
|
* @returns True if config matches default
|
||||||
|
*/
|
||||||
|
export function isDefaultSortConfig(config: SortConfig): boolean {
|
||||||
|
const defaultConfig = getDefaultSortConfig();
|
||||||
|
return (
|
||||||
|
config.field === defaultConfig.field &&
|
||||||
|
config.direction === defaultConfig.direction
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user