Skip to content

Commit 5df4897

Browse files
authored
Merge pull request #178 from MeshJS/feature/gov-updates
Feature/gov-updates
2 parents ccb48e5 + dbec24b commit 5df4897

File tree

6 files changed

+625
-318
lines changed

6 files changed

+625
-318
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React, { useEffect, useState } from "react";
2+
import BallotCard from "./ballot";
3+
import type { UTxO } from "@meshsdk/core";
4+
import { Vote, Minimize2 } from "lucide-react";
5+
6+
interface FloatingBallotSidebarProps {
7+
appWallet: any;
8+
selectedBallotId?: string;
9+
onSelectBallot: (id: string) => void;
10+
ballotCount: number;
11+
totalProposalCount: number;
12+
proposalCount: number;
13+
manualUtxos: UTxO[];
14+
/**
15+
* Optional controlled open state for the sidebar.
16+
* If provided together with onOpenChange, the sidebar becomes controlled.
17+
*/
18+
open?: boolean;
19+
onOpenChange?: (open: boolean) => void;
20+
/**
21+
* Optional current proposal context (used on the proposal page).
22+
* When provided, the ballot card can show contextual UI like
23+
* an \"Add to ballot\" button and highlighting.
24+
*/
25+
currentProposalId?: string;
26+
currentProposalTitle?: string;
27+
}
28+
29+
export default function FloatingBallotSidebar({
30+
appWallet,
31+
selectedBallotId,
32+
onSelectBallot,
33+
ballotCount,
34+
totalProposalCount,
35+
proposalCount,
36+
manualUtxos,
37+
open: controlledOpen,
38+
onOpenChange,
39+
currentProposalId,
40+
currentProposalTitle,
41+
}: FloatingBallotSidebarProps) {
42+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
43+
const [isMobile, setIsMobile] = useState(false);
44+
45+
const isControlled = controlledOpen !== undefined && !!onOpenChange;
46+
const open = isControlled ? controlledOpen : uncontrolledOpen;
47+
48+
const setOpen = (value: boolean | ((prev: boolean) => boolean)) => {
49+
if (isControlled && onOpenChange) {
50+
const next = typeof value === "function" ? value(open) : value;
51+
onOpenChange(next);
52+
} else {
53+
setUncontrolledOpen(value as boolean);
54+
}
55+
};
56+
57+
useEffect(() => {
58+
function handleResize() {
59+
setIsMobile(
60+
typeof window !== "undefined" ? window.innerWidth < 768 : false,
61+
);
62+
}
63+
handleResize();
64+
window.addEventListener("resize", handleResize);
65+
return () => window.removeEventListener("resize", handleResize);
66+
}, []);
67+
68+
if (isMobile) {
69+
return (
70+
<>
71+
<button
72+
className="fixed z-50 bottom-4 right-4 p-2 rounded-full bg-white/80 border shadow-md"
73+
onClick={() => setOpen(true)}
74+
aria-label="Open Ballots"
75+
>
76+
<div className="relative">
77+
<Vote size={32} className="text-gray-800 dark:text-white" />
78+
{(ballotCount > 0 ||
79+
totalProposalCount > 0 ||
80+
proposalCount > 0) && (
81+
<span className="absolute -top-1 -right-1 flex items-center justify-center h-4 w-4 rounded-full bg-red-600 text-white text-xs font-bold">
82+
{open
83+
? proposalCount > 0
84+
? proposalCount
85+
: ""
86+
: totalProposalCount > 0
87+
? totalProposalCount
88+
: ""}
89+
</span>
90+
)}
91+
</div>
92+
</button>
93+
94+
{open && (
95+
<div className="fixed z-50 left-0 bottom-0 w-full h-[85vh] bg-white dark:bg-gray-900 border-t p-4 shadow-xl animate-slideUp flex flex-col">
96+
<div className="flex justify-between items-center mb-2">
97+
<span className="font-semibold">Your Ballots</span>
98+
{proposalCount > 0 && (
99+
<span className="ml-2 inline-block text-xs font-medium text-white bg-blue-500 rounded-full px-2 py-0.5">
100+
{proposalCount}
101+
</span>
102+
)}
103+
<button
104+
className="p-2 rounded-full hover:bg-gray-200"
105+
onClick={() => setOpen(false)}
106+
aria-label="Close"
107+
>
108+
109+
</button>
110+
</div>
111+
<div className="flex-1 min-h-0 overflow-y-auto">
112+
<BallotCard
113+
appWallet={appWallet}
114+
selectedBallotId={selectedBallotId}
115+
onSelectBallot={onSelectBallot}
116+
utxos={manualUtxos}
117+
currentProposalId={currentProposalId}
118+
currentProposalTitle={currentProposalTitle}
119+
/>
120+
</div>
121+
</div>
122+
)}
123+
</>
124+
);
125+
}
126+
127+
return (
128+
<div
129+
className={`fixed z-50 bottom-4 right-4 transition-all duration-300 ${
130+
open ? "rounded-3xl max-w-md w-full md:w-[28rem] h-[50vh]" : "w-10 h-10"
131+
}`}
132+
style={{ pointerEvents: "auto" }}
133+
>
134+
<div
135+
className={`h-full flex flex-col transition-all duration-300
136+
${open ? "px-4 py-6" : "p-2"}
137+
`}
138+
>
139+
<button
140+
onClick={() => setOpen((o) => !o)}
141+
aria-label={open ? "Collapse Ballots" : "Expand Ballots"}
142+
title={
143+
open
144+
? "Click to minimise ballot panel"
145+
: "Click to open ballot panel"
146+
}
147+
className={
148+
open
149+
? "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"
150+
: "absolute top-0 right-0 p-1 group text-gray-800 dark:text-white"
151+
}
152+
>
153+
{open ? (
154+
<>
155+
<Vote
156+
size={32}
157+
className="block group-hover:hidden transition-colors"
158+
/>
159+
<Minimize2
160+
size={32}
161+
className="hidden group-hover:block transition-colors"
162+
/>
163+
</>
164+
) : (
165+
<Vote size={40} />
166+
)}
167+
{(ballotCount > 0 ||
168+
totalProposalCount > 0 ||
169+
proposalCount > 0) && (
170+
<span className="absolute -top-1 -right-1 flex items-center justify-center h-4 w-4 rounded-full bg-red-600 text-white text-xs font-bold">
171+
{open
172+
? proposalCount > 0
173+
? proposalCount
174+
: ""
175+
: totalProposalCount > 0
176+
? totalProposalCount
177+
: ""}
178+
</span>
179+
)}
180+
</button>
181+
{open && (
182+
<div
183+
className="flex-1 min-h-0 overflow-y-auto scrollbar-hide"
184+
style={{ scrollbarWidth: "none" }}
185+
>
186+
<BallotCard
187+
appWallet={appWallet}
188+
selectedBallotId={selectedBallotId}
189+
onSelectBallot={onSelectBallot}
190+
utxos={manualUtxos}
191+
currentProposalId={currentProposalId}
192+
currentProposalTitle={currentProposalTitle}
193+
/>
194+
</div>
195+
)}
196+
</div>
197+
</div>
198+
);
199+
}
200+
201+

0 commit comments

Comments
 (0)