/** * Generic quota section component. */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useQuotaStore, useThemeStore } from '@/stores'; import type { AuthFileItem, ResolvedTheme } from '@/types'; import { QuotaCard } from './QuotaCard'; import type { QuotaStatusState } from './QuotaCard'; import { useQuotaLoader } from './useQuotaLoader'; import type { QuotaConfig } from './quotaConfigs'; import { useGridColumns } from './useGridColumns'; import { IconRefreshCw } from '@/components/ui/icons'; import styles from '@/pages/QuotaPage.module.scss'; type QuotaUpdater = T | ((prev: T) => T); type QuotaSetter = (updater: QuotaUpdater) => void; type ViewMode = 'paged' | 'all'; const MAX_ITEMS_PER_PAGE = 14; const MAX_SHOW_ALL_THRESHOLD = 30; interface QuotaPaginationState { pageSize: number; totalPages: number; currentPage: number; pageItems: T[]; setPageSize: (size: number) => void; goToPrev: () => void; goToNext: () => void; loading: boolean; loadingScope: 'page' | 'all' | null; setLoading: (loading: boolean, scope?: 'page' | 'all' | null) => void; } const useQuotaPagination = (items: T[], defaultPageSize = 6): QuotaPaginationState => { const [page, setPage] = useState(1); const [pageSize, setPageSizeState] = useState(defaultPageSize); const [loading, setLoadingState] = useState(false); const [loadingScope, setLoadingScope] = useState<'page' | 'all' | null>(null); const totalPages = useMemo( () => Math.max(1, Math.ceil(items.length / pageSize)), [items.length, pageSize] ); const currentPage = useMemo(() => Math.min(page, totalPages), [page, totalPages]); const pageItems = useMemo(() => { const start = (currentPage - 1) * pageSize; return items.slice(start, start + pageSize); }, [items, currentPage, pageSize]); const setPageSize = useCallback((size: number) => { setPageSizeState(size); setPage(1); }, []); const goToPrev = useCallback(() => { setPage((prev) => Math.max(1, prev - 1)); }, []); const goToNext = useCallback(() => { setPage((prev) => Math.min(totalPages, prev + 1)); }, [totalPages]); const setLoading = useCallback((isLoading: boolean, scope?: 'page' | 'all' | null) => { setLoadingState(isLoading); setLoadingScope(isLoading ? (scope ?? null) : null); }, []); return { pageSize, totalPages, currentPage, pageItems, setPageSize, goToPrev, goToNext, loading, loadingScope, setLoading }; }; interface QuotaSectionProps { config: QuotaConfig; files: AuthFileItem[]; loading: boolean; disabled: boolean; } export function QuotaSection({ config, files, loading, disabled }: QuotaSectionProps) { const { t } = useTranslation(); const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); const setQuota = useQuotaStore((state) => state[config.storeSetter]) as QuotaSetter< Record >; /* Removed useRef */ const [columns, gridRef] = useGridColumns(380); // Min card width 380px matches SCSS const [viewMode, setViewMode] = useState('paged'); const [showTooManyWarning, setShowTooManyWarning] = useState(false); const filteredFiles = useMemo(() => files.filter((file) => config.filterFn(file)), [ files, config ]); const showAllAllowed = filteredFiles.length <= MAX_SHOW_ALL_THRESHOLD; const effectiveViewMode: ViewMode = viewMode === 'all' && !showAllAllowed ? 'paged' : viewMode; const { pageSize, totalPages, currentPage, pageItems, setPageSize, goToPrev, goToNext, loading: sectionLoading, setLoading } = useQuotaPagination(filteredFiles); useEffect(() => { if (showAllAllowed) return; if (viewMode !== 'all') return; let cancelled = false; queueMicrotask(() => { if (cancelled) return; setViewMode('paged'); setShowTooManyWarning(true); }); return () => { cancelled = true; }; }, [showAllAllowed, viewMode]); // Update page size based on view mode and columns useEffect(() => { if (effectiveViewMode === 'all') { setPageSize(Math.max(1, filteredFiles.length)); } else { // Paged mode: 3 rows * columns, capped to avoid oversized pages. setPageSize(Math.min(columns * 3, MAX_ITEMS_PER_PAGE)); } }, [effectiveViewMode, columns, filteredFiles.length, setPageSize]); const { quota, loadQuota } = useQuotaLoader(config); const pendingQuotaRefreshRef = useRef(false); const prevFilesLoadingRef = useRef(loading); const handleRefresh = useCallback(() => { pendingQuotaRefreshRef.current = true; void triggerHeaderRefresh(); }, []); useEffect(() => { const wasLoading = prevFilesLoadingRef.current; prevFilesLoadingRef.current = loading; if (!pendingQuotaRefreshRef.current) return; if (loading) return; if (!wasLoading) return; pendingQuotaRefreshRef.current = false; const scope = effectiveViewMode === 'all' ? 'all' : 'page'; const targets = effectiveViewMode === 'all' ? filteredFiles : pageItems; if (targets.length === 0) return; loadQuota(targets, scope, setLoading); }, [loading, effectiveViewMode, filteredFiles, pageItems, loadQuota, setLoading]); useEffect(() => { if (loading) return; if (filteredFiles.length === 0) { setQuota({}); return; } setQuota((prev) => { const nextState: Record = {}; filteredFiles.forEach((file) => { const cached = prev[file.name]; if (cached) { nextState[file.name] = cached; } }); return nextState; }); }, [filteredFiles, loading, setQuota]); const titleNode = (
{t(`${config.i18nPrefix}.title`)} {filteredFiles.length > 0 && ( {filteredFiles.length} )}
); const isRefreshing = sectionLoading || loading; return (
} > {filteredFiles.length === 0 ? ( ) : ( <>
{pageItems.map((item) => ( ))}
{filteredFiles.length > pageSize && effectiveViewMode === 'paged' && (
{t('auth_files.pagination_info', { current: currentPage, total: totalPages, count: filteredFiles.length })}
)} )} {showTooManyWarning && (
setShowTooManyWarning(false)}>
e.stopPropagation()}>

{t('auth_files.too_many_files_warning')}

)}
); }