Skip to content

Commit cf9dc12

Browse files
evavirsedaJuligs
andauthored
feat: polish personalize avatar dialog (#416)
* feat(dapp): add DropdownMenuOptions * feat(dapp): enhance dropdown menu with additional options * feat(dapp): add text styles * polish popup * update popup * feat(dapp): update dropdown options * improvements * chore(dapp): remove dropdown option * chore(dapp): comment out non-functional options * remove dupliicated function --------- Co-authored-by: Juligs <julicodes29@gmail.com> Co-authored-by: Juliana <115430927+Juligs@users.noreply.github.com>
1 parent 0b65abc commit cf9dc12

File tree

6 files changed

+267
-114
lines changed

6 files changed

+267
-114
lines changed

dapp/src/app/(protected)/my-names/page.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use client';
55

66
import {
7+
Assets,
78
// Add,
89
// Assets,
910
// Calendar,
@@ -20,6 +21,7 @@ import { useMemo, useState } from 'react';
2021
import { UserAuctions } from '@/auctions/components/UserAuctions';
2122
import { DeleteNameDialog, UpdateNameDialog } from '@/components';
2223
import { CreateSubnameDialog } from '@/components/dialogs/CreateSubnameDialog';
24+
import { PersonalizeAvatarDialog } from '@/components/dialogs/PersonalizeAvatarDialog';
2325
import { DropdownMenuOption } from '@/components/DropdownMenuOptions';
2426
import { NameCard } from '@/components/name-card/NameCard';
2527
import { NameCardBody } from '@/components/name-card/NameCardBody';
@@ -33,6 +35,7 @@ export default function MyNamesPage(): JSX.Element {
3335
const [updateNameDialog, setUpdateNameDialog] = useState<string | null>(null);
3436
const [deleteNameDialog, setDeleteNameDialog] = useState<RegistrationNft | null>(null);
3537
const [subnameAddDialog, setSubnameAddDialog] = useState<RegistrationNft | null>(null);
38+
const [personalizeAvatarName, setPersonalizeAvatarName] = useState<string | null>(null);
3639

3740
const { data: names } = useRegistrationNfts('name');
3841
const { data: subnames } = useRegistrationNfts('subname');
@@ -68,11 +71,11 @@ export default function MyNamesPage(): JSX.Element {
6871
isHidden: !(nft.isExpired && !namesWithChildren.has(nft.name)),
6972
hideBottomBorder: true,
7073
},
71-
// {
72-
// onClick: () => {},
73-
// children: <DropdownMenuOption icon={<Assets />} label="Personalize Avatar" />,
74-
// hideBottomBorder: true,
75-
// },
74+
{
75+
onClick: () => setPersonalizeAvatarName(nft.name),
76+
children: <DropdownMenuOption icon={<Assets />} label="Personalize Avatar" />,
77+
hideBottomBorder: true,
78+
},
7679
// {
7780
// onClick: () => {},
7881
// children: <DropdownMenuOption icon={<Delete />} label="Remove Avatar" />,
@@ -196,6 +199,12 @@ export default function MyNamesPage(): JSX.Element {
196199
setOpen={() => setSubnameAddDialog(null)}
197200
/>
198201
)}
202+
{!!personalizeAvatarName && (
203+
<PersonalizeAvatarDialog
204+
name={personalizeAvatarName}
205+
setOpen={() => setPersonalizeAvatarName(null)}
206+
/>
207+
)}
199208
</div>
200209
);
201210
}

dapp/src/components/dialogs/AvatarSelectDialog.tsx

Lines changed: 0 additions & 72 deletions
This file was deleted.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Info, Loader } from '@iota/apps-ui-icons';
5+
import {
6+
Button,
7+
ButtonType,
8+
Dialog,
9+
DialogBody,
10+
DialogContent,
11+
DialogPosition,
12+
Header,
13+
InfoBox,
14+
InfoBoxStyle,
15+
InfoBoxType,
16+
LoadingIndicator,
17+
VisualAssetCard,
18+
} from '@iota/apps-ui-kit';
19+
import { useCurrentAccount, useIotaClient, useSignAndExecuteTransaction } from '@iota/dapp-kit';
20+
import { isSubname } from '@iota/iota-names-sdk';
21+
import { useMutation, useQueryClient } from '@tanstack/react-query';
22+
import { useState } from 'react';
23+
24+
import {
25+
NameRecordData,
26+
NameUpdate,
27+
queryKey,
28+
useNameRecord,
29+
useRegistrationNfts,
30+
useUpdateNameTransaction,
31+
} from '@/hooks';
32+
import { useGetVisualAssets } from '@/hooks/useGetVisualAssets';
33+
import { normalizeNameInput } from '@/lib/utils/format/formatNames';
34+
import { getNameObject } from '@/lib/utils/names';
35+
import { BrandedAssets } from '@/public/icons';
36+
37+
interface PersonalizeAvatarDialogProps {
38+
setOpen: (bool: boolean) => void;
39+
name: string;
40+
}
41+
export function PersonalizeAvatarDialog({ setOpen, name }: PersonalizeAvatarDialogProps) {
42+
const account = useCurrentAccount();
43+
const iotaClient = useIotaClient();
44+
const queryClient = useQueryClient();
45+
const address = useCurrentAccount()?.address ?? '';
46+
47+
const { data: nameRecordData } = useNameRecord(name);
48+
const { data: subnamesOwned } = useRegistrationNfts('subname');
49+
const { data: visualAssets, isLoading } = useGetVisualAssets(address);
50+
51+
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
52+
53+
const nameRecord = nameRecordData as
54+
| Extract<NameRecordData, { type: 'unavailable' }>
55+
| undefined;
56+
const isNameSubname = nameRecord?.nameRecord ? isSubname(nameRecord.nameRecord.name) : null;
57+
const cleanName = normalizeNameInput(name);
58+
const updates: NameUpdate[] = [];
59+
60+
if (selectedAssetId && selectedAssetId !== nameRecord?.nameRecord.avatar && nameRecord) {
61+
const nftId = isNameSubname
62+
? getNameObject(subnamesOwned ?? [], nameRecord.nameRecord.name)
63+
: nameRecord.nameRecord.nftId;
64+
if (nftId) {
65+
updates.push({
66+
type: 'set-avatar',
67+
nftId,
68+
avatarNftId: selectedAssetId,
69+
});
70+
}
71+
}
72+
73+
const { data: updateTransaction } = useUpdateNameTransaction({
74+
address: account?.address || '',
75+
updates,
76+
});
77+
78+
const { mutateAsync: signAndExecuteTransaction, isPending: isSigning } =
79+
useSignAndExecuteTransaction();
80+
81+
const { mutate: saveAvatar, isPending: isSaving } = useMutation({
82+
async mutationFn() {
83+
if (!updateTransaction) return;
84+
const tx = await signAndExecuteTransaction({ transaction: updateTransaction });
85+
86+
await iotaClient.waitForTransaction({ digest: tx.digest });
87+
88+
queryClient.invalidateQueries({ queryKey: queryKey.nameRecord(name) });
89+
},
90+
onSuccess() {
91+
setOpen(false);
92+
},
93+
});
94+
95+
function handleSelectAsset() {
96+
if (!selectedAssetId || !updateTransaction) return;
97+
saveAvatar();
98+
}
99+
return (
100+
<Dialog open onOpenChange={setOpen}>
101+
<DialogContent
102+
customWidth="w-full max-w-[90vw] md:max-w-[50vw] xl:max-w-[60vw]"
103+
position={DialogPosition.Right}
104+
>
105+
<Header
106+
title="Personalize Avatar"
107+
titleCentered
108+
onClose={() => setOpen(false)}
109+
onBack={() => setOpen(false)}
110+
/>
111+
112+
<DialogBody>
113+
<div className="flex flex-col gap-md items-center">
114+
<div className="flex flex-col gap-md items-center">
115+
<BrandedAssets className="w-12 h-12" />
116+
<div className="flex flex-col gap-xs text-center">
117+
<span className="text-title-md text-names-neutral-92">
118+
@{cleanName}
119+
</span>
120+
<span className="text-body-md text-names-neutral-70">
121+
Use an NFT to personalize your avatar
122+
</span>
123+
</div>
124+
</div>
125+
{isLoading ? (
126+
<div className="flex items-center justify-center w-full h-full py-lg">
127+
<LoadingIndicator text="Loading Assets..." />
128+
</div>
129+
) : !visualAssets || visualAssets?.length === 0 ? (
130+
<div className="flex items-center justify-center w-full py-lg">
131+
<InfoBox
132+
title="No Eligible NFTs"
133+
supportingText="There are no NFTs in your wallet that can be used as an avatar"
134+
icon={<Info />}
135+
type={InfoBoxType.Warning}
136+
style={InfoBoxStyle.Default}
137+
/>
138+
</div>
139+
) : (
140+
<div className="max-h-[400px] w-full grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-md">
141+
{visualAssets.map((asset) => {
142+
const isSelected = selectedAssetId === asset.objectId;
143+
144+
return (
145+
<div
146+
key={asset.objectId}
147+
className={`rounded-xl p-[1px] transition-all ${
148+
isSelected
149+
? 'bg-names-gradient-primary'
150+
: 'bg-transparent'
151+
}`}
152+
>
153+
<div className="bg-names-neutral-6 rounded-xl">
154+
<VisualAssetCard
155+
src={asset.display?.data?.image_url || ''}
156+
altText={asset.display?.data?.name || 'NFT'}
157+
isHoverable
158+
onClick={() =>
159+
setSelectedAssetId(asset.objectId)
160+
}
161+
/>
162+
</div>
163+
</div>
164+
);
165+
})}
166+
</div>
167+
)}
168+
</div>
169+
</DialogBody>
170+
171+
<div className="flex w-full flex-row justify-center gap-2 px-md--rs pb-md--rs pt-md--rs">
172+
<Button
173+
type={ButtonType.Secondary}
174+
text="Cancel"
175+
onClick={() => setOpen(false)}
176+
fullWidth
177+
/>
178+
<Button
179+
type={ButtonType.Primary}
180+
text={isSaving || isSigning ? 'Uploading...' : 'Upload Avatar'}
181+
onClick={handleSelectAsset}
182+
disabled={isSaving || isSigning || !selectedAssetId}
183+
fullWidth
184+
icon={
185+
isSaving || isSigning ? (
186+
<Loader className="animate-spin" data-testid="loading-indicator" />
187+
) : null
188+
}
189+
/>
190+
</div>
191+
</DialogContent>
192+
</Dialog>
193+
);
194+
}

0 commit comments

Comments
 (0)