Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(collections): add collection stats #170

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/arkmarket/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"moment": "^2.30.1",
"next": "^14.2.3",
"nuqs": "^1.18.0",
"query-string": "^9.1.0",
"react": "catalog:react18",
"react-dom": "catalog:react18",
"react-icons": "^5.0.1",
Expand Down
15 changes: 15 additions & 0 deletions apps/arkmarket/src/app/collections/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import CollectionsContainer from "~/components/collections/collections-container";
import getCollections from "~/lib/getCollections";

export default async function CollectionsPage() {
const collections = await getCollections({});

return (
<div className="">
<div className="p-6 text-3xl font-extrabold md:text-5xl">
All Collections
</div>
<CollectionsContainer initialData={collections} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { useState } from "react";

import type {
CollectionSortBy,
CollectionSortDirection,
CollectionStats,
CollectionTimerange,
} from "~/types";
import useCollections from "~/hooks/useCollections";
import CollectionList from "./collections-list";
import CollectionsToolbar from "./collections-toolbar";

interface CollectionsContainerProps {
initialData: CollectionStats[];
}

export default function CollectionsContainer({
initialData,
}: CollectionsContainerProps) {
const [searchQuery, setSearchQuery] = useState("");
const {
data,
sortBy,
setSortBy,
sortDirection,
setSortDirection,
timerange,
setTimerange,
} = useCollections({ initialData });

const onSortChange = async (
by: CollectionSortBy,
direction: CollectionSortDirection,
) => {
await setSortBy(by);
await setSortDirection(direction);
};

const handleTimerangeChange = async (timerange: CollectionTimerange) => {
await setTimerange(timerange);
};

const items = data.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()),
);

return (
<>
<CollectionsToolbar
timerange={timerange}
onTimerangeChange={handleTimerangeChange}
onSearchChange={(query) => setSearchQuery(query)}
/>
<CollectionList
items={items}
onSortChange={onSortChange}
sortBy={sortBy}
sortDirection={sortDirection}
/>
</>
);
}
249 changes: 249 additions & 0 deletions apps/arkmarket/src/components/collections/collections-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
import { TableVirtuoso } from "react-virtuoso";
import { formatEther } from "viem";

import { cn } from "@ark-market/ui";
import { Button } from "@ark-market/ui/button";
import { NoResult } from "@ark-market/ui/icons";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@ark-market/ui/table";

import type {
CollectionSortBy,
CollectionSortDirection,
CollectionStats,
} from "~/types";
import Media from "../media";

function SortButton({
isActive,
label,
sortBy,
sortDirection,
onChange,
}: {
isActive: boolean;
label?: string;
sortBy: CollectionSortBy;
sortDirection: CollectionSortDirection;
onChange: (by: CollectionSortBy, direction: CollectionSortDirection) => void;
}) {
return (
<Button
variant="unstyled"
className="p-0"
onClick={() =>
onChange(
sortBy,
isActive ? (sortDirection === "asc" ? "desc" : "asc") : "desc",
)
}
>
{label ?? sortBy}
<div className="flex flex-col">
<ChevronUp
className={cn(
"-mb-1 size-3",
isActive && sortDirection === "asc" && "text-primary",
)}
/>
<ChevronDown
className={cn(
"-mb-1 size-3",
isActive && sortDirection === "desc" && "text-primary",
)}
/>
</div>
</Button>
);
}

interface CollectionsListProps {
items: CollectionStats[];
sortBy: CollectionSortBy;
sortDirection: CollectionSortDirection;
onSortChange: (
sortBy: CollectionSortBy,
sortDirection: CollectionSortDirection,
) => void;
}

export default function CollectionsList({
items,
sortBy,
sortDirection,
onSortChange,
}: CollectionsListProps) {
if (!items.length) {
return (
<div className="mt-10 flex items-center justify-center">
<div className="flex flex-col items-center">
<NoResult className="size-12" />
<p className="pl-3 text-center text-xl font-semibold text-muted-foreground">
No collections found, try another search.
</p>
</div>
</div>
);
}

return (
<TableVirtuoso
data={items}
className="mb-12 min-w-[1024px]"
useWindowScroll
totalCount={items.length}
overscan={10}
initialTopMostItemIndex={0}
components={{
Table,
// Table: forwardRef((props, ref) => (
// <Table {...props} ref={ref} className="table-auto" />
// )),
TableBody,
TableHead: (props) => <TableHeader {...props} className="sticky" />,
TableRow,
}}
fixedHeaderContent={() => (
<TableRow>
<TableHead className="sticky w-[250px] pl-6">Name</TableHead>
<TableHead className="w-[100px] text-right">
<SortButton
onChange={onSortChange}
sortBy="floor_price"
sortDirection={sortDirection}
isActive={sortBy === "floor_price"}
label="Floor"
/>
</TableHead>
<TableHead className="text-right">
<SortButton
onChange={onSortChange}
sortBy="volume"
sortDirection={sortDirection}
isActive={sortBy === "volume"}
label="Volume"
/>
</TableHead>
<TableHead className="text-right">
<SortButton
onChange={onSortChange}
sortBy="marketcap"
sortDirection={sortDirection}
isActive={sortBy === "marketcap"}
label="Marketcap"
/>
</TableHead>
<TableHead className="text-right">
<SortButton
onChange={onSortChange}
sortBy="floor_percentage"
sortDirection={sortDirection}
isActive={sortBy === "floor_percentage"}
label="Floor %"
/>
</TableHead>
<TableHead className="text-right">
<SortButton
onChange={onSortChange}
sortBy="top_bid"
sortDirection={sortDirection}
isActive={sortBy === "top_bid"}
label="Top Bid"
/>
</TableHead>
<TableHead className="text-right">
<SortButton
onChange={onSortChange}
sortBy="number_of_sales"
sortDirection={sortDirection}
isActive={sortBy === "number_of_sales"}
label="Sales"
/>
</TableHead>
<TableHead className="pr-6 text-right">
<SortButton
onChange={onSortChange}
sortBy="listed"
sortDirection={sortDirection}
isActive={sortBy === "listed"}
label="Listed"
/>
</TableHead>
</TableRow>
)}
itemContent={(_, item) => (
<>
<TableCell className="flex w-[250px] items-center gap-10 pl-6">
<Link
href={`/collection/${item.address}`}
className="flex w-full items-center gap-4"
>
<Media
className="size-[60px] rounded-xs"
height={120}
width={120}
alt={item.name}
src={item.image}
// mediaKey={item.image}
/>
<div className="truncate text-primary">{item.name}</div>
</Link>
</TableCell>
<TableCell className="sticky text-right">
{item.floor ? (
<>
{formatEther(BigInt(item.floor))}
<span className="text-muted-foreground"> ETH</span>
</>
) : (
"--"
)}
</TableCell>
<TableCell className="text-right">
{item.volume ? (
<>
{formatEther(BigInt(item.volume))}
<span className="text-muted-foreground"> ETH</span>
</>
) : (
"--"
)}
</TableCell>
<TableCell className="text-right">
{item.marketcap ? (
<>
{formatEther(BigInt(item.marketcap))}
<span className="text-muted-foreground"> ETH</span>
</>
) : (
"--"
)}
</TableCell>
<TableCell className="text-right">
{item.floor_percentage} %
</TableCell>
<TableCell className="text-right">
{item.top_offer ? (
<>
{formatEther(BigInt(item.top_offer))}
<span className="text-muted-foreground"> ETH</span>
</>
) : (
"--"
)}
</TableCell>
<TableCell className="text-right">{item.sales}</TableCell>
<TableCell className="pr-6 text-right">{item.listed_items}</TableCell>
</>
)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SearchInput } from "@ark-market/ui/search-input";

export default function CollectionsSearch() {
return (
<div className="flex gap-4">
<SearchInput placeholder="Search collection" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ToggleGroup, ToggleGroupItem } from "@ark-market/ui/toggle-group";

import type { CollectionTimerange } from "~/types";

const TIMERANGES = ["10m", "1h", "6h", "1d", "7d", "30d"];

interface CollectionsTimerangesProps {
timerange: CollectionTimerange;
onChange: (timerange: CollectionTimerange) => void;
}

export default function CollectionsTimeranges({
timerange,
onChange,
}: CollectionsTimerangesProps) {
return (
<ToggleGroup type="single" value={timerange} onValueChange={onChange}>
{TIMERANGES.map((t) => (
<ToggleGroupItem
key={t}
value={t}
aria-label={t}
className="w-10 uppercase"
onClick={(e) => {
if (t === timerange) {
e.preventDefault();
}
}}
>
{t}
</ToggleGroupItem>
))}
</ToggleGroup>
);
}
Loading
Loading