Skip to content

Commit a374122

Browse files
authored
feat(dapp): add general info popup (#424)
* feat(dapp): add DropdownMenuOptions * feat(dapp): enhance dropdown menu with additional options * feat(dapp): add text styles * feat(dapp): update dropdown options * feat(dapp): add general info popup * chore(dapp): remove dropdown option * feat(dapp): add links * chore(dapp): comment out non-functional options * refactor(dapp): update GeneralInfoDialog * refactor(dapp): update collapsibleInfo * chore(dapp): update generalinfoDialog after review * chore(dapp): update some styles * refactor(dapp): update GeneralInfoDialog
1 parent 4a22906 commit a374122

File tree

5 files changed

+182
-8
lines changed

5 files changed

+182
-8
lines changed

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Calendar,
1010
// Calendar,
1111
// Delete,
12-
// Info,
12+
Info,
1313
// Link,
1414
// Pined,
1515
Settings,
@@ -21,6 +21,7 @@ import { useMemo, useState } from 'react';
2121
import { UserAuctions } from '@/auctions/components/UserAuctions';
2222
import { DeleteNameDialog, UpdateNameDialog } from '@/components';
2323
import { CreateSubnameDialog } from '@/components/dialogs/CreateSubnameDialog';
24+
import { GeneralInfoDialog } from '@/components/dialogs/GeneralInfoDialog';
2425
import { PersonalizeAvatarDialog } from '@/components/dialogs/PersonalizeAvatarDialog';
2526
import { RenewNameDialog } from '@/components/dialogs/RenewNameDialog';
2627
import { DropdownMenuOption } from '@/components/DropdownMenuOptions';
@@ -34,6 +35,7 @@ import { normalizeNameInput, splitNameInParts } from '@/lib/utils/format/formatN
3435

3536
export default function MyNamesPage(): JSX.Element {
3637
const [updateNameDialog, setUpdateNameDialog] = useState<string | null>(null);
38+
const [generalInfoDialog, setGeneralInfoDialog] = useState<string | null>(null);
3739
const [deleteNameDialog, setDeleteNameDialog] = useState<RegistrationNft | null>(null);
3840
const [createSubnameDialog, setCreateSubnameDialog] = useState<RegistrationNft | null>(null);
3941
const [personalizeAvatarName, setPersonalizeAvatarName] = useState<string | null>(null);
@@ -91,16 +93,17 @@ export default function MyNamesPage(): JSX.Element {
9193
// onClick: () => {},
9294
// children: <DropdownMenuOption icon={<Link />} label="Link to Wallet Address" />,
9395
// },
96+
9497
{
9598
onClick: () => setRenewName(nft),
9699
children: <DropdownMenuOption icon={<Calendar />} label="Renew Name" />,
97100
hideBottomBorder: true,
98101
},
99-
// {
100-
// onClick: () => {},
101-
// children: <DropdownMenuOption icon={<Info />} label="View All Info" />,
102-
// hideBottomBorder: true,
103-
// },
102+
{
103+
onClick: () => setGeneralInfoDialog(nft.name),
104+
children: <DropdownMenuOption icon={<Info />} label="View All Info" />,
105+
hideBottomBorder: true,
106+
},
104107
];
105108

106109
return (
@@ -112,6 +115,14 @@ export default function MyNamesPage(): JSX.Element {
112115
setOpen={() => setUpdateNameDialog(null)}
113116
/>
114117
) : null}
118+
{generalInfoDialog ? (
119+
<GeneralInfoDialog
120+
name={generalInfoDialog}
121+
open
122+
setOpen={() => setGeneralInfoDialog(null)}
123+
/>
124+
) : null}
125+
115126
{deleteNameDialog ? (
116127
<DeleteNameDialog
117128
nft={deleteNameDialog}
@@ -156,7 +167,6 @@ export default function MyNamesPage(): JSX.Element {
156167
<div className="pt-md">
157168
<Title title="My subnames" />
158169
</div>
159-
160170
{subnames?.length ? (
161171
<div className="flex flex-row gap-sm items-stretch justify-center flex-wrap w-full">
162172
{subnames.map((subname) => {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Accordion, AccordionContent, AccordionHeader, Title, TitleSize } from '@iota/apps-ui-kit';
5+
import { useState } from 'react';
6+
7+
interface CollapsibleProps {
8+
title: string;
9+
titleSize?: TitleSize;
10+
}
11+
export function Collapsible({
12+
title,
13+
titleSize,
14+
children,
15+
}: React.PropsWithChildren<CollapsibleProps>) {
16+
const [isOpen, setIsOpen] = useState(true);
17+
return (
18+
<Accordion hideBorder={false}>
19+
<AccordionHeader isExpanded={isOpen} onToggle={() => setIsOpen((prev) => !prev)}>
20+
<Title title={title} size={titleSize} />
21+
</AccordionHeader>
22+
<AccordionContent isExpanded={isOpen}>{children}</AccordionContent>
23+
</Accordion>
24+
);
25+
}

dapp/src/components/DropdownMenuOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
interface DropdownMenuOptionProps {
5-
icon: JSX.Element;
5+
icon: React.ReactNode;
66
label: string;
77
}
88

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use client';
5+
6+
import {
7+
Dialog,
8+
DialogBody,
9+
DialogContent,
10+
DialogPosition,
11+
Header,
12+
KeyValueInfo,
13+
TitleSize,
14+
truncate,
15+
} from '@iota/apps-ui-kit';
16+
import { useCurrentAccount } from '@iota/dapp-kit';
17+
import { isSubname } from '@iota/iota-names-sdk';
18+
import Link from 'next/link';
19+
20+
import { NameRecordData, useNameRecord, useRegistrationNfts } from '@/hooks';
21+
import { formatDate } from '@/lib/utils/format/formatDate';
22+
import { normalizeNameInput, splitNameInParts } from '@/lib/utils/format/formatNames';
23+
24+
import { Collapsible } from '../Collapsible';
25+
import { AvatarDisplay } from '../name-record/AvatarDisplay';
26+
27+
interface GeneralInfoDialogProps {
28+
name: string;
29+
open: boolean;
30+
setOpen: (bool: boolean) => void;
31+
}
32+
33+
interface InfoLinks {
34+
key: string;
35+
value: string;
36+
href: string;
37+
}
38+
39+
export function GeneralInfoDialog({ name, open, setOpen }: GeneralInfoDialogProps) {
40+
const account = useCurrentAccount();
41+
const address = account?.address || '';
42+
43+
const { data: nameRecordData } = useNameRecord(name);
44+
const { data: subnames } = useRegistrationNfts('subname');
45+
46+
const nameRecord = nameRecordData as
47+
| Extract<NameRecordData, { type: 'unavailable' }>
48+
| undefined;
49+
const targetAddress = nameRecord?.nameRecord.targetAddress;
50+
const isNameSubname = isSubname(name);
51+
const { id, expirationTimestampMs } =
52+
(isNameSubname
53+
? subnames?.find((n) => n.name === name)
54+
: { id: nameRecord?.nameRecord.nftId, ...nameRecord?.nameRecord }) ?? {};
55+
56+
const infoLinks: InfoLinks[] = [
57+
{
58+
key: 'Owner',
59+
value: address,
60+
href: `https://explorer.iota.org/address/${address}?network=devnet`,
61+
},
62+
targetAddress && {
63+
key: 'Target address',
64+
value: targetAddress,
65+
href: `https://explorer.iota.org/address/${targetAddress}?network=devnet`,
66+
},
67+
{
68+
key: 'Object ID',
69+
value: id,
70+
href: `https://explorer.iota.org/address/${id}?network=devnet`,
71+
},
72+
].filter((item): item is InfoLinks => Boolean(item));
73+
74+
function handleClose() {
75+
setOpen(false);
76+
}
77+
78+
const { subnamePart, namePart } = splitNameInParts(name);
79+
return (
80+
<Dialog open={open} onOpenChange={setOpen}>
81+
<DialogContent isFixedPosition position={DialogPosition.Right}>
82+
<Header title="General Info" onClose={handleClose} />
83+
<DialogBody>
84+
<div className="flex flex-col justify-center items-center gap-lg">
85+
<AvatarDisplay name={name} />
86+
<span className="text-headline-sm text-names-neutral-92 break-words max-w-full">
87+
{subnamePart}@{normalizeNameInput(namePart)}
88+
</span>
89+
</div>
90+
<div className="flex flex-col gap-md mt-lg">
91+
<Collapsible title="Info" titleSize={TitleSize.Small}>
92+
<div className="flex flex-col pb-xs px-md--rs">
93+
{infoLinks.map(({ key, value, href }) => (
94+
<KeyValueInfo
95+
key={key}
96+
isTruncated
97+
keyText={key}
98+
value={
99+
<Link
100+
href={href}
101+
className="text-names-primary-80 text-body-md"
102+
target="_blank"
103+
rel="noopener noreferrer"
104+
>
105+
{truncate(value)}
106+
</Link>
107+
}
108+
fullwidth
109+
/>
110+
))}
111+
</div>
112+
</Collapsible>
113+
<Collapsible title="Name Object Info" titleSize={TitleSize.Small}>
114+
<div className="pb-xs px-md--rs">
115+
<KeyValueInfo
116+
keyText="Expiration Time"
117+
value={formatDate(expirationTimestampMs)}
118+
fullwidth
119+
/>
120+
</div>
121+
</Collapsible>
122+
</div>
123+
</DialogBody>
124+
</DialogContent>
125+
</Dialog>
126+
);
127+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export function formatDate(timestampMs?: number): string {
5+
if (!timestampMs) return '—';
6+
7+
return new Date(timestampMs).toLocaleDateString('en-US', {
8+
year: 'numeric',
9+
month: 'long',
10+
day: 'numeric',
11+
});
12+
}

0 commit comments

Comments
 (0)