Refactor: Simplify architecture

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 22:27:58 +00:00
parent 27c7f36ca4
commit 2bd0b414e1
5 changed files with 117 additions and 164 deletions

View File

@@ -105,12 +105,12 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
<QueueFilters <QueueFilters
activeEntityFilter={queueManager.filters.entityFilter} activeEntityFilter={queueManager.filters.entityFilter}
activeStatusFilter={queueManager.filters.statusFilter} activeStatusFilter={queueManager.filters.statusFilter}
sortConfig={queueManager.sort.config} sortConfig={queueManager.filters.sortConfig}
isMobile={isMobile} isMobile={isMobile}
isLoading={queueManager.loadingState === 'loading'} isLoading={queueManager.loadingState === 'loading'}
onEntityFilterChange={queueManager.filters.setEntityFilter} onEntityFilterChange={queueManager.filters.setEntityFilter}
onStatusFilterChange={queueManager.filters.setStatusFilter} onStatusFilterChange={queueManager.filters.setStatusFilter}
onSortChange={queueManager.sort.setConfig} onSortChange={queueManager.filters.setSortConfig}
onClearFilters={queueManager.filters.clearFilters} onClearFilters={queueManager.filters.clearFilters}
showClearButton={queueManager.filters.hasActiveFilters} showClearButton={queueManager.filters.hasActiveFilters}
/> />

View File

@@ -12,14 +12,9 @@ export type { CachedProfile } from './useProfileCache';
export { useModerationFilters } from './useModerationFilters'; export { useModerationFilters } from './useModerationFilters';
export type { ModerationFilters, ModerationFiltersConfig } from './useModerationFilters'; export type { ModerationFilters, ModerationFiltersConfig } from './useModerationFilters';
// Removed - sorting functionality deleted
export { usePagination } from './usePagination'; export { usePagination } from './usePagination';
export type { PaginationState, PaginationConfig } from './usePagination'; export type { PaginationState, PaginationConfig } from './usePagination';
export { useModerationSort } from './useModerationSort';
export type { UseModerationSortReturn } from './useModerationSort';
export { useRealtimeSubscriptions } from './useRealtimeSubscriptions'; export { useRealtimeSubscriptions } from './useRealtimeSubscriptions';
export type { export type {
RealtimeSubscriptionConfig, RealtimeSubscriptionConfig,

View File

@@ -10,7 +10,7 @@
import { useState, useCallback, useEffect, useMemo } from 'react'; import { useState, useCallback, useEffect, useMemo } from 'react';
import { useDebounce } from '@/hooks/useDebounce'; import { useDebounce } from '@/hooks/useDebounce';
import type { EntityFilter, StatusFilter, QueueTab } from '@/types/moderation'; import type { EntityFilter, StatusFilter, QueueTab, SortConfig, SortField } from '@/types/moderation';
export interface ModerationFiltersConfig { export interface ModerationFiltersConfig {
/** Initial entity filter */ /** Initial entity filter */
@@ -30,6 +30,9 @@ export interface ModerationFiltersConfig {
/** localStorage key prefix for persistence */ /** localStorage key prefix for persistence */
storageKey?: string; storageKey?: string;
/** Initial sort configuration */
initialSortConfig?: SortConfig;
} }
export interface ModerationFilters { export interface ModerationFilters {
@@ -62,6 +65,24 @@ export interface ModerationFilters {
/** Check if any non-default filters are active */ /** Check if any non-default filters are active */
hasActiveFilters: boolean; hasActiveFilters: boolean;
/** Current sort configuration (immediate) */
sortConfig: SortConfig;
/** Debounced sort configuration (use this for queries) */
debouncedSortConfig: SortConfig;
/** Update the sort configuration */
setSortConfig: (config: SortConfig) => void;
/** Sort by a specific field, toggling direction if already sorting by that field */
sortBy: (field: SortField) => void;
/** Toggle the sort direction */
toggleSortDirection: () => void;
/** Reset sort to default */
resetSort: () => void;
} }
/** /**
@@ -91,6 +112,7 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
debounceDelay = 300, debounceDelay = 300,
persist = true, persist = true,
storageKey = 'moderationQueue_filters', storageKey = 'moderationQueue_filters',
initialSortConfig = { field: 'created_at', direction: 'asc' },
} = config; } = config;
// Load persisted filters on mount // Load persisted filters on mount
@@ -109,6 +131,25 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
return null; return null;
}, [persist, storageKey]); }, [persist, storageKey]);
// Load persisted sort
const loadPersistedSort = useCallback((): SortConfig => {
if (!persist) return initialSortConfig;
try {
const saved = localStorage.getItem(`${storageKey}_sort`);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.field && parsed.direction) {
return parsed;
}
}
} catch (error) {
console.error('Failed to load persisted sort:', error);
}
return initialSortConfig;
}, [persist, storageKey, initialSortConfig]);
const persisted = loadPersistedFilters(); const persisted = loadPersistedFilters();
// Filter state // Filter state
@@ -122,9 +163,15 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
persisted?.activeTab || initialTab persisted?.activeTab || initialTab
); );
// Sort state
const [sortConfig, setSortConfigState] = useState<SortConfig>(loadPersistedSort);
// Debounced filters for API calls // Debounced filters for API calls
const debouncedEntityFilter = useDebounce(entityFilter, debounceDelay); const debouncedEntityFilter = useDebounce(entityFilter, debounceDelay);
const debouncedStatusFilter = useDebounce(statusFilter, debounceDelay); const debouncedStatusFilter = useDebounce(statusFilter, debounceDelay);
// Debounced sort (0ms for immediate feedback)
const debouncedSortConfig = useDebounce(sortConfig, 0);
// Persist filters to localStorage // Persist filters to localStorage
useEffect(() => { useEffect(() => {
@@ -144,6 +191,17 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
} }
}, [entityFilter, statusFilter, activeTab, persist, storageKey]); }, [entityFilter, statusFilter, activeTab, persist, storageKey]);
// Persist sort to localStorage
useEffect(() => {
if (persist) {
try {
localStorage.setItem(`${storageKey}_sort`, JSON.stringify(sortConfig));
} catch (error) {
console.error('Failed to persist sort:', error);
}
}
}, [sortConfig, persist, storageKey]);
// Set entity filter with logging // Set entity filter with logging
const setEntityFilter = useCallback((filter: EntityFilter) => { const setEntityFilter = useCallback((filter: EntityFilter) => {
console.log('🔍 Entity filter changed:', filter); console.log('🔍 Entity filter changed:', filter);
@@ -162,19 +220,48 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
setActiveTabState(tab); setActiveTabState(tab);
}, []); }, []);
// Sort callbacks
const setSortConfig = useCallback((config: SortConfig) => {
console.log('📝 [SORT] Sort config changed:', config);
setSortConfigState(config);
}, []);
const sortBy = useCallback((field: SortField) => {
setSortConfigState(prev => ({
field,
direction: prev.field === field
? (prev.direction === 'asc' ? 'desc' : 'asc')
: 'asc'
}));
}, []);
const toggleSortDirection = useCallback(() => {
setSortConfigState(prev => ({
...prev,
direction: prev.direction === 'asc' ? 'desc' : 'asc'
}));
}, []);
const resetSort = useCallback(() => {
setSortConfigState(initialSortConfig);
}, [initialSortConfig]);
// Clear all filters // Clear all filters
const clearFilters = useCallback(() => { const clearFilters = useCallback(() => {
console.log('🔍 Filters cleared'); console.log('🔍 Filters cleared');
setEntityFilterState(initialEntityFilter); setEntityFilterState(initialEntityFilter);
setStatusFilterState(initialStatusFilter); setStatusFilterState(initialStatusFilter);
setActiveTabState(initialTab); setActiveTabState(initialTab);
}, [initialEntityFilter, initialStatusFilter, initialTab]); setSortConfigState(initialSortConfig);
}, [initialEntityFilter, initialStatusFilter, initialTab, initialSortConfig]);
// Check if non-default filters are active // Check if non-default filters are active
const hasActiveFilters = const hasActiveFilters =
entityFilter !== initialEntityFilter || entityFilter !== initialEntityFilter ||
statusFilter !== initialStatusFilter || statusFilter !== initialStatusFilter ||
activeTab !== initialTab; activeTab !== initialTab ||
sortConfig.field !== initialSortConfig.field ||
sortConfig.direction !== initialSortConfig.direction;
return useMemo(() => ({ return useMemo(() => ({
entityFilter, entityFilter,
@@ -187,6 +274,12 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
setActiveTab, setActiveTab,
clearFilters, clearFilters,
hasActiveFilters, hasActiveFilters,
sortConfig,
debouncedSortConfig,
setSortConfig,
sortBy,
toggleSortDirection,
resetSort,
}), [ }), [
entityFilter, entityFilter,
statusFilter, statusFilter,
@@ -198,5 +291,11 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode
setActiveTab, setActiveTab,
clearFilters, clearFilters,
hasActiveFilters, hasActiveFilters,
sortConfig,
debouncedSortConfig,
setSortConfig,
sortBy,
toggleSortDirection,
resetSort,
]); ]);
} }

View File

@@ -7,11 +7,10 @@ import {
useProfileCache, useProfileCache,
useModerationFilters, useModerationFilters,
usePagination, usePagination,
useModerationSort,
useRealtimeSubscriptions, useRealtimeSubscriptions,
} from "./index"; } from "./index";
import { useModerationQueue } from "@/hooks/useModerationQueue"; import { useModerationQueue } from "@/hooks/useModerationQueue";
import { smartMergeArray } from "@/lib/smartStateUpdate";
import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState } from "@/types/moderation";
/** /**
@@ -43,7 +42,6 @@ export interface ModerationQueueManager {
// Sub-hooks (exposed for granular control) // Sub-hooks (exposed for granular control)
filters: ReturnType<typeof useModerationFilters>; filters: ReturnType<typeof useModerationFilters>;
pagination: ReturnType<typeof usePagination>; pagination: ReturnType<typeof usePagination>;
sort: ReturnType<typeof useModerationSort>;
queue: ReturnType<typeof useModerationQueue>; queue: ReturnType<typeof useModerationQueue>;
// Realtime // Realtime
@@ -110,8 +108,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}, },
}); });
const sort = useModerationSort();
const queue = useModerationQueue(); const queue = useModerationQueue();
const entityCache = useEntityCache(); const entityCache = useEntityCache();
const profileCache = useProfileCache(); const profileCache = useProfileCache();
@@ -133,10 +129,10 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
const FETCH_COOLDOWN_MS = 1000; const FETCH_COOLDOWN_MS = 1000;
// Store settings, filters, sort, and pagination in refs to stabilize fetchItems // Store settings, filters, and pagination in refs to stabilize fetchItems
const settingsRef = useRef(settings); const settingsRef = useRef(settings);
const filtersRef = useRef(filters); const filtersRef = useRef(filters);
const sortRef = useRef(sort.debouncedConfig); const sortRef = useRef(filters.debouncedSortConfig);
const paginationRef = useRef(pagination); const paginationRef = useRef(pagination);
// Sync refs with state // Sync refs with state
@@ -149,8 +145,8 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}, [filters]); }, [filters]);
useEffect(() => { useEffect(() => {
sortRef.current = sort.debouncedConfig; sortRef.current = filters.debouncedSortConfig;
}, [sort.debouncedConfig]); }, [filters.debouncedSortConfig]);
useEffect(() => { useEffect(() => {
paginationRef.current = pagination; paginationRef.current = pagination;
@@ -465,25 +461,10 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
break; break;
case "replace": case "replace":
const mergeResult = smartMergeArray(items, moderationItems, { // Full replace - simple and predictable
compareFields: [ setItems(moderationItems);
"status", console.log("🔄 Queue updated (replace mode)");
"content",
"reviewed_at",
"reviewed_by",
"reviewer_notes",
"assigned_to",
"locked_until",
],
preserveOrder: false,
addToTop: false,
});
if (mergeResult.hasChanges) {
setItems(mergeResult.items);
console.log("🔄 Queue updated (replace mode)");
}
if (!currentPreserveInteraction) { if (!currentPreserveInteraction) {
setPendingNewItems([]); setPendingNewItems([]);
setNewItemsCount(0); setNewItemsCount(0);
@@ -515,14 +496,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
user, user,
isAdmin, isAdmin,
isSuperuser, isSuperuser,
filters.debouncedEntityFilter,
filters.debouncedStatusFilter,
filters.activeTab,
pagination.setTotalCount,
pagination.startIndex,
pagination.endIndex,
sort.debouncedConfig.field,
sort.debouncedConfig.direction,
profileCache, profileCache,
entityCache, entityCache,
toast toast
@@ -856,8 +829,8 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
console.log('🔄 [SORT/FILTER CHANGE] Detected change:', { console.log('🔄 [SORT/FILTER CHANGE] Detected change:', {
entityFilter: filters.debouncedEntityFilter, entityFilter: filters.debouncedEntityFilter,
statusFilter: filters.debouncedStatusFilter, statusFilter: filters.debouncedStatusFilter,
sortField: sort.debouncedConfig.field, sortField: filters.debouncedSortConfig.field,
sortDirection: sort.debouncedConfig.direction, sortDirection: filters.debouncedSortConfig.direction,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
@@ -866,8 +839,8 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}, [ }, [
filters.debouncedEntityFilter, filters.debouncedEntityFilter,
filters.debouncedStatusFilter, filters.debouncedStatusFilter,
sort.debouncedConfig.field, filters.debouncedSortConfig.field,
sort.debouncedConfig.direction, filters.debouncedSortConfig.direction,
user, user,
fetchItems, fetchItems,
pagination pagination
@@ -948,7 +921,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
actionLoading, actionLoading,
filters, filters,
pagination, pagination,
sort,
queue, queue,
newItemsCount, newItemsCount,
pendingNewItems, pendingNewItems,

View File

@@ -1,113 +0,0 @@
import { useState, useCallback, useEffect } from 'react';
import { useDebounce } from '@/hooks/useDebounce';
import type { SortConfig, SortField } from '@/types/moderation';
const STORAGE_KEY = 'moderationQueue_sortConfig';
const SORT_DEBOUNCE_MS = 0; // Changed from 300ms to 0ms for immediate feedback
/**
* Default sort configuration
* Sorts by creation date ascending (oldest first)
*/
const DEFAULT_SORT: SortConfig = {
field: 'created_at',
direction: 'asc'
};
/**
* Load sort configuration from localStorage
*/
function loadSortConfig(): SortConfig {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
// Validate structure
if (parsed.field && parsed.direction) {
return parsed;
}
}
} catch (error) {
console.warn('Failed to load sort config:', error);
}
return DEFAULT_SORT;
}
/**
* Save sort configuration to localStorage
*/
function saveSortConfig(config: SortConfig): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
} catch (error) {
console.warn('Failed to save sort config:', error);
}
}
export interface UseModerationSortReturn {
/** Current sort configuration (immediate) */
config: SortConfig;
/** Debounced sort configuration (use this for queries) */
debouncedConfig: SortConfig;
/** Update the sort configuration */
setConfig: (config: SortConfig) => void;
/** Sort by a specific field, toggling direction if already sorting by that field */
sortBy: (field: SortField) => void;
/** Toggle the sort direction */
toggleDirection: () => void;
/** Reset to default sort */
reset: () => void;
}
/**
* Hook for managing moderation queue sort state
*
* Provides sort configuration with localStorage persistence
* and convenient methods for updating sort settings.
*/
export function useModerationSort(): UseModerationSortReturn {
const [config, setConfigState] = useState<SortConfig>(loadSortConfig);
// Debounce the config to prevent rapid-fire fetches
const debouncedConfig = useDebounce(config, SORT_DEBOUNCE_MS);
// Persist to localStorage whenever config changes
useEffect(() => {
saveSortConfig(config);
}, [config]);
const setConfig = useCallback((newConfig: SortConfig) => {
console.log('📝 [SORT] setConfig called:', newConfig);
setConfigState(newConfig);
}, []);
const sortBy = useCallback((field: SortField) => {
setConfigState(prev => ({
field,
// Toggle direction if clicking the same field, otherwise default to ascending
direction: prev.field === field
? (prev.direction === 'asc' ? 'desc' : 'asc')
: 'asc'
}));
}, []);
const toggleDirection = useCallback(() => {
setConfigState(prev => ({
...prev,
direction: prev.direction === 'asc' ? 'desc' : 'asc'
}));
}, []);
const reset = useCallback(() => {
setConfigState(DEFAULT_SORT);
}, []);
return {
config,
debouncedConfig,
setConfig,
sortBy,
toggleDirection,
reset
};
}