From 54e6d333e9425fc2f64faf1aed3fade2b5fa922d Mon Sep 17 00:00:00 2001 From: paul launay Date: Thu, 19 Sep 2024 18:45:14 +0200 Subject: [PATCH 1/3] feat(portfolio): add valid portfolio value --- .../components/portfolio-value.tsx | 52 ++++++++++--------- .../arkmarket/src/hooks/usePortfolioStats.tsx | 23 ++++++++ apps/arkmarket/src/lib/getPortfolioStats.ts | 26 ++++++++++ apps/arkmarket/src/types/index.ts | 4 ++ 4 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 apps/arkmarket/src/hooks/usePortfolioStats.tsx create mode 100644 apps/arkmarket/src/lib/getPortfolioStats.ts diff --git a/apps/arkmarket/src/app/wallet/[walletAddress]/components/portfolio-value.tsx b/apps/arkmarket/src/app/wallet/[walletAddress]/components/portfolio-value.tsx index 86f5c1d7..8316d065 100644 --- a/apps/arkmarket/src/app/wallet/[walletAddress]/components/portfolio-value.tsx +++ b/apps/arkmarket/src/app/wallet/[walletAddress]/components/portfolio-value.tsx @@ -1,40 +1,44 @@ -import { Ethereum } from "@ark-market/ui/icons"; +"use client"; -import { ETH } from "~/constants/tokens"; -import useBalance from "~/hooks/useBalance"; +import { parseEther } from "viem"; + +import { Skeleton } from "@ark-market/ui/skeleton"; + +import usePortfolioStats from "~/hooks/usePortfolioStats"; import usePrices from "~/hooks/usePrices"; interface PortfolioValueProps { - address?: string; + address: string; } export default function PortfolioValue({ address }: PortfolioValueProps) { - const { data: ethBalance, isPending } = useBalance({ address, token: ETH }); - const { convertInUsd, isLoading: isLoadingPrices } = usePrices(); - const ethBalanceInUsd = convertInUsd({ amount: ethBalance?.value }); + const { data } = usePortfolioStats({ address }); + const { convertInUsd, isLoading } = usePrices(); - if (isLoadingPrices || isPending) { - return null; - } + const totalValueInUsd = convertInUsd({ + amount: parseEther(data.total_value), + }); return ( -
-
-
-

Portfolio value

-

- -

- {ethBalance?.rounded}{" "} - ETH +
+
+
+ Portfolio value +
+
+
+ {data.total_value}{" "} +
ETH
+
+ {isLoading ? ( + + ) : ( +
+ ${totalValueInUsd}
-

+ )}
- -
- ${ethBalanceInUsd} -
); } diff --git a/apps/arkmarket/src/hooks/usePortfolioStats.tsx b/apps/arkmarket/src/hooks/usePortfolioStats.tsx new file mode 100644 index 00000000..15637ad4 --- /dev/null +++ b/apps/arkmarket/src/hooks/usePortfolioStats.tsx @@ -0,0 +1,23 @@ +import { useQuery } from "@tanstack/react-query"; + +import getPortfolioStats from "~/lib/getPortfolioStats"; + +interface UsePortfolioStatsProps { + address: string; +} + +const REFETCH_INTERVAL = 5_000; + +export default function usePortfolioStats({ address }: UsePortfolioStatsProps) { + const result = useQuery({ + queryKey: ["portfolioStats", address], + queryFn: () => getPortfolioStats({ address }), + refetchInterval: REFETCH_INTERVAL, + initialData: { + total_value: "0.00", + }, + enabled: !!address, + }); + + return result; +} diff --git a/apps/arkmarket/src/lib/getPortfolioStats.ts b/apps/arkmarket/src/lib/getPortfolioStats.ts new file mode 100644 index 00000000..adb8194c --- /dev/null +++ b/apps/arkmarket/src/lib/getPortfolioStats.ts @@ -0,0 +1,26 @@ +import type { PortfolioStats } from "~/types"; +import { env } from "~/env"; + +export interface PortfolioStatsApiResponse { + data: PortfolioStats; +} + +interface GetPortfolioStatsParams { + address: string; +} + +export default async function getPortfolioStats({ + address, +}: GetPortfolioStatsParams) { + const response = await fetch( + `${env.NEXT_PUBLIC_MARKETPLACE_API_URL}/portfolio/${address}/stats`, + ); + + if (!response.ok) { + throw new Error("Failed to fetch portfolio stats data"); + } + + const { data } = (await response.json()) as PortfolioStatsApiResponse; + + return data; +} diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index 6ec640a9..6d96f936 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -207,6 +207,10 @@ export interface OwnersTokensApiResponse { result: Token[]; } +export interface PortfolioStats { + total_value: string; +} + export interface TokenMarketData { buy_in_progress: boolean; created_timestamp: number | null; From 8f4e1951266503c0dc54ac16637404f8d78bda01 Mon Sep 17 00:00:00 2001 From: paul launay Date: Fri, 20 Sep 2024 16:49:30 +0200 Subject: [PATCH 2/3] feat(collection): add collection token id search --- .../components/collection-items-data.tsx | 70 +++++++++++++++---- .../components/collection-items-toolbar.tsx | 17 +++-- .../components/collection-items.tsx | 11 +++ packages/ui/src/search-input.tsx | 6 +- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx index 64e4eb7a..54041238 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { memo, useEffect, useMemo } from "react"; import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; import type { ViewType } from "../../../../components/view-type-toggle-group"; @@ -10,7 +10,6 @@ import type { CollectionTokensApiResponse, } from "~/lib/getCollectionTokens"; import type { CollectionToken, Filters } from "~/types"; -import useInfiniteWindowScroll from "~/hooks/useInfiniteWindowScroll"; import { getCollectionTokens } from "~/lib/getCollectionTokens"; import CollectionItemsDataGridView from "./collection-items-data-grid-view"; import CollectionItemsDataListView from "./collection-items-data-list-view"; @@ -21,20 +20,23 @@ interface CollectionItemsDataProps { sortDirection: CollectionSortDirection; viewType: ViewType; filters: Filters; + searchQuery: string; } -export default function CollectionItemsData({ +function CollectionItemsData({ collectionAddress, sortBy, sortDirection, viewType, filters, + searchQuery, }: CollectionItemsDataProps) { const { data: infiniteData, fetchNextPage, hasNextPage, - isFetchingNextPage, + // isFetchingNextPage, + isRefetching, } = useSuspenseInfiniteQuery({ queryKey: [ "collectionTokens", @@ -46,7 +48,7 @@ export default function CollectionItemsData({ refetchInterval: 10_000, getNextPageParam: (lastPage: CollectionTokensApiResponse) => lastPage.next_page, - initialPageParam: undefined as number | undefined, + initialPageParam: undefined, queryFn: ({ pageParam }) => getCollectionTokens({ collectionAddress, @@ -57,17 +59,59 @@ export default function CollectionItemsData({ }), }); - useInfiniteWindowScroll({ - fetchNextPage, - hasNextPage: !!hasNextPage, - isFetchingNextPage, - }); + // useInfiniteWindowScroll({ + // fetchNextPage, + // hasNextPage: !!hasNextPage, + // isFetchingNextPage, + // }); const collectionTokens: CollectionToken[] = useMemo( - () => infiniteData.pages.flatMap((page) => page.data), - [infiniteData], + () => + infiniteData.pages + .flatMap((page) => page.data) + .filter((token) => token.token_id.includes(searchQuery)), + [infiniteData, searchQuery], ); + const totalTokensCount = collectionTokens.length; + + useEffect(() => { + const run = async () => { + // console.log("totalTokensCount", totalTokensCount); + if ( + isRefetching || + !hasNextPage || + !searchQuery || + !totalTokensCount || + totalTokensCount > 50 + ) { + return; + } + + console.log("fetchNextPage > ", searchQuery, totalTokensCount); + // if (searchQuery && totalTokensCount < 10 && hasNextPage) { + await fetchNextPage({ cancelRefetch: false }); + // } + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + run(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery, totalTokensCount]); + + console.log("CollectionItemsData.render"); + + if (collectionTokens.length === 0) { + return ( +
+
No items found.
+
+ Try updating your search criteria to find what you're looking for. +
+
+ ); + } + return viewType === "list" ? ( ) : ( @@ -77,3 +121,5 @@ export default function CollectionItemsData({ /> ); } + +export default memo(CollectionItemsData); diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx index b7b556cd..f32a1269 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx @@ -17,8 +17,6 @@ import ViewTypeToggleButton from "../../../../components/view-type-toggle-button import ViewTypeToggleGroup from "../../../../components/view-type-toggle-group"; import CollectionItemsSortingSelect from "./collection-item-sorting-select"; -// import LiveResultsIndicator from "./live-results-indicator"; - interface CollectionItemsToolbarProps { setSortBy: (sortBy: CollectionSortBy) => void; setSortDirection: (sortDirection: CollectionSortDirection) => void; @@ -32,6 +30,8 @@ interface CollectionItemsToolbarProps { filtersDialogOpen: boolean; openFiltersDialog: () => void; filtersCount: number; + searchQuery: string; + setSearchQuery: (query: string) => void; } export default function CollectionItemsToolbar({ @@ -47,9 +47,13 @@ export default function CollectionItemsToolbar({ // filtersDialogOpen, openFiltersDialog, filtersCount, + searchQuery, + setSearchQuery, }: CollectionItemsToolbarProps) { const isDesktop = useMediaQuery("(min-width: 1024px)"); + console.log("CollectionItemsToolbar.render"); + return (
@@ -67,8 +71,13 @@ export default function CollectionItemsToolbar({ )} - {/* */} - + setSearchQuery(e.currentTarget.value)} + /> ("large-grid"); const [sortDirection, setSortDirection] = useQueryState( collectionSortDirectionKey, @@ -97,6 +100,11 @@ export default function CollectionItems({ 0, ); + console.log("CollectionItems.render", { + searchQuery, + collectionTokenCount, + }); + return (
@@ -131,6 +139,8 @@ export default function CollectionItems({ openFiltersDialog={() => setFiltersDialogOpen(true)} filtersDialogOpen={filtersDialogOpen} filtersCount={filtersCount} + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} />
diff --git a/packages/ui/src/search-input.tsx b/packages/ui/src/search-input.tsx index 4c0dc9c9..f1b8f444 100644 --- a/packages/ui/src/search-input.tsx +++ b/packages/ui/src/search-input.tsx @@ -1,13 +1,13 @@ import * as React from "react"; -import { Search } from "@ark-market/ui/icons"; import { cn } from "@ark-market/ui"; +import { Search } from "@ark-market/ui/icons"; import type { InputProps } from "./input"; import { Input } from "./input"; const SearchInput = React.forwardRef( - ({ className, ...props }, ref) => { + ({ className, type = "search", ...props }, ref) => { return (
( /> From a008e15391b8af8f4eed3c68c3c3125faafac80d Mon Sep 17 00:00:00 2001 From: paul launay Date: Mon, 23 Sep 2024 11:42:53 +0200 Subject: [PATCH 3/3] feat(collection): use virtuoso onEndReached instead of custom oberver --- .../collection-items-data-grid-view.tsx | 14 ++- .../components/collection-items-data.tsx | 94 +++++++------------ .../components/collection-items-toolbar.tsx | 6 -- .../components/collection-items.tsx | 8 -- .../src/hooks/useCollectionTokens.ts | 54 +++++++++++ pnpm-lock.yaml | 8 +- 6 files changed, 99 insertions(+), 85 deletions(-) create mode 100644 apps/arkmarket/src/hooks/useCollectionTokens.ts diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx index d5865009..8db680ee 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx @@ -45,13 +45,15 @@ const SmallGridContainer = forwardRef< SmallGridContainer.displayName = "SmallGridContainer"; interface CollectionItemsDataGridViewProps { - collectionTokens: CollectionToken[]; + items: CollectionToken[]; viewType: ViewType; + onEndReached: () => void; } export default function CollectionItemsDataGridView({ - collectionTokens, + items, viewType, + onEndReached, }: CollectionItemsDataGridViewProps) { const components = { List: viewType === "large-grid" ? LargeGridContainer : SmallGridContainer, @@ -60,12 +62,14 @@ export default function CollectionItemsDataGridView({ return (
{ - const token = collectionTokens[index]; + const token = items[index]; if (!token) { return null; diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx index 54041238..70d8e83e 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx @@ -1,16 +1,14 @@ "use client"; -import { memo, useEffect, useMemo } from "react"; -import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; +import { useEffect, useMemo } from "react"; -import type { ViewType } from "../../../../components/view-type-toggle-group"; +import type { ViewType } from "~/components/view-type-toggle-group"; import type { CollectionSortBy, CollectionSortDirection, - CollectionTokensApiResponse, } from "~/lib/getCollectionTokens"; import type { CollectionToken, Filters } from "~/types"; -import { getCollectionTokens } from "~/lib/getCollectionTokens"; +import useCollectionTokens from "~/hooks/useCollectionTokens"; import CollectionItemsDataGridView from "./collection-items-data-grid-view"; import CollectionItemsDataListView from "./collection-items-data-list-view"; @@ -31,77 +29,48 @@ function CollectionItemsData({ filters, searchQuery, }: CollectionItemsDataProps) { - const { - data: infiniteData, - fetchNextPage, - hasNextPage, - // isFetchingNextPage, - isRefetching, - } = useSuspenseInfiniteQuery({ - queryKey: [ - "collectionTokens", + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useCollectionTokens({ + collectionAddress, sortDirection, sortBy, - collectionAddress, filters, - ], - refetchInterval: 10_000, - getNextPageParam: (lastPage: CollectionTokensApiResponse) => - lastPage.next_page, - initialPageParam: undefined, - queryFn: ({ pageParam }) => - getCollectionTokens({ - collectionAddress, - page: pageParam, - sortDirection, - sortBy, - filters, - }), - }); - - // useInfiniteWindowScroll({ - // fetchNextPage, - // hasNextPage: !!hasNextPage, - // isFetchingNextPage, - // }); + }); - const collectionTokens: CollectionToken[] = useMemo( + const items: CollectionToken[] = useMemo( () => - infiniteData.pages + data.pages .flatMap((page) => page.data) .filter((token) => token.token_id.includes(searchQuery)), - [infiniteData, searchQuery], + [data.pages, searchQuery], ); - const totalTokensCount = collectionTokens.length; + console.log("isFetchingNextPage", isFetchingNextPage); useEffect(() => { - const run = async () => { - // console.log("totalTokensCount", totalTokensCount); - if ( - isRefetching || - !hasNextPage || - !searchQuery || - !totalTokensCount || - totalTokensCount > 50 - ) { - return; - } + if (items.length > 0 || !hasNextPage || isFetchingNextPage) { + return; + } - console.log("fetchNextPage > ", searchQuery, totalTokensCount); - // if (searchQuery && totalTokensCount < 10 && hasNextPage) { - await fetchNextPage({ cancelRefetch: false }); - // } + const run = async () => { + console.log("fetchNextPage 1"); + await fetchNextPage(); }; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - run(); + void run(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, totalTokensCount]); + }, [items]); + + const handleEndReached = async () => { + if (isFetchingNextPage || !hasNextPage) { + return; + } - console.log("CollectionItemsData.render"); + console.log("fetchNextPage 2"); + await fetchNextPage(); + }; - if (collectionTokens.length === 0) { + if (items.length === 0) { return (
No items found.
@@ -113,13 +82,14 @@ function CollectionItemsData({ } return viewType === "list" ? ( - + ) : ( ); } -export default memo(CollectionItemsData); +export default CollectionItemsData; diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx index f32a1269..9b57cf81 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-toolbar.tsx @@ -25,9 +25,7 @@ interface CollectionItemsToolbarProps { toggleFiltersPanel: () => void; viewType: ViewType; setViewType: (viewType: ViewType) => void; - totalTokensCount: number; filtersPanelOpen: boolean; - filtersDialogOpen: boolean; openFiltersDialog: () => void; filtersCount: number; searchQuery: string; @@ -42,9 +40,7 @@ export default function CollectionItemsToolbar({ toggleFiltersPanel, viewType, setViewType, - // totalTokensCount, filtersPanelOpen, - // filtersDialogOpen, openFiltersDialog, filtersCount, searchQuery, @@ -52,8 +48,6 @@ export default function CollectionItemsToolbar({ }: CollectionItemsToolbarProps) { const isDesktop = useMediaQuery("(min-width: 1024px)"); - console.log("CollectionItemsToolbar.render"); - return (
diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items.tsx index 20d29d7b..68f49863 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items.tsx @@ -30,7 +30,6 @@ const isValidViewType = (value: string): value is ViewType => export default function CollectionItems({ collectionAddress, - collectionTokenCount, }: CollectionProps) { const [filtersPanelOpen, setFiltersPanelOpen] = useState(false); const [filtersDialogOpen, setFiltersDialogOpen] = useState(false); @@ -100,11 +99,6 @@ export default function CollectionItems({ 0, ); - console.log("CollectionItems.render", { - searchQuery, - collectionTokenCount, - }); - return (
@@ -133,11 +127,9 @@ export default function CollectionItems({ setSortBy={setSortBy} viewType={viewType} setViewType={handleViewTypeChange} - totalTokensCount={collectionTokenCount} toggleFiltersPanel={toggleFiltersPanel} filtersPanelOpen={filtersPanelOpen} openFiltersDialog={() => setFiltersDialogOpen(true)} - filtersDialogOpen={filtersDialogOpen} filtersCount={filtersCount} searchQuery={searchQuery} setSearchQuery={setSearchQuery} diff --git a/apps/arkmarket/src/hooks/useCollectionTokens.ts b/apps/arkmarket/src/hooks/useCollectionTokens.ts new file mode 100644 index 00000000..bf995578 --- /dev/null +++ b/apps/arkmarket/src/hooks/useCollectionTokens.ts @@ -0,0 +1,54 @@ +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; + +import type { + CollectionSortBy, + CollectionSortDirection, + CollectionTokensApiResponse, +} from "~/lib/getCollectionTokens"; +import type { Filters } from "~/types"; +import { getCollectionTokens } from "~/lib/getCollectionTokens"; + +interface UseCollectionTokensParams { + collectionAddress: string; + sortDirection: CollectionSortDirection; + sortBy: CollectionSortBy; + filters: Filters; +} + +export default function useCollectionTokens({ + collectionAddress, + sortDirection, + sortBy, + filters, +}: UseCollectionTokensParams) { + const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + useSuspenseInfiniteQuery({ + refetchInterval: 10_000, + initialPageParam: undefined, + queryKey: [ + "collectionTokens", + collectionAddress, + sortDirection, + sortBy, + filters, + ], + queryFn: ({ pageParam }) => + getCollectionTokens({ + collectionAddress, + page: pageParam, + sortDirection, + sortBy, + filters, + }), + getNextPageParam: (lastPage: CollectionTokensApiResponse) => + lastPage.next_page, + }); + + return { + data, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3a4623d..d679f90d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -376,7 +376,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-3dfd5d9e-20240910(eslint@9.9.1(jiti@1.21.6)) + version: 5.1.0-rc-e4953922-20240919(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)) @@ -2690,8 +2690,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-react-hooks@5.1.0-rc-3dfd5d9e-20240910: - resolution: {integrity: sha512-bB2mPM2fKSgb+rrHtksYrawCribuatIBq8t1gZTyEwNbrWTDFc1H0r7FWZVgqOvR9n5hwOxF16KVXkgFwvGQMw==} + eslint-plugin-react-hooks@5.1.0-rc-e4953922-20240919: + resolution: {integrity: sha512-W/CJVTs49Wn34P3RTMGb5jx7JW8ftUIUzNqYDsWqO/tz26DU3fXrou44jxAvu1HR5VH9DutXgxq7DCgDM2MBGQ==} 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 @@ -7614,7 +7614,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@5.1.0-rc-3dfd5d9e-20240910(eslint@9.9.1(jiti@1.21.6)): + eslint-plugin-react-hooks@5.1.0-rc-e4953922-20240919(eslint@9.9.1(jiti@1.21.6)): dependencies: eslint: 9.9.1(jiti@1.21.6)