+
{row.contributor.slice(0, 12)}…
+
{row.hasPending ? (
- Pending
+
+ Pending
+
) : null}
@@ -181,7 +232,8 @@ export function ContributorSummary({ pledges, assetCode, isLoading }: Contributo
{" "}
- ({row.activePledgeCount} pledge{row.activePledgeCount === 1 ? "" : "s"})
+ ({row.activePledgeCount} pledge
+ {row.activePledgeCount === 1 ? "" : "s"})
) : (
diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx
new file mode 100644
index 0000000..a6411fa
--- /dev/null
+++ b/frontend/src/components/CopyButton.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+
+interface CopyButtonProps {
+ value: string;
+ ariaLabel?: string;
+ className?: string;
+}
+
+export function CopyButton({
+ value,
+ ariaLabel = "Copy value",
+ className = "",
+}: CopyButtonProps) {
+ const [copied, setCopied] = useState(false);
+
+ async function handleCopy() {
+ try {
+ await navigator.clipboard.writeText(value);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ // navigator.clipboard may be unavailable in some embedded contexts; fallback
+ const ta = document.createElement("textarea");
+ ta.value = value;
+ ta.style.position = "fixed";
+ ta.style.opacity = "0";
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ document.execCommand("copy");
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 2000);
+ } catch (e) {
+ // ignore
+ } finally {
+ document.body.removeChild(ta);
+ }
+ }
+ }
+
+ return (
+
+ {copied ? "Copied" : "Copy"}
+
+ );
+}
+
+export default CopyButton;