Skip to content
Open
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
180 changes: 180 additions & 0 deletions components/txn-pending-indication-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, X } from "lucide-react";
import { TxnPendingIndicator } from "./ui/murphy/Txn-Feedback/txn-pending-indicator";

interface PendingTransaction {
id: string;
signature?: string;
description: string;
startTime: number;
}

export default function TxnPendingIndicatorPreview() {
const [pendingTransactions, setPendingTransactions] = useState<
PendingTransaction[]
>([]);
const [position, setPosition] = useState<
"top-left" | "top-right" | "bottom-left" | "bottom-right"
>("bottom-right");

const addTransaction = (description: string) => {
const newTransaction: PendingTransaction = {
id: Date.now().toString(),
description,
startTime: Date.now(),
};
setPendingTransactions((prev) => [...prev, newTransaction]);

setTimeout(() => {
setPendingTransactions((prev) =>
prev.filter((txn) => txn.id !== newTransaction.id)
);
}, 15000);
};

const removeTransaction = (id: string) => {
setPendingTransactions((prev) => prev.filter((txn) => txn.id !== id));
};

const clearAllTransactions = () => {
setPendingTransactions([]);
};

const addBatchTransactions = () => {
const batchTransactions = [
"Transfer to Alice",
"Transfer to Bob",
"Transfer to Charlie",
].map((desc, index) => ({
id: `batch_${Date.now()}_${index}`,
description: desc,
startTime: Date.now(),
}));

setPendingTransactions((prev) => [...prev, ...batchTransactions]);

setTimeout(() => {
batchTransactions.forEach((txn) => {
setPendingTransactions((prev) => prev.filter((t) => t.id !== txn.id));
});
}, 20000);
};

return (
<div className="container mx-auto p-6 max-w-4xl">
<div className="grid gap-6">
<Card className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800">
<CardHeader>
<CardTitle className="text-zinc-900 dark:text-zinc-100">
Example Usage
</CardTitle>
<CardDescription className="text-zinc-600 dark:text-zinc-400">
Add transactions to see the pending indicator appear. It will show
in the <span className="font-medium">{position}</span> corner.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<Button
onClick={() =>
addTransaction(
"Mint NFT #" + Math.floor(Math.random() * 1000)
)
}
className="bg-purple-600 hover:bg-purple-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Add NFT Mint
</Button>
<Button
onClick={() =>
addTransaction(
"Transfer " + Math.floor(Math.random() * 100) + " USDC"
)
}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Add Token Transfer
</Button>
<Button
onClick={() => addTransaction("Swap SOL → USDC")}
className="bg-green-600 hover:bg-green-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Add Token Swap
</Button>
</div>

<div className="flex gap-2">
<Button onClick={addBatchTransactions} variant="outline">
Add Batch (3 transactions)
</Button>
<Button onClick={clearAllTransactions} variant="outline">
<X className="w-4 h-4 mr-2" />
Clear All
</Button>
</div>

<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-200">
Current pending:
</span>
<Badge variant="secondary">{pendingTransactions.length}</Badge>
</div>
</CardContent>
</Card>

<Card className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800">
<CardHeader>
<CardTitle className="text-zinc-900 dark:text-zinc-100">
Position Options
</CardTitle>
<CardDescription className="text-zinc-600 dark:text-zinc-400">
Change the position of the pending indicator on screen
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{(
[
"top-left",
"top-right",
"bottom-left",
"bottom-right",
] as const
).map((pos) => (
<Button
key={pos}
onClick={() => setPosition(pos)}
variant={position === pos ? "default" : "outline"}
size="sm"
className="text-xs"
>
{pos}
</Button>
))}
</div>
</CardContent>
</Card>
</div>

<TxnPendingIndicator
transactions={pendingTransactions}
onCancel={removeTransaction}
position={position}
/>
</div>
);
}
120 changes: 120 additions & 0 deletions components/ui/murphy/Txn-Feedback/txn-pending-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import { useState, useEffect } from "react";
import { Loader2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

interface PendingTransaction {
id: string;
signature?: string;
description: string;
startTime: number;
}

interface TxnPendingIndicatorProps {
transactions: PendingTransaction[];
onCancel?: (id: string) => void;
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
className?: string;
}

export function TxnPendingIndicator({
transactions,
onCancel,
position = "bottom-right",
className,
}: TxnPendingIndicatorProps) {
const [isExpanded, setIsExpanded] = useState(false);

useEffect(() => {
if (transactions.length === 0) {
setIsExpanded(false);
}
}, [transactions.length]);

if (transactions.length === 0) return null;

const positionClasses = {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-right": "bottom-4 right-4",
};

const getElapsedTime = (startTime: number) => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
if (elapsed < 60) return `${elapsed}s`;
return `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
};

return (
<div
className={cn(
"fixed z-50 max-w-sm",
positionClasses[position],
className
)}
>
{!isExpanded ? (
<Button
onClick={() => setIsExpanded(true)}
className="rounded-full shadow-lg"
size="sm"
>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{transactions.length} pending
<Badge variant="secondary" className="ml-2">
{transactions.length}
</Badge>
</Button>
) : (
<div className="bg-white rounded-lg shadow-lg border p-4 min-w-[300px]">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium text-gray-900">Pending Transactions</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setIsExpanded(false)}
className="h-6 w-6 p-0"
>
<X className="w-4 h-4" />
</Button>
</div>

<div className="space-y-2 max-h-60 overflow-y-auto">
{transactions.map((tx) => (
<div
key={tx.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
<Loader2 className="w-4 h-4 animate-spin text-blue-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">
{tx.description}
</p>
<p className="text-xs text-gray-500">
{getElapsedTime(tx.startTime)}
</p>
</div>
</div>
{onCancel && (
<Button
size="sm"
variant="ghost"
onClick={() => onCancel(tx.id)}
className="h-6 w-6 p-0 ml-2 flex-shrink-0"
>
<X className="w-3 h-3" />
</Button>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
6 changes: 4 additions & 2 deletions components/ui/murphy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import CandyMachineForm from "./candy-machine-form";
import CoreCandyMachineForm from "./core-candy-machine-form";
import BubblegumLegacyForm from "./bubblegum-legacy-form";
import ImprovedCNFTManager from "./improved-cnft-manager";
import CompressedNFTViewer from "./compressed-nft-viewer"
import CompressedNFTViewer from "./compressed-nft-viewer";
import { CreateMerkleTree } from "./create-merkleTree-form";
import { TokenList } from "./token-list";
import { StakeForm } from "./stake-token-form";
Expand All @@ -37,6 +37,7 @@ import { CoreAssetLaunchpad } from "./core-asset-launchpad";
import { HydraFanoutForm } from "./hydra-fanout-form";
import { MPLHybridForm } from "./mpl-hybrid-form";
import { TokenMetadataViewer } from "./token-metadata-viewer";
import { TxnPendingIndicator } from "./Txn-Feedback/txn-pending-indicator";

export {
ConnectWalletButton,
Expand Down Expand Up @@ -78,5 +79,6 @@ export {
TMLaunchpadForm,
HydraFanoutForm,
MPLHybridForm,
TokenMetadataViewer
TokenMetadataViewer,
TxnPendingIndicator,
};
Loading