Skip to content

Instantly share code, notes, and snippets.

@ECBSJ
Last active September 18, 2024 13:48
Show Gist options
  • Save ECBSJ/0cd822062a998ada1393a830a48c1043 to your computer and use it in GitHub Desktop.
Save ECBSJ/0cd822062a998ada1393a830a48c1043 to your computer and use it in GitHub Desktop.
Copy/Paste snippets for the Runes Dashboard end-to-end tutorial.
// insert in the header.tsx file after the Runes-Logo `Link` element
<Link
href={"https://leather.io/learn/bitcoin-runes"}
target="_blank"
className={buttonVariants({ variant: "link" })}
>
What are Runes?
</Link>
<Link
href={"https://www.hiro.so/blog/introducing-the-runes-api"}
target="_blank"
className={buttonVariants({ variant: "link" })}
>
What is the Runes API?
</Link>
<Link
href={"https://github.com/hirosystems/runehook"}
target="_blank"
className={buttonVariants({ variant: "link" })}
>
Runehook
</Link>
// Get address balances /runes/v1/addresses/{address}/balances
export async function getAddressBalances(address: string) {
// fetch api end
// check if response has results
// map through results array to grab Runes symbol and convert to custom AddressBalances type
// await Promises
// sort array in descending order by balances
// return completed array of AddressBalances
return null
}
// Get Runes activity for an address /runes/v1/etchings/{etching}/activity/{address}
export async function getYourRunesActivity(data: AddressBalances[]) {
return null
}
// Get activity for a block /runes/v1/blocks/{block}/activity
export async function getBlockActivity(block_height: string) {
return null
}
// Get API Status /runes/v1
export async function getApiStatus() {
return null
}
// Get etching /runes/v1/etchings/{etching}
export async function getRunesEtchingInfo(id: any): Promise<Etching> {
return null
}
// for data.ts
import {
type AddressBalances,
addressBalancesToClient,
type AddressActivityForRune,
addressActivityForRuneToClient,
type BlockActivity,
blockActivityToClient,
type ApiStatus,
type Etching
} from "@/types"
import { mostFrequent } from "./helpers"
// Get address' Runes balances /runes/v1/addresses/{address}/balances
// https://docs.hiro.so/bitcoin/runes/api/balances/address
export async function getAddressBalances(address: string) {
const response = await fetch(
`https://api.hiro.so/runes/v1/addresses/${address}/balances?offset=0&limit=60`,
{
method: "GET",
cache: "no-store"
}
)
let data = await response.json()
if (data.results.length === 0) {
return []
}
let addressBalances: AddressBalances[] = data.results.map(async (o: any) => {
let { symbol } = await getRunesEtchingInfo(o.rune.id)
o.symbol = symbol
let finalObject = addressBalancesToClient(o)
return finalObject
})
let completedArrayOfObjects = await Promise.all(addressBalances)
completedArrayOfObjects.sort(
(a: AddressBalances, b: AddressBalances) => b.balance - a.balance
)
return completedArrayOfObjects
}
// Get Runes activity for an address /runes/v1/etchings/{etching}/activity/{address}
// https://docs.hiro.so/bitcoin/runes/api/activities/for-address
export async function getYourRunesActivity(data: AddressBalances[]) {
if (data.length === 0) {
return []
}
let responses = data.map(async eachRune => {
let id = eachRune.id
let address = eachRune.address
let symbol = eachRune.symbol
let name = eachRune.name
let spaced_name = eachRune.spaced_name
const response = await fetch(
`https://api.hiro.so/runes/v1/etchings/${id}/activity/${address}?offset=0&limit=60`,
{
method: "GET",
cache: "no-store"
}
)
let data = await response.json()
let results: AddressActivityForRune[] = data.results.map((data: any) => {
let result = addressActivityForRuneToClient(data, id, symbol, name, spaced_name)
return result
})
return results
})
let arrayOfArrays = await Promise.all(responses)
let flattenedArray = arrayOfArrays.flat(1)
flattenedArray.sort(
(a: AddressActivityForRune, b: AddressActivityForRune) => b.timestamp - a.timestamp
)
return flattenedArray
}
// Get activity for a block /runes/v1/blocks/{block}/activity
// https://docs.hiro.so/bitcoin/runes/api/activities/for-block
export async function getBlockActivity(block_height: string) {
let response = await fetch(
`https://api.hiro.so/runes/v1/blocks/${block_height}/activity?offset=0&limit=60`,
{
method: "GET",
cache: "no-store"
}
)
let data = await response.json()
let totalRunesActivity: number = data.total
let results: BlockActivity[] = data.results.map(blockActivityToClient)
let mostFrequentRunes = mostFrequent(results, p => p.id)
return { results, totalRunesActivity, mostFrequentRunes }
}
// Get API Status /runes/v1
// https://docs.hiro.so/bitcoin/runes/api/info/status
export async function getApiStatus() {
let response = await fetch("https://api.hiro.so/runes/v1/", {
method: "GET",
cache: "no-store"
})
let data = await response.json()
let api_status: ApiStatus = data
return api_status
}
// Get etching /runes/v1/etchings/{etching}
// https://docs.hiro.so/bitcoin/runes/api/etchings/get-etching
export async function getRunesEtchingInfo(id: any): Promise<Etching> {
let response = await fetch(`https://api.hiro.so/runes/v1/etchings/${id}`, {
method: "GET",
cache: "no-store"
})
let data = await response.json()
return data
}
// for /types/index
// these will be all the types and converters we need
export type AddressBalances = {
address: string
balance: number
id: string
name: string
spaced_name: string
symbol: string
}
export function addressBalancesToClient(data: any) {
return {
address: data.address,
balance: +data.balance,
id: data.rune.id,
name: data.rune.name,
spaced_name: data.rune.spaced_name,
symbol: data.symbol
}
}
export type AddressActivityForRune = {
rune_id: string
address: string
amount: number
operation: string
block_height: number
tx_id: string
timestamp: number
symbol: string
name: string
spaced_name: string
}
export function addressActivityForRuneToClient(
data: any,
id: string,
symbol: string,
name: string,
spaced_name: string
) {
return {
rune_id: id,
address: data.address,
amount: +data.amount,
operation: data.operation,
block_height: data.location.block_height,
tx_id: data.location.tx_id,
timestamp: data.location.timestamp,
symbol: symbol,
name: name,
spaced_name: spaced_name
}
}
export type BlockActivity = {
id: string
name: string
spaced_name: string
operation: string
block_height: number
tx_id: string
timestamp: number
amount: number
}
export function blockActivityToClient(data: any) {
return {
id: data.rune.id,
name: data.rune.name,
spaced_name: data.rune.spaced_name,
operation: data.operation,
block_height: data.location.block_height,
tx_id: data.location.tx_id,
timestamp: data.location.timestamp,
amount: data.amount
}
}
export type ApiStatus = {
server_version: string
status: string
block_height: number
}
export type Etching = {
id: string
name: string
spaced_name: string
number: number
divisibility: number
symbol: string
turbo: boolean
mint_terms: {
amount: string
cap: string
height_start: number
height_end: number
offset_start: number
offset_end: number
}
supply: {
current: string
minted: string
total_mints: string
mint_percentage: string
mintable: boolean
burned: string
total_burns: string
premine: string
}
location: {
block_hash: string
block_height: number
tx_id: string
tx_index: number
vout: number
output: string
timestamp: number
}
}
// for lib/helpers.ts to sort an array by its most frequent property/value
export const mostFrequent = <T, K extends PropertyKey>(
arr: T[],
mapFn: (x: T) => K = x => x as unknown as K
): K | null => {
const frequencyMap = arr.reduce<Record<PropertyKey, number>>((a, v) => {
const k = mapFn(v)
a[k] = (a[k] ?? 0) + 1
return a
}, {} as Record<PropertyKey, number>)
return Object.entries(frequencyMap).reduce<[K | null, number]>(
(a, [k, v]) => (v >= a[1] ? [k as K, v] : a),
[null, 0]
)[0]
}
// for components/dashboard-line-item.tsx
"use client"
import { Separator } from "./ui/separator"
type DashboardLineItemParams = {
// runes name or symbol
a: string
// operation or runes id
b: string
// block height or tx id or address
c?: number | string
// amount
d: number
// symbol
e?: string
}
export default function DashboardLineItem({
a,
b,
c,
d,
e
}: DashboardLineItemParams) {
return (
<div className="rounded-md border px-4 py-3 font-mono text-sm flex justify-between items-center my-3">
<span>{a}</span>
<Separator orientation="horizontal" />
<span>{b}</span>
{c ? (
<>
<Separator orientation="horizontal" />
<span>{typeof c === "string" ? c.slice(-6) : c}</span>
</>
) : null}
<Separator orientation="horizontal" />
<span>{d?.toLocaleString()}</span>
<span className="text-2xl">&nbsp;{e}</span>
</div>
)
}
// for basic connect wallet skeleton in components/connect-wallet.tsx
"use client"
import { authenticate } from "@stacks/connect"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
import { UserSession } from "@stacks/connect"
import { useEffect, useState } from "react"
import { useSelectedLayoutSegment } from "next/navigation"
type props = {
buttonLabel: string
}
export default function ConnectWallet({ buttonLabel }: props) {
return null
}
// used for data-table.tsx in app/components
"use client"
import * as React from "react"
import { Button, buttonVariants } from "@/components/ui/button"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table"
import { Input } from "@/components/ui/input"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import { Columns3, LayoutGrid } from "lucide-react"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility
}
})
const searchParams = useSearchParams()
const userAddress = searchParams.get("userAddress")
return (
<div>
<div className="flex items-center py-4">
<div className="flex items-center justify-start gap-4">
<Link
href={{
pathname: "/dashboard",
query: {
userAddress
}
}}
className={buttonVariants({ variant: "secondary" })}
prefetch={true}
>
<LayoutGrid className="mr-1.5 w-4"/>Dashboard
</Link>
<Input
placeholder="Filter By Runes Name"
value={table.getColumn("name")?.getFilterValue() as string}
onChange={event => {
table.getColumn("name")?.setFilterValue(event.target.value)
}}
className="max-w-sm"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
<Columns3 className="mr-1.5 w-4"/>Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(column => column.getCanHide())
.map(column => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={value => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
// used for columns-activity.tsx under app/components/columns
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal, ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
type Data = {
rune_id: string
address: string
amount: number
operation: string
block_height: number
tx_id: string
timestamp: number
symbol: string
name: string
spaced_name: string
}
export const columns: ColumnDef<Data>[] = [
{
accessorKey: "spaced_name",
header: "Runes"
},
{
accessorKey: "rune_id",
header: "ID"
},
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "operation",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Operation
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
}
},
{
accessorKey: "timestamp",
header: "Date"
},
{
accessorKey: "block_height",
header: "Block"
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "decimal"
}).format(amount)
return <div className="text-right font-medium">{formatted}</div>
}
},
{
accessorKey: "symbol",
header: "Symbol",
cell: ({ row }) => {
const symbol: string = row.getValue("symbol")
return <span className="text-4xl">{symbol}</span>
}
},
{
id: "actions",
cell: ({ row }) => {
const data = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(data.tx_id)}
>
Copy TX ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => window.open(`https://magiceden.us/runes/${data.spaced_name}`, '_blank')}>View Etching</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.open(`https://mempool.space/tx/${data.tx_id}`, '_blank')}>View Transaction</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
}
]
// used for columns-balances.tsx under app/components/columns
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal, ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
type Data = {
address: string
balance: number
id: string
name: string
spaced_name: string
symbol: string
}
export const columns: ColumnDef<Data>[] = [
{
accessorKey: "spaced_name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Runes
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
}
},
{
accessorKey: "id",
header: "ID"
},
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "balance",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const balance = parseFloat(row.getValue("balance"))
const formatted = new Intl.NumberFormat("en-US", {
style: "decimal"
}).format(balance)
return <div className="text-right font-medium">{formatted}</div>
}
},
{
accessorKey: "symbol",
header: "Symbol",
cell: ({ row }) => {
const symbol: string = row.getValue("symbol")
return <span className="text-4xl">{symbol}</span>
}
},
{
id: "actions",
cell: ({ row }) => {
const data = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(data.id)}
>
Copy Runes ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => window.open(`https://magiceden.us/runes/${data.spaced_name}`, '_blank')}>View Etching</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
}
]
// used for columns-block-activity.tsx under app/components/columns
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal, ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
type Data = {
id: string
name: string
spaced_name: string
operation: string
block_height: number
tx_id: string
timestamp: number
amount: number
}
export const columns: ColumnDef<Data>[] = [
{
accessorKey: "spaced_name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Runes
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
}
},
{
accessorKey: "id",
header: "ID"
},
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "operation",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Operation
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
}
},
{
accessorKey: "tx_id",
header: "TX ID",
cell: ({ row }) => {
const tx_id: string = row.getValue("tx_id")
return (
<div className="font-medium">
{tx_id.slice(0, 10) + "..." + tx_id.slice(-10)}
</div>
)
}
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "decimal"
}).format(amount)
return <div className="text-right font-medium">{formatted}</div>
}
},
{
id: "actions",
cell: ({ row }) => {
const data = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(data.tx_id)}
>
Copy TX ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => window.open(`https://magiceden.us/runes/${data.spaced_name}`, '_blank')}>View Etching</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.open(`https://mempool.space/tx/${data.tx_id}`, '_blank')}>View Transaction</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
}
]
export default function Footer() {
return (
<div className="px-10 py-10 flex items-center">
Powered by the &nbsp; <a href="https://www.hiro.so/" target="_blank"><img src="/HiroIcon-Rounded-Orange.png" alt="Hiro Logo" width="20" /></a>&nbsp; Runes API.
</div>
)
}
// You can download the HiroIcon-Rounded-Orange.png file here: https://github.com/ECBSJ/my-runes-app/blob/main/public/HiroIcon-Rounded-Orange.png
// used for loading.tsx in app/dashboard
import { Skeleton } from "@/components/ui/skeleton"
function SkeletonCard() {
return (
<main className="flex h-[968px] items-start justify-center gap-4 p-24">
<div className="flex flex-col flex-1 gap-4">
<div className="flex flex-col space-y-3">
<Skeleton className="h-[250px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-10/12" />
</div>
</div>
<div className="flex flex-col space-y-3">
<Skeleton className="h-[250px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-10/12" />
</div>
</div>
</div>
<div className="flex flex-col flex-1 gap-4">
<div className="flex flex-col space-y-3">
<Skeleton className="h-[250px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-10/12" />
</div>
</div>
<div className="flex flex-col space-y-3">
<Skeleton className="h-[250px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-10/12" />
</div>
</div>
</div>
</main>
)
}
export default function Loading() {
return <SkeletonCard />
}
// used for error.tsx in app/dashboard
"use client"
import { useEffect } from "react"
import { AlertCircle } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
export default function Error({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<main className="flex h-[968px] items-start justify-center gap-4 p-24">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong! <br /> Did you login with a Bitcoin Web3 wallet? <br /> Is there an issue with the API calls? <br /> Or try refreshing the page?
</AlertDescription>
</Alert>
</main>
)
}
// Insert in app/page as the returned tsx
<main className="flex min-h-[800px] flex-col items-center justify-center p-10 gap-10 text-center">
<img src="/black-runestone.png" alt="black runestone" width="200" />
<h1 className='text-7xl font-bold'>Your <span className='text-orange-500'>Runes</span> <br /> Your App</h1>
<div className='flex items-center justify-between gap-10 text-left'>
<p>Connect your Bitcoin Web3 wallet <br />to view your Runes dashboard.</p>
<Button variant="outline">View Dashboard</Button>
</div>
</main>
// You can download the black-runestone.png file here: https://github.com/ECBSJ/my-runes-app/blob/main/public/black-runestone.png
// insert in app/dashboard/page as general skeleton for the dashboard
<main className="flex h-[968px] items-start justify-center gap-2 p-24">
<div className="flex flex-col flex-1 gap-2">
<span>1</span>
<span>2</span>
</div>
<div className="flex flex-col flex-1 gap-2">
<span>3</span>
<span>4</span>
</div>
</main>
// basic skeleton for activity card
"use client"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@/components/ui/collapsible"
import { ChevronsUpDown, FolderKanban } from "lucide-react"
import Link from "next/link"
import { useState } from "react"
function Activity() {
const [isOpen, setIsOpen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-start gap-2">
<FolderKanban />
Your Runes Activity
</CardTitle>
<CardDescription>Most recent Runes activity by block height</CardDescription>
</CardHeader>
<CardContent>
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-2">
<div className="flex items-center justify-between space-x-4 px-4">
<h4 className="text-sm font-semibold">Your recent Runes activity</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
<CollapsibleContent className="space-y-2">
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/colors
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@stitches/react
</div>
</CollapsibleContent>
</Collapsible>
</CardContent>
<CardFooter className="flex items-center justify-end">
<Button disabled>View More</Button>
</CardFooter>
</Card>
)
}
export default Activity
**********
// dashboard line items for activity card
<DashboardLineItem
a={
addressActivityForRune[0] != undefined
? addressActivityForRune[0].spaced_name
: "."
}
b={
addressActivityForRune[0] != undefined
? addressActivityForRune[0].operation
: "."
}
c={
addressActivityForRune[0] != undefined
? addressActivityForRune[0].block_height
: "."
}
d={
addressActivityForRune[0] != undefined ? addressActivityForRune[0].amount : 0
}
e={
addressActivityForRune[0] != undefined ? addressActivityForRune[0].symbol : ""
}
/>
<CollapsibleContent className="space-y-2">
<DashboardLineItem
a={
addressActivityForRune[1] != undefined
? addressActivityForRune[1].spaced_name
: "."
}
b={
addressActivityForRune[1] != undefined
? addressActivityForRune[1].operation
: "."
}
c={
addressActivityForRune[1] != undefined
? addressActivityForRune[1].block_height
: "."
}
d={
addressActivityForRune[1] != undefined
? addressActivityForRune[1].amount
: 0
}
e={
addressActivityForRune[1] != undefined ? addressActivityForRune[1].symbol : ""
}
/>
<DashboardLineItem
a={
addressActivityForRune[2] != undefined
? addressActivityForRune[2].spaced_name
: "."
}
b={
addressActivityForRune[2] != undefined
? addressActivityForRune[2].operation
: "."
}
c={
addressActivityForRune[2] != undefined
? addressActivityForRune[2].block_height
: "."
}
d={
addressActivityForRune[2] != undefined
? addressActivityForRune[2].amount
: 0
}
e={
addressActivityForRune[2] != undefined ? addressActivityForRune[2].symbol : ""
}
/>
</CollapsibleContent>
*******
// conditional button
{addressActivityForRune.length > 0 ? (
<Link
href={{
pathname: "/dashboard/activity",
query: {
userAddress
}
}}
className={buttonVariants({ variant: "default" })}
prefetch={true}
>
View More
</Link>
) : (
<Button disabled>No Activity</Button>
)}
// basic skeleton for balances card
"use client"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@/components/ui/collapsible"
import { ChevronsUpDown, WalletCards } from "lucide-react"
import Link from "next/link"
import { useState } from "react"
export default function BalancesCard() {
const [isOpen, setIsOpen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-start gap-2">
<WalletCards />
Your Runes Balances
</CardTitle>
<CardDescription>Snapshot of your Runes balances</CardDescription>
</CardHeader>
<CardContent>
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-2">
<div className="flex items-center justify-between space-x-4 px-4">
<h4 className="text-sm font-semibold">Balances of your top 3 Runes</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
<CollapsibleContent className="space-y-2">
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
</CollapsibleContent>
</Collapsible>
</CardContent>
<CardFooter className="flex items-center justify-between">
<Button
variant="secondary"
onClick={() => window.open(`https://ordinals.hiro.so/address/${'x'}`, "_blank")}
>
Ordinals?
</Button>
<Button disabled>View More</Button>
</CardFooter>
</Card>
)
}
***********
// dashboard line items for balances card
<DashboardLineItem
a={
addressBalances[0] != undefined ? addressBalances[0].spaced_name : "."
}
b={addressBalances[0] != undefined ? addressBalances[0].id : "."}
d={addressBalances[0] != undefined ? addressBalances[0].balance : 0}
e={addressBalances[0] != undefined ? addressBalances[0].symbol : "."}
/>
<CollapsibleContent className="space-y-2">
<DashboardLineItem
a={
addressBalances[1] != undefined
? addressBalances[1].spaced_name
: "."
}
b={addressBalances[1] != undefined ? addressBalances[1].id : "."}
d={addressBalances[1] != undefined ? addressBalances[1].balance : 0}
e={addressBalances[1] != undefined ? addressBalances[1].symbol : "."}
/>
<DashboardLineItem
a={
addressBalances[2] != undefined
? addressBalances[2].spaced_name
: "."
}
b={addressBalances[2] != undefined ? addressBalances[2].id : "."}
d={addressBalances[2] != undefined ? addressBalances[2].balance : 0}
e={addressBalances[2] != undefined ? addressBalances[2].symbol : "."}
/>
</CollapsibleContent>
*********
// conditional button
{addressBalances.length > 0 ? (
<Link
href={{
pathname: "/dashboard/balances",
query: {
userAddress
}
}}
className={buttonVariants({ variant: "default" })}
prefetch={true}
>
View More
</Link>
) : (
<Button disabled>No Runes Found</Button>
)}
// basic skeleton for featured card
"use client"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
import { Button, buttonVariants } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
import Link from "next/link"
import { Sparkles } from "lucide-react"
export default function FeaturedCard() {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-start gap-2">
<Sparkles />
Featured Runes
</CardTitle>
<CardDescription>
Most active Runes in recent Bitcoin block height {"Placeholder block height"}
</CardDescription>
</CardHeader>
<CardContent>
<span>Featured Runes</span>
</CardContent>
<CardFooter className="flex items-center justify-between">
<Button
variant="secondary"
onClick={() => window.open(`https://ordiscan.com/rune/${"x"}`, "_blank")}
>
View Etching
</Button>
<Link
href={`https://magiceden.us/runes/${"x"}`}
className={buttonVariants({ variant: "default" })}
prefetch={true}
target="_blank"
>
View Market
</Link>
</CardFooter>
</Card>
)
}
*******
// insert in CardContent
<p>
<span className="font-semibold">
{featuredRunes?.spaced_name}
</span>{" "}
| {featuredRunes?.id}
</p>
<div className="flex items-center justify-between my-4">
<span className="text-8xl">{featuredRunes?.symbol}</span>
<Separator orientation="horizontal" />
<span className="flex flex-col">
<p className="font-semibold m-0 p-0">Current Supply:</p> <br />
{Number(featuredRunes?.supply.current).toLocaleString() + featuredRunes?.symbol}
</span>
</div>
<span className="flex items-center justify-start gap-2">
<Badge variant="secondary">
{featuredRunes?.supply.mintable ? "Mintable" : "Unmintable"}
</Badge>
<Badge variant="secondary">
{featuredRunes?.turbo ? "Turbo" : "Not Turbo"}
</Badge>
<Badge variant="secondary">
{featuredRunes?.location.block_height}
</Badge>
</span>
<span></span>
// basic skeleton for recent block activity card
"use client"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@/components/ui/collapsible"
import { ChevronsUpDown, SquareActivity } from "lucide-react"
import Link from "next/link"
import { useState } from "react"
export default function BlockActivity() {
const [isOpen, setIsOpen] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-start gap-2">
<SquareActivity />
Activity in Block
</CardTitle>
<CardDescription>
Total of {"x"} Runes activity in last confirmed block {"x"}
</CardDescription>
</CardHeader>
<CardContent>
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-2">
<div className="flex items-center justify-between space-x-4 px-4">
<h4 className="text-sm font-semibold">
3 most recent activity in last confirmed block
</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
<CollapsibleContent className="space-y-2">
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
<div className="rounded-md border px-4 py-3 font-mono text-sm">
@radix-ui/primitives
</div>
</CollapsibleContent>
</Collapsible>
</CardContent>
<CardFooter className="flex items-center justify-between">
<Button
variant="secondary"
onClick={() => window.open(`https://ordiscan.com/block/${"x"}/runes`, "_blank")}
>
View Block
</Button>
<Button>View More</Button>
</CardFooter>
</Card>
)
}
*******
// dashboard line items
<DashboardLineItem
a={blockActivity ? blockActivity[0].spaced_name : "."}
b={blockActivity ? blockActivity[0].operation : "."}
c={blockActivity ? blockActivity[0].tx_id : "."}
d={blockActivity ? blockActivity[0].amount : 0}
/>
<CollapsibleContent className="space-y-2">
<DashboardLineItem
a={blockActivity ? blockActivity[1].spaced_name : "."}
b={blockActivity ? blockActivity[1].operation : "."}
c={blockActivity ? blockActivity[1].tx_id : "."}
d={blockActivity ? blockActivity[1].amount : 0}
/>
<DashboardLineItem
a={blockActivity ? blockActivity[2].spaced_name : "."}
b={blockActivity ? blockActivity[2].operation : "."}
c={blockActivity ? blockActivity[2].tx_id : "."}
d={blockActivity ? blockActivity[2].amount : 0}
/>
</CollapsibleContent>
********
// conditional button to view more block activity
<Link
href={{
pathname: "/dashboard/recent-block-activity",
query: {
userAddress
}
}}
className={buttonVariants({ variant: "default" })}
prefetch={true}
>
View More
</Link>
// leaf segment for app/dashboard/activity/page
export default async function YourActivity() {
return (
<div className="container mx-auto py-10 mt-10">
DataTable
</div>
)
}
// leaf segment for app/dashboard/balances/page
export default async function YourBalances() {
return (
<div className="container mx-auto py-10 mt-10">
DataTable
</div>
)
}
// leaf segment for app/dashboard/recent-block-activity/page
export default async function BlockActivity() {
return (
<div className="container mx-auto py-10 mt-10">
DataTable
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment