From bc2c3c78a397da173bd8ebe446d1543e8fda2bab Mon Sep 17 00:00:00 2001 From: paul launay Date: Wed, 25 Sep 2024 18:21:37 +0200 Subject: [PATCH 1/4] feat(collections): add collection stats --- apps/arkmarket/package.json | 1 + apps/arkmarket/src/app/collections/page.tsx | 15 ++ .../collections/collections-container.tsx | 57 ++++++ .../collections/collections-list.tsx | 184 ++++++++++++++++++ .../collections/collections-search.tsx | 9 + .../collections/collections-timeranges.tsx | 34 ++++ .../collections/collections-toolbar.tsx | 30 +++ apps/arkmarket/src/hooks/useCollections.tsx | 57 ++++++ apps/arkmarket/src/lib/getCollections.ts | 70 +++++++ apps/arkmarket/src/types/index.ts | 33 ++++ packages/ui/package.json | 2 +- packages/ui/src/button.tsx | 1 + pnpm-lock.yaml | 93 ++++++--- 13 files changed, 554 insertions(+), 32 deletions(-) create mode 100644 apps/arkmarket/src/app/collections/page.tsx create mode 100644 apps/arkmarket/src/components/collections/collections-container.tsx create mode 100644 apps/arkmarket/src/components/collections/collections-list.tsx create mode 100644 apps/arkmarket/src/components/collections/collections-search.tsx create mode 100644 apps/arkmarket/src/components/collections/collections-timeranges.tsx create mode 100644 apps/arkmarket/src/components/collections/collections-toolbar.tsx create mode 100644 apps/arkmarket/src/hooks/useCollections.tsx create mode 100644 apps/arkmarket/src/lib/getCollections.ts diff --git a/apps/arkmarket/package.json b/apps/arkmarket/package.json index 5dc66feb..2f51ea8f 100644 --- a/apps/arkmarket/package.json +++ b/apps/arkmarket/package.json @@ -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", diff --git a/apps/arkmarket/src/app/collections/page.tsx b/apps/arkmarket/src/app/collections/page.tsx new file mode 100644 index 00000000..4f323c11 --- /dev/null +++ b/apps/arkmarket/src/app/collections/page.tsx @@ -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 ( +
+
+ All Collections +
+ +
+ ); +} diff --git a/apps/arkmarket/src/components/collections/collections-container.tsx b/apps/arkmarket/src/components/collections/collections-container.tsx new file mode 100644 index 00000000..7b471c6d --- /dev/null +++ b/apps/arkmarket/src/components/collections/collections-container.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/apps/arkmarket/src/components/collections/collections-list.tsx b/apps/arkmarket/src/components/collections/collections-list.tsx new file mode 100644 index 00000000..284557fa --- /dev/null +++ b/apps/arkmarket/src/components/collections/collections-list.tsx @@ -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 ( + + ); +} + +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 ( +
+ + + + Name + + + + + + + + + + + + + + + + + + + Listed + + + + {items?.map((item) => ( + + + + + + {item.name.substring(0, 2)} + +
{item.name}
+ +
+ + {formatEther(BigInt(item.floor))}{" "} + ETH + + + {item.marketcap.toLocaleString()}{" "} + ETH + + {item.floor_percentage}% + + {item.top_offer}{" "} + ETH + + + {item.volume} ETH + + {item.sales} + {item.listed_items} +
+ ))} +
+
+
+ ); +} diff --git a/apps/arkmarket/src/components/collections/collections-search.tsx b/apps/arkmarket/src/components/collections/collections-search.tsx new file mode 100644 index 00000000..07febe77 --- /dev/null +++ b/apps/arkmarket/src/components/collections/collections-search.tsx @@ -0,0 +1,9 @@ +import { SearchInput } from "@ark-market/ui/search-input"; + +export default function CollectionsSearch() { + return ( +
+ +
+ ); +} diff --git a/apps/arkmarket/src/components/collections/collections-timeranges.tsx b/apps/arkmarket/src/components/collections/collections-timeranges.tsx new file mode 100644 index 00000000..b9a31e6f --- /dev/null +++ b/apps/arkmarket/src/components/collections/collections-timeranges.tsx @@ -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 ( + + {TIMERANGES.map((t) => ( + { + if (t === timerange) { + e.preventDefault(); + } + }} + > + {t} + + ))} + + ); +} diff --git a/apps/arkmarket/src/components/collections/collections-toolbar.tsx b/apps/arkmarket/src/components/collections/collections-toolbar.tsx new file mode 100644 index 00000000..0bca7bad --- /dev/null +++ b/apps/arkmarket/src/components/collections/collections-toolbar.tsx @@ -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 ( +
+ + + +
+ ); +} diff --git a/apps/arkmarket/src/hooks/useCollections.tsx b/apps/arkmarket/src/hooks/useCollections.tsx new file mode 100644 index 00000000..ec82eecc --- /dev/null +++ b/apps/arkmarket/src/hooks/useCollections.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useQueryState } from "nuqs"; + +import type { + CollectionSortBy, + CollectionSortDirection, + CollectionStats, + CollectionTimerange, +} from "~/types"; +import getCollections from "~/lib/getCollections"; + +const REFETCH_INTERVAL = 10_000; + +interface UseCollectionsParams { + initialData: CollectionStats[]; +} + +export default function useCollections({ initialData }: UseCollectionsParams) { + const [sortBy, setSortBy] = useQueryState("sort", { + defaultValue: "volume", + parse: (value) => value as CollectionSortBy, + }); + const [sortDirection, setSortDirection] = + useQueryState("dir", { + defaultValue: "desc", + parse: (value) => value as CollectionSortDirection, + }); + const [timerange, setTimerange] = useQueryState( + "timerange", + { + defaultValue: "1d", + parse: (value) => value as CollectionTimerange, + }, + ); + const { data, isLoading, isError } = useQuery({ + queryKey: ["collections", { sortBy, sortDirection, timerange }], + queryFn: async () => + getCollections({ sortBy, sortDirection, timerange, itemsPerPage: 100 }), + refetchInterval: REFETCH_INTERVAL, + structuralSharing: false, + initialData, + }); + + return { + data, + isLoading, + isError, + sortBy, + setSortBy, + sortDirection, + setSortDirection, + timerange, + setTimerange, + }; +} diff --git a/apps/arkmarket/src/lib/getCollections.ts b/apps/arkmarket/src/lib/getCollections.ts new file mode 100644 index 00000000..2c2fcfe8 --- /dev/null +++ b/apps/arkmarket/src/lib/getCollections.ts @@ -0,0 +1,70 @@ +import queryString from "query-string"; + +import type { + CollectionSortBy, + CollectionSortDirection, + CollectionStats, + CollectionTimerange, +} from "~/types"; +import { env } from "~/env"; + +interface GetCollectionsParams { + page?: number; + itemsPerPage?: number; + sortBy?: CollectionSortBy; + sortDirection?: CollectionSortDirection; + timerange?: CollectionTimerange; +} + +export interface CollectionsApiResponse { + data: CollectionStats[]; + last_page: number; +} + +export default async function getCollections({ + page, + itemsPerPage = 50, + sortBy = "volume", + sortDirection = "desc", + timerange = "10m", +}: GetCollectionsParams) { + const params = queryString.stringify({ + page, + itemsPerPage, + sortBy, + sortDirection, + timerange, + }); + + // return [...Array(100).keys()].map((index) => ({ + // address: `${index}x02acee8c430f62333cf0e0e7a94b2347b5513b4c25f699461dd8d7b23c072478`, + // floor: parseEther("0.1"), + // floor_percentage: "10", + // image: "string", + // is_verified: true, + // top_offer: parseEther("0.1"), + // listed_items: 1244, + // listed_percentage: 23, + // marketcap: "123.45", + // name: `Collection ${index}`, + // owner_count: 0, + // sales: 0, + // token_count: 0, + // total_sales: 0, + // total_volume: "1123.4", + // volume: 0, + // })) as CollectionStats[]; + + const response = await fetch( + `${env.NEXT_PUBLIC_MARKETPLACE_API_URL}/collections?${params}`, + ); + + if (!response.ok) { + console.error("Failed to fetch collection data"); + return []; + } + + const { data } = (await response.json()) as CollectionsApiResponse; + + return data; +} diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index a86e6a6c..e55f37fb 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -1,6 +1,26 @@ +export interface CollectionStats { + address: string; + floor: string; + floor_percentage: string; + image: string; + is_verified: boolean; + listed_items: number; + listed_percentage: number; + marketcap: string; + name: string; + owner_count: number; + sales: number; + token_count: number; + total_sales: number; + total_volume: number; + volume: number; + top_offer: string; +} + export interface Collection { address: string; floor?: string; + floor_percentage?: string; image?: string; is_verified: boolean; listed_items: number; @@ -330,3 +350,16 @@ export interface LiveAuctions { end_timestamp: number; metadata?: TokenMetadata; } +export type CollectionTimerange = "10m" | "1h" | "6h" | "1d" | "7d" | "30d"; + +export type CollectionSortBy = + | "floor_price" + | "total_volume" + | "floor_percentage" + | "volume" + | "top_bid" + | "number_of_sales" + | "marketcap" + | "listed"; + +export type CollectionSortDirection = "asc" | "desc"; diff --git a/packages/ui/package.json b/packages/ui/package.json index addbb036..bbbbb296 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,7 +22,7 @@ "dependencies": { "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx index b7d562b7..4b47d7bf 100644 --- a/packages/ui/src/button.tsx +++ b/packages/ui/src/button.tsx @@ -11,6 +11,7 @@ const buttonVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", + unstyled: "", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99349618..652936b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: nuqs: specifier: ^1.18.0 version: 1.18.0(next@14.2.4(@babel/core@7.25.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + query-string: + specifier: ^9.1.0 + version: 9.1.0 react: specifier: catalog:react18 version: 18.3.1 @@ -251,7 +254,7 @@ importers: specifier: ^1.1.2 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': - specifier: ^1.0.4 + specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.1 @@ -397,7 +400,7 @@ importers: version: 7.35.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react-hooks: specifier: rc - version: 5.1.0-rc-459fd418-20241001(eslint@9.9.1(jiti@1.21.6)) + version: 5.1.0-rc-1460d67c-20241003(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-turbo: specifier: ^2.0.13 version: 2.1.0(eslint@9.9.1(jiti@1.21.6)) @@ -2930,6 +2933,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -3184,8 +3191,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-react-hooks@5.1.0-rc-459fd418-20241001: - resolution: {integrity: sha512-vBUzji1JDwLxFAsmVtdbWQBo6LMnN7J2ZpDrGWo/ve4NnJGDeNFQOGqMJM1j0uDzLC1/sgQcFEOHofb8BsvJUQ==} + eslint-plugin-react-hooks@5.1.0-rc-1460d67c-20241003: + resolution: {integrity: sha512-6RB5ld4hvk8hDc0JNMyid5/+SQ0ZyRtmmZV2lSvnWAA0zLIANUpSf7QNb7gtPYVop4qSu79hJpBsDGwdtXluig==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -3322,6 +3329,10 @@ packages: resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} engines: {node: '>=0.10.0'} + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -4594,6 +4605,10 @@ packages: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} + query-string@9.1.0: + resolution: {integrity: sha512-t6dqMECpCkqfyv2FfwVS1xcB6lgXW/0XZSaKdsCNGYkqMO76AFiJEg4vINzoDKcZa6MS7JX+OHIjwh06K5vczw==} + engines: {node: '>=18'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -4919,6 +4934,10 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -5763,7 +5782,7 @@ snapshots: '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 - picocolors: 1.0.1 + picocolors: 1.1.0 '@babel/compat-data@7.24.7': {} @@ -5782,7 +5801,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5982,14 +6001,14 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.0 '@babel/highlight@7.25.7': dependencies: '@babel/helper-validator-identifier': 7.25.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.0 '@babel/parser@7.24.7': dependencies: @@ -6055,7 +6074,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - debug: 4.3.5 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6176,7 +6195,7 @@ snapshots: '@eslint/config-array@0.17.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.5 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6184,7 +6203,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.5 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6192,7 +6211,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.5 + debug: 4.3.7 espree: 10.1.0 globals: 14.0.0 ignore: 5.3.1 @@ -8429,6 +8448,8 @@ snapshots: decode-uri-component@0.2.2: {} + decode-uri-component@0.4.1: {} + deep-eql@5.0.2: {} deep-equal@2.2.3: @@ -8801,7 +8822,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@5.1.0-rc-459fd418-20241001(eslint@9.9.1(jiti@1.21.6)): + eslint-plugin-react-hooks@5.1.0-rc-1460d67c-20241003(eslint@9.9.1(jiti@1.21.6)): dependencies: eslint: 9.9.1(jiti@1.21.6) @@ -8898,7 +8919,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5 + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 @@ -9036,6 +9057,8 @@ snapshots: filter-obj@1.1.0: {} + filter-obj@5.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -10277,29 +10300,29 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.41): + postcss-import@15.1.0(postcss@8.4.47): dependencies: - postcss: 8.4.41 + postcss: 8.4.47 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.41): + postcss-js@4.0.1(postcss@8.4.47): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.41 + postcss: 8.4.47 - postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.2)(typescript@5.5.4)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.2)(typescript@5.5.4)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: - postcss: 8.4.41 + postcss: 8.4.47 ts-node: 10.9.2(@types/node@20.16.2)(typescript@5.5.4) - postcss-nested@6.0.1(postcss@8.4.41): + postcss-nested@6.0.1(postcss@8.4.47): dependencies: - postcss: 8.4.41 + postcss: 8.4.47 postcss-selector-parser: 6.1.0 postcss-selector-parser@6.1.0: @@ -10362,7 +10385,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.3.7 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 lru-cache: 7.18.3 @@ -10385,6 +10408,12 @@ snapshots: split-on-first: 1.1.0 strict-uri-encode: 2.0.0 + query-string@9.1.0: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -10723,6 +10752,8 @@ snapshots: split-on-first@1.1.0: {} + split-on-first@3.0.0: {} + split2@4.2.0: {} sprintf-js@1.1.3: {} @@ -10951,12 +10982,12 @@ snapshots: micromatch: 4.0.7 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.41 - postcss-import: 15.1.0(postcss@8.4.41) - postcss-js: 4.0.1(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.16.2)(typescript@5.5.4)) - postcss-nested: 6.0.1(postcss@8.4.41) + picocolors: 1.1.0 + postcss: 8.4.47 + postcss-import: 15.1.0(postcss@8.4.47) + postcss-js: 4.0.1(postcss@8.4.47) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.2)(typescript@5.5.4)) + postcss-nested: 6.0.1(postcss@8.4.47) postcss-selector-parser: 6.1.0 resolve: 1.22.8 sucrase: 3.35.0 @@ -11250,13 +11281,13 @@ snapshots: dependencies: browserslist: 4.23.1 escalade: 3.1.2 - picocolors: 1.0.1 + picocolors: 1.1.0 update-browserslist-db@1.1.0(browserslist@4.24.0): dependencies: browserslist: 4.24.0 escalade: 3.1.2 - picocolors: 1.0.1 + picocolors: 1.1.0 update-check@1.5.4: dependencies: From d46e846a4d8637e8b9a526241495300766f30bb2 Mon Sep 17 00:00:00 2001 From: paul launay Date: Mon, 7 Oct 2024 15:31:43 +0200 Subject: [PATCH 2/4] feat(collection): use latest collection API --- .../collections/collections-container.tsx | 1 - .../collections/collections-list.tsx | 253 +++++++++++------- .../collections/collections-timeranges.tsx | 1 + apps/arkmarket/src/components/footer.tsx | 9 +- apps/arkmarket/src/lib/getCollections.ts | 27 +- apps/arkmarket/src/types/index.ts | 25 +- 6 files changed, 182 insertions(+), 134 deletions(-) diff --git a/apps/arkmarket/src/components/collections/collections-container.tsx b/apps/arkmarket/src/components/collections/collections-container.tsx index 7b471c6d..b979a932 100644 --- a/apps/arkmarket/src/components/collections/collections-container.tsx +++ b/apps/arkmarket/src/components/collections/collections-container.tsx @@ -36,7 +36,6 @@ export default function CollectionsContainer({ }; const handleTimerangeChange = async (timerange: CollectionTimerange) => { - console.log("CollectionsContainer.handleTimerangeChange", timerange); await setTimerange(timerange); }; diff --git a/apps/arkmarket/src/components/collections/collections-list.tsx b/apps/arkmarket/src/components/collections/collections-list.tsx index 284557fa..d8d62495 100644 --- a/apps/arkmarket/src/components/collections/collections-list.tsx +++ b/apps/arkmarket/src/components/collections/collections-list.tsx @@ -1,9 +1,9 @@ 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 { Avatar, AvatarFallback, AvatarImage } from "@ark-market/ui/avatar"; import { Button } from "@ark-market/ui/button"; import { Table, @@ -19,6 +19,7 @@ import type { CollectionSortDirection, CollectionStats, } from "~/types"; +import Media from "../media"; function SortButton({ isActive, @@ -64,7 +65,7 @@ function SortButton({ } interface CollectionsListProps { - items: CollectionStats[] | null | undefined; + items: CollectionStats[]; sortBy: CollectionSortBy; sortDirection: CollectionSortDirection; onSortChange: ( @@ -80,105 +81,155 @@ export default function CollectionsList({ onSortChange, }: CollectionsListProps) { return ( -
- - - - Name - - ( + //
+ // )), + TableBody, + TableHead: (props) => , + TableRow, + }} + fixedHeaderContent={() => ( + + Name + + + + + + + + + + + + + + + + + + + + + + + )} + itemContent={(_, item) => ( + <> + + + - - - - - - - - - - - - - - - - - Listed - - - - {items?.map((item) => ( - - - - - - {item.name.substring(0, 2)} - -
{item.name}
- -
- - {formatEther(BigInt(item.floor))}{" "} - ETH - - - {item.marketcap.toLocaleString()}{" "} - ETH - - {item.floor_percentage}% - - {item.top_offer}{" "} - ETH - - - {item.volume} ETH - - {item.sales} - {item.listed_items} -
- ))} -
-
-
+
{item.name}
+ + + + {item.floor ? ( + <> + {formatEther(BigInt(item.floor))} + ETH + + ) : ( + "--" + )} + + + {item.volume ? ( + <> + {formatEther(BigInt(item.volume))} + ETH + + ) : ( + "--" + )} + + + {item.marketcap ? ( + <> + {formatEther(BigInt(item.marketcap))} + ETH + + ) : ( + "--" + )} + + + {item.floor_percentage} % + + + {item.top_offer ? ( + <> + {formatEther(BigInt(item.top_offer))} + ETH + + ) : ( + "--" + )} + + {item.sales} + {item.listed_items} + + )} + /> ); } diff --git a/apps/arkmarket/src/components/collections/collections-timeranges.tsx b/apps/arkmarket/src/components/collections/collections-timeranges.tsx index b9a31e6f..39cb3c0a 100644 --- a/apps/arkmarket/src/components/collections/collections-timeranges.tsx +++ b/apps/arkmarket/src/components/collections/collections-timeranges.tsx @@ -17,6 +17,7 @@ export default function CollectionsTimeranges({ {TIMERANGES.map((t) => ( pathname.startsWith(path))) { return null; } diff --git a/apps/arkmarket/src/lib/getCollections.ts b/apps/arkmarket/src/lib/getCollections.ts index 2c2fcfe8..e4f56c02 100644 --- a/apps/arkmarket/src/lib/getCollections.ts +++ b/apps/arkmarket/src/lib/getCollections.ts @@ -30,31 +30,12 @@ export default async function getCollections({ }: GetCollectionsParams) { const params = queryString.stringify({ page, - itemsPerPage, - sortBy, - sortDirection, - timerange, + items_per_page: itemsPerPage, + sort: sortBy, + direction: sortDirection, + time_range: timerange, }); - // return [...Array(100).keys()].map((index) => ({ - // address: `${index}x02acee8c430f62333cf0e0e7a94b2347b5513b4c25f699461dd8d7b23c072478`, - // floor: parseEther("0.1"), - // floor_percentage: "10", - // image: "string", - // is_verified: true, - // top_offer: parseEther("0.1"), - // listed_items: 1244, - // listed_percentage: 23, - // marketcap: "123.45", - // name: `Collection ${index}`, - // owner_count: 0, - // sales: 0, - // token_count: 0, - // total_sales: 0, - // total_volume: "1123.4", - // volume: 0, - // })) as CollectionStats[]; - const response = await fetch( `${env.NEXT_PUBLIC_MARKETPLACE_API_URL}/collections?${params}`, ); diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index e55f37fb..0fe652be 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -1,20 +1,37 @@ +// "address": "0x00001401c6ef90c718b912ca8e5965501889924a47b1fecb5dc0f71b7b9b4000", +// "floor": null, +// "floor_percentage": null, +// "image": null, +// "is_verified": false, +// "listed_items": 0, +// "listed_percentage": 0, +// "marketcap": null, +// "name": "szwmprincqt", +// "owner_count": 1, +// "sales": null, +// "token_count": 1, +// "top_offer": null, +// "total_sales": 0, +// "total_volume": 0, +// "volume": null + export interface CollectionStats { address: string; - floor: string; + floor: string | null; floor_percentage: string; image: string; is_verified: boolean; listed_items: number; listed_percentage: number; - marketcap: string; + marketcap: string | null; name: string; owner_count: number; - sales: number; + sales: number | null; token_count: number; + top_offer: string | null; total_sales: number; total_volume: number; volume: number; - top_offer: string; } export interface Collection { From 365530f1a19c086b28d0fd514e390c499020c326 Mon Sep 17 00:00:00 2001 From: paul launay Date: Mon, 7 Oct 2024 15:59:39 +0200 Subject: [PATCH 3/4] feat(collections): hide footer on collections page --- apps/arkmarket/src/components/footer.tsx | 8 +++----- apps/arkmarket/src/components/unframed-footer.tsx | 12 ++++-------- apps/arkmarket/src/hooks/useFooterVisibility.ts | 9 +++++++++ 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 apps/arkmarket/src/hooks/useFooterVisibility.ts diff --git a/apps/arkmarket/src/components/footer.tsx b/apps/arkmarket/src/components/footer.tsx index ed847f01..61261c62 100644 --- a/apps/arkmarket/src/components/footer.tsx +++ b/apps/arkmarket/src/components/footer.tsx @@ -1,22 +1,20 @@ "use client"; import Link from "next/link"; -import { usePathname } from "next/navigation"; import { cn, focusableStyles } from "@ark-market/ui"; import { Button } from "@ark-market/ui/button"; import { Discord, Telegram, XIcon } from "@ark-market/ui/icons"; import { siteConfig } from "~/config/site"; +import useFooterVisibility from "~/hooks/useFooterVisibility"; import ExternalLink from "./external-link"; import { Icons } from "./icons"; -const HIDDEN_PATHS = ["/wallet", "/collection", "/token", "/collections"]; - export default function Footer() { - const pathname = usePathname(); + const isVisible = useFooterVisibility(); - if (HIDDEN_PATHS.some((path) => pathname.startsWith(path))) { + if (!isVisible) { return null; } diff --git a/apps/arkmarket/src/components/unframed-footer.tsx b/apps/arkmarket/src/components/unframed-footer.tsx index 25e87499..e222fc2b 100644 --- a/apps/arkmarket/src/components/unframed-footer.tsx +++ b/apps/arkmarket/src/components/unframed-footer.tsx @@ -1,21 +1,17 @@ "use client"; -import { usePathname } from "next/navigation"; - import { Button } from "@ark-market/ui/button"; import { Discord, Telegram, XIcon } from "@ark-market/ui/icons"; import { siteConfig } from "~/config/site"; +import useFooterVisibility from "~/hooks/useFooterVisibility"; import ExternalLink from "./external-link"; import { Icons } from "./icons"; export default function UnframedFooter() { - const pathname = usePathname(); - if ( - pathname.includes("/wallet/") || - pathname.includes("/collection/") || - pathname.includes("/token/") - ) { + const isVisible = useFooterVisibility(); + + if (!isVisible) { return null; } diff --git a/apps/arkmarket/src/hooks/useFooterVisibility.ts b/apps/arkmarket/src/hooks/useFooterVisibility.ts new file mode 100644 index 00000000..56d9c368 --- /dev/null +++ b/apps/arkmarket/src/hooks/useFooterVisibility.ts @@ -0,0 +1,9 @@ +import { usePathname } from "next/navigation"; + +const HIDDEN_PATHS = ["/wallet", "/collection", "/token", "/collections"]; + +export default function useFooterVisibility() { + const pathname = usePathname(); + + return !HIDDEN_PATHS.some((path) => pathname.startsWith(path)); +} From ec1ed67b724e018ef5b9fe605c0c3e0f7b8393aa Mon Sep 17 00:00:00 2001 From: paul launay Date: Mon, 7 Oct 2024 17:55:51 +0200 Subject: [PATCH 4/4] feat(collections): add clientside search --- .../collections/collections-container.tsx | 14 +++++++++++--- .../components/collections/collections-list.tsx | 14 ++++++++++++++ .../components/collections/collections-toolbar.tsx | 13 ++++++------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/apps/arkmarket/src/components/collections/collections-container.tsx b/apps/arkmarket/src/components/collections/collections-container.tsx index b979a932..d8565c84 100644 --- a/apps/arkmarket/src/components/collections/collections-container.tsx +++ b/apps/arkmarket/src/components/collections/collections-container.tsx @@ -1,5 +1,7 @@ "use client"; +import { useState } from "react"; + import type { CollectionSortBy, CollectionSortDirection, @@ -17,6 +19,7 @@ interface CollectionsContainerProps { export default function CollectionsContainer({ initialData, }: CollectionsContainerProps) { + const [searchQuery, setSearchQuery] = useState(""); const { data, sortBy, @@ -39,18 +42,23 @@ export default function CollectionsContainer({ await setTimerange(timerange); }; + const items = data.filter((item) => + item.name.toLowerCase().includes(searchQuery.toLowerCase()), + ); + return ( -
+ <> setSearchQuery(query)} /> -
+ ); } diff --git a/apps/arkmarket/src/components/collections/collections-list.tsx b/apps/arkmarket/src/components/collections/collections-list.tsx index d8d62495..6d5d0a9f 100644 --- a/apps/arkmarket/src/components/collections/collections-list.tsx +++ b/apps/arkmarket/src/components/collections/collections-list.tsx @@ -5,6 +5,7 @@ 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, @@ -80,6 +81,19 @@ export default function CollectionsList({ sortDirection, onSortChange, }: CollectionsListProps) { + if (!items.length) { + return ( +
+
+ +

+ No collections found, try another search. +

+
+
+ ); + } + return ( void; + onSearchChange: (query: string) => void; } export default function CollectionsToolbar({ timerange, onTimerangeChange, + onSearchChange, }: CollectionsToolbarProps) { return (
- - + onSearchChange(e.currentTarget.value)} + />