diff --git a/src/app/policies/page.tsx b/src/app/policies/page.tsx index 312bf49..79e6511 100644 --- a/src/app/policies/page.tsx +++ b/src/app/policies/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { useWallet } from '@/hooks/useWallet'; import { usePolicies } from '@/hooks/usePolicies'; import { PolicyCard } from '@/components/PolicyCard'; @@ -11,16 +11,35 @@ import { Badge } from '@/components/Badge'; import Link from 'next/link'; type Filter = 'All' | 'Active' | 'Pending' | 'Processing' | 'Expired' | 'Claimed' | 'Cancelled'; +type SortOption = 'newest' | 'oldest' | 'amount-high' | 'amount-low' | 'expiry'; +type ViewMode = 'grid' | 'list'; const FILTERS: Filter[] = ['All', 'Active', 'Pending', 'Processing', 'Expired', 'Claimed', 'Cancelled']; +// Loading skeleton count for better perceived performance +const SKELETON_COUNT = 6; + +// Sort options for policy display +const SORT_OPTIONS: { value: SortOption; label: string }[] = [ + { value: 'newest', label: 'Newest First' }, + { value: 'oldest', label: 'Oldest First' }, + { value: 'amount-high', label: 'Highest Amount' }, + { value: 'amount-low', label: 'Lowest Amount' }, + { value: 'expiry', label: 'Expiring Soon' }, +]; + export default function PoliciesPage() { const { address, connected } = useWallet(); const { policies, loading, error, refetch } = usePolicies(address); - const [filter, setFilter] = useState('All'); - const [page, setPage] = useState(0); + const [filter, setFilter] = useState('All'); + const [page, setPage] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('newest'); + const [viewMode, setViewMode] = useState('grid'); + const [isRefreshing, setIsRefreshing] = useState(false); const POLICIES_PER_PAGE = 12; + // Early return for disconnected wallet if (!connected) { return (
@@ -29,64 +48,297 @@ export default function PoliciesPage() { ); } - const statusCounts = policies.reduce>((acc, p) => { - acc[p.status] = (acc[p.status] ?? 0) + 1; - return acc; - }, {}); + // Handle manual refresh + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + await refetch(); + } finally { + // Ensure minimum 500ms for visual feedback + setTimeout(() => setIsRefreshing(false), 500); + } + }, [refetch]); + + // Memoize status counts to prevent recalculation on every render + const statusCounts = useMemo( + () => + policies.reduce>((acc, p) => { + acc[p.status] = (acc[p.status] ?? 0) + 1; + return acc; + }, {}), + [policies] + ); + + // Memoize filter counts for performance + const filterCounts: Record = useMemo( + () => ({ + All: policies.length, + Active: statusCounts['Active'] ?? 0, + Pending: statusCounts['Pending'] ?? 0, + Processing: statusCounts['Processing'] ?? 0, + Expired: statusCounts['Expired'] ?? 0, + Claimed: statusCounts['Claimed'] ?? 0, + Cancelled: statusCounts['Cancelled'] ?? 0, + }), + [policies.length, statusCounts] + ); - const filterCounts: Record = { - All: policies.length, - Active: statusCounts['Active'] ?? 0, - Pending: statusCounts['Pending'] ?? 0, - Processing: statusCounts['Processing'] ?? 0, - Expired: statusCounts['Expired'] ?? 0, - Claimed: statusCounts['Claimed'] ?? 0, - Cancelled: statusCounts['Cancelled'] ?? 0, - }; + // Filter policies by status + const filteredByStatus = useMemo( + () => (filter === 'All' ? policies : policies.filter((p) => p.status === filter)), + [filter, policies] + ); - const filteredPolicies = filter === 'All' - ? policies - : policies.filter((p) => p.status === filter); + // Search filtered policies + const searchedPolicies = useMemo(() => { + if (!searchQuery.trim()) return filteredByStatus; + + const query = searchQuery.toLowerCase(); + return filteredByStatus.filter((p) => { + // Search in policy ID, status, and amount + return ( + p.id.toLowerCase().includes(query) || + p.status.toLowerCase().includes(query) || + (p.amount && p.amount.toString().includes(query)) || + (p.productName && p.productName.toLowerCase().includes(query)) + ); + }); + }, [filteredByStatus, searchQuery]); - const totalPages = Math.ceil(filteredPolicies.length / POLICIES_PER_PAGE); - const paginatedPolicies = filteredPolicies.slice(page * POLICIES_PER_PAGE, (page + 1) * POLICIES_PER_PAGE); + // Sort policies + const sortedPolicies = useMemo(() => { + const sorted = [...searchedPolicies]; + + switch (sortBy) { + case 'newest': + return sorted.sort((a, b) => + new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime() + ); + case 'oldest': + return sorted.sort((a, b) => + new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime() + ); + case 'amount-high': + return sorted.sort((a, b) => (b.amount || 0) - (a.amount || 0)); + case 'amount-low': + return sorted.sort((a, b) => (a.amount || 0) - (b.amount || 0)); + case 'expiry': + return sorted.sort((a, b) => { + if (!a.expiresAt) return 1; + if (!b.expiresAt) return -1; + return new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime(); + }); + default: + return sorted; + } + }, [searchedPolicies, sortBy]); + + const totalPages = Math.ceil(sortedPolicies.length / POLICIES_PER_PAGE); + const paginatedPolicies = sortedPolicies.slice( + page * POLICIES_PER_PAGE, + (page + 1) * POLICIES_PER_PAGE + ); + + // Reset to first page when filter, search, or sort changes + const handleFilterChange = useCallback((newFilter: Filter) => { + setFilter(newFilter); + setPage(0); + }, []); + + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + setPage(0); + }, []); + + const handleSortChange = useCallback((e: React.ChangeEvent) => { + setSortBy(e.target.value as SortOption); + setPage(0); + }, []); + + const clearSearch = useCallback(() => { + setSearchQuery(''); + setPage(0); + }, []); return (
+ {/* Header Section */}

My Policies

- {filter === 'All' - ? `${policies.length} total policies` - : `${filteredPolicies.length} ${filter.toLowerCase()} policies`} + {loading ? ( + Loading policies... + ) : filter === 'All' ? ( + `${policies.length} total ${policies.length === 1 ? 'policy' : 'policies'}` + ) : ( + `${sortedPolicies.length} ${filter.toLowerCase()} ${ + sortedPolicies.length === 1 ? 'policy' : 'policies' + }` + )}

-
+
+ {/* Refresh Button */} + {!loading && ( + + )} + + {/* View Mode Toggle */} + {!loading && policies.length > 0 && ( +
+ + +
+ )} +
+
+ + {/* Status Summary Badges */} + {!loading && policies.length > 0 && ( +
{Object.entries(statusCounts).map(([status, count]) => ( - + - {count} + + {count} + ))}
-
+ )} - {policies.length > 0 && ( -
+ {/* Search and Sort Controls */} + {!loading && policies.length > 0 && ( +
+ {/* Search Bar */} +
+ + + + + {searchQuery && ( + + )} +
+ + {/* Sort Dropdown */} +
+ + + + +
+
+ )} + + {!loading && policies.length > 0 && ( +
{FILTERS.map((tab) => { const count = filterCounts[tab]; const isActive = filter === tab; return ( + } + /> ) : ( <> -
- {paginatedPolicies.map((p) => )} +
+ {paginatedPolicies.map((p) => ( + + ))}
{totalPages > 1 && ( -
+

- Showing {page * POLICIES_PER_PAGE + 1}–{Math.min((page + 1) * POLICIES_PER_PAGE, filteredPolicies.length)} of {filteredPolicies.length} + Showing {page * POLICIES_PER_PAGE + 1}– + {Math.min((page + 1) * POLICIES_PER_PAGE, sortedPolicies.length)} of{' '} + {sortedPolicies.length} + {searchQuery && ` (filtered from ${filteredByStatus.length})`}

-
+
+ + {/* Page Numbers */} +
+ {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { + let pageNum: number; + if (totalPages <= 5) { + pageNum = i; + } else if (page < 3) { + pageNum = i; + } else if (page > totalPages - 4) { + pageNum = totalPages - 5 + i; + } else { + pageNum = page - 2 + i; + } + + return ( + + ); + })} +
+