Skip to content

Commit

Permalink
feat: token list page (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
pradel authored Oct 6, 2024
1 parent d2f95ae commit d21feef
Show file tree
Hide file tree
Showing 19 changed files with 356 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-numbers-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stackspulse": patch
---

Add "Data" section in about page.
5 changes: 5 additions & 0 deletions .changeset/nice-lizards-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackspulse/server": minor
---

Create new `/api/tokens/resolve` route.
5 changes: 5 additions & 0 deletions .changeset/proud-laws-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackspulse/server": minor
---

Create new `/api/tokens/markets` route.
5 changes: 5 additions & 0 deletions .changeset/tricky-mirrors-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stackspulse": minor
---

Create new token list page.
1 change: 1 addition & 0 deletions apps/server/.env.production.local
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ ADMIN_API_TOKEN="encrypted:BIbtsUXc1YZP1rJDw13H4m/BWWUxRdJkuJFV5EZUMYELYQHhmF91g
TURSO_DATABASE_URL="encrypted:BFROeaR/Lo5esJOuxqWc7/NdVpxdz8EykAovi9W08PANkfxIsxDLgiOLPB9FUend3EIYA9M7XNOkxxb4jyZrqMwq0qnkQyEOSSIIDQOH+KTnSR6l+RSKItOFYvRSt2MhSSUzCLzf9z9Es55Ut3CmDZuZN2u3hutKNavTo3P62B4gzTGmkmuSaUaIxAOa5Q=="
TURSO_AUTH_TOKEN="encrypted:BI9tCPv11ZM3cBZHGXMctKpHDo+cpW6Sq7c3YYoXxNag8PpVSiGGQQMWLtYZqy4OrFauYJtSp+ehyoYuW+ZburNKZs18aaZnOaBzkwZfQG00jvOhfS7k6XhMDOj46Is/zSRS88pbI+YBLxhLQH/Sz7bwn7PGIp72H06C4m/qEyPMpKoRUS9hP1YV86FPZqasGPPEEvp03YhI6WKOvUTYcSmnZbPw9G3aIVN7DIXYdlqWeQWUh+JKQ60G7riPhBLaTPaw1y3qvPH4PVCV6IeoZ4TScd1XX9t3++xRZgAc1iT2Vj91t9lvBtzPr1PyBlduYyYebArBZiP43Fdov6FnRw0OCW29ZS/2Mrw97aTgHjrrVaoaCHk+lYbRCmzOzaAZlCupMOn0gznms3nEfZfyZRyIp2ezjGhhNWe73g=="
SENTRY_DSN="encrypted:BCys3N+cVEYBJIUIVpV/cu+VZkbhdVgDxSFv5SIs7bTfaaDp7kaVVAzTuLlwIXamSltCfzOIwJ3Z7o19ls2f2eR8wX2G/32g1+Z+KnNTaBHexT2eHbrbQ7Hv9zD6rOOFZh4W91xo/wb21IvfEn55QwhZ8N4AFB9qGoAy70ej6WJdKYric9LVwhYbf4LU76MwFvK+8+TyjOt7mPDr8Fz8PyLsJrIuOZtvje5zYg/OPOfeqeKMnw=="
COINGECKO_API_KEY="encrypted:BOnTS+pjMioUn3cROrIDZ4glP+q9hfIBP082qZIePiKQgraXHYnZXPgLq4hb2qqrDrJc55SRQTu/NFWdVf+pqstUz1vDkjKbogIvwbZUsP4ltsn+De2STeERbpYvyu5yAJY+uajL97P4ceqgpeaqaFAMPqZdWUCrKf6tnQ=="
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"deploy": "fly deploy --remote-only"
},
"dependencies": {
"@dotenvx/dotenvx": "1.14.2",
"@dotenvx/dotenvx": "1.15.0",
"@libsql/client": "0.8.0",
"@sentry/node": "8.33.1",
"@stacks/blockchain-api-client": "8.0.3",
Expand Down
36 changes: 36 additions & 0 deletions apps/server/src/api/tokens/markets.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { env } from "~/env";
import { apiCacheConfig } from "~/lib/api";

type CoingeckoCoinsMarketsResponse = {
id: string;
symbol: string;
name: string;
image: string;
current_price: number;
market_cap: number;
price_change_percentage_24h: number;
}[];

type TokensMarketsRouteResponse = CoingeckoCoinsMarketsResponse;

export default defineCachedEventHandler(
async () => {
const url =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&category=stacks-ecosystem";

const data: CoingeckoCoinsMarketsResponse[] = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
"x-cg-demo-api-key": env.COINGECKO_API_KEY,
},
}).then((res) => res.json());

return data;
},
{
...apiCacheConfig,
// Cache for 24 hours
maxAge: 60 * 60 * 24,
},
);
48 changes: 48 additions & 0 deletions apps/server/src/api/tokens/resolve.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from "zod";
import { env } from "~/env";
import { apiCacheConfig } from "~/lib/api";
import { getValidatedQueryZod } from "~/lib/nitro";

type CoingeckoCoinsIdResponse = {
id: string;
symbol: string;
name: string;
contract_address: string;
};

type TokensResolveRouteResponse = CoingeckoCoinsIdResponse;

const tokensResolveRouteSchema = z.object({
id: z.string(),
});

/**
* Resolves a coingecko token id to a deployed token contract address
*/
export default defineCachedEventHandler(
async (event) => {
const query = await getValidatedQueryZod(event, tokensResolveRouteSchema);

const url = `https://api.coingecko.com/api/v3/coins/${query.id}`;

const data: CoingeckoCoinsIdResponse = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
"x-cg-demo-api-key": env.COINGECKO_API_KEY,
},
}).then((res) => res.json());

return {
id: data.id,
symbol: data.symbol,
name: data.name,
contract_address: data.contract_address,
};
},
{
...apiCacheConfig,
// Cache for 3 days
maxAge: 60 * 60 * 24 * 3,
},
);
1 change: 1 addition & 0 deletions apps/server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const env = createEnv({
TURSO_DATABASE_URL: z.string().url(),
TURSO_AUTH_TOKEN: z.string().optional(),
SENTRY_DSN: z.string().url().optional(),
COINGECKO_API_KEY: z.string(),
},

runtimeEnv: process.env,
Expand Down
4 changes: 4 additions & 0 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const nextConfig = {
protocol: "https",
hostname: "**.hiro.so",
},
{
protocol: "https",
hostname: "**.coingecko.com",
},
],
},
};
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"deploy": "fly deploy --remote-only"
},
"dependencies": {
"@dotenvx/dotenvx": "1.14.2",
"@dotenvx/dotenvx": "1.15.0",
"@hirosystems/token-metadata-api-client": "1.3.0",
"@radix-ui/themes": "3.0.5",
"@sentry/nextjs": "8.33.1",
Expand Down
27 changes: 21 additions & 6 deletions apps/web/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ export const metadata: Metadata = {
export default async function AboutPage() {
return (
<Container size="2" className="px-4 pt-10">
<div>
<Heading as="h1" size="4">
stackpulse
</Heading>
</div>
<Heading as="h1" size="5">
stackpulse
</Heading>

<div className="mt-5 space-y-2">
<Text as="p" size="3" color="gray">
Expand All @@ -39,8 +37,25 @@ export default async function AboutPage() {
</Text>
</div>

<div className="mt-5 space-y-2">
<Heading as="h2" size="4">
Data
</Heading>
<Text as="p" size="3" color="gray">
Data is extracted from a self-hosted Stacks blockchain node and
aggregated using PostgreSQL.
</Text>
<Text as="p" size="3" color="gray">
Token prices and volume is provided by{" "}
<Link href="https://coingecko.com/" target="_blank">
CoinGecko
</Link>
.
</Text>
</div>

<div className="mt-10">
<Heading as="h3" size="4">
<Heading as="h2" size="4">
Open Stats
</Heading>
<iframe
Expand Down
102 changes: 102 additions & 0 deletions apps/web/src/app/tokens/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { env } from "@/env";
import type { TokensMarketsRouteResponse } from "@/lib/api";
import { Container, Link, Table, Text } from "@radix-ui/themes";
import type { Metadata } from "next";
import Image from "next/image";
import NextLink from "next/link";

export const dynamic = "force-dynamic";

export async function generateMetadata(): Promise<Metadata> {
return {
title: "stackspulse - tokens",
description: "Explore Stacks tokens by market cap, volume, and price",
alternates: {
canonical: "/tokens",
},
};
}

export default async function ProtocolPage() {
const data: TokensMarketsRouteResponse = await fetch(
`${env.NEXT_PUBLIC_API_URL}/api/tokens/markets`,
).then((res) => res.json());

return (
<Container size="2" className="px-4 pt-5">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Token</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">Price</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">24h</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">
Market Cap
</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>

<Table.Body>
{data.map((market) => (
<Table.Row key={market.id}>
<Table.Cell className="flex gap-2">
<Image
className="rounded-full"
src={market.image}
alt={market.name}
width={20}
height={20}
/>
{market.id === "blockstack" ? (
<Text size="2">{market.name}</Text>
) : (
<Link color="gray" highContrast size="2" asChild>
<NextLink href={`/tokens/resolve/${market.id}`}>
{market.name}
</NextLink>
</Link>
)}
</Table.Cell>
<Table.Cell align="right">
$
{market.current_price.toLocaleString("en-US", {
maximumFractionDigits: 12,
})}
</Table.Cell>
<Table.Cell align="right">
<Text
size="2"
color={
market.price_change_percentage_24h >= 0 ? "green" : "red"
}
>
{market.price_change_percentage_24h
? `${market.price_change_percentage_24h.toLocaleString(
"en-US",
{
maximumFractionDigits: 2,
},
)}%`
: ""}
</Text>
</Table.Cell>
<Table.Cell align="right">
${market.market_cap.toLocaleString("en-US")}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>

<div className="mt-5 flex justify-end">
<Text size="1" color="gray">
Data provided by{" "}
<Link href="https://coingecko.com/" target="_blank">
CoinGecko
</Link>
.
</Text>
</div>
</Container>
);
}
21 changes: 21 additions & 0 deletions apps/web/src/app/tokens/resolve/[token]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { env } from "@/env";
import type { TokensResolveRouteResponse } from "@/lib/api";
import { notFound, redirect } from "next/navigation";

export const dynamic = "force-dynamic";

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

export default async function ProtocolPage({ params }: PageProps) {
const data: TokensResolveRouteResponse = await fetch(
`${env.NEXT_PUBLIC_API_URL}/api/tokens/resolve?id=${params.token}`,
).then((res) => res.json());

if (data.contract_address) {
redirect(`/tokens/${data.contract_address}`);
}

notFound();
}
3 changes: 3 additions & 0 deletions apps/web/src/components/Layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const Header = () => {
})}
</DropdownMenu.Content>
</DropdownMenu.Root>
<Link color="gray" size="2" asChild>
<NextLink href="/tokens">Tokens</NextLink>
</Link>
<Link color="gray" size="2" asChild>
<NextLink href="/about">About</NextLink>
</Link>
Expand Down
Loading

0 comments on commit d21feef

Please sign in to comment.