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
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
Loading