Skip to content

Commit

Permalink
feat(collections): add collection stats
Browse files Browse the repository at this point in the history
  • Loading branch information
gershon committed Sep 25, 2024
1 parent b1305c8 commit 45ec0c2
Show file tree
Hide file tree
Showing 13 changed files with 529 additions and 6 deletions.
1 change: 1 addition & 0 deletions apps/arkmarket/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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,57 @@
"use client";

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 {
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) => {
console.log("CollectionsContainer.handleTimerangeChange", timerange);
await setTimerange(timerange);
};

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

import { cn } from "@ark-market/ui";
import { Avatar, AvatarFallback, AvatarImage } from "@ark-market/ui/avatar";
import { Button } from "@ark-market/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@ark-market/ui/table";

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

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[] | null | undefined;
sortBy: CollectionSortBy;
sortDirection: CollectionSortDirection;
onSortChange: (
sortBy: CollectionSortBy,
sortDirection: CollectionSortDirection,
) => void;
}

export default function CollectionsList({
items,
sortBy,
sortDirection,
onSortChange,
}: CollectionsListProps) {
return (
<div className="min-h-[800px]">
<Table className="pb-10">
<TableHeader className="">
<TableRow className="">
<TableHead className="sticky w-[250px] pl-6">Name</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="floor_price"
sortDirection={sortDirection}
isActive={sortBy === "floor_price"}
label="Floor"
/>
</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="volume"
sortDirection={sortDirection}
isActive={sortBy === "volume"}
label="Volume"
/>
</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="marketcap"
sortDirection={sortDirection}
isActive={sortBy === "marketcap"}
label="Marketcap"
/>
</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="floor_percentage"
sortDirection={sortDirection}
isActive={sortBy === "floor_percentage"}
label="Floor %"
/>
</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="top_bid"
sortDirection={sortDirection}
isActive={sortBy === "top_bid"}
label="Top Bid"
/>
</TableHead>
<TableHead className="">
<SortButton
onChange={onSortChange}
sortBy="number_of_sales"
sortDirection={sortDirection}
isActive={sortBy === "number_of_sales"}
label="Sales"
/>
</TableHead>
<TableHead className="">Listed</TableHead>
</TableRow>
</TableHeader>
<TableBody className="font-numbers">
{items?.map((item) => (
<TableRow key={item.address} className="">
<TableCell className="flex w-[200px] items-center gap-2 pl-6">
<Link
href={`/collection/${item.address}`}
className="flex items-center gap-2"
>
<Avatar>
<AvatarImage src={item.image} />
<AvatarFallback>{item.name.substring(0, 2)}</AvatarFallback>
</Avatar>
<div className="text-primary">{item.name}</div>
</Link>
</TableCell>
<TableCell className="sticky">
{formatEther(BigInt(item.floor))}{" "}
<span className="text-muted-foreground">ETH</span>
</TableCell>
<TableCell>
{item.marketcap.toLocaleString()}{" "}
<span className="text-muted-foreground">ETH</span>
</TableCell>
<TableCell>{item.floor_percentage}%</TableCell>
<TableCell>
{item.top_offer}{" "}
<span className="text-muted-foreground">ETH</span>
</TableCell>
<TableCell>
{item.volume} <span className="text-muted-foreground">ETH</span>
</TableCell>
<TableCell>{item.sales}</TableCell>
<TableCell>{item.listed_items}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
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,34 @@
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
value={t}
aria-label={t}
className="w-10 uppercase"
onClick={(e) => {
if (t === timerange) {
e.preventDefault();
}
}}
>
{t}
</ToggleGroupItem>
))}
</ToggleGroup>
);
}
30 changes: 30 additions & 0 deletions apps/arkmarket/src/components/collections/collections-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from "@ark-market/ui/button";
import { Filter } from "@ark-market/ui/icons";
import { SearchInput } from "@ark-market/ui/search-input";

import type { CollectionTimerange } from "~/types";
import CollectionsTimeranges from "./collections-timeranges";

interface CollectionsToolbarProps {
timerange: CollectionTimerange;
onTimerangeChange: (timerange: CollectionTimerange) => void;
}

export default function CollectionsToolbar({
timerange,
onTimerangeChange,
}: CollectionsToolbarProps) {
return (
<div className="flex gap-4 px-6 py-6">
<Button className="" variant="outline" size="xl">
<Filter className="size-3" />
<span className="hidden lg:block">Filters</span>
</Button>
<SearchInput placeholder="Search collection" />
<CollectionsTimeranges
timerange={timerange}
onChange={onTimerangeChange}
/>
</div>
);
}
Loading

0 comments on commit 45ec0c2

Please sign in to comment.