diff --git a/apps/dashboard/app/[daoId]/(secondary)/governance/offchain-proposal/[proposalId]/page.tsx b/apps/dashboard/app/[daoId]/(main)/governance/offchain-proposal/[proposalId]/page.tsx similarity index 56% rename from apps/dashboard/app/[daoId]/(secondary)/governance/offchain-proposal/[proposalId]/page.tsx rename to apps/dashboard/app/[daoId]/(main)/governance/offchain-proposal/[proposalId]/page.tsx index 3fd0760d9..d6a481838 100644 --- a/apps/dashboard/app/[daoId]/(secondary)/governance/offchain-proposal/[proposalId]/page.tsx +++ b/apps/dashboard/app/[daoId]/(main)/governance/offchain-proposal/[proposalId]/page.tsx @@ -1,10 +1,7 @@ import type { Metadata } from "next"; import { ProposalSection } from "@/features/governance/components/proposal-overview/ProposalSection"; -import { Footer } from "@/shared/components/design-system/footer/Footer"; import type { DaoIdEnum } from "@/shared/types/daos"; -import { HeaderSidebar, StickyPageHeader } from "@/widgets"; -import { HeaderMobile } from "@/widgets/HeaderMobile"; type Props = { params: Promise<{ daoId: string; proposalId: string }>; @@ -35,24 +32,8 @@ export async function generateMetadata(props: Props): Promise { export default function OffchainProposalPage() { return ( -
-
-
- -
-
-
-
- - -
-
-
- -
-
-
-
+
+
); } diff --git a/apps/dashboard/app/[daoId]/(secondary)/governance/opengraph-image.tsx b/apps/dashboard/app/[daoId]/(main)/governance/opengraph-image.tsx similarity index 100% rename from apps/dashboard/app/[daoId]/(secondary)/governance/opengraph-image.tsx rename to apps/dashboard/app/[daoId]/(main)/governance/opengraph-image.tsx diff --git a/apps/dashboard/app/[daoId]/(secondary)/governance/page.tsx b/apps/dashboard/app/[daoId]/(main)/governance/page.tsx similarity index 59% rename from apps/dashboard/app/[daoId]/(secondary)/governance/page.tsx rename to apps/dashboard/app/[daoId]/(main)/governance/page.tsx index 4bc964e70..0d6c6c753 100644 --- a/apps/dashboard/app/[daoId]/(secondary)/governance/page.tsx +++ b/apps/dashboard/app/[daoId]/(main)/governance/page.tsx @@ -1,10 +1,7 @@ import type { Metadata } from "next"; import { GovernanceSection } from "@/features/governance"; -import { Footer } from "@/shared/components/design-system/footer/Footer"; import type { DaoIdEnum } from "@/shared/types/daos"; -import { HeaderDAOSidebar, HeaderSidebar, StickyPageHeader } from "@/widgets"; -import { HeaderMobile } from "@/widgets/HeaderMobile"; type Props = { params: Promise<{ daoId: string }>; @@ -33,29 +30,10 @@ export async function generateMetadata(props: Props): Promise { }; } -export default function DaoPage() { +export default function GovernancePage() { return ( -
-
-
- -
-
- -
-
-
-
- - -
-
-
- -
-
-
-
+
+
); } diff --git a/apps/dashboard/app/[daoId]/(secondary)/governance/proposal/[proposalId]/opengraph-image.tsx b/apps/dashboard/app/[daoId]/(main)/governance/proposal/[proposalId]/opengraph-image.tsx similarity index 100% rename from apps/dashboard/app/[daoId]/(secondary)/governance/proposal/[proposalId]/opengraph-image.tsx rename to apps/dashboard/app/[daoId]/(main)/governance/proposal/[proposalId]/opengraph-image.tsx diff --git a/apps/dashboard/app/[daoId]/(secondary)/governance/proposal/[proposalId]/page.tsx b/apps/dashboard/app/[daoId]/(main)/governance/proposal/[proposalId]/page.tsx similarity index 64% rename from apps/dashboard/app/[daoId]/(secondary)/governance/proposal/[proposalId]/page.tsx rename to apps/dashboard/app/[daoId]/(main)/governance/proposal/[proposalId]/page.tsx index 6ade17911..d048405a7 100644 --- a/apps/dashboard/app/[daoId]/(secondary)/governance/proposal/[proposalId]/page.tsx +++ b/apps/dashboard/app/[daoId]/(main)/governance/proposal/[proposalId]/page.tsx @@ -1,10 +1,7 @@ import type { Metadata } from "next"; import { ProposalSection } from "@/features/governance/components/proposal-overview/ProposalSection"; -import { Footer } from "@/shared/components/design-system/footer/Footer"; import type { DaoIdEnum } from "@/shared/types/daos"; -import { HeaderSidebar, StickyPageHeader } from "@/widgets"; -import { HeaderMobile } from "@/widgets/HeaderMobile"; type Props = { params: Promise<{ daoId: string; proposalId: string }>; @@ -35,24 +32,8 @@ export async function generateMetadata(props: Props): Promise { export default function ProposalPage() { return ( -
-
-
- -
-
-
-
- - -
-
-
- -
-
-
-
+
+
); } diff --git a/apps/dashboard/app/aave/holders-and-delegates/page.tsx b/apps/dashboard/app/aave/holders-and-delegates/page.tsx index 28a13b418..ef2b551c9 100644 --- a/apps/dashboard/app/aave/holders-and-delegates/page.tsx +++ b/apps/dashboard/app/aave/holders-and-delegates/page.tsx @@ -10,7 +10,7 @@ import { Footer } from "@/shared/components/design-system/footer"; import { SwitcherDateMobile } from "@/shared/components/switchers/SwitcherDateMobile"; import { DaoIdEnum } from "@/shared/types/daos"; import { TimeInterval } from "@/shared/types/enums"; -import { HeaderDAOSidebar, HeaderSidebar } from "@/widgets"; +import { HeaderDAOSidebar, HeaderSidebar, StickyPageHeader } from "@/widgets"; import { HeaderMobile } from "@/widgets/HeaderMobile"; import { DelegationTable } from "@/app/aave/holders-and-delegates/DelegationTable"; @@ -64,46 +64,51 @@ function AavePageContent() {
-
+
- + +
- } - description={PAGES_CONSTANTS.holdersAndDelegates.description} - > - -
-
- {TABS.map((tab) => ( - +
+ } + description={PAGES_CONSTANTS.holdersAndDelegates.description} + > + +
+
+ {TABS.map((tab) => ( + + ))} +
+ - ))} -
- -
- {activeTab === "delegates" ? ( - - ) : ( - - )} - - -
+
+ {activeTab === "delegates" ? ( + + ) : ( + + )} + + +
+
+
); diff --git a/apps/dashboard/app/alerts/page.tsx b/apps/dashboard/app/alerts/page.tsx index aca1b1905..49e22209d 100644 --- a/apps/dashboard/app/alerts/page.tsx +++ b/apps/dashboard/app/alerts/page.tsx @@ -28,7 +28,7 @@ export default function DonatePage() {
- +
diff --git a/apps/dashboard/app/contact/ContactPageClient.tsx b/apps/dashboard/app/contact/ContactPageClient.tsx index bfaddd81d..cf02d4dfe 100644 --- a/apps/dashboard/app/contact/ContactPageClient.tsx +++ b/apps/dashboard/app/contact/ContactPageClient.tsx @@ -77,17 +77,15 @@ export default function ContactPage() {
-
- +
} description={PAGES_CONSTANTS.contact.description} - className="border-b-0!" + className="border-b-0! mt-14 lg:mt-0" > -
diff --git a/apps/dashboard/app/donate/page.tsx b/apps/dashboard/app/donate/page.tsx index 41565440a..41ed475b6 100644 --- a/apps/dashboard/app/donate/page.tsx +++ b/apps/dashboard/app/donate/page.tsx @@ -28,7 +28,7 @@ export default function DonatePage() {
- +
diff --git a/apps/dashboard/app/faq/page.tsx b/apps/dashboard/app/faq/page.tsx index d0993e3fe..9afccc9ad 100644 --- a/apps/dashboard/app/faq/page.tsx +++ b/apps/dashboard/app/faq/page.tsx @@ -28,8 +28,7 @@ export default function FAQPage() {
-
- +
diff --git a/apps/dashboard/app/glossary/GlossaryPageClient.tsx b/apps/dashboard/app/glossary/GlossaryPageClient.tsx index fd54d4b43..1b1cd55d3 100644 --- a/apps/dashboard/app/glossary/GlossaryPageClient.tsx +++ b/apps/dashboard/app/glossary/GlossaryPageClient.tsx @@ -42,8 +42,7 @@ export default function GlossaryPage() {
-
- +
@@ -52,9 +51,9 @@ export default function GlossaryPage() { title={PAGES_CONSTANTS.glossary.title} icon={} description={PAGES_CONSTANTS.glossary.description} - className="bg-surface-background! lg:mt-0! gap-4! lg:gap-6!" + className="bg-surface-background! lg:mt-0! gap-4! lg:gap-6! mt-14" > -
+
{/* Sticky Sidebar - Left Side */}
diff --git a/apps/dashboard/app/layout.tsx b/apps/dashboard/app/layout.tsx index 49308b113..e0e76ee69 100644 --- a/apps/dashboard/app/layout.tsx +++ b/apps/dashboard/app/layout.tsx @@ -7,7 +7,6 @@ import { Toaster } from "react-hot-toast"; import { CookieConsent } from "@/features/cookie"; import { HelpPopover } from "@/shared/components"; -import { ShutterAttackBanner } from "@/shared/components/banners/ShutterAttackBanner"; import { GlobalProviders } from "@/shared/providers/GlobalProviders"; import ConditionalPostHog from "@/shared/services/posthog/ConditionalPostHog"; import UmamiScript from "@/shared/services/umami"; @@ -73,7 +72,6 @@ export default function RootLayout({ children }: { children: ReactNode }) { className="border-light-dark mx-auto max-w-screen-2xl overflow-x-hidden border xl:overflow-hidden" > - {children} diff --git a/apps/dashboard/features/alerts/AlertsSection.tsx b/apps/dashboard/features/alerts/AlertsSection.tsx index 8abea45a1..c419078a8 100644 --- a/apps/dashboard/features/alerts/AlertsSection.tsx +++ b/apps/dashboard/features/alerts/AlertsSection.tsx @@ -13,12 +13,9 @@ export const AlertsSection = () => { description={ "With one click, get real-time governance alerts. Stay ahead of governance updates and take the path to being an active delegate without checking manually." } - className="bg-surface-background! mt-[56px]! lg:mt-0! border-b-0!" + className="bg-surface-background! lg:mt-0! border-b-0! mt-14" >
- {/* Dashed line separator - Mobile only */} -
-
{ALERTS_ITEMS.map((alert: AlertItem) => ( { title={PAGES_CONSTANTS.donate.title} icon={} description={PAGES_CONSTANTS.donate.description} - className="bg-surface-background! mt-[56px]! lg:mt-0!" + className="bg-surface-background! lg:mt-0! mt-14" >
- {/* Dashed line separator - Mobile only */} -
- {/* Main donation card with integrated benefits */} { title={PAGES_CONSTANTS.faq.title} icon={} description={PAGES_CONSTANTS.faq.description} - className="bg-surface-background! border-b-0!" + className="bg-surface-background! border-b-0! mt-14 lg:mt-0" >
- {/* Mobile-only dashed line separator */} -
- {/* FAQ Items */}
{ } return ( -
+
} diff --git a/apps/dashboard/features/governance/components/modals/GovernanceActionModal.tsx b/apps/dashboard/features/governance/components/modals/GovernanceActionModal.tsx new file mode 100644 index 000000000..9fa69f53d --- /dev/null +++ b/apps/dashboard/features/governance/components/modals/GovernanceActionModal.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { Check, Hourglass, PenLine } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import { useAccount, useWalletClient } from "wagmi"; + +import type { ProposalViewData } from "@/features/governance/types"; +import { showCustomToast } from "@/features/governance/utils/showCustomToast"; +import { + executeProposal, + queueProposal, + type GovernanceAction, +} from "@/features/governance/utils/submitGovernanceAction"; +import { Modal } from "@/shared/components/design-system/modal/Modal"; +import { SpinIcon } from "@/shared/components/icons/SpinIcon"; +import { DividerDefault } from "@/shared/components/design-system/divider/DividerDefault"; +import { cn } from "@/shared/utils/cn"; +import daoConfigByDaoId from "@/shared/dao-config"; +import type { DaoIdEnum } from "@/shared/types/daos"; + +type ActionStep = "waiting-signature" | "pending-tx" | "success" | "error"; + +interface GovernanceActionModalProps { + isOpen: boolean; + onClose: () => void; + action: GovernanceAction; + proposal: ProposalViewData; + daoId: DaoIdEnum; +} + +export const GovernanceActionModal = ({ + isOpen, + onClose, + action, + proposal, + daoId, +}: GovernanceActionModalProps) => { + const [step, setStep] = useState("waiting-signature"); + const [error, setError] = useState(null); + + const { address } = useAccount(); + const chain = daoConfigByDaoId[daoId].daoOverview.chain; + const { data: walletClient } = useWalletClient({ chainId: chain.id }); + + const title = action === "queue" ? "Confirm Queue" : "Confirm Execution"; + const confirmLabel = + action === "queue" + ? "Confirm queuing in your wallet" + : "Confirm execution in your wallet"; + + const handleAction = useCallback(async () => { + if (!address || !walletClient) return; + if (step === "pending-tx" || step === "success") return; + + setError(null); + setStep("waiting-signature"); + + try { + const handler = action === "queue" ? queueProposal : executeProposal; + await handler( + proposal.targets, + proposal.values, + proposal.calldatas ?? [], + proposal.description, + address, + daoId, + walletClient, + () => setStep("pending-tx"), + proposal.id, + ); + setStep("success"); + showCustomToast( + action === "queue" + ? "Proposal queued successfully!" + : "Proposal executed successfully!", + "success", + ); + onClose(); + setTimeout(() => window.location.reload(), 2000); + } catch (err) { + const message = err instanceof Error ? err.message : "Action failed."; + console.error("[GovernanceActionModal]", err); + let shortMessage: string; + if (message.includes("rejected") || message.includes("denied")) { + shortMessage = "Transaction rejected by user."; + } else if (message.includes("reverted")) { + shortMessage = + "Contract call reverted. The proposal may not be in the correct state."; + } else { + shortMessage = + message.split("\n")[0]?.slice(0, 100) ?? "Action failed."; + } + setError(shortMessage); + setStep("error"); + } + }, [address, walletClient, step, action, proposal, daoId, onClose, chain]); + + useEffect(() => { + if (!isOpen || !walletClient || step !== "waiting-signature") return; + handleAction(); + }, [isOpen, walletClient, handleAction, step]); + + const handleClose = () => { + setStep("waiting-signature"); + setError(null); + onClose(); + }; + + return ( + !open && handleClose()} + title={title} + > + {/* Proposal info */} +
+
+ + Proposal ID + + {proposal.id} +
+
+ + Proposal name + + + {proposal.title} + +
+
+ + {/* Stepper */} +
+ } + label={confirmLabel} + error={step === "error" ? error : undefined} + /> + + + + } + label="Wait for transaction to complete" + /> +
+
+ ); +}; + +interface StepRowProps { + done: boolean; + active: boolean; + icon: React.ReactNode; + label: string; + error?: string | null; +} + +const StepRow = ({ done, active, icon, label, error }: StepRowProps) => { + const getBackgroundColor = () => { + if (done) return "bg-surface-opacity-success"; + if (active) return "bg-primary"; + return "bg-border-default"; + }; + + return ( +
+
+
+ {active && ( + + )} +
+
+ {done ? : icon} +
+
+
+

{label}

+
+ + {error && ( +

+ {error} +

+ )} +
+ ); +}; diff --git a/apps/dashboard/features/governance/components/modals/VotingModal.tsx b/apps/dashboard/features/governance/components/modals/VotingModal.tsx index cfdcddd58..97c3228dc 100644 --- a/apps/dashboard/features/governance/components/modals/VotingModal.tsx +++ b/apps/dashboard/features/governance/components/modals/VotingModal.tsx @@ -11,9 +11,15 @@ import { VoteOption } from "@/features/governance/components/proposal-overview/V import type { ProposalDetails } from "@/features/governance/types"; import { showCustomToast } from "@/features/governance/utils/showCustomToast"; import { voteOnProposal } from "@/features/governance/utils/voteOnProposal"; -import { BadgeStatus, Button } from "@/shared/components"; +import { BadgeStatus } from "@/shared/components/design-system/badges/badge-status/BadgeStatus"; +import { Button } from "@/shared/components/design-system/buttons/button/Button"; +import { + DrawerContent, + DrawerRoot, +} from "@/shared/components/design-system/drawer"; +import { useScreenSize } from "@/shared/hooks"; import type { DaoIdEnum } from "@/shared/types/daos"; -import { formatNumberUserReadable } from "@/shared/utils"; +import { formatNumberUserReadable } from "@/shared/utils/formatNumberUserReadable"; interface VotingModalProps { isOpen: boolean; @@ -39,6 +45,8 @@ export const VotingModal = ({ const [isLoading, setIsLoading] = useState(false); const [transactionhash, setTransactionhash] = useState(""); + const { isMobile } = useScreenSize(); + // Parse user's voting power to BigInt for calculations const userVotingPowerBigInt = BigInt(rawVotingPower || "0"); @@ -103,36 +111,199 @@ export const VotingModal = ({ } }, [isOpen]); - // Prevent body scroll when modal is open + // Prevent body scroll when desktop modal is open useEffect(() => { - if (isOpen) { - document.body.classList.add("no-scroll"); - } else { - document.body.classList.remove("no-scroll"); + if (!isMobile) { + if (isOpen) { + document.body.classList.add("no-scroll"); + } else { + document.body.classList.remove("no-scroll"); + } + return () => { + document.body.classList.remove("no-scroll"); + }; } + }, [isOpen, isMobile]); - // Cleanup on unmount - return () => { - document.body.classList.remove("no-scroll"); - }; - }, [isOpen]); - - // Handle escape key + // Handle escape key for desktop modal useEffect(() => { + if (isMobile) return; const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { onClose(); } }; - if (isOpen) { document.addEventListener("keydown", handleEscape); } - return () => { document.removeEventListener("keydown", handleEscape); }; - }, [isOpen, onClose]); + }, [isOpen, onClose, isMobile]); + + const handleSubmit = async () => { + if (!address || !chain || !walletClient) return; + setIsLoading(true); + const hash = await voteOnProposal( + vote as "for" | "against" | "abstain", + proposal?.id as string, + address as unknown as Account, + chain, + daoId, + walletClient, + setTransactionhash, + comment, + ); + setIsLoading(false); + if (hash) { + onClose(); + window.location.reload(); + showCustomToast("Vote submitted successfully!", "success"); + } + }; + + const submitDisabled = + !address || + !chain || + !vote || + !walletClient || + isLoading || + !rawVotingPower || + rawVotingPower === "0"; + + const content = ( +
+ {/* Header */} +
+
+

+ Cast Your Vote +

+

+ Once you submit your vote, you cannot change it. +

+
+ + +
+ + {/* Content */} + {isLoading ? ( + + ) : ( + <> + {/* your vote */} +
+

+ Your vote +

+ + {/* For vote */} + + + {/* Against vote */} + + + {/* Abstain vote */} + + +
+ +

+ Quorum +

+

+ {userReadableQuorumVotes} / {userReadableQuorum} +

+ {isQuorumReached ? ( + + Reached + + ) : ( + Not Reached + )} +
+
+ + {/* Comment */} +
+

+ Comment (optional) +

+