diff --git a/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx b/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx
new file mode 100644
index 0000000..ded4ab2
--- /dev/null
+++ b/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useState } from "react";
+import BallotCard from "./ballot";
+import type { UTxO } from "@meshsdk/core";
+import { Vote, Minimize2 } from "lucide-react";
+
+interface FloatingBallotSidebarProps {
+ appWallet: any;
+ selectedBallotId?: string;
+ onSelectBallot: (id: string) => void;
+ ballotCount: number;
+ totalProposalCount: number;
+ proposalCount: number;
+ manualUtxos: UTxO[];
+ /**
+ * Optional controlled open state for the sidebar.
+ * If provided together with onOpenChange, the sidebar becomes controlled.
+ */
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ /**
+ * Optional current proposal context (used on the proposal page).
+ * When provided, the ballot card can show contextual UI like
+ * an \"Add to ballot\" button and highlighting.
+ */
+ currentProposalId?: string;
+ currentProposalTitle?: string;
+}
+
+export default function FloatingBallotSidebar({
+ appWallet,
+ selectedBallotId,
+ onSelectBallot,
+ ballotCount,
+ totalProposalCount,
+ proposalCount,
+ manualUtxos,
+ open: controlledOpen,
+ onOpenChange,
+ currentProposalId,
+ currentProposalTitle,
+}: FloatingBallotSidebarProps) {
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
+ const [isMobile, setIsMobile] = useState(false);
+
+ const isControlled = controlledOpen !== undefined && !!onOpenChange;
+ const open = isControlled ? controlledOpen : uncontrolledOpen;
+
+ const setOpen = (value: boolean | ((prev: boolean) => boolean)) => {
+ if (isControlled && onOpenChange) {
+ const next = typeof value === "function" ? value(open) : value;
+ onOpenChange(next);
+ } else {
+ setUncontrolledOpen(value as boolean);
+ }
+ };
+
+ useEffect(() => {
+ function handleResize() {
+ setIsMobile(
+ typeof window !== "undefined" ? window.innerWidth < 768 : false,
+ );
+ }
+ handleResize();
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ if (isMobile) {
+ return (
+ <>
+ setOpen(true)}
+ aria-label="Open Ballots"
+ >
+
+
+ {(ballotCount > 0 ||
+ totalProposalCount > 0 ||
+ proposalCount > 0) && (
+
+ {open
+ ? proposalCount > 0
+ ? proposalCount
+ : ""
+ : totalProposalCount > 0
+ ? totalProposalCount
+ : ""}
+
+ )}
+
+
+
+ {open && (
+
+
+ Your Ballots
+ {proposalCount > 0 && (
+
+ {proposalCount}
+
+ )}
+ setOpen(false)}
+ aria-label="Close"
+ >
+ ✕
+
+
+
+
+
+
+ )}
+ >
+ );
+ }
+
+ return (
+
+
+
setOpen((o) => !o)}
+ aria-label={open ? "Collapse Ballots" : "Expand Ballots"}
+ title={
+ open
+ ? "Click to minimise ballot panel"
+ : "Click to open ballot panel"
+ }
+ className={
+ open
+ ? "absolute -left-12 top-8 p-1.5 rounded-full bg-white/40 shadow border group text-gray-800 dark:text-white hover:bg-black hover:text-white"
+ : "absolute top-0 right-0 p-1 group text-gray-800 dark:text-white"
+ }
+ >
+ {open ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+ {(ballotCount > 0 ||
+ totalProposalCount > 0 ||
+ proposalCount > 0) && (
+
+ {open
+ ? proposalCount > 0
+ ? proposalCount
+ : ""
+ : totalProposalCount > 0
+ ? totalProposalCount
+ : ""}
+
+ )}
+
+ {open && (
+
+
+
+ )}
+
+
+ );
+}
+
+
diff --git a/src/components/pages/wallet/governance/ballot/ballot.tsx b/src/components/pages/wallet/governance/ballot/ballot.tsx
index ffcb793..79fecdf 100644
--- a/src/components/pages/wallet/governance/ballot/ballot.tsx
+++ b/src/components/pages/wallet/governance/ballot/ballot.tsx
@@ -21,6 +21,13 @@ import {
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { ToastAction } from "@/components/ui/toast";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
import { useProxy } from "@/hooks/useProxy";
import { MeshProxyContract } from "@/components/multisig/proxy/offchain";
@@ -44,18 +51,34 @@ export default function BallotCard({
selectedBallotId,
onBallotChanged,
utxos,
+ currentProposalId,
+ currentProposalTitle,
}: {
appWallet: any;
onSelectBallot?: (id: string) => void;
selectedBallotId?: string;
onBallotChanged?: () => void;
utxos: UTxO[];
+ /**
+ * Optional current proposal context from the proposal page.
+ * When provided, the ballot card can render an "Add to ballot"
+ * button and highlight that proposal in the table.
+ */
+ currentProposalId?: string;
+ currentProposalTitle?: string;
}) {
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [ballots, setBallots] = useState([]);
const [creating, setCreating] = useState(false);
+ // State for adding the current proposal to a ballot (including move flow)
+ const [moveModal, setMoveModal] = useState<{
+ targetBallotId: string;
+ conflictBallots: BallotType[];
+ } | null>(null);
+ const [moveLoading, setMoveLoading] = useState(false);
+
const { toast } = useToast();
// Ballot voting state
@@ -94,17 +117,53 @@ export default function BallotCard({
// Delete ballot mutation
const deleteBallot = api.ballot.delete.useMutation();
- // Refresh ballots after submit or on load
+ // Add / remove proposal mutations for managing ballot contents
+ const addProposalMutation = api.ballot.addProposalToBallot.useMutation();
+ const moveRemoveProposalMutation =
+ api.ballot.removeProposalFromBallot.useMutation();
+
+ // Refresh ballots after submit or on load and ensure a sensible default is selected
React.useEffect(() => {
- if (getBallots.data) {
- // Sort newest first
- const sorted = [...getBallots.data].sort(
- (a, b) =>
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
- );
- setBallots(sorted);
+ if (!getBallots.data) return;
+
+ // Sort newest first
+ const sorted = [...getBallots.data].sort(
+ (a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
+ );
+ setBallots(sorted);
+
+ // If nothing is selected yet (or the selection no longer exists),
+ // automatically select a sensible default:
+ // 1. Prefer the ballot that already contains the current proposal (when on a proposal page)
+ // 2. Otherwise, prefer the newest ballot that already has proposals
+ // 3. Fallback to the newest ballot
+ if (!onSelectBallot) return;
+
+ const hasSelected =
+ selectedBallotId && sorted.some((b) => b.id === selectedBallotId);
+
+ if (!hasSelected) {
+ let ballotToSelect: BallotType | undefined;
+
+ if (currentProposalId) {
+ ballotToSelect = sorted.find(
+ (b) => Array.isArray(b.items) && b.items.includes(currentProposalId),
+ );
+ }
+
+ if (!ballotToSelect) {
+ ballotToSelect =
+ sorted.find(
+ (b) => Array.isArray(b.items) && b.items.length > 0,
+ ) ?? sorted[0];
+ }
+
+ if (!ballotToSelect) return;
+
+ onSelectBallot(ballotToSelect.id);
}
- }, [getBallots.data]);
+ }, [getBallots.data, onSelectBallot, selectedBallotId, currentProposalId]);
// Proxy ballot vote submission logic
@@ -407,11 +466,95 @@ export default function BallotCard({
? ballots.find((b) => b.id === selectedBallotId)
: undefined;
+ const currentProposalAlreadyInSelected =
+ !!currentProposalId &&
+ !!selectedBallot &&
+ Array.isArray(selectedBallot.items) &&
+ selectedBallot.items.includes(currentProposalId);
+
+ async function performAddCurrentProposal(targetBallotId: string) {
+ if (!currentProposalId) return;
+ await addProposalMutation.mutateAsync({
+ ballotId: targetBallotId,
+ itemDescription:
+ currentProposalTitle ??
+ (selectedBallot?.description ?? "Untitled proposal"),
+ item: currentProposalId,
+ // Default to Abstain; user can fine-tune per-proposal choice in the table.
+ choice: "Abstain",
+ });
+ }
+
+ async function handleAddCurrentProposalToSelectedBallot() {
+ if (!currentProposalId || !selectedBallotId || !getBallots.data) return;
+
+ // Ensure a proposal can only exist on a single ballot at a time.
+ const ballotsWithProposal =
+ getBallots.data.filter(
+ (b) =>
+ b.id !== selectedBallotId &&
+ Array.isArray(b.items) &&
+ b.items.includes(currentProposalId),
+ ) ?? [];
+
+ if (ballotsWithProposal.length > 0) {
+ setMoveModal({
+ targetBallotId: selectedBallotId,
+ conflictBallots: ballotsWithProposal,
+ });
+ return;
+ }
+
+ await performAddCurrentProposal(selectedBallotId);
+ toast({
+ title: "Added to Ballot",
+ description: "Proposal successfully added to the ballot.",
+ duration: 800,
+ });
+ await getBallots.refetch();
+ onBallotChanged?.();
+ }
+
+ async function confirmMoveCurrentProposal() {
+ if (!moveModal || !currentProposalId) return;
+
+ try {
+ setMoveLoading(true);
+
+ // Remove proposal from all other ballots before adding to the selected one
+ for (const b of moveModal.conflictBallots) {
+ const index = b.items.findIndex(
+ (item: string) => item === currentProposalId,
+ );
+ if (index >= 0) {
+ await moveRemoveProposalMutation.mutateAsync({
+ ballotId: b.id,
+ index,
+ });
+ }
+ }
+
+ await performAddCurrentProposal(moveModal.targetBallotId);
+
+ toast({
+ title: "Proposal moved",
+ description:
+ "Proposal was moved from the other ballot to the selected ballot.",
+ duration: 1500,
+ });
+ await getBallots.refetch();
+ onBallotChanged?.();
+ } finally {
+ setMoveLoading(false);
+ setMoveModal(null);
+ }
+ }
+
return (
@@ -420,19 +563,36 @@ export default function BallotCard({
key={b.id}
className={`px-3 py-1 rounded-t-md font-medium transition ${
b.id === selectedBallotId
- ? "bg-white text-blue-600 border border-b-0 border-blue-400 dark:bg-gray-900 dark:text-blue-400"
- : "bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
+ ? "bg-white text-gray-900 border border-b-0 border-gray-300 shadow-sm dark:bg-slate-900 dark:text-gray-100 dark:border-slate-700 hover:border-gray-400 dark:hover:border-slate-500"
+ : "bg-white/70 text-gray-700 hover:bg-white dark:bg-slate-800 dark:text-gray-200 dark:hover:bg-slate-700 border border-transparent hover:border-gray-400 dark:hover:border-slate-500"
}`}
onClick={() => onSelectBallot && onSelectBallot(b.id)}
>
- {b.description || "Untitled"}
+
+ {b.description || "Untitled"}
+
- ))}
- setCreating(true)}
+ ))}
+ {
+ if (creating) {
+ // Hide the create-input row and clear any pending text
+ setCreating(false);
+ setDescription("");
+ } else {
+ // Show the create-input row
+ setCreating(true);
+ }
+ }}
>
- +
+ {creating ? "−" : "+"}
{creating && (
@@ -448,7 +608,7 @@ export default function BallotCard({
Add
@@ -462,8 +622,30 @@ export default function BallotCard({
) : null}
{/* Ballot overview table for selected ballot */}
-
-
+
+ {/* Contextual 'Add to ballot' button only on proposal pages
+ (i.e. when currentProposalId is provided).
+ - If not on this ballot yet: show Add button
+ - If already on this ballot: show an informational message */}
+ {selectedBallot && currentProposalId && (
+
+ {!currentProposalAlreadyInSelected ? (
+
+ Add current proposal to this ballot
+
+ ) : (
+
+ Proposal is on this ballot
+
+ )}
+
+ )}
+
{selectedBallot ? (
Array.isArray(selectedBallot.items) &&
selectedBallot.items.length > 0 ? (
@@ -472,6 +654,7 @@ export default function BallotCard({
ballotId={selectedBallot.id}
refetchBallots={getBallots.refetch}
onBallotChanged={onBallotChanged}
+ currentProposalId={currentProposalId}
/>
) : (
No proposals added yet.
@@ -493,16 +676,16 @@ export default function BallotCard({
)}
{loading ? "Loading..." : `Submit Ballot Vote${hasValidProxy ? " (Proxy Mode)" : ""}`}
{
+ variant="outline"
+ className="mt-4 mb-4 ml-3 bg-white/80 text-gray-700 hover:bg-white hover:text-red-600 dark:bg-white/5 dark:text-gray-200 dark:hover:bg-white/10 dark:hover:text-red-400 border border-gray-200 hover:border-red-400 dark:border-white/10 dark:hover:border-red-500"
+ onClick={async () => {
try {
await deleteBallot.mutateAsync({ ballotId: selectedBallot.id });
await getBallots.refetch();
@@ -524,6 +707,48 @@ export default function BallotCard({
)}
+ {/* Modal to confirm moving proposal between ballots when adding from a proposal page */}
+ {
+ if (!open) setMoveModal(null);
+ }}
+ >
+
+
+ Move proposal to selected ballot?
+
+ {moveModal && (
+
+ This proposal is already on ballot{" "}
+ {moveModal.conflictBallots
+ .map((b) => b.description || "Untitled ballot")
+ .join(", ")}
+ . If you continue, it will be removed from that ballot and
+ added to the currently selected ballot.
+
+ )}
+
+
+
+ setMoveModal(null)}
+ disabled={moveLoading}
+ >
+ Cancel
+
+
+ {moveLoading ? "Moving..." : "Move proposal"}
+
+
+
+
);
}
@@ -534,11 +759,13 @@ function BallotOverviewTable({
ballotId,
refetchBallots,
onBallotChanged,
+ currentProposalId,
}: {
ballot: BallotType;
ballotId: string;
refetchBallots: () => Promise;
onBallotChanged?: () => void;
+ currentProposalId?: string;
}) {
// Add state for updating and the update mutation
const updateChoiceMutation = api.ballot.updateChoice.useMutation();
@@ -560,75 +787,84 @@ function BallotOverviewTable({
return (
-
+
-
+
#
Title
- Choice / Delete
+ Choice / Delete
- {ballot.items.map((item: string, idx: number) => (
-
- {idx + 1}
-
- {ballot.itemDescriptions?.[idx] || (
- -
- )}
-
-
-
- {
- setUpdatingIdx(idx);
- try {
- await updateChoiceMutation.mutateAsync({
- ballotId,
- index: idx,
- choice: newValue,
- });
- await refetchBallots();
- onBallotChanged?.();
- } catch (error: unknown) {}
- setUpdatingIdx(null);
- }}
- disabled={updatingIdx === idx}
- >
-
-
-
-
-
- Yes
- No
- Abstain
-
-
-
-
-
-
- handleDelete(idx)}
- >
- {removingIdx === idx ? "..." : "Delete"}
-
-
-
-
- ))}
+ {ballot.items.map((item: string, idx: number) => {
+ const isCurrent = !!currentProposalId && item === currentProposalId;
+ return (
+
+ {idx + 1}
+
+ {ballot.itemDescriptions?.[idx] || (
+ -
+ )}
+
+
+
+ {
+ setUpdatingIdx(idx);
+ try {
+ await updateChoiceMutation.mutateAsync({
+ ballotId,
+ index: idx,
+ choice: newValue,
+ });
+ await refetchBallots();
+ onBallotChanged?.();
+ } catch (error: unknown) {}
+ setUpdatingIdx(null);
+ }}
+ disabled={updatingIdx === idx}
+ >
+
+
+
+
+
+ Yes
+ No
+ Abstain
+
+
+
+
+ handleDelete(idx)}
+ >
+ {removingIdx === idx ? "..." : "Delete"}
+
+
+
+
+ );
+ })}
diff --git a/src/components/pages/wallet/governance/index.tsx b/src/components/pages/wallet/governance/index.tsx
index caac526..781a3ee 100644
--- a/src/components/pages/wallet/governance/index.tsx
+++ b/src/components/pages/wallet/governance/index.tsx
@@ -1,5 +1,4 @@
import CardInfo from "./card-info";
-import { useWalletsStore } from "@/lib/zustand/wallets";
import { useSiteStore } from "@/lib/zustand/site";
import AllProposals from "./proposals";
import useAppWallet from "@/hooks/useAppWallet";
@@ -7,11 +6,10 @@ import VoteCard from "./vote-card";
import ClarityCard from "./clarity/card-clarity";
import VoteCC from "./cCommitee/voteCC";
import UTxOSelector from "../new-transaction/utxoSelector";
-import { UTxO } from "@meshsdk/core";
-import BallotCard from "./ballot/ballot";
-import { useState, useEffect, useMemo } from "react";
+import type { UTxO } from "@meshsdk/core";
+import { useState } from "react";
import { useBallot } from "@/hooks/useBallot";
-import { Vote } from "lucide-react";
+import FloatingBallotSidebar from "./ballot/FloatingBallotSidebar";
export default function PageGovernance() {
const { appWallet } = useAppWallet();
@@ -20,9 +18,14 @@ export default function PageGovernance() {
const [manualSelected, setManualSelected] = useState(false);
const [selectedBallotId, setSelectedBallotId] = useState(undefined);
- const { refresh, ballots } = useBallot(appWallet?.id);
+ const { ballots } = useBallot(appWallet?.id);
const selected = ballots?.find((b) => b.id === selectedBallotId);
const proposalCount = selected?.items?.length ?? 0;
+ const totalProposalCount =
+ ballots?.reduce(
+ (sum, b) => sum + (Array.isArray(b.items) ? b.items.length : 0),
+ 0,
+ ) ?? 0;
if (appWallet === undefined) return <>>;
return (
@@ -71,127 +74,10 @@ export default function PageGovernance() {
selectedBallotId={selectedBallotId}
onSelectBallot={setSelectedBallotId}
ballotCount={ballots?.length ?? 0}
+ totalProposalCount={totalProposalCount}
proposalCount={proposalCount}
manualUtxos={manualUtxos}
/>
);
}
-
-// FloatingBallotSidebar component
-interface FloatingBallotSidebarProps {
- appWallet: any;
- selectedBallotId?: string;
- onSelectBallot: (id: string) => void;
- ballotCount: number;
- proposalCount: number;
- manualUtxos: UTxO[];
-}
-
-function FloatingBallotSidebar({
- appWallet,
- selectedBallotId,
- onSelectBallot,
- ballotCount,
- proposalCount,
- manualUtxos
-}: FloatingBallotSidebarProps) {
- const [open, setOpen] = useState(false);
- const [isMobile, setIsMobile] = useState(false);
-
- useEffect(() => {
- function handleResize() {
- setIsMobile(typeof window !== "undefined" ? window.innerWidth < 768 : false);
- }
- handleResize();
- window.addEventListener("resize", handleResize);
- return () => window.removeEventListener("resize", handleResize);
- }, []);
-
- if (isMobile) {
- return (
- <>
- setOpen(true)}
- aria-label="Open Ballots"
- >
-
-
- {(ballotCount > 0 || proposalCount > 0) && (
-
- {proposalCount > 0 ? proposalCount : ""}
-
- )}
-
-
-
- {open && (
-
-
- Your Ballots
- {proposalCount > 0 && (
-
- {proposalCount}
-
- )}
- setOpen(false)}
- aria-label="Close"
- >
- ✕
-
-
-
-
-
-
- )}
- >
- );
- }
-
- return (
-
-
-
setOpen((o) => !o)}
- aria-label={open ? "Collapse Ballots" : "Expand Ballots"}
- className={open ? "absolute -left-12 top-8 p-1.5 rounded-full bg-white/80 shadow hover:bg-gray-100 border" : "absolute top-0 right-0 p-1"}
- >
-
- {(ballotCount > 0 || proposalCount > 0) && (
-
- {proposalCount > 0 ? proposalCount : ''}
-
- )}
-
- {open && (
-
-
-
- ) }
-
-
- );
-}
diff --git a/src/components/pages/wallet/governance/proposal/index.tsx b/src/components/pages/wallet/governance/proposal/index.tsx
index 64e0358..c18c2c1 100644
--- a/src/components/pages/wallet/governance/proposal/index.tsx
+++ b/src/components/pages/wallet/governance/proposal/index.tsx
@@ -2,47 +2,52 @@ import CardUI from "@/components/ui/card-content";
import { getProvider } from "@/utils/get-provider";
import RowLabelInfo from "@/components/common/row-label-info";
import { useSiteStore } from "@/lib/zustand/site";
-import { ProposalMetadata } from "@/types/governance";
+import type { ProposalMetadata } from "@/types/governance";
import { useEffect, useState } from "react";
import Link from "next/link";
import Button from "@/components/common/button";
-import { useWalletsStore } from "@/lib/zustand/wallets";
import useAppWallet from "@/hooks/useAppWallet";
import VoteCard from "../vote-card";
-import { UTxO } from "@meshsdk/core";
+import type { UTxO } from "@meshsdk/core";
import UTxOSelector from "../../new-transaction/utxoSelector";
-import BallotCard from "../ballot/ballot";
+import FloatingBallotSidebar from "../ballot/FloatingBallotSidebar";
+import { useBallot } from "@/hooks/useBallot";
-export default function WalletGovernanceProposal({
- id,
-}: {
- id: string;
-}) {
+export default function WalletGovernanceProposal({ id }: { id: string }) {
const network = useSiteStore((state) => state.network);
const [proposalMetadata, setProposalMetadata] = useState<
ProposalMetadata | undefined
>(undefined);
- const drepInfo = useWalletsStore((state) => state.drepInfo);
const { appWallet } = useAppWallet();
- const loading = useSiteStore((state) => state.loading);
const [manualUtxos, setManualUtxos] = useState([]);
- const [manualSelected, setManualSelected] = useState(false);
- const [selectedBallotId, setSelectedBallotId] = useState(undefined);
+ const [selectedBallotId, setSelectedBallotId] = useState(
+ undefined,
+ );
+ const [isBallotSidebarOpen, setIsBallotSidebarOpen] = useState(false);
+
+ const { ballots } = useBallot(appWallet?.id);
+ const selected = ballots?.find((b) => b.id === selectedBallotId);
+ const proposalCount = selected?.items?.length ?? 0;
+ const totalProposalCount =
+ ballots?.reduce(
+ (sum, b) => sum + (Array.isArray(b.items) ? b.items.length : 0),
+ 0,
+ ) ?? 0;
useEffect(() => {
const blockchainProvider = getProvider(network);
async function get() {
const [txHash, certIndex] = id.split(":");
- const proposalData = await blockchainProvider.get(
+ const proposalData = (await blockchainProvider.get(
`/governance/proposals/${txHash}/${certIndex}/metadata`,
- );
+ )) as ProposalMetadata;
if (proposalData) {
setProposalMetadata(proposalData);
}
}
- get();
- }, []);
+ void get();
+ }, [id, network]);
if (!proposalMetadata) return <>>;
@@ -76,8 +81,8 @@ export default function WalletGovernanceProposal({
>
author.name)
+ value={(proposalMetadata.json_metadata.authors as { name: string }[])
+ .map((author) => author.name)
.join(", ")}
allowOverflow={true}
/>
@@ -104,26 +109,34 @@ export default function WalletGovernanceProposal({
{
- setManualUtxos(utxos);
- setManualSelected(manual);
- }}
+ onSelectionChange={(utxos) => {
+ setManualUtxos(utxos);
+ }}
/>
)}
{appWallet && (
- setSelectedBallotId(ballotId)}
+ proposalId={`${proposalMetadata.tx_hash}#${proposalMetadata.cert_index}`}
+ selectedBallotId={selectedBallotId}
+ proposalTitle={proposalMetadata.json_metadata.body.title}
+ onOpenBallotSidebar={() => setIsBallotSidebarOpen(true)}
/>
)}
{appWallet && (
-
)}
diff --git a/src/components/pages/wallet/governance/proposal/voteButtton.tsx b/src/components/pages/wallet/governance/proposal/voteButtton.tsx
index 8c99ab2..9b3d2f5 100644
--- a/src/components/pages/wallet/governance/proposal/voteButtton.tsx
+++ b/src/components/pages/wallet/governance/proposal/voteButtton.tsx
@@ -18,11 +18,19 @@ import {
import { ToastAction } from "@/components/ui/toast";
import useMultisigWallet from "@/hooks/useMultisigWallet";
import { api } from "@/utils/api";
-import {useBallot} from "@/hooks/useBallot";
+import { useBallot } from "@/hooks/useBallot";
import { useProxy } from "@/hooks/useProxy";
import { MeshProxyContract } from "@/components/multisig/proxy/offchain";
import { useWallet } from "@meshsdk/react";
import { useUserStore } from "@/lib/zustand/user";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { BallotType } from "../ballot/ballot";
interface VoteButtonProps {
appWallet: Wallet;
@@ -32,6 +40,12 @@ interface VoteButtonProps {
utxos: UTxO[];
selectedBallotId?: string;
proposalTitle?: string;
+ /**
+ * Optional handler from the proposal page to open the ballot sidebar.
+ * When provided, the \"Add proposal to ballot\" button will simply
+ * open the ballot card instead of mutating ballots directly.
+ */
+ onOpenBallotSidebar?: () => void;
}
export default function VoteButton({
@@ -42,27 +56,21 @@ export default function VoteButton({
utxos,
selectedBallotId,
proposalTitle,
+ onOpenBallotSidebar,
}: VoteButtonProps) {
- // Use the custom hook for ballots
- const { ballots, refresh } = useBallot(appWallet?.id);
- const selectedBallot = useMemo(() => {
- return ballots?.find((b) => b.id === selectedBallotId);
- }, [ballots, selectedBallotId]);
-
- const proposalIndex = selectedBallot?.items.findIndex((item) => item === proposalId);
- const isInBallot = proposalIndex !== undefined && proposalIndex >= 0;
-
- const addProposalMutation = api.ballot.addProposalToBallot.useMutation({
- onSuccess: () => {
- refresh();
- },
- });
+ // Use the custom hook for ballots (still used for proxy / context where needed)
+ const { ballots } = useBallot(appWallet?.id);
- const removeProposalMutation = api.ballot.removeProposalFromBallot.useMutation({
- onSuccess: () => {
- refresh();
- },
- });
+ // Determine if this proposal already exists on any ballot
+ const isOnAnyBallot = useMemo(
+ () =>
+ !!proposalId &&
+ Array.isArray(ballots) &&
+ ballots.some(
+ (b: BallotType) => Array.isArray(b.items) && b.items.includes(proposalId),
+ ),
+ [ballots, proposalId],
+ );
const drepInfo = useWalletsStore((state) => state.drepInfo);
const [loading, setLoading] = useState(false);
@@ -315,52 +323,6 @@ export default function VoteButton({
}
}
- async function addProposalToBallot() {
- if (!selectedBallotId) return;
- try {
- await addProposalMutation.mutateAsync({
- ballotId: selectedBallotId,
- itemDescription: proposalTitle ?? description,
- item: proposalId,
- choice: voteKind,
- });
- toast({
- title: "Added to Ballot",
- description: "Proposal successfully added to the ballot.",
- duration: 500,
- });
- } catch (error) {
- toast({
- title: "Failed to Add to Ballot",
- description: `Error: ${error}`,
- duration: 10000,
- variant: "destructive",
- });
- }
- }
-
- async function removeProposalFromBallot() {
- if (!selectedBallotId || proposalIndex === undefined || proposalIndex < 0) return;
- try {
- await removeProposalMutation.mutateAsync({
- ballotId: selectedBallotId,
- index: proposalIndex,
- });
- toast({
- title: "Removed from Ballot",
- description: "Proposal successfully removed from the ballot.",
- duration: 500,
- });
- } catch (error) {
- toast({
- title: "Failed to Remove from Ballot",
- description: `Error: ${error}`,
- duration: 10000,
- variant: "destructive",
- });
- }
- }
-
return (
- {loading ? "Voting..." : utxos.length > 0 ? `Vote${hasValidProxy ? " (Proxy Mode)" : ""}` : "No UTxOs Available"}
+ {loading
+ ? "Voting..."
+ : utxos.length > 0
+ ? `Vote${hasValidProxy ? " (Proxy Mode)" : ""}`
+ : "No UTxOs Available"}
- {selectedBallotId && (
+ {onOpenBallotSidebar && (
- {isInBallot ? "Remove proposal from ballot" : "Add proposal to ballot"}
+ {isOnAnyBallot ? "Manage proposal on ballots" : "Add proposal to ballot"}
)}
diff --git a/src/components/pages/wallet/governance/vote-card.tsx b/src/components/pages/wallet/governance/vote-card.tsx
index 23cd9d1..9d78181 100644
--- a/src/components/pages/wallet/governance/vote-card.tsx
+++ b/src/components/pages/wallet/governance/vote-card.tsx
@@ -6,7 +6,7 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useState } from "react";
import VoteButton from "./proposal/voteButtton";
-import { UTxO } from "@meshsdk/core";
+import type { UTxO } from "@meshsdk/core";
interface VoteCardProps {
appWallet: Wallet;
@@ -14,9 +14,17 @@ interface VoteCardProps {
proposalId?: string; // Optional proposalId
selectedBallotId?: string;
proposalTitle?: string;
+ onOpenBallotSidebar?: () => void;
}
-export default function VoteCard({ appWallet, utxos, proposalId, selectedBallotId, proposalTitle }: VoteCardProps) {
+export default function VoteCard({
+ appWallet,
+ utxos,
+ proposalId,
+ selectedBallotId,
+ proposalTitle,
+ onOpenBallotSidebar,
+}: VoteCardProps) {
const drepInfo = useWalletsStore((state) => state.drepInfo);
const [localProposalId, setLocalProposalId] = useState(proposalId ?? "");
const [description, setDescription] = useState("");
@@ -76,6 +84,7 @@ export default function VoteCard({ appWallet, utxos, proposalId, selectedBallotI
utxos={utxos}
selectedBallotId={selectedBallotId}
proposalTitle={proposalTitle}
+ onOpenBallotSidebar={onOpenBallotSidebar}
/>