From 0a2ace204d3a62bfb50082437e51c1e65855f85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Thu, 22 Feb 2024 12:22:37 +0100 Subject: [PATCH 1/5] Refactoring the alert context to make it general purpose and accept multiple alerts --- bun.lockb | Bin 590251 -> 590251 bytes components/alert/alert-container.tsx | 36 ++++++++++++++++++ components/alert/alerts.tsx | 18 --------- components/alert/index.tsx | 54 -------------------------- context/AlertContext.tsx | 55 +++++++++++++++++++++------ context/index.tsx | 2 - pages/_app.tsx | 2 + utils/types.ts | 8 ++-- 8 files changed, 86 insertions(+), 89 deletions(-) create mode 100644 components/alert/alert-container.tsx delete mode 100644 components/alert/alerts.tsx delete mode 100644 components/alert/index.tsx diff --git a/bun.lockb b/bun.lockb index 08591c869d6e9677477eae022eb4c0aa00b4f8eb..64ce89ee29dc3dac4a12f2263240b36bb16ade38 100755 GIT binary patch delta 45 ycmZ48tg^aUrJ;qfg{g(Pg=Gt?@*Q@@IAc8nJ=1orJFGy=2E^>!weD~@Y(UJuUF!} { + const { alerts } = useAlertContext(); + + return ( +
+ {alerts.map((alert: IAlert) => ( + + ))} +
+ ); +}; + +function resolveVariant(type: IAlert["type"]) { + let result: AlertVariant; + switch (type) { + case "error": + result = "critical"; + break; + default: + result = type; + } + return result; +} + +export default AlertContainer; diff --git a/components/alert/alerts.tsx b/components/alert/alerts.tsx deleted file mode 100644 index 3dc4015..0000000 --- a/components/alert/alerts.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Alert from '@/components/alert' -import { useAlertContext } from '@/context/AlertContext' -import { IAlert } from '@/utils/types' -import { FC } from 'react' - -const Alerts: FC = () => { - const { alerts } = useAlertContext() - - return ( - <> - {alerts.map((alert: IAlert) => ( - - ))} - - ) -} - -export default Alerts diff --git a/components/alert/index.tsx b/components/alert/index.tsx deleted file mode 100644 index ee05b8e..0000000 --- a/components/alert/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { AlertContext } from "@/context/AlertContext"; -import React, { useEffect, useContext, useState, useRef } from "react"; -import { IAlert } from "@/utils/types"; -import { useWaitForTransactionReceipt } from "wagmi"; -import { AlertCard } from "@aragon/ods"; - -const ALERT_TIMEOUT = 9 * 1000; - -const Alert: React.FC = ({ message, txHash, id }) => { - const alertContext = useContext(AlertContext); - const removeAlert = alertContext ? alertContext.removeAlert : () => { }; - const [hide, setHide] = useState(false); - const { isSuccess } = useWaitForTransactionReceipt({ - hash: txHash as `0x${string}`, - }); - const timerRef = useRef(null); - - useEffect(() => { - timerRef.current = setTimeout(() => { - setHide(true); - }, ALERT_TIMEOUT); - - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - }, [id, setHide]); - - useEffect(() => { - if (isSuccess) { - removeAlert(id); - setHide(false); - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => { - setHide(true); - }, ALERT_TIMEOUT); - } - }, [isSuccess, removeAlert]); - - if (hide) return
; - - /** bg-success-50 bg-primary-50 text-success-900 text-primary-900 */ - return ( -
- - -
- ); -}; - -export default Alert; diff --git a/context/AlertContext.tsx b/context/AlertContext.tsx index d559b91..a8da558 100644 --- a/context/AlertContext.tsx +++ b/context/AlertContext.tsx @@ -1,29 +1,60 @@ -import React, { createContext, useState, useContext } from 'react'; -import { IAlert } from '@/utils/types' +import React, { createContext, useState, useContext } from "react"; +import { IAlert } from "@/utils/types"; +import { usePublicClient } from "wagmi"; + +const DEFAULT_ALERT_TIMEOUT = 7 * 1000; + +export type NewAlert = { + type: "success" | "info" | "error"; + message: string; + description?: string; + txHash?: string; + timeout?: number; +}; export interface AlertContextProps { alerts: IAlert[]; - addAlert: (message: string, txHash: string) => void; - removeAlert: (id: number) => void; + addAlert: (newAlert: NewAlert) => void; } -export const AlertContext = createContext(undefined); +export const AlertContext = createContext( + undefined +); -export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const [alerts, setAlerts] = useState([]); + const client = usePublicClient(); + + // Add a new alert to the list + const addAlert = (alert: NewAlert) => { + const newAlert: IAlert = { + id: Date.now(), + message: alert.message, + description: alert.description, + type: alert.type, + }; + if (alert.txHash && client) { + newAlert.explorerLink = + client.chain.blockExplorers?.default.url + "/tx/" + alert.txHash; + } + setAlerts(alerts.concat(newAlert)); - // Function to add a new alert - const addAlert = (message: string, txHash: string) => { - setAlerts([...alerts, { message, txHash, id: Date.now() }]); + // Schedule the clean-up + const timeout = alert.timeout ?? DEFAULT_ALERT_TIMEOUT; + setTimeout(() => { + removeAlert(newAlert.id); + }, timeout); }; // Function to remove an alert const removeAlert = (id: number) => { - setAlerts(alerts?.filter((alert) => alert.id !== id)); + setAlerts(alerts.filter((alert) => alert.id !== id)); }; return ( - + {children} ); @@ -33,7 +64,7 @@ export const useAlertContext = () => { const context = useContext(AlertContext); if (!context) { - throw new Error('useThemeContext must be used inside the AlertProvider'); + throw new Error("useContext must be used inside the AlertProvider"); } return context; diff --git a/context/index.tsx b/context/index.tsx index 275d310..cb2a6f3 100644 --- a/context/index.tsx +++ b/context/index.tsx @@ -1,5 +1,4 @@ import { AlertProvider } from "./AlertContext"; -import Alerts from "@/components/alert/alerts"; import { ReactNode } from "react"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { config } from "@/context/Web3Modal"; @@ -22,7 +21,6 @@ export function RootContextProvider({ children, initialState }: { children: Reac {children} - diff --git a/pages/_app.tsx b/pages/_app.tsx index 4737ee6..f357c14 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,5 +1,6 @@ import { RootContextProvider } from "@/context"; import { Layout } from "@/components/layout"; +import AlertContainer from "@/components/alert/alert-container"; import { Manrope } from "next/font/google"; import "@aragon/ods/index.css"; import "@/pages/globals.css"; @@ -22,6 +23,7 @@ export default function AragonetteApp({ Component, pageProps }: any) { +
); diff --git a/utils/types.ts b/utils/types.ts index 3a169a4..be83054 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -2,10 +2,12 @@ export type Action = { to: string; value: bigint; data: string; -} +}; export interface IAlert { - message: string; - txHash: string; id: number; + type: "success" | "info" | "error"; + message: string; + description?: string; + explorerLink?: string; } From feb1ed4f9720cc1bb6365be54af83dfcd282e11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Thu, 22 Feb 2024 12:56:05 +0100 Subject: [PATCH 2/5] Using the new alert framework --- context/AlertContext.tsx | 24 ++++- .../components/proposal/header.tsx | 95 ++++++++++++++----- plugins/dualGovernance/pages/new.tsx | 37 ++++++-- plugins/dualGovernance/pages/proposal.tsx | 27 +++++- plugins/tokenVoting/pages/new.tsx | 36 +++++-- plugins/tokenVoting/pages/proposal.tsx | 28 +++++- 6 files changed, 192 insertions(+), 55 deletions(-) diff --git a/context/AlertContext.tsx b/context/AlertContext.tsx index a8da558..ed14208 100644 --- a/context/AlertContext.tsx +++ b/context/AlertContext.tsx @@ -15,6 +15,9 @@ export type NewAlert = { export interface AlertContextProps { alerts: IAlert[]; addAlert: (newAlert: NewAlert) => void; + addSuccessAlert: (message: string) => void; + addInfoAlert: (message: string) => void; + addErrorAlert: (message: string) => void; } export const AlertContext = createContext( @@ -29,6 +32,12 @@ export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ // Add a new alert to the list const addAlert = (alert: NewAlert) => { + // Clean duplicates + const idx = alerts.findIndex( + (a) => a.message === alert.message && a.description === alert.description + ); + if (idx >= 0) removeAlert(idx); + const newAlert: IAlert = { id: Date.now(), message: alert.message, @@ -48,13 +57,26 @@ export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ }, timeout); }; + // Convenience aliases + const addSuccessAlert = (message: string) => { + addAlert({ message, type: "success" }); + }; + const addInfoAlert = (message: string) => { + addAlert({ message, type: "info" }); + }; + const addErrorAlert = (message: string) => { + addAlert({ message, type: "error" }); + }; + // Function to remove an alert const removeAlert = (id: number) => { setAlerts(alerts.filter((alert) => alert.id !== id)); }; return ( - + {children} ); diff --git a/plugins/dualGovernance/components/proposal/header.tsx b/plugins/dualGovernance/components/proposal/header.tsx index c77a5e1..aca7983 100644 --- a/plugins/dualGovernance/components/proposal/header.tsx +++ b/plugins/dualGovernance/components/proposal/header.tsx @@ -4,12 +4,13 @@ import { Proposal } from "@/plugins/dualGovernance/utils/types"; import { AlertVariant } from "@aragon/ods"; import { Else, If, IfCase, Then } from "@/components/if"; import { AddressText } from "@/components/text/address"; -import { useWriteContract } from "wagmi"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { OptimisticTokenVotingPluginAbi } from "../../artifacts/OptimisticTokenVotingPlugin.sol"; import { AlertContextProps, useAlertContext } from "@/context/AlertContext"; import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; import { PleaseWaitSpinner } from "@/components/please-wait"; +import { useRouter } from "next/router"; const DEFAULT_PROPOSAL_TITLE = "(No proposal title)"; @@ -28,23 +29,65 @@ const ProposalHeader: React.FC = ({ transactionLoading, onVetoPressed, }) => { - const { writeContract: executeWrite, data: executeResponse } = useWriteContract() - const { addAlert } = useAlertContext() as AlertContextProps; + const { reload } = useRouter(); + const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; const proposalVariant = useProposalVariantStatus(proposal); + const { + writeContract: executeWrite, + data: executeTxHash, + error, + status, + } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash: executeTxHash }); + const executeButtonPressed = () => { executeWrite({ chainId: PUB_CHAIN.id, abi: OptimisticTokenVotingPluginAbi, address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, - functionName: 'execute', - args: [proposalNumber] - }) - } + functionName: "execute", + args: [proposalNumber], + }); + }; useEffect(() => { - if (executeResponse) addAlert('Your execution has been submitted', executeResponse) - }, [executeResponse]) + if (status === "idle" || status === "pending") return; + else if (status === "error") { + if (error?.message?.startsWith("User rejected the request")) { + addAlert({ + message: "Transaction rejected by the user", + type: "error", + timeout: 4 * 1000, + }); + } else { + addErrorAlert("Could not create the proposal"); + } + return; + } + + // success + if (!executeTxHash) return; + else if (isConfirming) { + addAlert({ + message: "Proposal submitted", + description: "Waiting for the transaction to be validated", + type: "info", + txHash: executeTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert({ + message: "Proposal created", + description: "The transaction has been validated", + type: "success", + txHash: executeTxHash, + }); + + setTimeout(() => reload(), 1000 * 2); + }, [status, executeTxHash, isConfirming, isConfirmed]); return (
@@ -54,13 +97,13 @@ const ProposalHeader: React.FC = ({ {/** bg-info-200 bg-success-200 bg-critical-200 * text-info-800 text-success-800 text-critical-800 */} -
- -
+
+ +
Proposal {proposalNumber + 1} @@ -76,8 +119,8 @@ const ProposalHeader: React.FC = ({ size="lg" variant="primary" onClick={() => onVetoPressed()} - > - Veto + > + Veto @@ -88,15 +131,15 @@ const ProposalHeader: React.FC = ({ - + + className="flex h-5 items-center" + size="lg" + variant="success" + onClick={() => executeButtonPressed()} + > + Execute + diff --git a/plugins/dualGovernance/pages/new.tsx b/plugins/dualGovernance/pages/new.tsx index 0e79a96..39af35f 100644 --- a/plugins/dualGovernance/pages/new.tsx +++ b/plugins/dualGovernance/pages/new.tsx @@ -42,7 +42,7 @@ export default function Create() { const [title, setTitle] = useState(""); const [summary, setSummary] = useState(""); const [actions, setActions] = useState([]); - const { addAlert } = useAlertContext(); + const { addAlert, addErrorAlert } = useAlertContext(); const { writeContract: createProposalWrite, data: createTxHash, @@ -63,19 +63,36 @@ export default function Create() { useEffect(() => { if (status === "idle" || status === "pending") return; else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) return; - alert("Could not create the proposal"); + if (error?.message?.startsWith("User rejected the request")) { + addAlert({ + message: "Transaction rejected by the user", + type: "error", + timeout: 4 * 1000, + }); + } else { + addErrorAlert("Could not create the proposal"); + } return; } // success if (!createTxHash) return; else if (isConfirming) { - addAlert("The proposal has been submitted", createTxHash); + addAlert({ + message: "Proposal submitted", + description: "Waiting for the transaction to be validated", + type: "info", + txHash: createTxHash, + }); return; } else if (!isConfirmed) return; - addAlert("The proposal has been confirmed", createTxHash); + addAlert({ + message: "Proposal created", + description: "The transaction has been validated", + type: "success", + txHash: createTxHash, + }); setTimeout(() => { push("#/"); }, 1000 * 2); @@ -83,11 +100,13 @@ export default function Create() { const submitProposal = async () => { // Check metadata - if (!title.trim()) return alert("Please, enter a title"); + if (!title.trim()) return addErrorAlert("Please, enter a title"); const plainSummary = getPlainText(summary).trim(); if (!plainSummary.trim()) - return alert("Please, enter a summary of what the proposal is about"); + return addErrorAlert( + "Please, enter a summary of what the proposal is about" + ); // Check the action switch (actionType) { @@ -95,14 +114,14 @@ export default function Create() { break; case ActionType.Withdrawal: if (!actions.length) { - return alert( + return addErrorAlert( "Please ensure that the withdrawal address and the amount to transfer are valid" ); } break; default: if (!actions.length || !actions[0].data || actions[0].data === "0x") { - return alert( + return addErrorAlert( "Please ensure that the values of the action to execute are correct" ); } diff --git a/plugins/dualGovernance/pages/proposal.tsx b/plugins/dualGovernance/pages/proposal.tsx index 46c152e..09c1025 100644 --- a/plugins/dualGovernance/pages/proposal.tsx +++ b/plugins/dualGovernance/pages/proposal.tsx @@ -44,7 +44,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { const [bottomSection, setBottomSection] = useState("description"); - const { addAlert } = useAlertContext() as AlertContextProps; + const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; const { writeContract: vetoWrite, data: vetoTxHash, @@ -57,19 +57,36 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { useEffect(() => { if (status === "idle" || status === "pending") return; else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) return; - alert("Could not create the proposal"); + if (error?.message?.startsWith("User rejected the request")) { + addAlert({ + message: "Transaction rejected by the user", + type: "error", + timeout: 4 * 1000, + }); + } else { + addErrorAlert("Could not create the proposal"); + } return; } // success if (!vetoTxHash) return; else if (isConfirming) { - addAlert("The veto has been submitted", vetoTxHash); + addAlert({ + message: "Veto submitted", + description: "Waiting for the transaction to be validated", + type: "info", + txHash: vetoTxHash, + }); return; } else if (!isConfirmed) return; - // addAlert("The veto has been registered", vetoTxHash); + addAlert({ + message: "Veto registered", + description: "The transaction has been validated", + type: "success", + txHash: vetoTxHash, + }); reload(); }, [status, vetoTxHash, isConfirming, isConfirmed]); diff --git a/plugins/tokenVoting/pages/new.tsx b/plugins/tokenVoting/pages/new.tsx index e24aead..78659a8 100644 --- a/plugins/tokenVoting/pages/new.tsx +++ b/plugins/tokenVoting/pages/new.tsx @@ -35,7 +35,7 @@ export default function Create() { const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); const [actions, setActions] = useState([]); - const { addAlert } = useAlertContext() + const { addAlert, addErrorAlert } = useAlertContext() const { writeContract: createProposalWrite, data: createTxHash, @@ -54,19 +54,37 @@ export default function Create() { useEffect(() => { if (status === "idle" || status === "pending") return; else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) return; - alert("Could not create the proposal"); + if (error?.message?.startsWith("User rejected the request")) { + addAlert({ + message: "Transaction rejected by the user", + type: "error", + timeout: 4 * 1000, + }); + } else { + addErrorAlert("Could not create the proposal"); + } return; } // success if (!createTxHash) return; else if (isConfirming) { - addAlert("The proposal has been submitted", createTxHash); + addAlert({ + message: "Proposal submitted", + description: "Waiting for the transaction to be validated", + type: "info", + txHash: createTxHash, + }); return; } else if (!isConfirmed) return; - addAlert("The proposal has been confirmed", createTxHash); + addAlert({ + message: "Proposal created", + description: "The transaction has been validated", + type: "success", + txHash: createTxHash, + }); + setTimeout(() => { push("#/"); }, 1000 * 2); @@ -74,22 +92,22 @@ export default function Create() { const submitProposal = async () => { // Check metadata - if (!title.trim()) return alert("Please, enter a title"); + if (!title.trim()) return addErrorAlert("Please, enter a title"); const plainSummary = getPlainText(summary).trim() - if (!plainSummary.trim()) return alert("Please, enter a summary of what the proposal is about"); + if (!plainSummary.trim()) return addErrorAlert("Please, enter a summary of what the proposal is about"); // Check the action switch (actionType) { case ActionType.Signaling: break; case ActionType.Withdrawal: if (!actions.length) { - return alert("Please ensure that the withdrawal address and the amount to transfer are valid"); + return addErrorAlert("Please ensure that the withdrawal address and the amount to transfer are valid"); } break default: if (!actions.length || !actions[0].data || actions[0].data === "0x") { - return alert("Please ensure that the values of the action to execute are correct"); + return addErrorAlert("Please ensure that the values of the action to execute are correct"); } } diff --git a/plugins/tokenVoting/pages/proposal.tsx b/plugins/tokenVoting/pages/proposal.tsx index 1f92732..a6c8f6b 100644 --- a/plugins/tokenVoting/pages/proposal.tsx +++ b/plugins/tokenVoting/pages/proposal.tsx @@ -54,7 +54,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { const [votedOption, setVotedOption] = useState(undefined); const [showVotingModal, setShowVotingModal] = useState(false); const [selectedVoteOption, setSelectedVoteOption] = useState(); - const { addAlert } = useAlertContext() as AlertContextProps; + const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; const { address } = useAccount(); const { writeContract: voteWrite, @@ -99,19 +99,37 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { useEffect(() => { if (status === "idle" || status === "pending") return; else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) return; - alert("Could not create the proposal"); + if (error?.message?.startsWith("User rejected the request")) { + addAlert({ + message: "Transaction rejected by the user", + type: "error", + timeout: 4 * 1000, + }); + } else { + addErrorAlert("Could not create the proposal"); + } return; } // success if (!votingTxHash) return; else if (isConfirming) { - addAlert("The vote has been submitted", votingTxHash); + addAlert({ + message: "Vote submitted", + description: "Waiting for the transaction to be validated", + type: "info", + txHash: votingTxHash, + }); return; } else if (!isConfirmed) return; - // addAlert("The vote has been registered", votingTxHash); + addAlert({ + message: "Vote registered", + description: "The transaction has been validated", + type: "success", + txHash: votingTxHash, + }); + reload(); }, [status, votingTxHash, isConfirming, isConfirmed]); From ffcb2279098bfe1ecc28e796bd8b7033bbdfd024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Thu, 22 Feb 2024 13:27:53 +0100 Subject: [PATCH 3/5] Nicer alerts with shadow --- components/alert/alert-container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/alert/alert-container.tsx b/components/alert/alert-container.tsx index 5509baa..30a43fd 100644 --- a/components/alert/alert-container.tsx +++ b/components/alert/alert-container.tsx @@ -10,7 +10,7 @@ const AlertContainer: FC = () => {
{alerts.map((alert: IAlert) => ( Date: Thu, 22 Feb 2024 13:52:05 +0100 Subject: [PATCH 4/5] Minor edit --- plugins/dualGovernance/components/proposal/header.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/dualGovernance/components/proposal/header.tsx b/plugins/dualGovernance/components/proposal/header.tsx index aca7983..b6f312b 100644 --- a/plugins/dualGovernance/components/proposal/header.tsx +++ b/plugins/dualGovernance/components/proposal/header.tsx @@ -62,7 +62,8 @@ const ProposalHeader: React.FC = ({ timeout: 4 * 1000, }); } else { - addErrorAlert("Could not create the proposal"); + console.error(error); + addErrorAlert("Could not execute the proposal"); } return; } @@ -80,7 +81,7 @@ const ProposalHeader: React.FC = ({ } else if (!isConfirmed) return; addAlert({ - message: "Proposal created", + message: "Proposal executed", description: "The transaction has been validated", type: "success", txHash: executeTxHash, From 8d3640b4fa2b228e827e36ac7a6e5a3643bf1a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:11:31 +0100 Subject: [PATCH 5/5] Removing all alert shorthands --- context/AlertContext.tsx | 49 +- .../components/proposal/header.tsx | 14 +- plugins/dualGovernance/pages/new.tsx | 34 +- plugins/dualGovernance/pages/proposal.tsx | 15 +- plugins/tokenVoting/pages/new.tsx | 446 ++++++++++-------- plugins/tokenVoting/pages/proposal.tsx | 15 +- 6 files changed, 295 insertions(+), 278 deletions(-) diff --git a/context/AlertContext.tsx b/context/AlertContext.tsx index ed14208..5a027bd 100644 --- a/context/AlertContext.tsx +++ b/context/AlertContext.tsx @@ -4,9 +4,8 @@ import { usePublicClient } from "wagmi"; const DEFAULT_ALERT_TIMEOUT = 7 * 1000; -export type NewAlert = { - type: "success" | "info" | "error"; - message: string; +export type AlertOptions = { + type?: "success" | "info" | "error"; description?: string; txHash?: string; timeout?: number; @@ -14,10 +13,7 @@ export type NewAlert = { export interface AlertContextProps { alerts: IAlert[]; - addAlert: (newAlert: NewAlert) => void; - addSuccessAlert: (message: string) => void; - addInfoAlert: (message: string) => void; - addErrorAlert: (message: string) => void; + addAlert: (message: string, alertOptions?: AlertOptions) => void; } export const AlertContext = createContext( @@ -31,52 +27,43 @@ export const AlertProvider: React.FC<{ children: React.ReactNode }> = ({ const client = usePublicClient(); // Add a new alert to the list - const addAlert = (alert: NewAlert) => { + const addAlert = (message: string, alertOptions?: AlertOptions) => { // Clean duplicates - const idx = alerts.findIndex( - (a) => a.message === alert.message && a.description === alert.description - ); + const idx = alerts.findIndex((a) => { + if (a.message !== message) return false; + else if (a.description !== alertOptions?.description) return false; + else if (a.type !== alertOptions?.type) return false; + + return true; + }); if (idx >= 0) removeAlert(idx); const newAlert: IAlert = { id: Date.now(), - message: alert.message, - description: alert.description, - type: alert.type, + message, + description: alertOptions?.description, + type: alertOptions?.type ?? "info", }; - if (alert.txHash && client) { + if (alertOptions?.txHash && client) { newAlert.explorerLink = - client.chain.blockExplorers?.default.url + "/tx/" + alert.txHash; + client.chain.blockExplorers?.default.url + "/tx/" + alertOptions.txHash; } setAlerts(alerts.concat(newAlert)); // Schedule the clean-up - const timeout = alert.timeout ?? DEFAULT_ALERT_TIMEOUT; + const timeout = alertOptions?.timeout ?? DEFAULT_ALERT_TIMEOUT; setTimeout(() => { removeAlert(newAlert.id); }, timeout); }; - // Convenience aliases - const addSuccessAlert = (message: string) => { - addAlert({ message, type: "success" }); - }; - const addInfoAlert = (message: string) => { - addAlert({ message, type: "info" }); - }; - const addErrorAlert = (message: string) => { - addAlert({ message, type: "error" }); - }; - // Function to remove an alert const removeAlert = (id: number) => { setAlerts(alerts.filter((alert) => alert.id !== id)); }; return ( - + {children} ); diff --git a/plugins/dualGovernance/components/proposal/header.tsx b/plugins/dualGovernance/components/proposal/header.tsx index b6f312b..7081b1a 100644 --- a/plugins/dualGovernance/components/proposal/header.tsx +++ b/plugins/dualGovernance/components/proposal/header.tsx @@ -30,7 +30,7 @@ const ProposalHeader: React.FC = ({ onVetoPressed, }) => { const { reload } = useRouter(); - const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; + const { addAlert } = useAlertContext() as AlertContextProps; const proposalVariant = useProposalVariantStatus(proposal); const { @@ -56,14 +56,12 @@ const ProposalHeader: React.FC = ({ if (status === "idle" || status === "pending") return; else if (status === "error") { if (error?.message?.startsWith("User rejected the request")) { - addAlert({ - message: "Transaction rejected by the user", - type: "error", + addAlert("Transaction rejected by the user", { timeout: 4 * 1000, }); } else { console.error(error); - addErrorAlert("Could not execute the proposal"); + addAlert("Could not execute the proposal", { type: "error" }); } return; } @@ -71,8 +69,7 @@ const ProposalHeader: React.FC = ({ // success if (!executeTxHash) return; else if (isConfirming) { - addAlert({ - message: "Proposal submitted", + addAlert("Proposal submitted", { description: "Waiting for the transaction to be validated", type: "info", txHash: executeTxHash, @@ -80,8 +77,7 @@ const ProposalHeader: React.FC = ({ return; } else if (!isConfirmed) return; - addAlert({ - message: "Proposal executed", + addAlert("Proposal executed", { description: "The transaction has been validated", type: "success", txHash: executeTxHash, diff --git a/plugins/dualGovernance/pages/new.tsx b/plugins/dualGovernance/pages/new.tsx index 39af35f..132dc71 100644 --- a/plugins/dualGovernance/pages/new.tsx +++ b/plugins/dualGovernance/pages/new.tsx @@ -42,7 +42,7 @@ export default function Create() { const [title, setTitle] = useState(""); const [summary, setSummary] = useState(""); const [actions, setActions] = useState([]); - const { addAlert, addErrorAlert } = useAlertContext(); + const { addAlert } = useAlertContext(); const { writeContract: createProposalWrite, data: createTxHash, @@ -64,13 +64,11 @@ export default function Create() { if (status === "idle" || status === "pending") return; else if (status === "error") { if (error?.message?.startsWith("User rejected the request")) { - addAlert({ - message: "Transaction rejected by the user", - type: "error", + addAlert("Transaction rejected by the user", { timeout: 4 * 1000, }); } else { - addErrorAlert("Could not create the proposal"); + addAlert("Could not create the proposal", { type: "error" }); } return; } @@ -78,17 +76,14 @@ export default function Create() { // success if (!createTxHash) return; else if (isConfirming) { - addAlert({ - message: "Proposal submitted", + addAlert("Proposal submitted", { description: "Waiting for the transaction to be validated", - type: "info", txHash: createTxHash, }); return; } else if (!isConfirmed) return; - addAlert({ - message: "Proposal created", + addAlert("Proposal created", { description: "The transaction has been validated", type: "success", txHash: createTxHash, @@ -100,13 +95,14 @@ export default function Create() { const submitProposal = async () => { // Check metadata - if (!title.trim()) return addErrorAlert("Please, enter a title"); + if (!title.trim()) + return addAlert("Please, enter a title", { type: "error" }); const plainSummary = getPlainText(summary).trim(); if (!plainSummary.trim()) - return addErrorAlert( - "Please, enter a summary of what the proposal is about" - ); + return addAlert("Please, enter a summary of what the proposal is about", { + type: "error", + }); // Check the action switch (actionType) { @@ -114,15 +110,17 @@ export default function Create() { break; case ActionType.Withdrawal: if (!actions.length) { - return addErrorAlert( - "Please ensure that the withdrawal address and the amount to transfer are valid" + return addAlert( + "Please ensure that the withdrawal address and the amount to transfer are valid", + { type: "error" } ); } break; default: if (!actions.length || !actions[0].data || actions[0].data === "0x") { - return addErrorAlert( - "Please ensure that the values of the action to execute are correct" + return addAlert( + "Please ensure that the values of the action to execute are correct", + { type: "error" } ); } } diff --git a/plugins/dualGovernance/pages/proposal.tsx b/plugins/dualGovernance/pages/proposal.tsx index 09c1025..e6a612f 100644 --- a/plugins/dualGovernance/pages/proposal.tsx +++ b/plugins/dualGovernance/pages/proposal.tsx @@ -44,7 +44,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { const [bottomSection, setBottomSection] = useState("description"); - const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; + const { addAlert } = useAlertContext() as AlertContextProps; const { writeContract: vetoWrite, data: vetoTxHash, @@ -58,13 +58,11 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { if (status === "idle" || status === "pending") return; else if (status === "error") { if (error?.message?.startsWith("User rejected the request")) { - addAlert({ - message: "Transaction rejected by the user", - type: "error", + addAlert("Transaction rejected by the user", { timeout: 4 * 1000, }); } else { - addErrorAlert("Could not create the proposal"); + addAlert("Could not create the proposal", { type: "error" }); } return; } @@ -72,17 +70,14 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { // success if (!vetoTxHash) return; else if (isConfirming) { - addAlert({ - message: "Veto submitted", + addAlert("Veto submitted", { description: "Waiting for the transaction to be validated", - type: "info", txHash: vetoTxHash, }); return; } else if (!isConfirmed) return; - addAlert({ - message: "Veto registered", + addAlert("Veto registered", { description: "The transaction has been validated", type: "success", txHash: vetoTxHash, diff --git a/plugins/tokenVoting/pages/new.tsx b/plugins/tokenVoting/pages/new.tsx index 78659a8..96f2b60 100644 --- a/plugins/tokenVoting/pages/new.tsx +++ b/plugins/tokenVoting/pages/new.tsx @@ -1,217 +1,263 @@ -import { create } from 'ipfs-http-client'; -import { Button, IconType, Icon, InputText, TextAreaRichText } from '@aragon/ods' -import React, { useEffect, useState } from 'react' -import { uploadToIPFS } from '@/utils/ipfs' -import { useWaitForTransactionReceipt, useWriteContract } from 'wagmi'; -import { toHex } from 'viem' -import { TokenVotingAbi } from '@/plugins/tokenVoting/artifacts/TokenVoting.sol'; -import { useAlertContext } from '@/context/AlertContext'; -import WithdrawalInput from '@/components/input/withdrawal' -import CustomActionInput from '@/components/input/custom-action' -import { Action } from '@/utils/types' -import { getPlainText } from '@/utils/html'; -import { useRouter } from 'next/router'; -import { Else, IfCase, Then } from '@/components/if'; -import { PleaseWaitSpinner } from '@/components/please-wait'; +import { create } from "ipfs-http-client"; import { - PUB_IPFS_API_KEY, - PUB_IPFS_ENDPOINT, - PUB_TOKEN_VOTING_PLUGIN_ADDRESS -} from '@/constants'; + Button, + IconType, + Icon, + InputText, + TextAreaRichText, +} from "@aragon/ods"; +import React, { useEffect, useState } from "react"; +import { uploadToIPFS } from "@/utils/ipfs"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { toHex } from "viem"; +import { TokenVotingAbi } from "@/plugins/tokenVoting/artifacts/TokenVoting.sol"; +import { useAlertContext } from "@/context/AlertContext"; +import WithdrawalInput from "@/components/input/withdrawal"; +import CustomActionInput from "@/components/input/custom-action"; +import { Action } from "@/utils/types"; +import { getPlainText } from "@/utils/html"; +import { useRouter } from "next/router"; +import { Else, IfCase, Then } from "@/components/if"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { + PUB_IPFS_API_KEY, + PUB_IPFS_ENDPOINT, + PUB_TOKEN_VOTING_PLUGIN_ADDRESS, +} from "@/constants"; enum ActionType { - Signaling, - Withdrawal, - Custom + Signaling, + Withdrawal, + Custom, } const ipfsClient = create({ - url: PUB_IPFS_ENDPOINT, - headers: { 'X-API-KEY': PUB_IPFS_API_KEY, 'Accept': 'application/json' } + url: PUB_IPFS_ENDPOINT, + headers: { "X-API-KEY": PUB_IPFS_API_KEY, Accept: "application/json" }, }); export default function Create() { - const { push } = useRouter() - const [title, setTitle] = useState(''); - const [summary, setSummary] = useState(''); - const [actions, setActions] = useState([]); - const { addAlert, addErrorAlert } = useAlertContext() - const { - writeContract: createProposalWrite, - data: createTxHash, - status, - error - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = - useWaitForTransactionReceipt({ hash: createTxHash }); - const [actionType, setActionType] = useState(ActionType.Signaling) - - const changeActionType = (actionType: ActionType) => { - setActions([]) - setActionType(actionType) + const { push } = useRouter(); + const [title, setTitle] = useState(""); + const [summary, setSummary] = useState(""); + const [actions, setActions] = useState([]); + const { addAlert } = useAlertContext(); + const { + writeContract: createProposalWrite, + data: createTxHash, + status, + error, + } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash: createTxHash }); + const [actionType, setActionType] = useState( + ActionType.Signaling + ); + + const changeActionType = (actionType: ActionType) => { + setActions([]); + setActionType(actionType); + }; + + useEffect(() => { + if (status === "idle" || status === "pending") return; + else if (status === "error") { + if (error?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + addAlert("Could not create the proposal", { type: "error" }); + } + return; } - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert({ - message: "Transaction rejected by the user", - type: "error", - timeout: 4 * 1000, - }); - } else { - addErrorAlert("Could not create the proposal"); - } - return; + // success + if (!createTxHash) return; + else if (isConfirming) { + addAlert("Proposal submitted", { + description: "Waiting for the transaction to be validated", + txHash: createTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert("Proposal created", { + description: "The transaction has been validated", + type: "success", + txHash: createTxHash, + }); + + setTimeout(() => { + push("#/"); + }, 1000 * 2); + }, [status, createTxHash, isConfirming, isConfirmed]); + + const submitProposal = async () => { + // Check metadata + if (!title.trim()) + return addAlert("Please, enter a title", { type: "error" }); + + const plainSummary = getPlainText(summary).trim(); + if (!plainSummary.trim()) + return addAlert("Please, enter a summary of what the proposal is about", { + type: "error", + }); + + // Check the action + switch (actionType) { + case ActionType.Signaling: + break; + case ActionType.Withdrawal: + if (!actions.length) { + return addAlert( + "Please ensure that the withdrawal address and the amount to transfer are valid", + { type: "error" } + ); } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert({ - message: "Proposal submitted", - description: "Waiting for the transaction to be validated", - type: "info", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert({ - message: "Proposal created", - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - - setTimeout(() => { - push("#/"); - }, 1000 * 2); - }, [status, createTxHash, isConfirming, isConfirmed]); - - const submitProposal = async () => { - // Check metadata - if (!title.trim()) return addErrorAlert("Please, enter a title"); - - const plainSummary = getPlainText(summary).trim() - if (!plainSummary.trim()) return addErrorAlert("Please, enter a summary of what the proposal is about"); - - // Check the action - switch (actionType) { - case ActionType.Signaling: break; - case ActionType.Withdrawal: - if (!actions.length) { - return addErrorAlert("Please ensure that the withdrawal address and the amount to transfer are valid"); - } - break - default: - if (!actions.length || !actions[0].data || actions[0].data === "0x") { - return addErrorAlert("Please ensure that the values of the action to execute are correct"); - } + break; + default: + if (!actions.length || !actions[0].data || actions[0].data === "0x") { + return addAlert( + "Please ensure that the values of the action to execute are correct", + { type: "error" } + ); } - - const proposalMetadataJsonObject = { title, summary }; - const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], { type: 'application/json' }); - - const ipfsPin = await uploadToIPFS(ipfsClient, blob); - createProposalWrite({ - abi: TokenVotingAbi, - address: PUB_TOKEN_VOTING_PLUGIN_ADDRESS, - functionName: 'createProposal', - args: [toHex(ipfsPin), actions, 0, 0, 0, 0, 0], - }) } - const handleTitleInput = (event: React.ChangeEvent) => { - setTitle(event?.target?.value); - }; - - const showLoading = status === "pending" || isConfirming; - - return ( -
-
-

Create Proposal

-
- -
-
- -
-
- Select proposal action -
-
{changeActionType(ActionType.Signaling)}} - className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${actionType === ActionType.Signaling ? 'border-primary-300' : 'border-neutral-100'}`}> - - Signaling -
-
changeActionType(ActionType.Withdrawal)} - className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${actionType === ActionType.Withdrawal ? 'border-primary-300' : 'border-neutral-100'}`}> - - DAO Payment -
-
changeActionType(ActionType.Custom)} - className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${actionType === ActionType.Custom ? 'border-primary-300' : 'border-neutral-100'}`}> - - Custom action -
-
-
- {actionType === ActionType.Withdrawal && ()} - {actionType === ActionType.Custom && ()} -
-
- - - -
- -
-
- - - -
+ const proposalMetadataJsonObject = { title, summary }; + const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], { + type: "application/json", + }); + + const ipfsPin = await uploadToIPFS(ipfsClient, blob); + createProposalWrite({ + abi: TokenVotingAbi, + address: PUB_TOKEN_VOTING_PLUGIN_ADDRESS, + functionName: "createProposal", + args: [toHex(ipfsPin), actions, 0, 0, 0, 0, 0], + }); + }; + + const handleTitleInput = (event: React.ChangeEvent) => { + setTitle(event?.target?.value); + }; + + const showLoading = status === "pending" || isConfirming; + + return ( +
+
+

+ Create Proposal +

+
+ +
+
+ +
+
+ + Select proposal action + +
+
{ + changeActionType(ActionType.Signaling); + }} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Signaling + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + Signaling +
-
- ) -} +
changeActionType(ActionType.Withdrawal)} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Withdrawal + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + DAO Payment + +
+
changeActionType(ActionType.Custom)} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Custom + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + Custom action + +
+
+
+ {actionType === ActionType.Withdrawal && ( + + )} + {actionType === ActionType.Custom && ( + + )} +
+
+ + +
+ +
+
+ + + +
+
+ + ); +} diff --git a/plugins/tokenVoting/pages/proposal.tsx b/plugins/tokenVoting/pages/proposal.tsx index a6c8f6b..85bf7ca 100644 --- a/plugins/tokenVoting/pages/proposal.tsx +++ b/plugins/tokenVoting/pages/proposal.tsx @@ -54,7 +54,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { const [votedOption, setVotedOption] = useState(undefined); const [showVotingModal, setShowVotingModal] = useState(false); const [selectedVoteOption, setSelectedVoteOption] = useState(); - const { addAlert, addErrorAlert } = useAlertContext() as AlertContextProps; + const { addAlert } = useAlertContext() as AlertContextProps; const { address } = useAccount(); const { writeContract: voteWrite, @@ -100,13 +100,11 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { if (status === "idle" || status === "pending") return; else if (status === "error") { if (error?.message?.startsWith("User rejected the request")) { - addAlert({ - message: "Transaction rejected by the user", - type: "error", + addAlert("Transaction rejected by the user", { timeout: 4 * 1000, }); } else { - addErrorAlert("Could not create the proposal"); + addAlert("Could not create the proposal", { type: "error" }); } return; } @@ -114,17 +112,14 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { // success if (!votingTxHash) return; else if (isConfirming) { - addAlert({ - message: "Vote submitted", + addAlert("Vote submitted", { description: "Waiting for the transaction to be validated", - type: "info", txHash: votingTxHash, }); return; } else if (!isConfirmed) return; - addAlert({ - message: "Vote registered", + addAlert("Vote registered", { description: "The transaction has been validated", type: "success", txHash: votingTxHash,