Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions packages/nextjs/app/contact-book/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { useEffect, useMemo, useRef, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { Contact, ContactGroup } from "@polypay/shared";
import { Search } from "lucide-react";
import { ChevronDown, Search } from "lucide-react";
import { ContactDetailDrawer } from "~~/components/contact-book/ContactDetailDrawer";
import { ContactList } from "~~/components/contact-book/ContactList";
import { EditContact } from "~~/components/contact-book/Editcontact";
import { modalManager } from "~~/components/modals/ModalLayout";
import { useContacts, useGroups } from "~~/hooks";
import { useClickOutside } from "~~/hooks/useClickOutside";
import { useAccountStore } from "~~/services/store";
import { formatAddress } from "~~/utils/format";

Expand All @@ -23,7 +24,10 @@ export default function AddressBookPage() {
const [searchTerm, setSearchTerm] = useState("");
const [editing, setEditing] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [batchDropdownOpen, setBatchDropdownOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const batchDropdownRef = useRef<HTMLDivElement>(null);
useClickOutside(batchDropdownRef, () => setBatchDropdownOpen(false), { isActive: batchDropdownOpen });

const { data: groups = [], refetch: refetchGroups } = useGroups(accountId);
const {
Expand Down Expand Up @@ -131,17 +135,40 @@ export default function AddressBookPage() {
<Image src="/contact-book/new-group.svg" alt="New group" width={20} height={20} />
New group
</button>
<button
className="bg-main-black text-white rounded-lg text-sm font-medium transition-colors cursor-pointer h-12 px-3 flex items-center gap-1"
onClick={() =>
modalManager.openModal?.("createBatchFromContacts", {
accountId,
})
}
>
<Image src="/contact-book/create-batch.svg" alt="Create batch" width={20} height={20} />
Create batch
</button>
<div ref={batchDropdownRef} className="relative">
<button
className="bg-main-black text-white rounded-lg text-sm font-medium transition-colors cursor-pointer h-12 px-3 flex items-center gap-1"
onClick={() => setBatchDropdownOpen(prev => !prev)}
>
<Image src="/contact-book/create-batch.svg" alt="Create batch" width={20} height={20} />
Create batch
<ChevronDown size={16} />
</button>
{batchDropdownOpen && (
<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">
<button
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"
onClick={() => {
setBatchDropdownOpen(false);
modalManager.openModal?.("createBatchFromContacts", { accountId });
}}
>
<Image src="/icons/batch/user-edit.svg" alt="Manual" width={16} height={16} />
Manual
</button>
<button
className="flex items-center gap-[5px] p-3 text-sm font-medium text-main-black hover:bg-grey-50 w-full cursor-pointer"
onClick={() => {
setBatchDropdownOpen(false);
modalManager.openModal?.("createBatchFromContacts", { accountId, mode: "csv" });
}}
>
<Image src="/icons/batch/export-upload.svg" alt="Upload CSV" width={16} height={16} />
Upload CSV
</button>
</div>
)}
</div>
<button
className="bg-main-violet text-white rounded-lg text-sm font-medium transition-colors cursor-pointer h-12 px-3 flex items-center gap-1"
onClick={() =>
Expand Down
95 changes: 74 additions & 21 deletions packages/nextjs/components/Dashboard/TransactionRow/TxHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"use client";

import React from "react";
import Image from "next/image";
import { AddressWithContact } from "./AddressWithContact";
import { getExpandedHeaderText } from "./utils";
import { TxType, getTokenByAddress } from "@polypay/shared";
import { TransactionRowData, VoteStatus, useNetworkTokens } from "~~/hooks";
import { TxType, ZERO_ADDRESS, formatTokenAmount, getTokenByAddress } from "@polypay/shared";
import { Contact } from "@polypay/shared";
import { BatchContactEntry } from "~~/components/modals/CreateBatchFromContactsModal";
import { modalManager } from "~~/components/modals/ModalLayout";
import { BatchTransfer, TransactionRowData, VoteStatus, useNetworkTokens } from "~~/hooks";
import { useAccountStore } from "~~/services/store";
import { formatAddress, formatAmount } from "~~/utils/format";

interface TxHeaderProps {
Expand All @@ -14,6 +20,7 @@ interface TxHeaderProps {
loading: boolean;
initiatorName?: string;
initiatorCommitment: string;
batchData?: BatchTransfer[];
}

function SignerBadgeList({ signerData }: { signerData: TransactionRowData["signerData"] }) {
Expand Down Expand Up @@ -56,46 +63,92 @@ export function TxHeader({
loading,
initiatorCommitment,
initiatorName,
batchData,
}: TxHeaderProps) {
const headerText = getExpandedHeaderText(tx.type);
const shortCommitment = formatAddress(initiatorCommitment, { start: 4, end: 4 });
const { chainId } = useNetworkTokens();
const { currentAccount } = useAccountStore();

const handleDuplicate = () => {
if (!batchData) return;

const initialBatchItems: BatchContactEntry[] = batchData.map(transfer => {
const token = getTokenByAddress(transfer.tokenAddress, chainId);
const amount = formatTokenAmount(transfer.amount, token.decimals);

const contact: Contact = {
id: crypto.randomUUID(),
name: transfer.contactName || formatAddress(transfer.recipient, { start: 6, end: 4 }),
address: transfer.recipient,
accountId: "",
groups: [],
createdAt: "",
updatedAt: "",
} as Contact;

return {
contact,
amount,
tokenAddress: transfer.tokenAddress || ZERO_ADDRESS,
isSynthetic: true,
};
});

modalManager.openModal?.("createBatchFromContacts", {
accountId: currentAccount?.id,
initialBatchItems,
});
};

const renderHeaderRow = () => (
<div className="flex items-center justify-between mb-4">
<div className="text-lg font-semibold">
{tx.type === TxType.BATCH ? (
<span>{tx.batchData?.length ?? 0} transactions</span>
<span>{tx.batchData?.length ?? 0} Transactions</span>
) : (
<span>
{headerText} {initiatorName ? `${initiatorName} (${shortCommitment})` : shortCommitment}
</span>
)}
</div>
{myVoteStatus === null && (
<div className="flex items-center gap-2">
<button
onClick={e => {
e.stopPropagation();
onDeny();
}}
disabled={loading}
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"
>
Deny
</button>
<div className="flex items-center gap-2">
{tx.type === TxType.BATCH && batchData && (
<button
onClick={e => {
e.stopPropagation();
onApprove();
handleDuplicate();
}}
disabled={loading}
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"
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"
>
{loading ? "Processing..." : "Approve"}
Duplicate
</button>
</div>
)}
)}
{myVoteStatus === null && (
<>
<button
onClick={e => {
e.stopPropagation();
onDeny();
}}
disabled={loading}
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"
>
Deny
</button>
<button
onClick={e => {
e.stopPropagation();
onApprove();
}}
disabled={loading}
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"
>
{loading ? "Processing..." : "Approve"}
</button>
</>
)}
</div>
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
<span className="text-sm font-medium text-grey-500 tracking-tight">{getTxTypeLabel(tx.type)}</span>
{!expanded && <TxDetails tx={tx} />}
</div>
<div onClick={e => e.stopPropagation()}>{renderRightSide()}</div>
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
{renderRightSide()}
</div>
</div>

{expanded && (
Expand All @@ -140,6 +142,7 @@ export function TransactionRow({ tx, onSuccess }: TransactionRowProps) {
loading={loading}
initiatorCommitment={initiatorCommitment}
initiatorName={initiatorName}
batchData={tx.batchData}
/>
<SignerList
members={tx.members}
Expand Down
Loading
Loading