Skip to content

Commit

Permalink
feat(wallet-dashboard): style selected visual Assets (#4085)
Browse files Browse the repository at this point in the history
* feat(wallet-dashboard): style selected visual Assets.

* refactor(core): destructure metaKeys and metaValues from attributes

* refactor(wallet): move Collapsible component to core.

* feat(dashboard): integrate useAssetsDialog for asset details view

* fix(assets): update import path and enhance text styling in DetailsView

* refactor(wallet-dashboard): move state to page

* refactor(wallet-dashboard): rename handler functions for consistency and clarity

* refactor(dashboard): update state for asset view, improve code

* fix(wallet-dashboard): unify asset transfer success and error handling in AssetDialog

* refactor(dashboard): adjust z-index for dialog and notifications;

* feat(dashboard): add refetch functionality after asset transfered

* refactor(dashboard): rename callbacks for clarity in AssetDialog

* refactor(dashboard, cove): rename hooks, remove duplication.

* refactor(dashboard): remove unused asset details page

---------

Co-authored-by: Bran <[email protected]>
Co-authored-by: Marc Espin <[email protected]>
  • Loading branch information
3 people authored Dec 4, 2024
1 parent d796edd commit c7aa8e5
Show file tree
Hide file tree
Showing 43 changed files with 587 additions and 230 deletions.
4 changes: 4 additions & 0 deletions apps/core/src/components/collapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './Collapsible';
2 changes: 1 addition & 1 deletion apps/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export * from './coin';
export * from './icon';
export * from './Inputs';
export * from './QR';

export * from './collapsible';
export * from './providers';
5 changes: 4 additions & 1 deletion apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export * from './useQueryTransactionsByAddress';
export * from './useGetTransaction';
export * from './useExtendedTransactionSummary';
export * from './useSortedCoinsByCategories';
export * from './useGetNFTMeta';
export * from './useGetNFTDisplay';
export * from './useIotaAddressValidation';
export * from './useUnlockTimelockedObjectsTransaction';
export * from './useGetAllOwnedObjects';
Expand All @@ -43,6 +43,9 @@ export * from './useTransactionData';
export * from './useGetStakingValidatorDetails';
export * from './useCursorPagination';
export * from './useTheme';
export * from './useNFTBasicData';
export * from './useOwnedNFT';
export * from './useNftDetails';
export * from './useCountdownByTimestamp';

export * from './stake';
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useGetObject } from './';
import { useGetObject } from '.';
import { useMemo } from 'react';

export type NFTMetadata = {
Expand All @@ -11,7 +11,7 @@ export type NFTMetadata = {
url: string;
};

export function useGetNFTMeta(objectID: string) {
export function useGetNFTDisplay(objectID: string) {
const resp = useGetObject(objectID);
const nftMeta = useMemo(() => {
if (!resp.data) return null;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IotaObjectData } from '@iota/iota-sdk/client';
import useFileExtensionType from './useFileExtensionType';
import useMediaUrl from './useMediaUrl';

export default function useNFTBasicData(nftObj: IotaObjectData | null) {
export function useNFTBasicData(nftObj: IotaObjectData | null) {
const nftObjectID = nftObj?.objectId || null;
const filePath = useMediaUrl(nftObj?.content || null);
let objType = null;
Expand Down
98 changes: 98 additions & 0 deletions apps/core/src/hooks/useNftDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import {
useGetNFTDisplay,
useOwnedNFT,
useNFTBasicData,
useGetKioskContents,
useIsAssetTransferable,
} from './';
import { formatAddress } from '@iota/iota-sdk/utils';
import { truncateString } from '../utils';

type NftField = { keys: string[]; values: string[] };

type NftFields = {
metadata?: { fields?: { attributes?: { fields?: NftField } } };
};

export function useNftDetails(nftId: string, accountAddress: string | null) {
const { data: objectData, isPending: isNftLoading } = useOwnedNFT(nftId || '', accountAddress);
const { data } = useGetKioskContents(accountAddress);

const isContainedInKiosk = data?.lookup.get(nftId!);
const kioskItem = data?.list.find((k) => k.data?.objectId === nftId);

const { data: isAssetTransferable, isLoading: isCheckingAssetTransferability } =
useIsAssetTransferable(objectData);

const { nftFields } = useNFTBasicData(objectData);

const { data: nftDisplayData, isPending: isPendingNftDislpay } = useGetNFTDisplay(nftId);

const nftName = nftDisplayData?.name || formatAddress(nftId);
const nftImageUrl = nftDisplayData?.imageUrl || '';

// Extract either the attributes, or use the top-level NFT fields:
const { keys: metaKeys, values: metaValues } =
(nftFields as NftFields)?.metadata?.fields?.attributes?.fields ||
Object.entries(nftFields ?? {})
.filter(([key]) => key !== 'id')
.reduce<NftField>(
(acc, [key, value]) => {
acc.keys.push(key);
acc.values.push(value as string);
return acc;
},
{ keys: [], values: [] },
);

const ownerAddress =
(objectData?.owner &&
typeof objectData?.owner === 'object' &&
'AddressOwner' in objectData.owner &&
objectData.owner.AddressOwner) ||
'';

function formatMetaValue(value: string | object) {
if (typeof value === 'object') {
return {
value: JSON.stringify(value),
valueLink: undefined,
};
} else {
if (value.includes('http')) {
return {
value: value.startsWith('http')
? truncateString(value, 20, 8)
: formatAddress(value),
valueLink: value,
};
}
return {
value: value,
valueLink: undefined,
};
}
}

const isLoading = isNftLoading || isCheckingAssetTransferability || isPendingNftDislpay;

return {
isLoading,
objectData,
isNftLoading,
nftName,
nftImageUrl,
ownerAddress,
isCheckingAssetTransferability,
isAssetTransferable,
metaKeys,
metaValues,
formatMetaValue,
isContainedInKiosk,
kioskItem,
nftDisplayData,
isPendingNftDislpay,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useGetKioskContents, useGetObject } from '@iota/core';
import { useGetKioskContents, useGetObject } from './';
import { useMemo } from 'react';

export function useOwnedNFT(nftObjectId: string | null, address: string | null) {
Expand Down
1 change: 1 addition & 0 deletions apps/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './getDelegationDataByStakeId';
export * from './api-env';
export * from './getExplorerPaths';
export * from './getExplorerLink';
export * from './truncateString';

export * from './stake';
export * from './transaction';
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ArrowDown } from '@iota/ui-icons';
import { Button, ButtonType } from '@/lib';
import { ICON_STYLE } from './accordion.classes';

interface AccordionHeaderProps {
export interface AccordionHeaderProps {
/**
* Flag for show/hide content
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/ui-kit/src/lib/components/organisms/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const DialogContent = React.forwardRef<
<RadixDialog.Content
ref={ref}
className={cx(
'fixed z-[99999] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6 md:w-96',
'fixed z-[99998] flex flex-col justify-center overflow-hidden bg-primary-100 dark:bg-neutral-6 md:w-96',
positionClass,
)}
{...props}
Expand Down
44 changes: 0 additions & 44 deletions apps/wallet-dashboard/app/(protected)/assets/[objectId]/page.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions apps/wallet-dashboard/app/(protected)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IotaObjectData } from '@iota/iota-sdk/client';
import { useState } from 'react';
import { AssetCategory } from '@/lib/enums';
import { AssetList } from '@/components/AssetsList';
import { AssetDialog } from '@/components/Dialogs/Assets';

const OBJECTS_PER_REQ = 50;

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

export default function AssetsDashboardPage(): React.JSX.Element {
const [selectedAsset, setSelectedAsset] = useState<IotaObjectData | null>(null);
const [selectedCategory, setSelectedCategory] = useState<AssetCategory>(AssetCategory.Visual);
const account = useCurrentAccount();
const { data, isFetching, fetchNextPage, hasNextPage } = useGetOwnedObjects(
Expand All @@ -49,6 +51,10 @@ export default function AssetsDashboardPage(): React.JSX.Element {
}
}

function onAssetClick(asset: IotaObjectData) {
setSelectedAsset(asset);
}

return (
<Panel>
<Title title="Assets" size={TitleSize.Medium} />
Expand All @@ -67,10 +73,14 @@ export default function AssetsDashboardPage(): React.JSX.Element {
<AssetList
assets={assets}
selectedCategory={selectedCategory}
onClick={onAssetClick}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetching}
fetchNextPage={fetchNextPage}
/>
{selectedAsset && (
<AssetDialog onClose={() => setSelectedAsset(null)} asset={selectedAsset} />
)}
</div>
</Panel>
);
Expand Down
9 changes: 8 additions & 1 deletion apps/wallet-dashboard/components/AssetsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface AssetListProps {
hasNextPage: boolean;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
onClick: (asset: IotaObjectData) => void;
}

const ASSET_LAYOUT: Record<AssetCategory, string> = {
Expand All @@ -29,6 +30,7 @@ export function AssetList({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
onClick,
}: AssetListProps): React.JSX.Element {
const observerElem = useRef<HTMLDivElement | null>(null);
const { isIntersecting } = useOnScreen(observerElem);
Expand All @@ -43,7 +45,12 @@ export function AssetList({
return (
<div className={cl('max-h-[600px]', ASSET_LAYOUT[selectedCategory])}>
{assets.map((asset) => (
<AssetTileLink key={asset.digest} asset={asset} type={selectedCategory} />
<AssetTileLink
key={asset.digest}
asset={asset}
type={selectedCategory}
onClick={onClick}
/>
))}
<div ref={observerElem}>
{isSpinnerVisible ? (
Expand Down
81 changes: 81 additions & 0 deletions apps/wallet-dashboard/components/Dialogs/Assets/AssetDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React, { useState } from 'react';
import { Dialog } from '@iota/apps-ui-kit';
import { FormikProvider, useFormik } from 'formik';
import { useCurrentAccount } from '@iota/dapp-kit';
import { createNftSendValidationSchema } from '@iota/core';
import { DetailsView, SendView } from './views';
import { IotaObjectData } from '@iota/iota-sdk/client';
import { AssetsDialogView } from './constants';
import { useCreateSendAssetTransaction, useNotifications } from '@/hooks';
import { NotificationType } from '@/stores/notificationStore';

interface AssetsDialogProps {
onClose: () => void;
asset: IotaObjectData;
}

interface FormValues {
to: string;
}

const INITIAL_VALUES: FormValues = {
to: '',
};

export function AssetDialog({ onClose, asset }: AssetsDialogProps): JSX.Element {
const [view, setView] = useState<AssetsDialogView>(AssetsDialogView.Details);
const account = useCurrentAccount();
const activeAddress = account?.address ?? '';
const objectId = asset?.objectId ?? '';
const { addNotification } = useNotifications();
const validationSchema = createNftSendValidationSchema(activeAddress, objectId);

const { mutation: sendAsset } = useCreateSendAssetTransaction(objectId);

const formik = useFormik<FormValues>({
initialValues: INITIAL_VALUES,
validationSchema: validationSchema,
onSubmit: onSubmit,
validateOnChange: true,
});

async function onSubmit(values: FormValues) {
try {
await sendAsset.mutateAsync(values.to);
addNotification('Transfer transaction successful', NotificationType.Success);
onClose();
setView(AssetsDialogView.Details);
} catch {
addNotification('Transfer transaction failed', NotificationType.Error);
}
}

function onDetailsSend() {
setView(AssetsDialogView.Send);
}

function onSendViewBack() {
setView(AssetsDialogView.Details);
}
function onOpenChange() {
setView(AssetsDialogView.Details);
onClose();
}
return (
<Dialog open onOpenChange={onOpenChange}>
<FormikProvider value={formik}>
<>
{view === AssetsDialogView.Details && (
<DetailsView asset={asset} onClose={onOpenChange} onSend={onDetailsSend} />
)}
{view === AssetsDialogView.Send && (
<SendView asset={asset} onClose={onOpenChange} onBack={onSendViewBack} />
)}
</>
</FormikProvider>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export enum AssetsDialogView {
Details = 'Details',
Send = 'Send',
}
Loading

0 comments on commit c7aa8e5

Please sign in to comment.