Skip to content

Commit

Permalink
feat(wallet-dashboard): add grid for visual assets (#3758)
Browse files Browse the repository at this point in the history
* feat(wallet-dashboard): add protected layout

* feat: add missing TopNav

* fix: tests

* fix: imports

* feat(wallet-dashboard): add assets page

* fix: add missing themes

* feat: improve connect button size

* revert: "feat: improve connect button size"

* feat(wallet-dashboard): add grid for visual assets

* refactor: remove unnecessary useMemo

* fix: remove commented lines

* fix: improve type

* fix: update IotaLogo

---------

Co-authored-by: evavirseda <[email protected]>
  • Loading branch information
VmMad and evavirseda authored Nov 7, 2024
1 parent bd07963 commit 0d967e5
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 103 deletions.
1 change: 1 addition & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export * from './useUnlockTimelockedObjectsTransaction';
export * from './useGetAllOwnedObjects';
export * from './useGetTimelockedStakedObjects';
export * from './useGetActiveValidatorsInfo';
export * from './useCursorPagination';

export * from './stake';
56 changes: 56 additions & 0 deletions apps/core/src/hooks/useCursorPagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useState } from 'react';
import { type InfiniteData, type UseInfiniteQueryResult } from '@tanstack/react-query';

interface PaginationProps {
hasFirst: boolean;
hasPrev: boolean;
hasNext: boolean;
onFirst(): void;
onPrev(): void;
onNext(): void;
}

interface CursorPaginationProps extends PaginationProps {
currentPage: number;
}

export interface PaginationResponse<Cursor = string> {
nextCursor: Cursor | null;
hasNextPage: boolean;
}

export function useCursorPagination<T>(query: UseInfiniteQueryResult<InfiniteData<T>>) {
const [currentPage, setCurrentPage] = useState(0);

return {
...query,
data: query.data?.pages[currentPage],
pagination: {
onFirst: () => setCurrentPage(0),
onNext: () => {
if (!query.data || query.isFetchingNextPage) {
return;
}

// Make sure we are at the end before fetching another page
if (currentPage >= query.data.pages.length - 1) {
query.fetchNextPage();
}

setCurrentPage(currentPage + 1);
},
onPrev: () => {
setCurrentPage(Math.max(currentPage - 1, 0));
},
hasFirst: currentPage !== 0,
hasNext:
!query.isFetchingNextPage &&
(currentPage < (query.data?.pages.length ?? 0) - 1 || !!query.hasNextPage),
hasPrev: currentPage !== 0,
currentPage,
} satisfies CursorPaginationProps,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

import { InfoBox, InfoBoxStyle, InfoBoxType, Select, SelectSize } from '@iota/apps-ui-kit';
import { useIotaClientQuery, useIotaClient, useIotaClientInfiniteQuery } from '@iota/dapp-kit';
import { useCursorPagination } from '@iota/core';
import { Warning } from '@iota/ui-icons';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui';
import { PlaceholderTable, TableCard } from '~/components/ui';
import { PAGE_SIZES_RANGE_20_60 } from '~/lib/constants';
import { generateEpochsTableColumns } from '~/lib/ui';
import { numberSuffix } from '~/lib/utils';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import { useIotaClient } from '@iota/dapp-kit';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useRef, useState } from 'react';
import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui';
import { PlaceholderTable, TableCard } from '~/components/ui';
import { useCursorPagination } from '@iota/core';
import {
DEFAULT_TRANSACTIONS_LIMIT,
useGetTransactionBlocks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { InfoBox, InfoBoxStyle, InfoBoxType, Select, SelectSize } from '@iota/ap
import { useIotaClientQuery } from '@iota/dapp-kit';
import { Warning } from '@iota/ui-icons';
import { useMemo, useState } from 'react';
import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui';
import { PlaceholderTable, TableCard } from '~/components/ui';
import { DEFAULT_CHECKPOINTS_LIMIT, useGetCheckpoints } from '~/hooks/useGetCheckpoints';
import { PAGE_SIZES_RANGE_20_60 } from '~/lib/constants';
import { generateCheckpointsTableColumns } from '~/lib/ui';
import { numberSuffix } from '~/lib/utils';
import { useCursorPagination } from '@iota/core';

interface CheckpointsTableProps {
disablePagination?: boolean;
Expand Down
9 changes: 7 additions & 2 deletions apps/explorer/src/components/owned-objects/OwnedObjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useGetKioskContents, useGetOwnedObjects, useLocalStorage } from '@iota/core';
import {
useGetKioskContents,
useGetOwnedObjects,
useLocalStorage,
useCursorPagination,
} from '@iota/core';
import {
Button,
ButtonSize,
Expand All @@ -27,7 +32,7 @@ import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import { ListView, SmallThumbnailsView, ThumbnailsView } from '~/components';
import { ObjectViewMode } from '~/lib/enums';
import { Pagination, useCursorPagination } from '~/components/ui';
import { Pagination } from '~/components/ui';
import { PAGE_SIZES_RANGE_10_50 } from '~/lib/constants';

const SHOW_PAGINATION_MAX_ITEMS = 9;
Expand Down
40 changes: 1 addition & 39 deletions apps/explorer/src/components/ui/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import { Button, ButtonSize, ButtonType } from '@iota/apps-ui-kit';
import { ArrowLeft, ArrowRight, DoubleArrowLeft } from '@iota/ui-icons';
import { type InfiniteData, type UseInfiniteQueryResult } from '@tanstack/react-query';
import { useState } from 'react';

interface PaginationProps {
Expand All @@ -16,49 +15,12 @@ interface PaginationProps {
onNext(): void;
}

interface CursorPaginationProps extends PaginationProps {
currentPage: number;
}

export interface PaginationResponse<Cursor = string> {
nextCursor: Cursor | null;
hasNextPage: boolean;
}

export function useCursorPagination<T>(query: UseInfiniteQueryResult<InfiniteData<T>>) {
const [currentPage, setCurrentPage] = useState(0);

return {
...query,
data: query.data?.pages[currentPage],
pagination: {
onFirst: () => setCurrentPage(0),
onNext: () => {
if (!query.data || query.isFetchingNextPage) {
return;
}

// Make sure we are at the end before fetching another page
if (currentPage >= query.data.pages.length - 1) {
query.fetchNextPage();
}

setCurrentPage(currentPage + 1);
},
onPrev: () => {
setCurrentPage(Math.max(currentPage - 1, 0));
},
hasFirst: currentPage !== 0,
hasNext:
!query.isFetchingNextPage &&
(currentPage < (query.data?.pages.length ?? 0) - 1 || !!query.hasNextPage),
hasPrev: currentPage !== 0,
currentPage,
} satisfies CursorPaginationProps,
};
}

/** @deprecated Prefer `useCursorPagination` + `useInfiniteQuery` for pagination. */
/** @deprecated Prefer `useCursorPagination` from core + `useInfiniteQuery` for pagination. */
export function usePaginationStack<Cursor = string>() {
const [stack, setStack] = useState<Cursor[]>([]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import { DropdownPosition, Select, SelectSize } from '@iota/apps-ui-kit';
import { useState } from 'react';
import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui';
import { PlaceholderTable, TableCard } from '~/components/ui';
import { useCursorPagination } from '@iota/core';
import {
DEFAULT_TRANSACTIONS_LIMIT,
useGetTransactionBlocks,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export { VisualAssetCard } from './VisualAssetCard';
export * from './VisualAssetCard';
export * from './visual-asset-card.enums';
68 changes: 42 additions & 26 deletions apps/wallet-dashboard/app/(protected)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@

'use client';

import { AssetCard, VirtualList } from '@/components';
import { AssetCard, PageSizeSelector, PaginationOptions } from '@/components';
import { ASSETS_ROUTE } from '@/lib/constants/routes.constants';
import { Panel, Title, Chip } from '@iota/apps-ui-kit';
import { hasDisplayData, useGetOwnedObjects } from '@iota/core';
import { Panel, Title, Chip, TitleSize, DropdownPosition } from '@iota/apps-ui-kit';
import { hasDisplayData, useCursorPagination, useGetOwnedObjects } from '@iota/core';
import { useCurrentAccount } from '@iota/dapp-kit';
import { IotaObjectData } from '@iota/iota-sdk/client';
import { useState } from 'react';
import { AssetCategory } from '@/lib/enums';
import Link from 'next/link';
import { VisibilityOff } from '@iota/ui-icons';
import { useRouter } from 'next/navigation';

const PAGINATION_RANGE = [20, 40, 60];

const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [
{
Expand All @@ -26,22 +29,22 @@ const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [

export default function AssetsDashboardPage(): React.JSX.Element {
const [selectedCategory, setSelectedCategory] = useState<AssetCategory>(AssetCategory.Visual);
const [limit, setLimit] = useState<number>(PAGINATION_RANGE[1]);
const router = useRouter();

const account = useCurrentAccount();
const {
data: ownedObjects,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetOwnedObjects(account?.address);
const ownedObjectsQuery = useGetOwnedObjects(account?.address, undefined, limit);

const { data, pagination } = useCursorPagination(ownedObjectsQuery);

const { data: ownedObjects } = data || {};

const [visual, nonVisual] = (() => {
const visual: IotaObjectData[] = [];
const nonVisual: IotaObjectData[] = [];

ownedObjects?.pages
.flatMap((page) => page.data)
.filter((asset) => asset.data && asset.data.objectId)
ownedObjects
?.filter((asset) => asset.data && asset.data.objectId)
.forEach((asset) => {
if (asset.data) {
if (hasDisplayData(asset)) {
Expand All @@ -64,7 +67,7 @@ export default function AssetsDashboardPage(): React.JSX.Element {

return (
<Panel>
<Title title="Assets" />
<Title title="Assets" size={TitleSize.Medium} />
<div className="px-lg">
<div className="flex flex-row items-center justify-start gap-xs py-xs">
{ASSET_CATEGORIES.map((tab) => (
Expand All @@ -77,18 +80,31 @@ export default function AssetsDashboardPage(): React.JSX.Element {
))}
</div>

<div className="max-h-[600px] overflow-auto py-sm">
<VirtualList
items={assetList}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
estimateSize={() => 180}
render={(asset) => (
<Link href={ASSETS_ROUTE.path + `/${asset.objectId}`}>
<AssetCard asset={asset} />
</Link>
)}
<div className="grid-template-visual-assets grid max-h-[600px] gap-md overflow-auto py-sm">
{assetList.map((asset) => (
<div key={asset.digest}>
<AssetCard
asset={asset}
icon={<VisibilityOff />}
onClick={() =>
router.push(ASSETS_ROUTE.path + `/${asset.objectId}`)
}
/>
</div>
))}
</div>
<div className="flex flex-row items-center justify-end py-xs">
<PaginationOptions
pagination={pagination}
action={
<PageSizeSelector
pagination={pagination}
range={PAGINATION_RANGE}
dropdownPosition={DropdownPosition.Top}
setLimit={(e) => setLimit(e)}
limit={limit.toString()}
/>
}
/>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/wallet-dashboard/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,8 @@ body {
'coins activity activity';
}
}

.grid-template-visual-assets {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
47 changes: 23 additions & 24 deletions apps/wallet-dashboard/components/Cards/AssetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,37 @@

import { IotaObjectData } from '@iota/iota-sdk/client';
import React from 'react';
import { Box, ExternalImage } from '@/components/index';
import { useGetNFTMeta } from '@iota/core';
import { FlexDirection } from '@/lib/ui/enums';
import { VisualAssetCard, VisualAssetType, type VisualAssetCardProps } from '@iota/apps-ui-kit';

interface AssetCardProps {
interface AssetCardProps extends Pick<VisualAssetCardProps, 'onClick' | 'onIconClick' | 'icon'> {
asset: IotaObjectData;
flexDirection?: FlexDirection;
}

function AssetCard({ asset, flexDirection }: AssetCardProps): React.JSX.Element {
export function AssetCard({
asset,
onClick,
onIconClick,
icon,
}: AssetCardProps): React.JSX.Element | null {
const { data: nftMeta } = useGetNFTMeta(asset.objectId);

if (!asset.display || !nftMeta || !nftMeta.imageUrl) {
return null;
}

return (
<Box>
<div className={`flex ${flexDirection} w-full gap-2`}>
{asset.display && nftMeta && nftMeta.imageUrl && (
<ExternalImage
src={nftMeta.imageUrl}
alt={nftMeta.name ?? asset.display.data?.name}
width={80}
height={80}
className="object-cover"
/>
)}
<div>
<p>Digest: {asset.digest}</p>
<p>Object ID: {asset.objectId}</p>
{asset.type ? <p>Type: {asset.type}</p> : null}
<p>Version: {asset.version}</p>
</div>
</div>
</Box>
<VisualAssetCard
assetSrc={nftMeta?.imageUrl ?? asset?.display?.data?.imageUrl ?? ''}
assetTitle={nftMeta?.name ?? asset?.display?.data?.name}
assetType={VisualAssetType.Image}
altText={nftMeta?.name ?? (asset?.display?.data?.name || 'NFT')}
isHoverable
icon={icon}
onClick={onClick}
onIconClick={onIconClick}
/>
);
}

export default AssetCard;
2 changes: 1 addition & 1 deletion apps/wallet-dashboard/components/Cards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// SPDX-License-Identifier: Apache-2.0

export { default as StakeCard } from './StakeCard';
export { default as AssetCard } from './AssetCard';
export * from './AssetCard';
Loading

0 comments on commit 0d967e5

Please sign in to comment.