15 KiB
15 KiB
File Grid Navigation Documentation
📊 Tổng Quan
Date: October 15, 2025
Status: ✅ Completed
Component: FileGrid + FileGridNavigation
🎯 Mục Đích
Thêm pagination/navigation controls vào FileGrid component để hiển thị và điều hướng qua nhiều trang files.
📁 Components
1. FileGridNavigation.tsx
Purpose: Pagination controls với page numbers và navigation buttons
Features:
- Previous/Next buttons
- Page number buttons (smart ellipsis)
- Total items display
- Jump to page input (desktop)
- Responsive design (mobile-friendly)
- Loading state support
Props:
interface FileGridNavigationProps {
currentPage: number; // Trang hiện tại (1-based)
totalPages: number; // Tổng số trang
totalItems: number; // Tổng số items
itemsPerPage: number; // Số items mỗi trang
onPageChange: (page: number) => void; // Page change handler
loading?: boolean; // Loading state
className?: string; // Custom CSS classes
}
Lines: ~165 lines
2. FileGrid.tsx (Updated)
Purpose: Display files với optional pagination
New Props:
interface FileGridProps {
// ... existing props ...
// Pagination props (optional)
showPagination?: boolean; // Enable/disable pagination
currentPage?: number; // Current page number
totalPages?: number; // Total pages
totalItems?: number; // Total items count
itemsPerPage?: number; // Items per page
onPageChange?: (page: number) => void; // Page change handler
loading?: boolean; // Loading state
}
💡 Usage Examples
Example 1: Basic Usage (No Pagination)
import { FileGrid } from '@/components/storage';
function MyStoragePage() {
const [files, setFiles] = useState<FileResponse[]>([]);
return (
<FileGrid
files={files}
viewMode="grid"
onFilePreview={(file) => console.log('Preview:', file)}
// No pagination props = no pagination UI
/>
);
}
Example 2: With Pagination (Client-Side)
import { FileGrid } from '@/components/storage';
import { useState, useMemo } from 'react';
function MyStoragePageWithPagination() {
const [allFiles, setAllFiles] = useState<FileResponse[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
// Calculate pagination
const totalPages = Math.ceil(allFiles.length / itemsPerPage);
const paginatedFiles = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return allFiles.slice(startIndex, endIndex);
}, [allFiles, currentPage, itemsPerPage]);
return (
<FileGrid
files={paginatedFiles}
viewMode="grid"
showPagination={true}
currentPage={currentPage}
totalPages={totalPages}
totalItems={allFiles.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onFilePreview={(file) => console.log('Preview:', file)}
/>
);
}
Example 3: With Pagination (Server-Side)
import { FileGrid } from '@/components/storage';
import { useState, useEffect } from 'react';
function MyServerPaginatedStorage() {
const [files, setFiles] = useState<FileResponse[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const [loading, setLoading] = useState(false);
const itemsPerPage = 20;
const fetchFiles = async (page: number) => {
setLoading(true);
try {
const response = await storageService.getFiles({
page,
limit: itemsPerPage
});
if (response.success) {
setFiles(response.data.files);
setTotalItems(response.data.meta.totalItems);
}
} catch (error) {
console.error('Failed to fetch files:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchFiles(currentPage);
}, [currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
// Scroll to top when page changes
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const totalPages = Math.ceil(totalItems / itemsPerPage);
return (
<FileGrid
files={files}
viewMode="list"
showPagination={true}
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={handlePageChange}
loading={loading}
onFilePreview={(file) => console.log('Preview:', file)}
/>
);
}
Example 4: Complete Integration with StoragePageContent
// In your storage page
import { useState, useCallback } from 'react';
import { StoragePageContent } from '@/components/storage';
function StoragePage() {
const [files, setFiles] = useState<FileResponse[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const itemsPerPage = 24; // 4x6 grid
// Fetch files with pagination
const fetchFiles = useCallback(async (page: number) => {
const response = await storageService.getFiles({
page,
limit: itemsPerPage,
folderId: currentFolder?.id
});
if (response.success) {
setFiles(response.data.files);
setTotalItems(response.data.meta.totalItems);
}
}, [currentFolder]);
useEffect(() => {
fetchFiles(currentPage);
}, [currentPage, fetchFiles]);
return (
<StoragePageContent
{...otherProps}
filteredFiles={files}
// Pass pagination props to FileGrid via StoragePageContent
showPagination={true}
currentPage={currentPage}
totalPages={Math.ceil(totalItems / itemsPerPage)}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
/>
);
}
🎨 Navigation Features
1. Smart Page Numbers
- Shows up to 7 page buttons
- Uses ellipsis (...) for large page ranges
- Always shows first and last page
- Shows pages around current page
Example displays:
- 5 pages:
1 2 3 4 5 - 10 pages, current=1:
1 2 3 ... 10 - 10 pages, current=5:
1 ... 4 5 6 ... 10 - 10 pages, current=10:
1 ... 8 9 10
2. Responsive Design
- Desktop: Full pagination with page numbers
- Mobile: Simple "Page X / Y" indicator
- Tablet: Medium view with essential controls
3. Accessibility
- Keyboard navigation support
- Disabled state for loading
- Clear visual feedback
- ARIA labels for screen readers
4. Loading State
- Disabled controls during loading
- Visual feedback
- Prevents double-clicks
🔧 Integration Steps
Step 1: Update FileGrid Props
<FileGrid
files={paginatedFiles}
// ... other props ...
// Add pagination props
showPagination={true}
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={handlePageChange}
loading={loading}
/>
Step 2: Implement Page Change Handler
const handlePageChange = (newPage: number) => {
// Update current page
setCurrentPage(newPage);
// Optional: Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
// Optional: Fetch new data (server-side)
fetchFiles(newPage);
};
Step 3: Calculate Pagination (Client-Side)
const itemsPerPage = 20;
const totalPages = Math.ceil(allFiles.length / itemsPerPage);
const paginatedFiles = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
return allFiles.slice(start, end);
}, [allFiles, currentPage, itemsPerPage]);
📊 Pagination Strategies
1. Client-Side Pagination
Pros:
- Fast page switching
- No server requests
- Works offline
Cons:
- All data loaded upfront
- Memory intensive for large datasets
Best for:
- < 1000 files
- Fast local filtering
- Offline-first apps
Implementation:
const paginatedFiles = allFiles.slice(
(page - 1) * itemsPerPage,
page * itemsPerPage
);
2. Server-Side Pagination
Pros:
- Memory efficient
- Handles large datasets
- Real-time data
Cons:
- Network requests per page
- Slower page switching
Best for:
-
1000 files
- Real-time updates
- Large file libraries
Implementation:
const fetchFiles = async (page: number) => {
const response = await api.getFiles({
page,
limit: itemsPerPage
});
return response.data;
};
3. Hybrid Pagination
Pros:
- Best of both worlds
- Smart caching
- Good UX
Implementation:
const [cache, setCache] = useState<Map<number, FileResponse[]>>(new Map());
const fetchPage = async (page: number) => {
if (cache.has(page)) {
return cache.get(page)!;
}
const data = await api.getFiles({ page, limit: itemsPerPage });
setCache(prev => new Map(prev).set(page, data));
return data;
};
🎛️ Customization Options
Items Per Page Options
const ITEMS_PER_PAGE_OPTIONS = [12, 24, 48, 96];
function MyStorage() {
const [itemsPerPage, setItemsPerPage] = useState(24);
return (
<>
<select value={itemsPerPage} onChange={(e) => setItemsPerPage(+e.target.value)}>
{ITEMS_PER_PAGE_OPTIONS.map(option => (
<option key={option} value={option}>{option} per page</option>
))}
</select>
<FileGrid
files={files}
showPagination={true}
itemsPerPage={itemsPerPage}
{...otherProps}
/>
</>
);
}
Custom Navigation Style
<FileGridNavigation
{...paginationProps}
className="mt-6 sticky bottom-0 shadow-lg"
/>
🚀 Performance Tips
1. Optimize Re-renders
const paginatedFiles = useMemo(() => {
return files.slice((page - 1) * limit, page * limit);
}, [files, page, limit]);
2. Debounce Page Changes
const debouncedPageChange = useMemo(
() => debounce((page: number) => {
setCurrentPage(page);
}, 300),
[]
);
3. Virtual Scrolling (Alternative)
For very large datasets, consider virtual scrolling instead:
import { FixedSizeGrid } from 'react-window';
<FixedSizeGrid
columnCount={6}
rowCount={Math.ceil(files.length / 6)}
columnWidth={200}
rowHeight={200}
height={600}
width={1200}
>
{({ columnIndex, rowIndex, style }) => (
<div style={style}>
<FileItem file={files[rowIndex * 6 + columnIndex]} />
</div>
)}
</FixedSizeGrid>
📱 Mobile Optimizations
Touch-Friendly Buttons
- Minimum 44x44px touch targets
- Adequate spacing between buttons
- Clear visual feedback
Simplified Mobile View
- Shows "Page X / Y" instead of all page numbers
- Larger prev/next buttons
- Swipe gestures support (future enhancement)
🔄 State Management
Recommended Pattern
function StorageWithPagination() {
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(24);
// Data state
const [allFiles, setAllFiles] = useState<FileResponse[]>([]);
const [loading, setLoading] = useState(false);
// Computed values
const totalPages = Math.ceil(allFiles.length / itemsPerPage);
const paginatedFiles = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
return allFiles.slice(start, start + itemsPerPage);
}, [allFiles, currentPage, itemsPerPage]);
// Reset to page 1 when filters change
useEffect(() => {
setCurrentPage(1);
}, [searchQuery, sortBy, folderId]);
return (
<FileGrid
files={paginatedFiles}
showPagination={allFiles.length > itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
totalItems={allFiles.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
loading={loading}
/>
);
}
🎨 UI/UX Highlights
Visual States
-
Active Page
- Blue background
- White text
- Clear visual distinction
-
Hover State
- Gray background
- Smooth transition
- Cursor pointer
-
Disabled State
- Gray text
- No hover effect
- Cursor not-allowed
-
Loading State
- Disabled controls
- Reduced opacity
- Loading indicator (if needed)
Smart Features
-
Automatic Ellipsis
- Shows
...for skipped pages - Always visible: first, last, and nearby pages
- Shows
-
Jump to Page (Desktop)
- Quick navigation to specific page
- Input validation
- Only shows for 5+ pages
-
Responsive Text
- Desktop: "Hiển thị 1 - 20 trong tổng số 150 files"
- Mobile: "1 / 8"
🧪 Testing Checklist
- Component renders without pagination
- Component renders with pagination
- Previous button works
- Next button works
- Page number buttons work
- Ellipsis appears correctly
- Jump to page works (desktop)
- Mobile view displays correctly
- Loading state disables controls
- First/last page edge cases handled
- User testing in browser (pending)
📚 Related Files
client/src/components/storage/FileGrid.tsx- Main grid componentclient/src/components/storage/FileGridNavigation.tsx- Navigation componentclient/src/components/storage/index.ts- Exportsclient/src/app/[locale]/dashboard/storage/page.tsx- Usage example
🔍 Code Quality
| Metric | Value |
|---|---|
| TypeScript | ✅ Fully typed |
| Linter | ✅ No errors |
| Accessibility | ✅ ARIA labels |
| Responsive | ✅ Mobile-friendly |
| Performance | ✅ Optimized |
🎯 Future Enhancements
-
URL State Sync
const searchParams = useSearchParams(); const page = Number(searchParams.get('page')) || 1; const handlePageChange = (newPage: number) => { router.push(`?page=${newPage}`); }; -
Keyboard Navigation
useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'ArrowLeft') handlePageChange(currentPage - 1); if (e.key === 'ArrowRight') handlePageChange(currentPage + 1); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [currentPage]); -
Infinite Scroll Option
const { ref, inView } = useInView(); useEffect(() => { if (inView && hasMore && !loading) { loadMore(); } }, [inView, hasMore, loading]); -
Per-Page Selector
<select onChange={(e) => setItemsPerPage(+e.target.value)}> <option value={12}>12 per page</option> <option value={24}>24 per page</option> <option value={48}>48 per page</option> </select>
📅 Change Log
- 2025-10-15 - Initial implementation
- Created
FileGridNavigationcomponent - Updated
FileGridwith pagination support - Added comprehensive documentation
- No linter errors
- No TypeScript errors
- Fully responsive design
- Smart ellipsis algorithm
- Jump to page feature (desktop)
- Created