Skip to content

Commit 6671743

Browse files
authored
fix: polish product, policy, and claims UI (MergeFi#76)
1 parent fb5ea37 commit 6671743

6 files changed

Lines changed: 158 additions & 32 deletions

File tree

src/app/claims/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { useWallet } from '@/hooks/useWallet';
55
import { fetchUserClaims } from '@/lib/api';
66
import { ConnectWalletPrompt } from '@/components/ConnectWalletPrompt';
77
import { ClaimHistoryTable } from '@/components/ClaimHistoryTable';
8-
import { FullPageSpinner, LoadingSpinner } from '@/components/LoadingSpinner';
8+
import { LoadingSpinner } from '@/components/LoadingSpinner';
9+
import { SkeletonTable } from '@/components/Skeleton';
910
import type { Claim } from '@/types';
1011

1112
export default function ClaimsPage() {
@@ -55,7 +56,9 @@ export default function ClaimsPage() {
5556
</p>
5657

5758
{loading && !retrying ? (
58-
<FullPageSpinner />
59+
<div className="mt-8 rounded-2xl border border-white/10 bg-white/[0.02] p-6">
60+
<SkeletonTable rows={5} />
61+
</div>
5962
) : error ? (
6063
<div className="mt-8 rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-sm text-red-400">
6164
<p>{error}</p>

src/app/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useMemo } from 'react';
44
import { useProducts } from '@/hooks/useProducts';
5+
import { useLocalStorage } from '@/hooks/useLocalStorage';
56
import { useDebounce } from '@/hooks/useDebounce';
67
import { ProductCard } from '@/components/ProductCard';
78
import { SkeletonCard } from '@/components/Skeleton';
@@ -16,7 +17,7 @@ type CategoryFilterValue = Category | 'all';
1617
export default function HomePage() {
1718
const { products, loading, error, refetch } = useProducts();
1819
const [searchQuery, setSearchQuery] = useState('');
19-
const [category, setCategory] = useState<CategoryFilterValue>('all');
20+
const [category, setCategory] = useLocalStorage<CategoryFilterValue>('ps_category_filter', 'all');
2021
const debouncedQuery = useDebounce(searchQuery, 250);
2122

2223
const filteredProducts = useMemo(() => {

src/app/policies/[id]/page.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { formatUSDC, formatDate, timeLeft, shortenAddress } from "@/lib/format";
2424
import { CopyButton } from "@/components/CopyButton";
2525
import { ErrorBoundary } from "@/components/ErrorBoundary";
2626
import { ClaimStatus } from "@/components/ClaimStatus";
27+
import { Skeleton, SkeletonText } from "@/components/Skeleton";
2728

2829
export default function PolicyDetailPage({
2930
params,
@@ -48,40 +49,40 @@ export default function PolicyDetailPage({
4849

4950
if (loading) {
5051
return (
51-
<main className="mx-auto max-w-3xl px-6 py-12 animate-pulse">
52-
{/* Header skeleton */}
52+
<main className="mx-auto max-w-3xl px-6 py-12">
5353
<div className="mb-6 flex items-center gap-4">
54-
<div className="h-8 w-64 bg-white/10 rounded flex-1" />
55-
<div className="h-6 w-20 bg-white/10 rounded" />
54+
<Skeleton className="h-8 flex-1 rounded-xl" />
55+
<Skeleton className="h-6 w-20 rounded-full" />
5656
</div>
5757

58-
{/* Stats grid skeleton */}
5958
<div className="grid gap-4 sm:grid-cols-2">
6059
{[...Array(6)].map((_, i) => (
6160
<div
6261
key={i}
6362
className="rounded-2xl border border-white/10 bg-white/[0.03] p-5"
6463
>
65-
<div className="h-3 w-24 bg-white/10 rounded" />
66-
<div className="mt-2 h-4 w-32 bg-white/10 rounded" />
64+
<Skeleton className="h-3 w-24 rounded-full" />
65+
<div className="mt-2">
66+
<SkeletonText count={2} />
67+
</div>
6768
</div>
6869
))}
6970
</div>
7071

71-
{/* Timeline skeleton */}
72-
<div className="mt-8">
73-
<div className="h-4 w-32 bg-white/10 rounded mb-4" />
72+
<div className="mt-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5">
73+
<Skeleton className="h-3 w-32 rounded-full" />
7474
<div className="space-y-3">
7575
{[...Array(3)].map((_, i) => (
76-
<div key={i} className="h-12 bg-white/10 rounded" />
76+
<Skeleton key={i} className="h-12 rounded-xl" />
7777
))}
7878
</div>
7979
</div>
8080

81-
{/* Oracle widget skeleton */}
8281
<div className="mt-6">
83-
<div className="h-4 w-40 bg-white/10 rounded mb-3" />
84-
<div className="h-24 bg-white/10 rounded" />
82+
<Skeleton className="mb-3 h-4 w-40 rounded-full" />
83+
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-5">
84+
<SkeletonText count={3} />
85+
</div>
8586
</div>
8687
</main>
8788
);

src/app/policies/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useLocalStorage } from '@/hooks/useLocalStorage';
44
import { useWallet } from '@/hooks/useWallet';
55
import { usePolicies } from '@/hooks/usePolicies';
66
import { PolicyCard } from '@/components/PolicyCard';
@@ -17,7 +17,7 @@ const FILTERS: Filter[] = ['All', 'Active', 'Expired', 'Claimed'];
1717
export default function PoliciesPage() {
1818
const { address, connected } = useWallet();
1919
const { policies, loading, error, refetch } = usePolicies(address);
20-
const [filter, setFilter] = useState<Filter>('All');
20+
const [filter, setFilter] = useLocalStorage<Filter>('ps_policy_filter', 'All');
2121

2222
if (!connected) {
2323
return (

src/components/ClaimHistoryTable.tsx

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,102 @@
11
'use client';
22

33
import Link from 'next/link';
4+
import { useMemo, useState } from 'react';
45
import { Badge } from './Badge';
56
import { EmptyState } from './EmptyState';
67
import { TransactionLink } from './TransactionLink';
78
import type { Claim } from '@/types';
8-
import { formatUSDC, formatDateTime, shortenAddress } from '@/lib/format';
9+
import { formatUSDC, formatDateTime } from '@/lib/format';
910

1011
interface ClaimHistoryTableProps {
1112
claims: Claim[];
1213
className?: string;
1314
}
1415

1516
export function ClaimHistoryTable({ claims, className }: ClaimHistoryTableProps) {
17+
type SortColumn = 'submittedAt' | 'status' | 'payoutAmount' | 'triggerMet';
18+
type SortDirection = 'asc' | 'desc';
19+
20+
const [sort, setSort] = useState<{ column: SortColumn; direction: SortDirection }>({
21+
column: 'submittedAt',
22+
direction: 'desc',
23+
});
24+
25+
const sortedClaims = useMemo(() => {
26+
const compareValues = (a: Claim, b: Claim): number => {
27+
switch (sort.column) {
28+
case 'submittedAt':
29+
return a.submittedAt - b.submittedAt;
30+
case 'status':
31+
return a.status.localeCompare(b.status);
32+
case 'payoutAmount': {
33+
const aValue = a.payoutAmount ? BigInt(a.payoutAmount) : null;
34+
const bValue = b.payoutAmount ? BigInt(b.payoutAmount) : null;
35+
if (aValue === null && bValue === null) return 0;
36+
if (aValue === null) return 1;
37+
if (bValue === null) return -1;
38+
if (aValue === bValue) return 0;
39+
return aValue < bValue ? -1 : 1;
40+
}
41+
case 'triggerMet':
42+
return Number(a.triggerMet) - Number(b.triggerMet);
43+
}
44+
45+
return 0;
46+
};
47+
48+
const direction = sort.direction === 'asc' ? 1 : -1;
49+
50+
return claims
51+
.map((claim, index) => ({ claim, index }))
52+
.sort((left, right) => {
53+
if (sort.column === 'payoutAmount') {
54+
const leftValue = left.claim.payoutAmount ? BigInt(left.claim.payoutAmount) : null;
55+
const rightValue = right.claim.payoutAmount ? BigInt(right.claim.payoutAmount) : null;
56+
57+
if (leftValue === null && rightValue === null) {
58+
return left.index - right.index;
59+
}
60+
61+
if (leftValue === null) return 1;
62+
if (rightValue === null) return -1;
63+
64+
if (leftValue !== rightValue) {
65+
const payoutDirection = sort.direction === 'asc' ? 1 : -1;
66+
return (leftValue < rightValue ? -1 : 1) * payoutDirection;
67+
}
68+
}
69+
70+
const primary = compareValues(left.claim, right.claim);
71+
if (primary !== 0) {
72+
return primary * direction;
73+
}
74+
return left.index - right.index;
75+
})
76+
.map(({ claim }) => claim);
77+
}, [claims, sort]);
78+
79+
const toggleSort = (column: SortColumn) => {
80+
setSort((current) => {
81+
if (current.column === column) {
82+
return {
83+
column,
84+
direction: current.direction === 'asc' ? 'desc' : 'asc',
85+
};
86+
}
87+
88+
return {
89+
column,
90+
direction: column === 'status' ? 'asc' : 'desc',
91+
};
92+
});
93+
};
94+
95+
const sortIndicator = (column: SortColumn) => {
96+
if (sort.column !== column) return null;
97+
return sort.direction === 'asc' ? '↑' : '↓';
98+
};
99+
16100
if (!claims.length) {
17101
return (
18102
<EmptyState
@@ -31,15 +115,55 @@ export function ClaimHistoryTable({ claims, className }: ClaimHistoryTableProps)
31115
<tr className="border-b border-white/10 text-left text-xs uppercase tracking-wider text-gray-500">
32116
<th className="pb-3 pr-4">Claim ID</th>
33117
<th className="pb-3 pr-4">Policy</th>
34-
<th className="pb-3 pr-4">Trigger</th>
35-
<th className="pb-3 pr-4">Payout</th>
36-
<th className="pb-3 pr-4">Submitted</th>
118+
<th className="pb-3 pr-4">
119+
<button
120+
type="button"
121+
onClick={() => toggleSort('triggerMet')}
122+
className={`inline-flex items-center gap-1 transition-colors hover:text-white ${
123+
sort.column === 'triggerMet' ? 'text-white' : ''
124+
}`}
125+
>
126+
Trigger {sortIndicator('triggerMet')}
127+
</button>
128+
</th>
129+
<th className="pb-3 pr-4">
130+
<button
131+
type="button"
132+
onClick={() => toggleSort('payoutAmount')}
133+
className={`inline-flex items-center gap-1 transition-colors hover:text-white ${
134+
sort.column === 'payoutAmount' ? 'text-white' : ''
135+
}`}
136+
>
137+
Payout {sortIndicator('payoutAmount')}
138+
</button>
139+
</th>
140+
<th className="pb-3 pr-4">
141+
<button
142+
type="button"
143+
onClick={() => toggleSort('submittedAt')}
144+
className={`inline-flex items-center gap-1 transition-colors hover:text-white ${
145+
sort.column === 'submittedAt' ? 'text-white' : ''
146+
}`}
147+
>
148+
Submitted {sortIndicator('submittedAt')}
149+
</button>
150+
</th>
37151
<th className="pb-3 pr-4">Tx</th>
38-
<th className="pb-3">Status</th>
152+
<th className="pb-3">
153+
<button
154+
type="button"
155+
onClick={() => toggleSort('status')}
156+
className={`inline-flex items-center gap-1 transition-colors hover:text-white ${
157+
sort.column === 'status' ? 'text-white' : ''
158+
}`}
159+
>
160+
Status {sortIndicator('status')}
161+
</button>
162+
</th>
39163
</tr>
40164
</thead>
41165
<tbody>
42-
{claims.map((claim) => (
166+
{sortedClaims.map((claim) => (
43167
<tr
44168
key={claim.id}
45169
className="border-b border-white/5 transition-colors hover:bg-white/[0.02]"

src/components/ProductCard.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from 'react';
44
import type { Product } from '@/types';
55
import { BuyPolicyModal } from './BuyPolicyModal';
66
import { Badge } from './Badge';
7+
import { TriggerConditionBadge } from './TriggerConditionBadge';
78
import { basisPointsToPercent, formatUSDC } from '@/lib/format';
89
import { CATEGORY_ICONS } from '@/lib/constants';
910

@@ -29,13 +30,9 @@ export function ProductCard({ product }: ProductCardProps) {
2930

3031
<h3 className="mt-4 text-base font-semibold leading-snug text-white">{product.name}</h3>
3132

32-
<p className="mt-2 text-sm text-gray-400">
33-
Trigger:{' '}
34-
<span className="font-mono text-white">
35-
{product.comparison === 'LessThan' ? '<' : product.comparison === 'GreaterThan' ? '>' : '='}
36-
{' '}{product.threshold}
37-
</span>
38-
</p>
33+
<div className="mt-2">
34+
<TriggerConditionBadge product={product} />
35+
</div>
3936

4037
<dl className="mt-4 grid grid-cols-2 gap-3 text-sm">
4138
<div className="rounded-xl bg-white/5 p-3">

0 commit comments

Comments
 (0)