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
183 changes: 110 additions & 73 deletions packages/nextjs/components/Batch/BatchContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import React, { useMemo, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { Fragment } from "react";
import Image from "next/image";
import EditBatchPopover from "../popovers/EditBatchPopover";
import TransactionSummary from "./TransactionSummary";
import { TransactionSummaryDrawer } from "./TransactionSummaryDrawer";
import { BatchItem, Token, parseTokenAmount } from "@polypay/shared";
import { getTokenByAddress } from "@polypay/shared";
import AddressNamedTooltip from "~~/components/tooltips/AddressNamedTooltip";
Expand Down Expand Up @@ -268,6 +269,7 @@ export default function BatchContainer() {
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [activeItem, setActiveItem] = useState<string | null>(null);
const [isExiting] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);

// Get accountId from current account
const accountId = currentAccount?.id || null;
Expand All @@ -279,80 +281,114 @@ export default function BatchContainer() {
} = useBatchTransaction({
onSuccess: async () => {
setSelectedItems(new Set());
setIsDrawerOpen(false);
await refetchBatchItems();
},
});

const handleSelectAll = () => {
const handleSelectAll = useCallback(() => {
if (selectedItems.size === batchItems.length) {
setSelectedItems(new Set());
setIsDrawerOpen(false);
} else {
setSelectedItems(new Set(batchItems.map((item: BatchItem) => item.id)));
setIsDrawerOpen(true);
}
};
}, [selectedItems.size, batchItems]);

const handleSelectItem = (id: string) => {
const newSelected = new Set(selectedItems);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelectedItems(newSelected);
};

// Remove item handler
const handleRemove = async (id: string) => {
try {
await deleteBatchItem(id);
// Remove from selected if it was selected
const newSelected = new Set(selectedItems);
newSelected.delete(id);
setSelectedItems(newSelected);
// Clear active if it was active
if (activeItem === id) {
setActiveItem(null);
const handleSelectItem = useCallback((id: string) => {
setSelectedItems(prev => {
const newSelected = new Set(prev);
if (newSelected.has(id)) {
newSelected.delete(id);
if (newSelected.size === 0) {
setIsDrawerOpen(false);
}
} else {
newSelected.add(id);
setIsDrawerOpen(true);
}
notification.success("Batch item removed successfully");
} catch (error) {
console.error("Failed to remove batch item:", error);
}
};

const handleEdit = async (
id: string,
data: { recipient: string; amount: string; token: Token; contactId?: string },
) => {
try {
const amountInSmallestUnit = parseTokenAmount(data.amount, data.token.decimals);

await updateBatchItem({
id,
data: {
recipient: data.recipient,
amount: amountInSmallestUnit,
tokenAddress: data.token.address,
contactId: data.contactId,
},
});

notification.success("Batch item updated successfully");
return newSelected;
});
}, []);

await refetchBatchItems();
} catch (error) {
console.error("Failed to update batch item:", error);
notification.error("Failed to update batch item");
}
};
const handleRemove = useCallback(
async (id: string) => {
try {
await deleteBatchItem(id);

setSelectedItems(prev => {
const newSelected = new Set(prev);
newSelected.delete(id);
if (newSelected.size === 0) {
setIsDrawerOpen(false);
}
return newSelected;
});

if (activeItem === id) {
setActiveItem(null);
}

notification.success("Batch item removed successfully");
} catch (error) {
console.error("Failed to remove batch item:", error);
}
},
[deleteBatchItem, activeItem],
);

// Propose batch transaction handler
const handleProposeBatch = async () => {
const handleEdit = useCallback(
async (id: string, data: { recipient: string; amount: string; token: Token; contactId?: string }) => {
try {
const amountInSmallestUnit = parseTokenAmount(data.amount, data.token.decimals);

await updateBatchItem({
id,
data: {
recipient: data.recipient,
amount: amountInSmallestUnit,
tokenAddress: data.token.address,
contactId: data.contactId,
},
});

notification.success("Batch item updated successfully");
await refetchBatchItems();
} catch (error) {
console.error("Failed to update batch item:", error);
notification.error("Failed to update batch item");
}
},
[updateBatchItem, refetchBatchItems],
);

const handleProposeBatch = useCallback(async () => {
const selectedBatchItems = batchItems.filter(item => selectedItems.has(item.id));
await proposeBatch(selectedBatchItems);
};
}, [batchItems, selectedItems, proposeBatch]);

// Get selected batch items for summary
const selectedBatchItems = batchItems.filter((item: BatchItem) => selectedItems.has(item.id));
const selectedBatchItems = useMemo(
() => batchItems.filter((item: BatchItem) => selectedItems.has(item.id)),
[batchItems, selectedItems],
);

const transactionsSummary = useMemo(
() =>
selectedBatchItems.map(item => ({
id: item.id,
amount: formatAmount(item.amount, item.tokenAddress),
recipient: item.recipient,
contactName: item.contact?.name,
tokenIcon: getTokenByAddress(item.tokenAddress).icon,
tokenSymbol: getTokenByAddress(item.tokenAddress).symbol,
})),
[selectedBatchItems],
);

const handleCloseDrawer = useCallback(() => {
setIsDrawerOpen(false);
}, []);

return (
<div className="flex flex-row gap-1 w-full h-full bg-[#ECEDEC]">
Expand All @@ -363,10 +399,7 @@ export default function BatchContainer() {
background: "rgba(255, 255, 255, 0.70)",
}}
>
{/* Header */}
<Header />

{/* Batch Items List */}
<BatchTransactions
batchItems={batchItems}
selectedItems={selectedItems}
Expand All @@ -381,26 +414,30 @@ export default function BatchContainer() {
/>
</div>

{/* Transaction Summary Sidebar */}
{/* Transaction Summary Sidebar - Desktop Only */}
{selectedItems.size > 0 && (
<div className={`overflow-hidden ${isExiting ? "animate-slide-out" : "animate-slide-in"}`}>
<div className={`hidden lg:block overflow-hidden ${isExiting ? "animate-slide-out" : "animate-slide-in"}`}>
<TransactionSummary
className="xl:w-[500px] w-[250px]"
transactions={selectedBatchItems.map(item => ({
id: item.id,
amount: formatAmount(item.amount, item.tokenAddress),
recipient: item.recipient,
contactName: item.contact?.name,
tokenIcon: getTokenByAddress(item.tokenAddress).icon,
tokenSymbol: getTokenByAddress(item.tokenAddress).symbol,
}))}
className="xl:w-[420px] w-[250px]"
transactions={transactionsSummary}
onConfirm={handleProposeBatch}
isLoading={isProposing}
loadingState={loadingState}
accountId={accountId}
/>
</div>
)}

{/* Transaction Summary Drawer - Mobile Only */}
<TransactionSummaryDrawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
transactions={transactionsSummary}
onConfirm={handleProposeBatch}
isLoading={isProposing}
loadingState={loadingState}
accountId={accountId}
/>
</div>
);
}
77 changes: 77 additions & 0 deletions packages/nextjs/components/Batch/TransactionSummaryDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import { memo, useEffect, useState } from "react";
import TransactionSummary from "./TransactionSummary";
import { X } from "lucide-react";

interface TransactionSummaryDrawerProps {
isOpen: boolean;
onClose: () => void;
transactions: {
id: string;
amount: string;
recipient: string;
contactName?: string;
tokenIcon?: string;
tokenSymbol?: string;
}[];
accountId: string | null;
onConfirm?: () => void;
isLoading?: boolean;
loadingState?: string;
}

export const TransactionSummaryDrawer = memo(function TransactionSummaryDrawer({
isOpen,
onClose,
transactions,
accountId,
onConfirm,
isLoading = false,
loadingState = "",
}: TransactionSummaryDrawerProps) {
const [isAnimating, setIsAnimating] = useState(false);

useEffect(() => {
if (isOpen) {
setTimeout(() => setIsAnimating(true), 10);
} else {
setIsAnimating(false);
}
}, [isOpen]);

if (!isOpen) return null;

return (
<>
<div
className={`lg:hidden fixed inset-0 bg-[#2b2929ad] bg-opacity-50 z-40 transition-opacity duration-300 ${
isAnimating ? "opacity-100" : "opacity-0"
}`}
onClick={onClose}
></div>
<div
className={`lg:hidden fixed right-0 top-0 h-full rounded-2xl w-[400px] max-w-[90vw] bg-white z-50 shadow-2xl transform transition-all duration-300 ease-in-out ${
isAnimating ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="h-full flex flex-col relative">
<button
className="absolute top-4 right-4 z-50 w-8 h-8 flex items-center justify-center rounded-full bg-white shadow-md hover:bg-gray-100"
onClick={onClose}
>
<X width={16} height={16} />
</button>
<TransactionSummary
transactions={transactions}
onConfirm={onConfirm}
accountId={accountId}
isLoading={isLoading}
loadingState={loadingState}
className="h-full"
/>
</div>
</div>
</>
);
});