Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"Bash(timeout 30 pnpm exec tsc --noEmit --pretty)",
"Bash(identify:*)",
"Bash(sips:*)",
"Bash(awk:*)"
"Bash(awk:*)",
"Bash(pnpm dlx:*)"
],
"deny": []
}
Expand Down
9 changes: 5 additions & 4 deletions app/admin/stats/components/TransactionsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState, useMemo } from 'react';
import { FiChevronUp, FiChevronDown } from 'react-icons/fi';
import { TablePagination } from '@/components/common/TablePagination';
import { SupportedNetworks } from '@/utils/networks';
import { Transaction } from '@/utils/statsUtils';
import { findToken } from '@/utils/tokens';
import { Market } from '@/utils/types';
import { Pagination } from '../../../markets/components/Pagination';
import { TransactionTableBody } from './TransactionTableBody';

type TransactionsTableProps = {
Expand Down Expand Up @@ -241,12 +241,13 @@ export function TransactionsTable({
/>
</table>
<div className="p-4">
<Pagination
<TablePagination
totalPages={totalPages}
totalEntries={sortedData.length}
currentPage={currentPage}
pageSize={entriesPerPage}
onPageChange={setCurrentPage}
entriesPerPage={entriesPerPage}
isDataLoaded={sortedData.length > 0}
isLoading={false}
/>
</div>
</>
Expand Down
52 changes: 52 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
@tailwind components;
@tailwind utilities;

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

:root {
--quick-nav-display: none;
--component-highlights-item-width: calc(100vw - 100px);
Expand All @@ -23,6 +32,28 @@
--color-text: #16181a;
--color-text-secondary: #8e8e8e;

/* shadcn variables */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 14 100% 57%; /* Morpho orange #f45f2d */
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 100%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 14 100% 57%;
--radius: 0.5rem;

color-scheme: light;
}

Expand All @@ -43,6 +74,27 @@
--color-text: #fff;
--color-text-secondary: #8e8e8e;

/* shadcn dark variables */
--background: 222.2 84% 4.9%;
--foreground: 0 0% 100%;
--card: 222.2 84% 4.9%;
--card-foreground: 0 0% 100%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 0 0% 100%;
--primary: 14 100% 57%; /* Morpho orange #f45f2d */
--primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 0 0% 100%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 0 0% 100%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 100%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 14 100% 57%;

color-scheme: dark;
}

Expand Down
211 changes: 128 additions & 83 deletions app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
import { useMemo, useState } from 'react';
import { useState } from 'react';
import {
Pagination,
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
Tooltip,
} from '@heroui/react';
import moment from 'moment';
import { FiFilter } from 'react-icons/fi';
import { Address } from 'viem';
import { formatUnits } from 'viem';
import { Button } from '@/components/common';
import { AccountIdentity } from '@/components/common/AccountIdentity';
import { Badge } from '@/components/common/Badge';
import { Spinner } from '@/components/common/Spinner';
import { TablePagination } from '@/components/common/TablePagination';
import { TransactionIdentity } from '@/components/common/TransactionIdentity';
import { TokenIcon } from '@/components/TokenIcon';
import { TooltipContent } from '@/components/TooltipContent';
import { MONARCH_PRIMARY } from '@/constants/chartColors';
import { useMarketBorrows } from '@/hooks/useMarketBorrows';
import { formatSimple } from '@/utils/balance';
import { Market } from '@/utils/types';

type BorrowsTableProps = {
chainId: number;
market: Market;
minAssets: string;
onOpenFiltersModal: () => void;
};

export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
export function BorrowsTable({ chainId, market, minAssets, onOpenFiltersModal }: BorrowsTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;

const {
data: borrows,
data: paginatedData,
isLoading,
isFetching,
error,
} = useMarketBorrows(market?.uniqueKey, market.loanAsset.id, chainId);
} = useMarketBorrows(market?.uniqueKey, market.loanAsset.id, chainId, minAssets, currentPage, pageSize);

const totalPages = Math.ceil((borrows ?? []).length / pageSize);
const borrows = paginatedData?.items ?? [];
const totalCount = paginatedData?.totalCount ?? 0;
const totalPages = Math.ceil(totalCount / pageSize);

const handlePageChange = (page: number) => {
setCurrentPage(page);
};

const paginatedBorrows = useMemo(() => {
const sliced = (borrows ?? []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
return sliced;
}, [currentPage, borrows, pageSize]);

const hasActiveFilter = minAssets !== '0';
const tableKey = `borrows-table-${currentPage}`;
Comment on lines +35 to 55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset page index when filters change to avoid out-of-range pages

currentPage is preserved when minAssets changes. If the user is on a high page and tightens the filter so totalPages shrinks, you can end up with currentPage > totalPages, which makes the paginator show odd ranges and can request pages with no data.

Resetting to page 1 on filter changes keeps things consistent:

-import { useState } from 'react';
+import { useEffect, useState } from 'react';
@@
 export function BorrowsTable({ chainId, market, minAssets, onOpenFiltersModal }: BorrowsTableProps) {
   const [currentPage, setCurrentPage] = useState(1);
   const pageSize = 8;
+
+  useEffect(() => {
+    setCurrentPage(1);
+  }, [minAssets]);

Also applies to: 168-177

🤖 Prompt for AI Agents
In app/market/[chainId]/[marketid]/components/BorrowsTable.tsx around lines 35
to 55 (and similarly 168 to 177), currentPage is not reset when filters
(minAssets) or identifying props (market, chainId) change, which can leave
currentPage > totalPages and cause empty or odd paginator states; add a
useEffect that watches minAssets, market?.uniqueKey, market?.loanAsset.id, and
chainId and calls setCurrentPage(1) when any of them change, and optionally add
a small safeguard useEffect that clamps currentPage to Math.max(1,
Math.min(currentPage, totalPages)) whenever totalPages changes so the page never
exceeds available pages.


if (error) {
Expand All @@ -56,80 +64,117 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {

return (
<div className="mt-8">
<h4 className="mb-4 text-lg text-secondary">Borrow & Repay</h4>

<Table
key={tableKey}
aria-label="Borrow and repay activities"
classNames={{
wrapper: 'bg-surface shadow-sm rounded',
table: 'bg-surface',
}}
bottomContent={
totalPages > 1 ? (
<div className="flex w-full justify-center">
<Pagination
isCompact
showControls
color="primary"
page={currentPage}
total={totalPages}
onChange={handlePageChange}
<div className="mb-4 flex items-center justify-between">
<h4 className="text-lg text-secondary">Borrow & Repay</h4>
<div className="flex items-center gap-2">
<Tooltip
classNames={{
base: 'p-0 m-0 bg-transparent shadow-sm border-none',
content: 'p-0 m-0 bg-transparent shadow-sm border-none',
}}
content={
<TooltipContent
title="Filters"
detail="Filter transactions by minimum amount"
icon={<FiFilter size={14} />}
/>
}
>
<Button
isIconOnly
variant="light"
size="sm"
className="min-w-0 px-2 text-secondary"
aria-label="Transaction filters"
onPress={onOpenFiltersModal}
>
<FiFilter
size={14}
style={{ color: hasActiveFilter ? MONARCH_PRIMARY : undefined }}
/>
</div>
) : null
}
>
<TableHeader>
<TableColumn>USER</TableColumn>
<TableColumn>TYPE</TableColumn>
<TableColumn align="end">AMOUNT</TableColumn>
<TableColumn>TIME</TableColumn>
<TableColumn className="font-mono" align="end">
TRANSACTION
</TableColumn>
</TableHeader>
<TableBody
className="font-zen"
emptyContent={isLoading ? 'Loading...' : 'No borrow activities found for this market'}
isLoading={isLoading}
</Button>
</Tooltip>
</div>
</div>

<div className="relative">
{/* Loading overlay */}
{isFetching && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-surface/80 backdrop-blur-sm">
<Spinner size={24} />
</div>
)}

<Table
key={tableKey}
aria-label="Borrow and repay activities"
classNames={{
wrapper: 'bg-surface shadow-sm rounded',
table: 'bg-surface',
}}
>
{paginatedBorrows.map((borrow) => (
<TableRow key={borrow.hash}>
<TableCell>
<AccountIdentity
address={borrow.userAddress as Address}
variant="compact"
linkTo="profile"
/>
</TableCell>
<TableCell>
<Badge variant={borrow.type === 'MarketRepay' ? 'success' : 'danger'}>
{borrow.type === 'MarketBorrow' ? 'Borrow' : 'Repay'}
</Badge>
</TableCell>
<TableCell className="text-right">
{formatUnits(BigInt(borrow.amount), market.loanAsset.decimals)}
{market?.loanAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
<TokenIcon
address={market.loanAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.loanAsset.symbol}
width={16}
height={16}
/>
</span>
)}
</TableCell>
<TableCell>{moment.unix(borrow.timestamp).fromNow()}</TableCell>
<TableCell className="text-right">
<TransactionIdentity txHash={borrow.hash} chainId={chainId} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<TableHeader>
<TableColumn>ACCOUNT</TableColumn>
<TableColumn>TYPE</TableColumn>
<TableColumn align="end">AMOUNT</TableColumn>
<TableColumn>TIME</TableColumn>
<TableColumn className="font-mono" align="end">
TRANSACTION
</TableColumn>
</TableHeader>
<TableBody
className="font-zen"
emptyContent={isLoading ? 'Loading...' : 'No borrow activities found for this market'}
isLoading={isLoading}
>
{borrows.map((borrow) => (
<TableRow key={`borrow-${borrow.hash}-${borrow.amount.toString()}`}>
<TableCell>
<AccountIdentity
address={borrow.userAddress as Address}
variant="compact"
linkTo="profile"
/>
</TableCell>
<TableCell>
<Badge variant={borrow.type === 'MarketRepay' ? 'success' : 'danger'}>
{borrow.type === 'MarketBorrow' ? 'Borrow' : 'Repay'}
</Badge>
</TableCell>
<TableCell className="text-right">
{formatSimple(Number(formatUnits(BigInt(borrow.amount), market.loanAsset.decimals)))}
{market?.loanAsset?.symbol && (
<span className="ml-1 inline-flex items-center">
<TokenIcon
address={market.loanAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.loanAsset.symbol}
width={16}
height={16}
/>
</span>
)}
</TableCell>
<TableCell>{moment.unix(borrow.timestamp).fromNow()}</TableCell>
<TableCell className="text-right">
<TransactionIdentity txHash={borrow.hash} chainId={chainId} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>

{totalCount > 0 && (
<TablePagination
currentPage={currentPage}
totalPages={totalPages}
totalEntries={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
isLoading={isFetching}
/>
)}
</div>
);
}
Loading