Last active
September 18, 2024 13:48
-
-
Save ECBSJ/0cd822062a998ada1393a830a48c1043 to your computer and use it in GitHub Desktop.
Copy/Paste snippets for the Runes Dashboard end-to-end tutorial.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"> {e}</span> | |
</div> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
) | |
} | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
) | |
} | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
) | |
} | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function Footer() { | |
return ( | |
<div className="px-10 py-10 flex items-center"> | |
Powered by the <a href="https://www.hiro.so/" target="_blank"><img src="/HiroIcon-Rounded-Orange.png" alt="Hiro Logo" width="20" /></a> 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 /> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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