Skip to content

Commit

Permalink
fix: tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
clmntsnr committed Dec 3, 2024
1 parent 7bd4c14 commit 70c45e9
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 1 deletion.
40 changes: 39 additions & 1 deletion src/api/services/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { api } from "../index.server";
export abstract class TokenService {
static async #fetch<R, T extends { data: R; status: number }>(
call: () => Promise<T>,
resource = "Chain",
resource = "Token",
): Promise<NonNullable<T["data"]>> {
const { data, status } = await call();

Expand All @@ -15,6 +15,44 @@ export abstract class TokenService {
return data;
}

/**
* Retrieves tokens query params from page request
* @param request request containing query params such as pagination
* @param override params for which to override value
* @returns query
*/
static #getQueryFromRequest(
request: Request,
override?: Parameters<typeof api.v4.opportunities.index.get>[0]["query"],
) {
const page = new URL(request.url).searchParams.get("page");
const items = new URL(request.url).searchParams.get("items");
const search = new URL(request.url).searchParams.get("search");

const [sort, order] = new URL(request.url).searchParams.get("sort")?.split("-") ?? [];

const filters = Object.assign(
{ items, sort, order, name: search, page },
override ?? {},
page !== null && { page: Number(page) - 1 },
);

const query = Object.entries(filters).reduce(
(_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }),
{},
);

return query;
}

static async getManyFromRequest(request: Request): Promise<{ tokens: Token[], count: number }> {
const query = TokenService.#getQueryFromRequest(request);
const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query }));
const count = await TokenService.#fetch(async () => api.v4.tokens.count.get({ query }));

return { tokens, count};
}

static async getMany(query: Parameters<typeof api.v4.tokens.index.get>[0]["query"]): Promise<Token[]> {
const tokens = await TokenService.#fetch(async () => api.v4.tokens.index.get({ query }));

Expand Down
44 changes: 44 additions & 0 deletions src/components/element/token/TokenFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Chain } from "@merkl/api";
import { Form } from "@remix-run/react";
import { Group, Icon, Input } from "dappkit/src";
import { useState } from "react";
import useSearchParamState from "src/hooks/filtering/useSearchParamState";

const filters = ["search"] as const;
type ProtocolFilter = (typeof filters)[number];

export type OpportunityFilterProps = {
only?: ProtocolFilter[];
chains?: Chain[];
exclude?: ProtocolFilter[];
};

export default function ProtocolFilters(_props: OpportunityFilterProps) {
const [search, setSearch] = useSearchParamState<string>(
"search",
v => v,
v => v,
);
const [innerSearch, setInnerSearch] = useState<string>(search ?? "");

function onSearchSubmit() {
if (!innerSearch || innerSearch === search) return;

setSearch(innerSearch);
}

return (
<Group>
<Form>
<Input
name="search"
value={innerSearch}
state={[innerSearch, setInnerSearch]}
suffix={<Icon size="sm" remix="RiSearchLine" />}
onClick={onSearchSubmit}
placeholder="Search"
/>
</Form>
</Group>
);
}
32 changes: 32 additions & 0 deletions src/components/element/token/TokenLibrary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Token } from "@merkl/api";
import { Group } from "dappkit";
import { useMemo } from "react";
import { TokenTable } from "./TokenTable";
import OpportunityPagination from "../opportunity/OpportunityPagination";
import ProtocolFilters from "./TokenFilters";
import TokenTableRow from "./TokenTableRow";

export type TokenLibraryProps = {
tokens: Token[];
count?: number;
};

export default function TokenLibrary({ tokens, count }: TokenLibraryProps) {
const rows = useMemo(
() =>
tokens?.map(t => <TokenTableRow key={`${t.name}-${t.chainId}-${t.address}`} token={t} />),
[tokens],
);

return (
<TokenTable
footer={count !== undefined && <OpportunityPagination count={count} />}
header={
<Group className="justify-between w-full">
<ProtocolFilters />
</Group>
}>
{rows}
</TokenTable>
);
}
17 changes: 17 additions & 0 deletions src/components/element/token/TokenTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createTable } from "dappkit";

export const [TokenTable, TokenRow, tokenColumns] = createTable({
token: {
name: "TOKEN",
size: "minmax(350px,1fr)",
compact: "1fr",
className: "justify-start",
main: true,
},
price: {
name: "PRICE",
size: "minmax(min-content,150px)",
compactSize: "minmax(min-content,1fr)",
className: "justify-end",
},
});
49 changes: 49 additions & 0 deletions src/components/element/token/TokenTableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Protocol, Token } from "@merkl/api";
import { Link } from "@remix-run/react";
import { Button, Group, Icon, Value } from "dappkit";
import type { BoxProps } from "dappkit";
import { Title } from "dappkit";
import { mergeClass } from "dappkit";
import type { TagTypes } from "../Tag";
import { ProtocolRow, TokenRow } from "./TokenTable";

export type TokenTableRowProps = {
hideTags?: (keyof TagTypes)[];
token: Token;
} & BoxProps;

export default function TokenTableRow({ hideTags, token, className, ...props }: TokenTableRowProps) {

return (
<Link to={`/tokens/${token.symbol}`}>
<TokenRow
size="lg"
content="sm"
className={mergeClass("", className)}
{...props}
tokenColumn={
<Group className="py-md flex-col w-full text-nowrap whitespace-nowrap text-ellipsis">
<Group className="text-nowrap whitespace-nowrap text-ellipsis min-w-0 flex-nowrap overflow-hidden max-w-full">
<Title
h={3}
size={4}
className="text-nowrap flex gap-lg whitespace-nowrap text-ellipsis min-w-0 overflow-hidden">
<Icon src={token.icon}/>
{token.name}
</Title>
</Group>
</Group>
}
priceColumn={
<Group className="py-xl">
<Button look={"soft"} className="font-mono">
<Value value format="$0,0.0a">
{token.price ?? 0}
</Value>
</Button>
</Group>
}
/>
</Link>
);
}
22 changes: 22 additions & 0 deletions src/routes/_merkl.tokens.(all).(tokens).tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { Container, Space } from "packages/dappkit/src";
import { TokenService } from "src/api/services/token.service";
import TokenLibrary from "src/components/element/token/TokenLibrary";

export async function loader({ params: { id }, request }: LoaderFunctionArgs) {
const { tokens, count } = await TokenService.getManyFromRequest(request);

return json({ tokens, count });
}

export default function Index() {
const { tokens, count } = useLoaderData<typeof loader>();

return (
<Container>
<Space size="xl"/>
<TokenLibrary tokens={tokens} count={count} />
</Container>
);
}
14 changes: 14 additions & 0 deletions src/routes/_merkl.tokens.(all).tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Outlet } from "@remix-run/react";
import Hero from "src/components/composite/Hero";

export default function Index() {
return (
<Hero
icons={[{ remix: "RiCoinFill" }]}
title={"Tokens"}
breadcrumbs={[{ link: "/tokens", name: "Tokens" }]}
description={"Tokens indexed by Merkl"}>
<Outlet />
</Hero>
);
}

0 comments on commit 70c45e9

Please sign in to comment.