Skip to content

Commit

Permalink
Show rewards in portfolio (#1661)
Browse files Browse the repository at this point in the history
* Add dummy rewards response

* Break out the PortfolioTableHeading to own component

* Show rewards in portfolio

* Simplify markup
  • Loading branch information
DannyDelott authored Nov 20, 2024
1 parent dd1e814 commit f2e2a9f
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 18 deletions.
2 changes: 2 additions & 0 deletions apps/hyperdrive-trading/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@tanstack/query-core": "^4.36.1",
"@types/d3-format": "^3.0.4",
"@types/lodash.sortby": "^4.7.9",
"@types/lodash.groupby": "^4.6.9",
"@uniswap/token-lists": "^1.0.0-beta.34",
"@usecapsule/rainbowkit-wallet": "^0.9.4",
"@usecapsule/react-sdk": "^3.17.0",
Expand All @@ -61,6 +62,7 @@
"graphql": "^16.9.0",
"graphql-request": "^7.1.0",
"lodash.sortby": "^4.7.0",
"lodash.groupby": "^4.6.0",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
18 changes: 18 additions & 0 deletions apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ReactElement, ReactNode } from "react";

export function PortfolioTableHeading({
leftElement,
rightElement,
}: {
leftElement: ReactNode;
rightElement: ReactNode;
}): ReactElement {
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 font-chakraPetch text-h4">
{leftElement}
</div>
{rightElement}
</div>
);
}
23 changes: 13 additions & 10 deletions apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig";
import { ReactElement, ReactNode } from "react";
import { AssetStack } from "src/ui/markets/AssetStack";
import { PortfolioTableHeading } from "src/ui/portfolio/PortfolioTableHeading";

export function PositionTableHeading({
hyperdrive,
Expand All @@ -15,15 +16,17 @@ export function PositionTableHeading({
rightElement: ReactNode;
}): ReactElement {
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 font-chakraPetch text-h4">
<AssetStack
hyperdriveAddress={hyperdrive.address}
hyperdriveChainId={hyperdrive.chainId}
/>
<p className="text-h4">{hyperdriveName ?? hyperdrive.name}</p>
</div>
{rightElement}
</div>
<PortfolioTableHeading
leftElement={
<>
<AssetStack
hyperdriveAddress={hyperdrive.address}
hyperdriveChainId={hyperdrive.chainId}
/>
{hyperdriveName ?? hyperdrive.name}
</>
}
rightElement={rightElement}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { appConfig } from "@delvtech/hyperdrive-appconfig";
import { Link } from "@tanstack/react-router";
import groupBy from "lodash.groupby";
import { ReactElement } from "react";
import LoadingState from "src/ui/base/components/LoadingState";
import { NonIdealState } from "src/ui/base/components/NonIdealState";
import { NoWalletConnected } from "src/ui/portfolio/NoWalletConnected";
import { PortfolioTableHeading } from "src/ui/portfolio/PortfolioTableHeading";
import { PositionContainer } from "src/ui/portfolio/PositionContainer";
import { useRewardsData } from "src/ui/portfolio/rewards/useRewardsData";
import { RewardsTableDesktop } from "src/ui/portfolio/rewards/RewardsTableDesktop";
import { usePortfolioRewardsData } from "src/ui/portfolio/rewards/useRewardsData";
import { useAccount } from "wagmi";

export function RewardsContainer(): ReactElement {
const { address: account } = useAccount();
const { rewards, rewardsStatus } = useRewardsData({ account });
const { rewards, rewardsStatus } = usePortfolioRewardsData({ account });

if (!account) {
return <NoWalletConnected />;
Expand Down Expand Up @@ -37,7 +41,7 @@ export function RewardsContainer(): ReactElement {
);
}

const hasClaimableRewards = rewards?.some((reward) => reward);
const hasClaimableRewards = !rewards || rewards?.some((reward) => reward);

if (!hasClaimableRewards) {
return (
Expand All @@ -59,5 +63,29 @@ export function RewardsContainer(): ReactElement {
);
}

return <PositionContainer className="mt-10">TODO</PositionContainer>;
const rewardsByChain = groupBy(rewards, (reward) => reward.chainId);

return (
<PositionContainer className="mt-10">
{Object.entries(rewardsByChain).map(([chainId, rewards]) => {
const chainInfo = appConfig.chains[+chainId];
return (
<div className="flex flex-col gap-6" key={chainId}>
<PortfolioTableHeading
leftElement={
<div className="flex items-center gap-3">
<div className="daisy-avatar w-10">
<img src={chainInfo.iconUrl} className="rounded-full" />
</div>
{chainInfo.name} Rewards
</div>
}
rightElement={null}
/>
<RewardsTableDesktop account={account} rewards={rewards} />
</div>
);
})}
</PositionContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { appConfig, getToken } from "@delvtech/hyperdrive-appconfig";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import classNames from "classnames";
import { ReactElement } from "react";
import { Pagination } from "src/ui/base/components/Pagination";
import { formatBalance } from "src/ui/base/formatting/formatBalance";
import { Reward } from "src/ui/portfolio/rewards/useRewardsData";
import { Address } from "viem";

export function RewardsTableDesktop({
account,
rewards,
}: {
account: Address;
rewards: Reward[] | undefined;
}): ReactElement {
const tableInstance = useReactTable({
columns: getColumns(),
data: rewards || [],
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});

return (
<div className="daisy-card overflow-x-clip rounded-box bg-gray-750 pt-3">
<table className="daisy-table daisy-table-lg">
<thead>
{tableInstance.getHeaderGroups().map((headerGroup) => (
<tr className="border-b-0" key={headerGroup.id}>
{headerGroup.headers.map((header, headerIndex) => (
<th
key={header.id}
className="relative z-10 text-sm font-normal text-neutral-content/70"
>
<div
className={classNames({
"flex cursor-pointer select-none items-center gap-2":
header.column.getCanSort(),
"px-4": headerIndex === 0, // Add padding only to the first header cell. This is so that the headers line up vertically with the card title
})}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{
asc: <ChevronUpIcon height={15} />,
desc: <ChevronDownIcon height={15} />,
}[header.column.getIsSorted() as string] ?? null}
</div>
{/* Custom border with inset for the first and last header cells */}
<span
className={classNames(
"absolute bottom-0 border-b border-neutral-content/20",
{
"left-6 right-0": headerIndex === 0, // Inset border only on the left side for the first header cell
"left-0 right-6":
headerIndex === headerGroup.headers.length - 1, // Inset border only on the right side for the last header cell
"left-0 right-0":
headerIndex !== 0 &&
headerIndex !== headerGroup.headers.length - 1, // Full-width border for other header cells
},
)}
/>
</th>
))}
</tr>
))}
</thead>

<tbody>
{tableInstance.getRowModel().rows.map((row, index) => {
const isLastRow =
index === tableInstance.getRowModel().rows.length - 1;
return (
<tr key={row.id} className="h-32 !border-b-0 font-dmMono">
{row.getVisibleCells().map((cell, cellIndex) => (
<td
className={classNames(
"relative text-xs md:text-md", // Make the td relative for the pseudo-element
{
"px-10": cellIndex === 0, // Add padding only to the first cell. This is so that the data line up vertically with the header title
"rounded-b-none": isLastRow,
"rounded-bl-box": isLastRow && cellIndex === 0,
"rounded-br-box":
isLastRow &&
cellIndex === row.getVisibleCells().length - 1,
},
)}
key={cell.id}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{!isLastRow && (
<span
className={classNames(
// Most displays round half pixels to the nearest whole pixel. As a workaround, we can use a 1px border and scale it down so it appears as a 0.5px border.
"absolute bottom-0 left-0 right-0 scale-y-50 transform border-b border-neutral-content/20",
{
"left-6 right-0": cellIndex === 0, // Inset border only on the left side for the first cell
"left-0 right-6":
cellIndex === row.getVisibleCells().length - 1, // Inset border only on the right side for the last cell
"left-0 right-0":
cellIndex !== 0 &&
cellIndex !== row.getVisibleCells().length - 1, // Full width border for other cells
},
)}
/>
)}
</td>
))}
</tr>
);
})}
</tbody>
</table>
{tableInstance.getFilteredRowModel().rows.length > 10 ? (
<Pagination tableInstance={tableInstance} />
) : null}
</div>
);
}

const columnHelper = createColumnHelper<Reward>();

function getColumns() {
return [
columnHelper.display({
id: "asset",
header: "Asset",
cell: ({ row }) => {
const token = getToken({
appConfig,
chainId: row.original.chainId,
tokenAddress: row.original.rewardToken,
})!;
return (
<div className="flex items-center gap-2 font-inter">
<img src={token.iconUrl} className="size-10" />
{token.name}
</div>
);
},
}),
columnHelper.display({
id: "claimable",
header: "Claimable",
cell: ({ row }) => {
const token = getToken({
appConfig,
chainId: row.original.chainId,
tokenAddress: row.original.rewardToken,
})!;
return (
<div className="flex flex-col">
<span className="flex font-dmMono text-neutral-content">
{formatBalance({
balance: row.original.claimable || 0n,
decimals: token.decimals,
places: token.places,
})}{" "}
{token.symbol}
</span>
</div>
);
},
}),
columnHelper.display({
id: "claim",
cell: ({ row }) => {
return (
<button className="daisy-btn daisy-btn-ghost rounded-full bg-gray-600 font-inter hover:bg-gray-700">
Claim Rewards
</button>
);
},
}),
];
}
Loading

0 comments on commit f2e2a9f

Please sign in to comment.