Skip to content

Commit

Permalink
feat: added trade ranking
Browse files Browse the repository at this point in the history
  • Loading branch information
siinghd committed Apr 4, 2024
1 parent 7b5747e commit f396856
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 3 deletions.
14 changes: 14 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/ui/nfts-by-account")),
files: ["registry/default/ui/nfts-by-account.tsx"],
},
"trade-ranking": {
name: "trade-ranking",
type: "components:ui",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/ui/trade-ranking")),
files: ["registry/default/ui/trade-ranking.tsx"],
},
"nft-card-demo": {
name: "nft-card-demo",
type: "components:example",
Expand Down Expand Up @@ -96,5 +103,12 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/example/nfts-by-account-collection-demo")),
files: ["registry/default/example/nfts-by-account-collection-demo.tsx"],
},
"trade-ranking-demo": {
name: "trade-ranking-demo",
type: "components:example",
registryDependencies: [""],
component: React.lazy(() => import("@/registry/default/example/trade-ranking-demo")),
files: ["registry/default/example/trade-ranking-demo.tsx"],
},
},
}
5 changes: 5 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/nfts-by-account",
items: [],
},
{
title: "Trade Ranking",
href: "/docs/components/trade-ranking",
items: [],
},
],
},
],
Expand Down
3 changes: 0 additions & 3 deletions apps/www/content/docs/components/nfts-by-account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
title: NFTs by Account Showcase
description: This component dynamically displays NFTs associated with a specific account address, allowing users to view NFT metadata including images, names, and collection details. Designed to fetch and render NFT data directly from the blockchain, it supports pagination for a user-friendly browsing experience.
component: true
---
To align the documentation with your updated component that focuses on displaying NFTs by account with pagination support, here's the revised version that incorporates the additional functionality and nuances of your new implementation:

---
<ComponentPreview name="nfts-by-account-demo" />

Expand Down
75 changes: 75 additions & 0 deletions apps/www/content/docs/components/trade-ranking.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Trade Ranking Showcase
description: This component dynamically displays trading rankings for NFT collections, allowing users to view detailed metrics such as collection statistics, trading volumes, and prices. It supports customizable time frames, sorting options, and a responsive design for an optimal browsing experience. Utilizing the NFTScan API, it ensures up-to-date information is presented through an engaging and interactive UI.
component: true
---

<ComponentPreview name="trade-ranking-demo" />

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx code100x-ui@latest add trade-ranking
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy the provided code snippet and paste it into your project.</Step>

<ComponentSource name="trade-ranking" />

<Step>Ensure the import paths are correctly adjusted to match your project's structure.</Step>

</Steps>

</TabsContent>

</Tabs>

#### Environment Setup

The `TradeRanking` component requires the `NEXT_PUBLIC_NFTSCAN_KEY` environment variable. This key is used to fetch trading data via the NFTScan API.

Add this line to your `.env` file:

```env
NEXT_PUBLIC_NFTSCAN_KEY=your_nftscan_api_key_here
```

Replace `your_nftscan_api_key_here` with your actual API key, which you can obtain by registering at the [NFTScan developers portal](https://developer.nftscan.com/).

#### Usage

To use the `TradeRanking` component, first import it into your project:

```tsx
import { TradeRanking } from "@/components/ui/trade-ranking"
```

Then, include it in your UI, specifying any desired props for customization:

```tsx
<TradeRanking
time="1d"
sortBy="volume"
sort_order="desc"
/>
```

#### Props

- `time`: Time frame for trading data (`"1d"`, `"15m"`, `"30m"`, `"1h"`, `"6h"`, `"12h"`).
- `sortBy`: Metric to sort by (`"volume"` or `"sales"`).
- `sort_order`: Sorting order (`"asc"` or `"desc"`).

10 changes: 10 additions & 0 deletions apps/www/public/registry/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,15 @@
"ui/nfts-by-account.tsx"
],
"type": "components:ui"
},
{
"name": "trade-ranking",
"dependencies": [
"loading"
],
"files": [
"ui/trade-ranking.tsx"
],
"type": "components:ui"
}
]
13 changes: 13 additions & 0 deletions apps/www/public/registry/styles/default/trade-ranking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "trade-ranking",
"dependencies": [
"loading"
],
"files": [
{
"name": "trade-ranking.tsx",
"content": "import { useEffect, useState } from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\n\n\nimport { cn } from \"@/lib/utils\";\n\n\n\nimport { Card, CardContent, CardHeader } from \"./card\";\nimport Loading from \"./loading\";\n\n\nconst tradeRankingVariants = cva(\n \"inline-flex flex-col rounded-lg border p-4 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2\",\n {\n variants: {\n variant: {\n default: \"border-gray-200 bg-white text-black hover:bg-gray-50\",\n dark: \"border-gray-700 bg-gray-800 text-white hover:bg-gray-700\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\ninterface TradeRankingProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof tradeRankingVariants> {\n time?: \"1d\" | \"15m\" | \"30m\" | \"1h\" | \"6h\" | \"12h\"\n sortBy?: \"volume\" | \"sales\"\n sort_order?: \"asc\" | \"desc\"\n}\n\nconst TradeRanking = ({\n className,\n variant,\n time = \"1d\",\n sortBy = \"volume\",\n sort_order = \"desc\",\n ...props\n}: TradeRankingProps) => {\n const [tradeRanking, setTradeRanking] = useState<any>(null)\n\n const [loading, setLoading] = useState(false)\n useEffect(() => {\n setLoading(true)\n const cacheKeyPrefix = `tradingRanking`\n const cachedKey = `${cacheKeyPrefix}-${time}-${sortBy}-${sort_order}`\n const fetchCollectionData = async () => {\n const cachedData = sessionStorage.getItem(cachedKey)\n if (cachedData) {\n setTradeRanking(JSON.parse(cachedData))\n } else {\n let url = `https://solanaapi.nftscan.com/api/sol/statistics/ranking/trade?time=${time}&sort_field=${sortBy}&sort_direction=${sort_order}`\n\n try {\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"*/*\",\n \"X-API-KEY\": process.env.NEXT_PUBLIC_NFTSCAN_KEY!,\n },\n })\n const jsonData = await response.json()\n if (jsonData.code === 200) {\n try {\n sessionStorage.setItem(cachedKey, JSON.stringify(jsonData.data))\n } catch (e) {\n clearCache(cacheKeyPrefix)\n sessionStorage.setItem(cachedKey, JSON.stringify(jsonData.data))\n }\n setTradeRanking(jsonData.data)\n } else {\n console.error(\"Failed to fetch collection data:\", jsonData.message)\n }\n } catch (error) {\n console.error(\"Error fetching collection data:\", error)\n }\n }\n setLoading(false)\n }\n\n fetchCollectionData()\n }, [sortBy, sort_order, time])\n\n const clearCache = (prefix: string) => {\n for (let i = 0; i < sessionStorage.length; i++) {\n const key = sessionStorage.key(i)\n if (key && key.startsWith(prefix)) {\n sessionStorage.removeItem(key)\n }\n }\n }\n\n return (\n <div>\n {loading || !tradeRanking ? (\n <Loading />\n ) : (\n <div className=\"overflow-auto border sm:rounded-lg\">\n <table className=\"min-w-full\">\n <thead>\n <tr className=\"border-b text-sm font-medium text-gray-500 dark:text-gray-400\">\n <th className=\"px-4 py-3\">Collection</th>\n <th className=\"px-4 py-3\">Lowest Price</th>\n <th className=\"px-4 py-3\">Average Price</th>\n <th className=\"px-4 py-3\">Highest Price</th>\n <th className=\"px-4 py-3\">Volume</th>\n <th className=\"px-4 py-3\">Sales</th>\n <th className=\"px-4 py-3\">Mint Price Total</th>\n <th className=\"px-4 py-3\">Mint Gas Fee</th>\n <th className=\"px-4 py-3\">Items Total</th>\n <th className=\"px-4 py-3\">Owners Total</th>\n <th className=\"px-4 py-3\">Volume Change</th>\n <th className=\"px-4 py-3\">Average Price Change</th>\n <th className=\"px-4 py-3\">Market Cap</th>\n <th className=\"px-4 py-3\">Market Trend</th>\n <th className=\"px-4 py-3\">Mint Average Price</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-200 dark:divide-gray-800\">\n {tradeRanking.map((collection: any) => (\n <tr className=\"text-gray-500 dark:text-gray-400\">\n <td className=\"flex items-center gap-4 px-4 py-3\">\n {collection.logo_url && (\n <img\n alt={collection.collection}\n className=\"rounded-full\"\n height=\"40\"\n src={`https://images.hsingh.site/?url=${collection.logo_url}&output=webp&width=40&q=80w=40&h=40`}\n style={{\n aspectRatio: \"40/40\",\n objectFit: \"cover\",\n }}\n width=\"40\"\n />\n )}\n {collection.collection}\n </td>\n <td className=\"px-4 py-3\">{collection.lowest_price}</td>\n <td className=\"px-4 py-3\">{collection.average_price}</td>\n <td className=\"px-4 py-3\">{collection.highest_price}</td>\n <td className=\"px-4 py-3\">{collection.volume}</td>\n <td className=\"px-4 py-3\">{collection.sales}</td>\n\n <td className=\"px-4 py-3\">{collection.mint_price_total}</td>\n <td className=\"px-4 py-3\">{collection.mint_gas_fee}</td>\n <td className=\"px-4 py-3\">{collection.items_total}</td>\n <td className=\"px-4 py-3\">{collection.owners_total}</td>\n <td className=\"px-4 py-3\">{collection.volume_change}</td>\n <td className=\"px-4 py-3\">{collection.average_price_change}</td>\n <td className=\"px-4 py-3\">{collection.market_cap}</td>\n <td className=\"px-4 py-3\">{collection.market_trend}</td>\n <td className=\"px-4 py-3\">{collection.mint_average_price}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n )\n}\nTradeRanking.displayName = \"TradeRanking\"\nexport { TradeRanking, tradeRankingVariants }"
}
],
"type": "components:ui"
}
7 changes: 7 additions & 0 deletions apps/www/registry/default/example/trade-ranking-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TradeRanking } from "../ui/trade-ranking"

const TradeRankingDemo = () => {
return <TradeRanking time="1d" sort_order="desc" sortBy="volume" />
}

export default TradeRankingDemo
160 changes: 160 additions & 0 deletions apps/www/registry/default/ui/trade-ranking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useEffect, useState } from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

import { Card, CardContent, CardHeader } from "./card"
import Loading from "./loading"

const tradeRankingVariants = cva(
"inline-flex flex-col rounded-lg border p-4 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
{
variants: {
variant: {
default: "border-gray-200 bg-white text-black hover:bg-gray-50",
dark: "border-gray-700 bg-gray-800 text-white hover:bg-gray-700",
},
},
defaultVariants: {
variant: "default",
},
}
)

interface TradeRankingProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof tradeRankingVariants> {
time?: "1d" | "15m" | "30m" | "1h" | "6h" | "12h"
sortBy?: "volume" | "sales"
sort_order?: "asc" | "desc"
}

const TradeRanking = ({
className,
variant,
time = "1d",
sortBy = "volume",
sort_order = "desc",
...props
}: TradeRankingProps) => {
const [tradeRanking, setTradeRanking] = useState<any>(null)

const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
const cacheKeyPrefix = `tradingRanking`
const cachedKey = `${cacheKeyPrefix}-${time}-${sortBy}-${sort_order}`
const fetchCollectionData = async () => {
const cachedData = sessionStorage.getItem(cachedKey)
if (cachedData) {
setTradeRanking(JSON.parse(cachedData))
} else {
let url = `https://solanaapi.nftscan.com/api/sol/statistics/ranking/trade?time=${time}&sort_field=${sortBy}&sort_direction=${sort_order}`

try {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "*/*",
"X-API-KEY": process.env.NEXT_PUBLIC_NFTSCAN_KEY!,
},
})
const jsonData = await response.json()
if (jsonData.code === 200) {
try {
sessionStorage.setItem(cachedKey, JSON.stringify(jsonData.data))
} catch (e) {
clearCache(cacheKeyPrefix)
sessionStorage.setItem(cachedKey, JSON.stringify(jsonData.data))
}
setTradeRanking(jsonData.data)
} else {
console.error("Failed to fetch collection data:", jsonData.message)
}
} catch (error) {
console.error("Error fetching collection data:", error)
}
}
setLoading(false)
}

fetchCollectionData()
}, [sortBy, sort_order, time])

const clearCache = (prefix: string) => {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i)
if (key && key.startsWith(prefix)) {
sessionStorage.removeItem(key)
}
}
}

return loading || !tradeRanking ? (
<Loading />
) : (
<div className="overflow-auto border sm:rounded-lg">
<table className="min-w-full">
<thead>
<tr className="border-b text-sm font-medium text-gray-500 dark:text-gray-400">
<th className="px-4 py-3">Collection</th>
<th className="px-4 py-3">Lowest Price</th>
<th className="px-4 py-3">Average Price</th>
<th className="px-4 py-3">Highest Price</th>
<th className="px-4 py-3">Volume</th>
<th className="px-4 py-3">Sales</th>
<th className="px-4 py-3">Mint Price Total</th>
<th className="px-4 py-3">Mint Gas Fee</th>
<th className="px-4 py-3">Items Total</th>
<th className="px-4 py-3">Owners Total</th>
<th className="px-4 py-3">Volume Change</th>
<th className="px-4 py-3">Average Price Change</th>
<th className="px-4 py-3">Market Cap</th>
<th className="px-4 py-3">Market Trend</th>
<th className="px-4 py-3">Mint Average Price</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{tradeRanking.map((collection: any) => (
<tr className="text-gray-500 dark:text-gray-400">
<td className="flex items-center gap-4 px-4 py-3">
{collection.logo_url && (
<img
alt={collection.collection}
className="rounded-full"
height="40"
src={`https://images.hsingh.site/?url=${collection.logo_url}&output=webp&width=40&q=80w=40&h=40`}
style={{
aspectRatio: "40/40",
objectFit: "cover",
}}
width="40"
/>
)}
{collection.collection}
</td>
<td className="px-4 py-3">{collection.lowest_price}</td>
<td className="px-4 py-3">{collection.average_price}</td>
<td className="px-4 py-3">{collection.highest_price}</td>
<td className="px-4 py-3">{collection.volume}</td>
<td className="px-4 py-3">{collection.sales}</td>

<td className="px-4 py-3">{collection.mint_price_total}</td>
<td className="px-4 py-3">{collection.mint_gas_fee}</td>
<td className="px-4 py-3">{collection.items_total}</td>
<td className="px-4 py-3">{collection.owners_total}</td>
<td className="px-4 py-3">{collection.volume_change}</td>
<td className="px-4 py-3">{collection.average_price_change}</td>
<td className="px-4 py-3">{collection.market_cap}</td>
<td className="px-4 py-3">{collection.market_trend}</td>
<td className="px-4 py-3">{collection.mint_average_price}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
TradeRanking.displayName = "TradeRanking"
export { TradeRanking, tradeRankingVariants }
12 changes: 12 additions & 0 deletions apps/www/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const ui: Registry = [
dependencies: ["pagination", "loading"],
files: ["ui/nfts-by-account.tsx"],
},
{
name: "trade-ranking",
type: "components:ui",
dependencies: ["loading"],
files: ["ui/trade-ranking.tsx"],
},
]

const example: Registry = [
Expand Down Expand Up @@ -86,6 +92,12 @@ const example: Registry = [
registryDependencies: [""],
files: ["example/nfts-by-account-collection-demo.tsx"],
},
{
name: "trade-ranking-demo",
type: "components:example",
registryDependencies: [""],
files: ["example/trade-ranking-demo.tsx"],
},
]

export const registry: Registry = [...ui, ...example]

0 comments on commit f396856

Please sign in to comment.