antoncoding/monarch

View on GitHub
app/positions/components/FromAndToMarkets.tsx

Summary

Maintainability
C
1 day
Test Coverage
import React from 'react';
import { Input } from '@nextui-org/react';
import { Pagination } from '@nextui-org/react';
import Image from 'next/image';
import { formatUnits } from 'viem';
import { formatReadable } from '@/utils/balance';
import { getAssetURL } from '@/utils/external';
import { findToken } from '@/utils/tokens';
import { Market } from '@/utils/types';
import { MarketPosition } from '@/utils/types';
import {
  MarketAssetIndicator,
  MarketOracleIndicator,
  MarketDebtIndicator,
} from '../../markets/components/RiskIndicator';

import { PER_PAGE } from './RebalanceModal';

type MarketTablesProps = {
  eligibleMarkets: Market[];
  fromMarkets: (MarketPosition & { pendingDelta: number })[];
  toMarkets: Market[];
  fromFilter: string;
  toFilter: string;
  onFromFilterChange: (value: string) => void;
  onToFilterChange: (value: string) => void;
  onFromMarketSelect: (marketUniqueKey: string) => void;
  onToMarketSelect: (marketUniqueKey: string) => void;
  fromPagination: {
    currentPage: number;
    totalPages: number;
    onPageChange: (page: number) => void;
  };
  toPagination: {
    currentPage: number;
    totalPages: number;
    onPageChange: (page: number) => void;
  };
  selectedFromMarketUniqueKey: string;
  selectedToMarketUniqueKey: string;
};

export function FromAndToMarkets({
  eligibleMarkets,
  fromMarkets,
  toMarkets,
  fromFilter,
  toFilter,
  onFromFilterChange,
  onToFilterChange,
  onFromMarketSelect,
  onToMarketSelect,
  fromPagination,
  toPagination,
  selectedFromMarketUniqueKey,
  selectedToMarketUniqueKey,
}: MarketTablesProps) {
  const filteredFromMarkets = fromMarkets.filter(
    (marketPosition) =>
      marketPosition.market.uniqueKey.toLowerCase().includes(fromFilter.toLowerCase()) ||
      marketPosition.market.collateralAsset.symbol.toLowerCase().includes(fromFilter.toLowerCase()),
  );

  const filteredToMarkets = toMarkets.filter(
    (market) =>
      market.uniqueKey.toLowerCase().includes(toFilter.toLowerCase()) ||
      market.collateralAsset.symbol.toLowerCase().includes(toFilter.toLowerCase()),
  );

  const paginatedFromMarkets = filteredFromMarkets.slice(
    (fromPagination.currentPage - 1) * PER_PAGE,
    fromPagination.currentPage * PER_PAGE,
  );
  const paginatedToMarkets = filteredToMarkets.slice(
    (toPagination.currentPage - 1) * PER_PAGE,
    toPagination.currentPage * PER_PAGE,
  );

  const handleFromPaginationChange = (page: number) => {
    fromPagination.onPageChange(page);
  };

  const handleToPaginationChange = (page: number) => {
    toPagination.onPageChange(page);
  };

  return (
    <div className="flex gap-4 font-zen">
      <div className="w-2/5">
        <h3 className="mb-2 text-lg font-semibold">Your Market Positions</h3>
        <Input
          placeholder="Filter your positions or Market ID"
          value={fromFilter}
          onChange={(e) => onFromFilterChange(e.target.value)}
          className="mb-2"
        />
        <div className="relative min-h-[250px] w-full overflow-x-auto">
          {fromMarkets.length === 0 ? (
            <div className="absolute inset-0 flex items-center justify-center">
              <p className="text-sm text-secondary">Loading...</p>
            </div>
          ) : (
            <table className="responsive w-full rounded-md font-zen">
              <thead className="table-header bg-gray-50 text-sm dark:bg-gray-800">
                <tr>
                  <th className="px-4 py-2 text-left">Market</th>
                  <th className="px-4 py-2 text-left">Collateral</th>
                  <th className="px-4 py-2 text-left">LLTV</th>
                  <th className="px-4 py-2 text-left">APY</th>
                  <th className="px-4 py-2 text-left">Supplied Amount</th>
                </tr>
              </thead>
              <tbody className="text-sm">
                {paginatedFromMarkets.map((marketPosition) => {
                  const collateralToken = findToken(
                    marketPosition.market.collateralAsset.address,
                    marketPosition.market.morphoBlue.chain.id,
                  );
                  return (
                    <tr
                      key={marketPosition.market.uniqueKey}
                      onClick={() => onFromMarketSelect(marketPosition.market.uniqueKey)}
                      className={`cursor-pointer border-b border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800 ${
                        marketPosition.market.uniqueKey === selectedFromMarketUniqueKey
                          ? 'bg-gray-50 dark:bg-gray-800'
                          : ''
                      }`}
                    >
                      <td className="px-4 py-2 font-monospace text-xs">
                        {marketPosition.market.uniqueKey.slice(2, 8)}
                      </td>
                      <td className="px-4 py-2">
                        <div className="flex items-center gap-1">
                          {collateralToken?.img && (
                            <Image
                              src={collateralToken.img}
                              alt={marketPosition.market.collateralAsset.symbol}
                              width={18}
                              height={18}
                            />
                          )}
                          <a
                            href={getAssetURL(
                              marketPosition.market.collateralAsset.address,
                              marketPosition.market.morphoBlue.chain.id,
                            )}
                            target="_blank"
                            rel="noopener noreferrer"
                            onClick={(e) => e.stopPropagation()}
                            className="flex items-center gap-1 no-underline hover:underline"
                          >
                            {marketPosition.market.collateralAsset.symbol}
                          </a>
                        </div>
                      </td>
                      <td className="px-4 py-2">
                        {formatUnits(BigInt(marketPosition.market.lltv), 16)}%
                      </td>
                      <td className="px-4 py-2">
                        {formatReadable(marketPosition.market.dailyApys.netSupplyApy * 100)}%
                      </td>
                      <td className="px-4 py-2">
                        {formatReadable(
                          Number(marketPosition.supplyAssets) /
                            10 ** marketPosition.market.loanAsset.decimals,
                        )}{' '}
                        {marketPosition.market.loanAsset.symbol}
                        {marketPosition.pendingDelta !== 0 && (
                          <span
                            className={`ml-1 text-xs ${
                              marketPosition.pendingDelta > 0 ? 'text-green-500' : 'text-red-500'
                            }`}
                          >
                            ({marketPosition.pendingDelta > 0 ? '+' : '-'}
                            {formatReadable(
                              Math.abs(
                                Number(
                                  formatUnits(
                                    BigInt(marketPosition.pendingDelta),
                                    marketPosition.market.loanAsset.decimals,
                                  ),
                                ),
                              ),
                            )}
                            )
                          </span>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
        <div className="h-12">
          {' '}
          {/* Reserve height for pagination */}
          {fromPagination.totalPages > 1 && ( // Only show pagination if more than 1 page
            <div className="mt-2 flex justify-center">
              <Pagination
                total={fromPagination.totalPages}
                page={fromPagination.currentPage}
                onChange={handleFromPaginationChange}
                color="primary"
              />
            </div>
          )}
        </div>
      </div>

      <div className="w-3/5">
        <h3 className="mb-2 text-lg font-semibold">Available Markets for Rebalancing</h3>
        <Input
          placeholder="Filter available markets or Market ID"
          value={toFilter}
          onChange={(e) => onToFilterChange(e.target.value)}
          className="mb-2"
        />
        <div className="relative min-h-[250px] w-full overflow-x-auto">
          {toMarkets.length === 0 ? (
            <div className="absolute inset-0 flex items-center justify-center">
              <p className="text-sm text-secondary">Loading...</p>
            </div>
          ) : (
            <table className="responsive w-full rounded-md font-zen">
              <thead className="table-header bg-gray-50 text-sm dark:bg-gray-800">
                <tr>
                  <th className="px-4 py-2 text-left">Market</th>
                  <th className="px-4 py-2 text-left">Collateral</th>
                  <th className="px-4 py-2 text-left">LLTV</th>
                  <th className="px-4 py-2 text-left">APY</th>
                  <th className="px-4 py-2 text-left">Total Supply</th>
                  <th className="px-4 py-2 text-left">Util Rate</th>
                  <th className="px-4 py-2 text-left">Risks</th>
                </tr>
              </thead>
              <tbody className="text-sm">
                {paginatedToMarkets.map((market) => {
                  const collateralToken = findToken(
                    market.collateralAsset.address,
                    market.morphoBlue.chain.id,
                  );
                  const completeMarket = eligibleMarkets.find(
                    (m) => m.uniqueKey === market.uniqueKey,
                  );
                  return (
                    <tr
                      key={market.uniqueKey}
                      onClick={() => onToMarketSelect(market.uniqueKey)}
                      className={`cursor-pointer border-b border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800 ${
                        market.uniqueKey === selectedToMarketUniqueKey
                          ? 'bg-gray-50 dark:bg-gray-800'
                          : ''
                      }`}
                    >
                      <td className="px-4 py-2 font-monospace text-xs">
                        {market.uniqueKey.slice(2, 8)}
                      </td>
                      <td className="px-4">
                        <div className="flex items-center gap-1">
                          {collateralToken?.img && (
                            <Image
                              src={collateralToken.img}
                              alt={market.collateralAsset.symbol}
                              width={18}
                              height={18}
                            />
                          )}
                          <a
                            href={getAssetURL(
                              market.collateralAsset.address,
                              market.morphoBlue.chain.id,
                            )}
                            target="_blank"
                            rel="noopener noreferrer"
                            onClick={(e) => e.stopPropagation()}
                            className="flex items-center gap-1 no-underline hover:underline"
                          >
                            {market.collateralAsset.symbol}
                          </a>
                        </div>
                      </td>
                      <td className="px-4 py-2">{formatUnits(BigInt(market.lltv), 16)}%</td>
                      <td className="px-4 py-2">{formatReadable(market.state.supplyApy * 100)}%</td>
                      <td className="px-4 py-2">
                        {formatReadable(
                          Number(market.state.supplyAssets) / 10 ** market.loanAsset.decimals,
                        )}{' '}
                        {market.loanAsset.symbol}
                      </td>
                      <td className="px-4 py-2">
                        {formatReadable(market.state.utilization * 100)}%
                      </td>
                      <td className="px-4 py-2">
                        {completeMarket && (
                          <div className="flex items-center justify-center gap-1">
                            <MarketAssetIndicator market={completeMarket} />
                            <MarketOracleIndicator market={completeMarket} />
                            <MarketDebtIndicator market={completeMarket} />
                          </div>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
        <div className="h-12">
          {' '}
          {/* Reserve height for pagination */}
          {toPagination.totalPages > 1 && ( // Only show pagination if more than 1 page
            <div className="mt-2 flex justify-center">
              <Pagination
                total={toPagination.totalPages}
                initialPage={toPagination.currentPage}
                onChange={handleToPaginationChange}
                color="primary"
              />
            </div>
          )}
        </div>
      </div>
    </div>
  );
}