Skip to content

Commit 04c74bc

Browse files
committed
feat: improve batch UX with duplicate button, dropdown redesign, and CSV upload
Covers: - Duplicate button moved into expanded batch detail (removed BatchRowMenu) - Contact Book dropdown restyled with custom icons - CSV upload modal redesigned with multi-state file handling - CSV parser: robust header detection + merge duplicate address+token rows
1 parent 64e4bf0 commit 04c74bc

12 files changed

Lines changed: 321 additions & 169 deletions

File tree

packages/nextjs/app/contact-book/page.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
44
import Image from "next/image";
55
import { useRouter } from "next/navigation";
66
import { Contact, ContactGroup } from "@polypay/shared";
7-
import { ChevronDown, Search, Upload, Users } from "lucide-react";
7+
import { ChevronDown, Search } from "lucide-react";
88
import { ContactDetailDrawer } from "~~/components/contact-book/ContactDetailDrawer";
99
import { ContactList } from "~~/components/contact-book/ContactList";
1010
import { EditContact } from "~~/components/contact-book/Editcontact";
@@ -145,25 +145,25 @@ export default function AddressBookPage() {
145145
<ChevronDown size={16} />
146146
</button>
147147
{batchDropdownOpen && (
148-
<div className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-grey-200 py-1 z-50 min-w-[160px]">
148+
<div className="absolute right-0 top-full mt-1 bg-white rounded-lg border border-grey-200 shadow-[0px_0px_11px_0px_rgba(0,0,0,0.1)] z-50 min-w-[160px] overflow-hidden">
149149
<button
150-
className="flex items-center gap-2 px-4 py-2.5 text-sm text-grey-800 hover:bg-grey-50 w-full cursor-pointer"
150+
className="flex items-center gap-[5px] p-3 text-sm font-medium text-main-black hover:bg-grey-50 w-full cursor-pointer border-b border-b-grey-200"
151151
onClick={() => {
152152
setBatchDropdownOpen(false);
153153
modalManager.openModal?.("createBatchFromContacts", { accountId });
154154
}}
155155
>
156-
<Users size={16} />
156+
<Image src="/icons/batch/user-edit.svg" alt="Manual" width={16} height={16} />
157157
Manual
158158
</button>
159159
<button
160-
className="flex items-center gap-2 px-4 py-2.5 text-sm text-grey-800 hover:bg-grey-50 w-full cursor-pointer"
160+
className="flex items-center gap-[5px] p-3 text-sm font-medium text-main-black hover:bg-grey-50 w-full cursor-pointer"
161161
onClick={() => {
162162
setBatchDropdownOpen(false);
163163
modalManager.openModal?.("createBatchFromContacts", { accountId, mode: "csv" });
164164
}}
165165
>
166-
<Upload size={16} />
166+
<Image src="/icons/batch/export-upload.svg" alt="Upload CSV" width={16} height={16} />
167167
Upload CSV
168168
</button>
169169
</div>

packages/nextjs/components/Dashboard/TransactionRow/BatchRowMenu.tsx

Lines changed: 0 additions & 84 deletions
This file was deleted.

packages/nextjs/components/Dashboard/TransactionRow/TxHeader.tsx

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
"use client";
2+
13
import React from "react";
24
import Image from "next/image";
35
import { AddressWithContact } from "./AddressWithContact";
46
import { getExpandedHeaderText } from "./utils";
5-
import { TxType, getTokenByAddress } from "@polypay/shared";
6-
import { TransactionRowData, VoteStatus, useNetworkTokens } from "~~/hooks";
7+
import { TxType, ZERO_ADDRESS, formatTokenAmount, getTokenByAddress } from "@polypay/shared";
8+
import { Contact } from "@polypay/shared";
9+
import { BatchContactEntry } from "~~/components/modals/CreateBatchFromContactsModal";
10+
import { modalManager } from "~~/components/modals/ModalLayout";
11+
import { BatchTransfer, TransactionRowData, VoteStatus, useNetworkTokens } from "~~/hooks";
12+
import { useAccountStore } from "~~/services/store";
713
import { formatAddress, formatAmount } from "~~/utils/format";
814

915
interface TxHeaderProps {
@@ -14,6 +20,7 @@ interface TxHeaderProps {
1420
loading: boolean;
1521
initiatorName?: string;
1622
initiatorCommitment: string;
23+
batchData?: BatchTransfer[];
1724
}
1825

1926
function SignerBadgeList({ signerData }: { signerData: TransactionRowData["signerData"] }) {
@@ -56,46 +63,92 @@ export function TxHeader({
5663
loading,
5764
initiatorCommitment,
5865
initiatorName,
66+
batchData,
5967
}: TxHeaderProps) {
6068
const headerText = getExpandedHeaderText(tx.type);
6169
const shortCommitment = formatAddress(initiatorCommitment, { start: 4, end: 4 });
6270
const { chainId } = useNetworkTokens();
71+
const { currentAccount } = useAccountStore();
72+
73+
const handleDuplicate = () => {
74+
if (!batchData) return;
75+
76+
const initialBatchItems: BatchContactEntry[] = batchData.map(transfer => {
77+
const token = getTokenByAddress(transfer.tokenAddress, chainId);
78+
const amount = formatTokenAmount(transfer.amount, token.decimals);
79+
80+
const contact: Contact = {
81+
id: crypto.randomUUID(),
82+
name: transfer.contactName || formatAddress(transfer.recipient, { start: 6, end: 4 }),
83+
address: transfer.recipient,
84+
accountId: "",
85+
groups: [],
86+
createdAt: "",
87+
updatedAt: "",
88+
} as Contact;
89+
90+
return {
91+
contact,
92+
amount,
93+
tokenAddress: transfer.tokenAddress || ZERO_ADDRESS,
94+
isSynthetic: true,
95+
};
96+
});
97+
98+
modalManager.openModal?.("createBatchFromContacts", {
99+
accountId: currentAccount?.id,
100+
initialBatchItems,
101+
});
102+
};
63103

64104
const renderHeaderRow = () => (
65105
<div className="flex items-center justify-between mb-4">
66106
<div className="text-lg font-semibold">
67107
{tx.type === TxType.BATCH ? (
68-
<span>{tx.batchData?.length ?? 0} transactions</span>
108+
<span>{tx.batchData?.length ?? 0} Transactions</span>
69109
) : (
70110
<span>
71111
{headerText} {initiatorName ? `${initiatorName} (${shortCommitment})` : shortCommitment}
72112
</span>
73113
)}
74114
</div>
75-
{myVoteStatus === null && (
76-
<div className="flex items-center gap-2">
77-
<button
78-
onClick={e => {
79-
e.stopPropagation();
80-
onDeny();
81-
}}
82-
disabled={loading}
83-
className="px-6 py-2 text-sm font-medium text-main-black bg-white rounded-full hover:bg-gray-100 transition-colors cursor-pointer disabled:opacity-50"
84-
>
85-
Deny
86-
</button>
115+
<div className="flex items-center gap-2">
116+
{tx.type === TxType.BATCH && batchData && (
87117
<button
88118
onClick={e => {
89119
e.stopPropagation();
90-
onApprove();
120+
handleDuplicate();
91121
}}
92-
disabled={loading}
93-
className="px-6 py-2 text-sm font-medium text-main-black bg-pink-350 rounded-full hover:bg-pink-450 transition-colors cursor-pointer disabled:opacity-50"
122+
className="bg-grey-100 rounded-lg px-6 h-7 text-sm font-medium text-main-black cursor-pointer hover:bg-grey-200 transition-colors"
94123
>
95-
{loading ? "Processing..." : "Approve"}
124+
Duplicate
96125
</button>
97-
</div>
98-
)}
126+
)}
127+
{myVoteStatus === null && (
128+
<>
129+
<button
130+
onClick={e => {
131+
e.stopPropagation();
132+
onDeny();
133+
}}
134+
disabled={loading}
135+
className="px-6 py-2 text-sm font-medium text-main-black bg-white rounded-full hover:bg-gray-100 transition-colors cursor-pointer disabled:opacity-50"
136+
>
137+
Deny
138+
</button>
139+
<button
140+
onClick={e => {
141+
e.stopPropagation();
142+
onApprove();
143+
}}
144+
disabled={loading}
145+
className="px-6 py-2 text-sm font-medium text-main-black bg-pink-350 rounded-full hover:bg-pink-450 transition-colors cursor-pointer disabled:opacity-50"
146+
>
147+
{loading ? "Processing..." : "Approve"}
148+
</button>
149+
</>
150+
)}
151+
</div>
99152
</div>
100153
);
101154

packages/nextjs/components/Dashboard/TransactionRow/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import React, { useEffect, useState } from "react";
44
import { ActionButtons, AwaitingBadge, StatusBadge } from "./Badges";
5-
import { BatchRowMenu } from "./BatchRowMenu";
65
import { SignerList } from "./SignerList";
76
import { TxDetails } from "./TxDetails";
87
import { TxHeader } from "./TxHeader";
98
import { getTxTypeLabel } from "./utils";
10-
import { TxStatus, TxType } from "@polypay/shared";
9+
import { TxStatus } from "@polypay/shared";
1110
import { ChevronDown, ChevronRight } from "lucide-react";
1211
import { TransactionRowData, useTransactionVote, useWalletCommitments, useWalletThreshold } from "~~/hooks";
1312
import { formatAddress } from "~~/utils/format";
@@ -130,7 +129,6 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
130129
</div>
131130
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
132131
{renderRightSide()}
133-
{tx.type === TxType.BATCH && tx.batchData && <BatchRowMenu batchData={tx.batchData} />}
134132
</div>
135133
</div>
136134

@@ -144,6 +142,7 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
144142
loading={loading}
145143
initiatorCommitment={initiatorCommitment}
146144
initiatorName={initiatorName}
145+
batchData={tx.batchData}
147146
/>
148147
<SignerList
149148
members={tx.members}

0 commit comments

Comments
 (0)