-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(wallet-dashboard): style selected visual Assets. #4085
base: develop
Are you sure you want to change the base?
Changes from 13 commits
4074f2b
9bb5dd0
1f2eb82
f9530b1
934cd96
d055db0
2a2daeb
6304492
c22a756
124ddec
fc404d8
7bd0c26
61a13f8
ed3e381
336468e
4ce390b
16110bb
0bd5c50
84b2dba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import { useGetNFTMeta, useOwnedNFT, useNFTBasicData, useGetKioskContents } from './'; | ||
import { formatAddress } from '@iota/iota-sdk/utils'; | ||
import { isAssetTransferable, truncateString } from '../utils'; | ||
|
||
type NftFields = { | ||
metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } }; | ||
}; | ||
|
||
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 isTransferable = isAssetTransferable(objectData); | ||
|
||
const { nftFields } = useNFTBasicData(objectData); | ||
|
||
const { data: nftMeta, isPending: isPendingMeta } = useGetNFTMeta(nftId); | ||
|
||
const nftName = nftMeta?.name || formatAddress(nftId); | ||
const nftImageUrl = nftMeta?.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( | ||
(acc, [key, value]) => { | ||
acc.keys.push(key); | ||
acc.values.push(value as string); | ||
return acc; | ||
}, | ||
{ keys: [] as string[], values: [] as string[] }, | ||
); | ||
|
||
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, | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
objectData, | ||
isNftLoading, | ||
nftName, | ||
nftImageUrl, | ||
ownerAddress, | ||
isTransferable, | ||
metaKeys, | ||
metaValues, | ||
formatMetaValue, | ||
isContainedInKiosk, | ||
kioskItem, | ||
nftMeta, | ||
isPendingMeta, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 { AssetsDialog, AssetsDialogView } from '@/components/Dialogs/Assets'; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const OBJECTS_PER_REQ = 50; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
@@ -25,6 +26,8 @@ const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [ | |||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
export default function AssetsDashboardPage(): React.JSX.Element { | ||||||||||||||||||||||||||||||||
const [view, setView] = useState<AssetsDialogView | undefined>(AssetsDialogView.Details); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the view here is not necessary, it could be moved inside the AssetsDialog |
||||||||||||||||||||||||||||||||
const [selectedAsset, setSelectedAsset] = useState<IotaObjectData | null>(null); | ||||||||||||||||||||||||||||||||
const [selectedCategory, setSelectedCategory] = useState<AssetCategory>(AssetCategory.Visual); | ||||||||||||||||||||||||||||||||
const account = useCurrentAccount(); | ||||||||||||||||||||||||||||||||
const { data, isFetching, fetchNextPage, hasNextPage } = useGetOwnedObjects( | ||||||||||||||||||||||||||||||||
|
@@ -49,6 +52,16 @@ export default function AssetsDashboardPage(): React.JSX.Element { | |||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
function onClickAsset(asset: IotaObjectData) { | ||||||||||||||||||||||||||||||||
setSelectedAsset(asset); | ||||||||||||||||||||||||||||||||
setView(AssetsDialogView.Details); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
function onCloseDialog() { | ||||||||||||||||||||||||||||||||
setSelectedAsset(null); | ||||||||||||||||||||||||||||||||
setView(undefined); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||
<Panel> | ||||||||||||||||||||||||||||||||
<Title title="Assets" size={TitleSize.Medium} /> | ||||||||||||||||||||||||||||||||
|
@@ -67,10 +80,20 @@ export default function AssetsDashboardPage(): React.JSX.Element { | |||||||||||||||||||||||||||||||
<AssetList | ||||||||||||||||||||||||||||||||
assets={assets} | ||||||||||||||||||||||||||||||||
selectedCategory={selectedCategory} | ||||||||||||||||||||||||||||||||
onClick={onClickAsset} | ||||||||||||||||||||||||||||||||
hasNextPage={hasNextPage} | ||||||||||||||||||||||||||||||||
isFetchingNextPage={isFetching} | ||||||||||||||||||||||||||||||||
fetchNextPage={fetchNextPage} | ||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||
{view && ( | ||||||||||||||||||||||||||||||||
<AssetsDialog | ||||||||||||||||||||||||||||||||
view={view} | ||||||||||||||||||||||||||||||||
setView={setView} | ||||||||||||||||||||||||||||||||
isOpen={!!selectedAsset} | ||||||||||||||||||||||||||||||||
onClose={onCloseDialog} | ||||||||||||||||||||||||||||||||
asset={selectedAsset} | ||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||
</Panel> | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name should be singular, since it refers to a specific Asset, not multiple. Maybe even |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,103 @@ | ||||||||||||||||||||||||||||||||||||||
// Copyright (c) 2024 IOTA Stiftung | ||||||||||||||||||||||||||||||||||||||
// SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
import React 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'; | ||||||||||||||||||||||||||||||||||||||
import { ASSETS_ROUTE } from '@/lib/constants/routes.constants'; | ||||||||||||||||||||||||||||||||||||||
import { useRouter } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
interface AssetsDialogProps { | ||||||||||||||||||||||||||||||||||||||
isOpen: boolean; | ||||||||||||||||||||||||||||||||||||||
onClose: () => void; | ||||||||||||||||||||||||||||||||||||||
asset: IotaObjectData | null; | ||||||||||||||||||||||||||||||||||||||
view: AssetsDialogView; | ||||||||||||||||||||||||||||||||||||||
setView: (view: AssetsDialogView | undefined) => void; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
export interface FormValues { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this interface needs too be exported I would say to rename it to something more specific |
||||||||||||||||||||||||||||||||||||||
to: string; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const INITIAL_VALUES: FormValues = { | ||||||||||||||||||||||||||||||||||||||
to: '', | ||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
export function AssetsDialog({ | ||||||||||||||||||||||||||||||||||||||
isOpen, | ||||||||||||||||||||||||||||||||||||||
onClose, | ||||||||||||||||||||||||||||||||||||||
asset, | ||||||||||||||||||||||||||||||||||||||
setView, | ||||||||||||||||||||||||||||||||||||||
view, | ||||||||||||||||||||||||||||||||||||||
}: AssetsDialogProps): JSX.Element { | ||||||||||||||||||||||||||||||||||||||
const account = useCurrentAccount(); | ||||||||||||||||||||||||||||||||||||||
const activeAddress = account?.address ?? ''; | ||||||||||||||||||||||||||||||||||||||
const objectId = asset?.objectId ?? ''; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||
const { addNotification } = useNotifications(); | ||||||||||||||||||||||||||||||||||||||
const validationSchema = createNftSendValidationSchema(activeAddress, objectId); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
function onDetailsSend() { | ||||||||||||||||||||||||||||||||||||||
setView(AssetsDialogView.Send); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const { mutation: sendAsset } = useCreateSendAssetTransaction( | ||||||||||||||||||||||||||||||||||||||
objectId, | ||||||||||||||||||||||||||||||||||||||
onSendAssetSuccess, | ||||||||||||||||||||||||||||||||||||||
onSendAssetError, | ||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const formik = useFormik<FormValues>({ | ||||||||||||||||||||||||||||||||||||||
initialValues: INITIAL_VALUES, | ||||||||||||||||||||||||||||||||||||||
validationSchema: validationSchema, | ||||||||||||||||||||||||||||||||||||||
onSubmit: onSubmit, | ||||||||||||||||||||||||||||||||||||||
validateOnChange: true, | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
function onSendAssetSuccess() { | ||||||||||||||||||||||||||||||||||||||
addNotification('Transfer transaction successful', NotificationType.Success); | ||||||||||||||||||||||||||||||||||||||
router.push(ASSETS_ROUTE.path + '/assets'); | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not necessary, it's the same view, isn't it? |
||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
function onSendAssetError() { | ||||||||||||||||||||||||||||||||||||||
addNotification('Transfer transaction failed', NotificationType.Error); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
async function onSubmit(values: FormValues) { | ||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||
await sendAsset.mutateAsync(values.to); | ||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||
addNotification('Transfer transaction failed', NotificationType.Error); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After sending, first the dialog should be closed and then shown a notification, no? |
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
function onSendClose() { | ||||||||||||||||||||||||||||||||||||||
setView(undefined); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
function onSendBack() { | ||||||||||||||||||||||||||||||||||||||
setView(AssetsDialogView.Details); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel it's a bit weird that the functions are defined by their view, rather than what they do, like, the function |
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||
<Dialog open={isOpen} onOpenChange={() => onClose()}> | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
<FormikProvider value={formik}> | ||||||||||||||||||||||||||||||||||||||
<> | ||||||||||||||||||||||||||||||||||||||
{view === AssetsDialogView.Details && asset && ( | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This way asset will always be defined |
||||||||||||||||||||||||||||||||||||||
<DetailsView asset={asset} onClose={onClose} onSend={onDetailsSend} /> | ||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||
{view === AssetsDialogView.Send && asset && ( | ||||||||||||||||||||||||||||||||||||||
<SendView asset={asset} onClose={onSendClose} onBack={onSendBack} /> | ||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||
</> | ||||||||||||||||||||||||||||||||||||||
</FormikProvider> | ||||||||||||||||||||||||||||||||||||||
</Dialog> | ||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name of the folder and the file are wrong, this should be an enum folder, and the file shouldn't be pascal case |
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', | ||
} |
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 './AssetsDialogView'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export * from './AssetsDialog'; | ||
export * from './constants'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Line 8 and this one could share a type