Skip to content

Commit 45ec0c2

Browse files
committed
feat(collections): add collection stats
1 parent b1305c8 commit 45ec0c2

13 files changed

+529
-6
lines changed

apps/arkmarket/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"moment": "^2.30.1",
3838
"next": "^14.2.3",
3939
"nuqs": "^1.18.0",
40+
"query-string": "^9.1.0",
4041
"react": "catalog:react18",
4142
"react-dom": "catalog:react18",
4243
"react-icons": "^5.0.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import CollectionsContainer from "~/components/collections/collections-container";
2+
import getCollections from "~/lib/getCollections";
3+
4+
export default async function CollectionsPage() {
5+
const collections = await getCollections({});
6+
7+
return (
8+
<div className="">
9+
<div className="p-6 text-3xl font-extrabold md:text-5xl">
10+
All Collections
11+
</div>
12+
<CollectionsContainer initialData={collections} />
13+
</div>
14+
);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"use client";
2+
3+
import type {
4+
CollectionSortBy,
5+
CollectionSortDirection,
6+
CollectionStats,
7+
CollectionTimerange,
8+
} from "~/types";
9+
import useCollections from "~/hooks/useCollections";
10+
import CollectionList from "./collections-list";
11+
import CollectionsToolbar from "./collections-toolbar";
12+
13+
interface CollectionsContainerProps {
14+
initialData: CollectionStats[];
15+
}
16+
17+
export default function CollectionsContainer({
18+
initialData,
19+
}: CollectionsContainerProps) {
20+
const {
21+
data,
22+
sortBy,
23+
setSortBy,
24+
sortDirection,
25+
setSortDirection,
26+
timerange,
27+
setTimerange,
28+
} = useCollections({ initialData });
29+
30+
const onSortChange = async (
31+
by: CollectionSortBy,
32+
direction: CollectionSortDirection,
33+
) => {
34+
await setSortBy(by);
35+
await setSortDirection(direction);
36+
};
37+
38+
const handleTimerangeChange = async (timerange: CollectionTimerange) => {
39+
console.log("CollectionsContainer.handleTimerangeChange", timerange);
40+
await setTimerange(timerange);
41+
};
42+
43+
return (
44+
<div className="">
45+
<CollectionsToolbar
46+
timerange={timerange}
47+
onTimerangeChange={handleTimerangeChange}
48+
/>
49+
<CollectionList
50+
items={data}
51+
onSortChange={onSortChange}
52+
sortBy={sortBy}
53+
sortDirection={sortDirection}
54+
/>
55+
</div>
56+
);
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Link from "next/link";
2+
import { ChevronDown, ChevronUp } from "lucide-react";
3+
import { formatEther } from "viem";
4+
5+
import { cn } from "@ark-market/ui";
6+
import { Avatar, AvatarFallback, AvatarImage } from "@ark-market/ui/avatar";
7+
import { Button } from "@ark-market/ui/button";
8+
import {
9+
Table,
10+
TableBody,
11+
TableCell,
12+
TableHead,
13+
TableHeader,
14+
TableRow,
15+
} from "@ark-market/ui/table";
16+
17+
import type {
18+
CollectionSortBy,
19+
CollectionSortDirection,
20+
CollectionStats,
21+
} from "~/types";
22+
23+
function SortButton({
24+
isActive,
25+
label,
26+
sortBy,
27+
sortDirection,
28+
onChange,
29+
}: {
30+
isActive: boolean;
31+
label?: string;
32+
sortBy: CollectionSortBy;
33+
sortDirection: CollectionSortDirection;
34+
onChange: (by: CollectionSortBy, direction: CollectionSortDirection) => void;
35+
}) {
36+
return (
37+
<Button
38+
variant="unstyled"
39+
className="p-0"
40+
onClick={() =>
41+
onChange(
42+
sortBy,
43+
isActive ? (sortDirection === "asc" ? "desc" : "asc") : "desc",
44+
)
45+
}
46+
>
47+
{label ?? sortBy}
48+
<div className="flex flex-col">
49+
<ChevronUp
50+
className={cn(
51+
"-mb-1 size-3",
52+
isActive && sortDirection === "asc" && "text-primary",
53+
)}
54+
/>
55+
<ChevronDown
56+
className={cn(
57+
"-mb-1 size-3",
58+
isActive && sortDirection === "desc" && "text-primary",
59+
)}
60+
/>
61+
</div>
62+
</Button>
63+
);
64+
}
65+
66+
interface CollectionsListProps {
67+
items: CollectionStats[] | null | undefined;
68+
sortBy: CollectionSortBy;
69+
sortDirection: CollectionSortDirection;
70+
onSortChange: (
71+
sortBy: CollectionSortBy,
72+
sortDirection: CollectionSortDirection,
73+
) => void;
74+
}
75+
76+
export default function CollectionsList({
77+
items,
78+
sortBy,
79+
sortDirection,
80+
onSortChange,
81+
}: CollectionsListProps) {
82+
return (
83+
<div className="min-h-[800px]">
84+
<Table className="pb-10">
85+
<TableHeader className="">
86+
<TableRow className="">
87+
<TableHead className="sticky w-[250px] pl-6">Name</TableHead>
88+
<TableHead className="">
89+
<SortButton
90+
onChange={onSortChange}
91+
sortBy="floor_price"
92+
sortDirection={sortDirection}
93+
isActive={sortBy === "floor_price"}
94+
label="Floor"
95+
/>
96+
</TableHead>
97+
<TableHead className="">
98+
<SortButton
99+
onChange={onSortChange}
100+
sortBy="volume"
101+
sortDirection={sortDirection}
102+
isActive={sortBy === "volume"}
103+
label="Volume"
104+
/>
105+
</TableHead>
106+
<TableHead className="">
107+
<SortButton
108+
onChange={onSortChange}
109+
sortBy="marketcap"
110+
sortDirection={sortDirection}
111+
isActive={sortBy === "marketcap"}
112+
label="Marketcap"
113+
/>
114+
</TableHead>
115+
<TableHead className="">
116+
<SortButton
117+
onChange={onSortChange}
118+
sortBy="floor_percentage"
119+
sortDirection={sortDirection}
120+
isActive={sortBy === "floor_percentage"}
121+
label="Floor %"
122+
/>
123+
</TableHead>
124+
<TableHead className="">
125+
<SortButton
126+
onChange={onSortChange}
127+
sortBy="top_bid"
128+
sortDirection={sortDirection}
129+
isActive={sortBy === "top_bid"}
130+
label="Top Bid"
131+
/>
132+
</TableHead>
133+
<TableHead className="">
134+
<SortButton
135+
onChange={onSortChange}
136+
sortBy="number_of_sales"
137+
sortDirection={sortDirection}
138+
isActive={sortBy === "number_of_sales"}
139+
label="Sales"
140+
/>
141+
</TableHead>
142+
<TableHead className="">Listed</TableHead>
143+
</TableRow>
144+
</TableHeader>
145+
<TableBody className="font-numbers">
146+
{items?.map((item) => (
147+
<TableRow key={item.address} className="">
148+
<TableCell className="flex w-[200px] items-center gap-2 pl-6">
149+
<Link
150+
href={`/collection/${item.address}`}
151+
className="flex items-center gap-2"
152+
>
153+
<Avatar>
154+
<AvatarImage src={item.image} />
155+
<AvatarFallback>{item.name.substring(0, 2)}</AvatarFallback>
156+
</Avatar>
157+
<div className="text-primary">{item.name}</div>
158+
</Link>
159+
</TableCell>
160+
<TableCell className="sticky">
161+
{formatEther(BigInt(item.floor))}{" "}
162+
<span className="text-muted-foreground">ETH</span>
163+
</TableCell>
164+
<TableCell>
165+
{item.marketcap.toLocaleString()}{" "}
166+
<span className="text-muted-foreground">ETH</span>
167+
</TableCell>
168+
<TableCell>{item.floor_percentage}%</TableCell>
169+
<TableCell>
170+
{item.top_offer}{" "}
171+
<span className="text-muted-foreground">ETH</span>
172+
</TableCell>
173+
<TableCell>
174+
{item.volume} <span className="text-muted-foreground">ETH</span>
175+
</TableCell>
176+
<TableCell>{item.sales}</TableCell>
177+
<TableCell>{item.listed_items}</TableCell>
178+
</TableRow>
179+
))}
180+
</TableBody>
181+
</Table>
182+
</div>
183+
);
184+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { SearchInput } from "@ark-market/ui/search-input";
2+
3+
export default function CollectionsSearch() {
4+
return (
5+
<div className="flex gap-4">
6+
<SearchInput placeholder="Search collection" />
7+
</div>
8+
);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ToggleGroup, ToggleGroupItem } from "@ark-market/ui/toggle-group";
2+
3+
import type { CollectionTimerange } from "~/types";
4+
5+
const TIMERANGES = ["10m", "1h", "6h", "1d", "7d", "30d"];
6+
7+
interface CollectionsTimerangesProps {
8+
timerange: CollectionTimerange;
9+
onChange: (timerange: CollectionTimerange) => void;
10+
}
11+
12+
export default function CollectionsTimeranges({
13+
timerange,
14+
onChange,
15+
}: CollectionsTimerangesProps) {
16+
return (
17+
<ToggleGroup type="single" value={timerange} onValueChange={onChange}>
18+
{TIMERANGES.map((t) => (
19+
<ToggleGroupItem
20+
value={t}
21+
aria-label={t}
22+
className="w-10 uppercase"
23+
onClick={(e) => {
24+
if (t === timerange) {
25+
e.preventDefault();
26+
}
27+
}}
28+
>
29+
{t}
30+
</ToggleGroupItem>
31+
))}
32+
</ToggleGroup>
33+
);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Button } from "@ark-market/ui/button";
2+
import { Filter } from "@ark-market/ui/icons";
3+
import { SearchInput } from "@ark-market/ui/search-input";
4+
5+
import type { CollectionTimerange } from "~/types";
6+
import CollectionsTimeranges from "./collections-timeranges";
7+
8+
interface CollectionsToolbarProps {
9+
timerange: CollectionTimerange;
10+
onTimerangeChange: (timerange: CollectionTimerange) => void;
11+
}
12+
13+
export default function CollectionsToolbar({
14+
timerange,
15+
onTimerangeChange,
16+
}: CollectionsToolbarProps) {
17+
return (
18+
<div className="flex gap-4 px-6 py-6">
19+
<Button className="" variant="outline" size="xl">
20+
<Filter className="size-3" />
21+
<span className="hidden lg:block">Filters</span>
22+
</Button>
23+
<SearchInput placeholder="Search collection" />
24+
<CollectionsTimeranges
25+
timerange={timerange}
26+
onChange={onTimerangeChange}
27+
/>
28+
</div>
29+
);
30+
}

0 commit comments

Comments
 (0)