Skip to content

Commit

Permalink
feat: create new token page (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
pradel authored Sep 30, 2024
1 parent e46cb5c commit 7a7d7f2
Show file tree
Hide file tree
Showing 21 changed files with 1,518 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-shrimps-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stackspulse": minor
---

Create new token page.
4 changes: 2 additions & 2 deletions apps/server/src/api/tokens/holders.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { stacksClient } from "~/lib/stacks";

const tokensHoldersRouteSchema = z.object({
token: z.string(),
limit: z.number().min(1).max(100).optional(),
offset: z.number().min(0).optional(),
limit: z.coerce.number().min(1).max(100).optional(),
offset: z.coerce.number().min(0).optional(),
});

type TokensHoldersRouteResponse = {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { withSentryConfig } from "@sentry/nextjs";
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**.hiro.so",
},
],
},
};

export default withSentryConfig(nextConfig, {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@stackspulse/protocols": "workspace:*",
"@t3-oss/env-core": "0.11.1",
"@t3-oss/env-nextjs": "0.11.1",
"@tabler/icons-react": "3.18.0",
"@tabler/icons-react": "3.19.0",
"@tanstack/react-query": "5.56.2",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
Expand Down
47 changes: 47 additions & 0 deletions apps/web/src/app/tokens/[token]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { TokenHoldersTable } from "@/components/Token/TokenHoldersTable";
import { TokenInfo } from "@/components/Token/TokenInfo";
import { TokenStats } from "@/components/Token/TokenStats";
import { TokenTransactionsVolume } from "@/components/Token/TokenTransactionsVolume";
import { stacksTokensApi } from "@/lib/stacks";
import { Container } from "@radix-ui/themes";
import { notFound } from "next/navigation";
import { Suspense } from "react";

export const dynamic = "force-dynamic";

interface PageProps {
params: { token: string };
}

export default async function ProtocolPage({ params }: PageProps) {
const token = decodeURIComponent(params.token);
const tokenInfo = await stacksTokensApi
.getFtMetadata(token.split("::")[0])
.catch((error) => {
if (error.status === 404) {
return null;
}
throw error;
});
if (!tokenInfo) {
notFound();
}

return (
<Container size="2" className="px-4 pt-10">
<TokenInfo tokenInfo={tokenInfo} />

<Suspense>
<TokenStats token={token} tokenInfo={tokenInfo} />
</Suspense>

<Suspense>
<TokenTransactionsVolume token={token} tokenInfo={tokenInfo} />
</Suspense>

<Suspense>
<TokenHoldersTable token={token} tokenInfo={tokenInfo} />
</Suspense>
</Container>
);
}
68 changes: 68 additions & 0 deletions apps/web/src/components/Token/TokenHoldersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { useGetTokenHolders } from "@/hooks/api/useGetTokenHolders";
import type { FtMetadataResponse } from "@hirosystems/token-metadata-api-client";
import { Link, Table, Text } from "@radix-ui/themes";

interface TokenHoldersTableProps {
token: string;
tokenInfo: FtMetadataResponse;
}

export const TokenHoldersTable = ({
token,
tokenInfo,
}: TokenHoldersTableProps) => {
const { data } = useGetTokenHolders({ token, limit: 10 });

const calculatePercentage = (balance: string) => {
const holderBalance = Number.parseFloat(balance);
const totalSupply = Number.parseFloat(data.total_supply);
return ((holderBalance / totalSupply) * 100).toFixed(2);
};

return (
<Table.Root className="mt-10">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Rank</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Address</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">Balance</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">
% of Supply
</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>

<Table.Body>
{data.results.map((holder, index) => (
<Table.Row key={holder.address}>
<Table.Cell>
<Text size="2">{index + 1}</Text>
</Table.Cell>
<Table.Cell>
<Link
href={`https://explorer.hiro.so/address/${holder.address}?chain=mainnet`}
target="_blank"
color="gray"
>
{holder.address}
</Link>
</Table.Cell>
<Table.Cell align="right">
<Text size="2">
{(
Number(holder.balance) /
Number(10 ** (tokenInfo.decimals ?? 0))
).toLocaleString("en-US")}
</Text>
</Table.Cell>
<Table.Cell align="right">
<Text size="2">{calculatePercentage(holder.balance)}%</Text>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
33 changes: 33 additions & 0 deletions apps/web/src/components/Token/TokenInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { FtMetadataResponse } from "@hirosystems/token-metadata-api-client";
import { Heading, Text } from "@radix-ui/themes";
import Image from "next/image";

interface TokenInfoProps {
tokenInfo: FtMetadataResponse;
}

export const TokenInfo = ({ tokenInfo }: TokenInfoProps) => {
const tokenImage = tokenInfo.image_thumbnail_uri || tokenInfo.image_uri;
return (
<div className="flex items-start gap-5">
{tokenImage ? (
<Image
className="rounded-full"
src={tokenImage}
alt={`${tokenInfo.name} logo`}
width={50}
height={50}
priority
/>
) : null}
<div>
<Heading as="h1" size="5" color="gray" highContrast>
{tokenInfo.symbol} - {tokenInfo.name}
</Heading>
<Text className="mt-1" as="p" size="2" color="gray">
{tokenInfo.description}
</Text>
</div>
</div>
);
};
43 changes: 43 additions & 0 deletions apps/web/src/components/Token/TokenStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { useGetTokenHolders } from "@/hooks/api/useGetTokenHolders";
import type { FtMetadataResponse } from "@hirosystems/token-metadata-api-client";
import { Card, Text } from "@radix-ui/themes";

interface TokenStatsProps {
token: string;
tokenInfo: FtMetadataResponse;
}

export const TokenStats = ({ token, tokenInfo }: TokenStatsProps) => {
const { data } = useGetTokenHolders({ token, limit: 1 });

return (
<div className="mt-5 grid grid-cols-2 gap-5">
<Card size="2">
<Text as="div" size="2" color="gray">
Supply
</Text>
<Text
as="div"
mt="2"
size="5"
weight="medium"
title={data.total_supply}
>
{Number(
Number(data.total_supply) / Number(10 ** (tokenInfo.decimals ?? 0)),
).toLocaleString("en-US")}
</Text>
</Card>
<Card size="2">
<Text as="div" size="2" color="gray">
Holders
</Text>
<Text as="div" mt="2" size="5" weight="medium">
{data.total.toLocaleString("en-US")}
</Text>
</Card>
</div>
);
};
51 changes: 51 additions & 0 deletions apps/web/src/components/Token/TokenTransactionsVolume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { useGetTransactionVolume } from "@/hooks/api/useGetTransactionVolume";
import type { FtMetadataResponse } from "@hirosystems/token-metadata-api-client";
import { Card, Inset, Separator, Text } from "@radix-ui/themes";
import { useMemo } from "react";
import { AreaChart } from "../ui/AreaChart";
import { bigNumberValueFormatter, numberValueFormatter } from "../ui/utils";

interface TokenStatsProps {
token: string;
tokenInfo: FtMetadataResponse;
}

export const TokenTransactionsVolume = ({
token,
tokenInfo,
}: TokenStatsProps) => {
const { data } = useGetTransactionVolume({ token });

const formattedData = useMemo(() => {
return data.map((d) => ({
date: d.date,
daily_volume:
Number(d.daily_volume) / Number(10 ** (tokenInfo.decimals ?? 0)),
}));
}, [data, tokenInfo.decimals]);

return (
<Card size="2" className="mt-5">
<Text as="div" size="2" weight="medium" color="gray" highContrast>
Transactions volume
</Text>
<Text className="mt-1" as="div" size="1" color="gray">
The total volume of transactions for the token.
</Text>
<Inset py="current" side="bottom">
<Separator size="4" />
</Inset>
<AreaChart
className="mt-3 pr-3"
data={formattedData}
index="date"
categories={["daily_volume"]}
colors={["orange"]}
valueFormatter={numberValueFormatter}
valueFormatterYAxis={bigNumberValueFormatter}
/>
</Card>
);
};
Loading

0 comments on commit 7a7d7f2

Please sign in to comment.