diff --git a/ui/components/nance/FinalReportEditor.tsx b/ui/components/nance/FinalReportEditor.tsx new file mode 100644 index 00000000..bc9c7702 --- /dev/null +++ b/ui/components/nance/FinalReportEditor.tsx @@ -0,0 +1,241 @@ +import { GetMarkdown, SetMarkdown } from '@nance/nance-editor' +import { useProposal } from '@nance/nance-hooks' +import { RequestBudget } from '@nance/nance-sdk' +import { useAddress, useContract } from '@thirdweb-dev/react' +import HatsABI from 'const/abis/Hats.json' +import ProjectsABI from 'const/abis/Project.json' +import ProjectTableABI from 'const/abis/ProjectTable.json' +import { + HATS_ADDRESS, + PROJECT_ADDRESSES, + PROJECT_TABLE_ADDRESSES, + TABLELAND_ENDPOINT, +} from 'const/config' +import { StringParam, useQueryParams } from 'next-query-params' +import dynamic from 'next/dynamic' +import { useContext, useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import toast from 'react-hot-toast' +import { NANCE_SPACE_NAME } from '../../lib/nance/constants' +import { pinBlobOrFile } from '@/lib/ipfs/pinBlobOrFile' +import toastStyle from '@/lib/marketplace/marketplace-utils/toastConfig' +import { FINAL_REPORT_TEMPLATE } from '@/lib/nance' +import useProjectData, { Project } from '@/lib/project/useProjectData' +import ChainContext from '@/lib/thirdweb/chain-context' +import { classNames } from '@/lib/utils/tailwind' +import '@nance/nance-editor/lib/css/dark.css' +import '@nance/nance-editor/lib/css/editor.css' +import Head from '@/components/layout/Head' +import { LoadingSpinner } from '@/components/layout/LoadingSpinner' +import ProposalTitleInput from '@/components/nance/ProposalTitleInput' +import ProjectsDropdown from '@/components/project/ProjectsDropdown' +import EditorMarkdownUpload from './EditorMarkdownUpload' + +type SignStatus = 'idle' | 'loading' | 'success' | 'error' + +type FinalReportEditorProps = { + projectsWithoutReport: Project[] | undefined +} + +let getMarkdown: GetMarkdown +let setMarkdown: SetMarkdown + +const NanceEditor = dynamic( + async () => { + getMarkdown = (await import('@nance/nance-editor')).getMarkdown + setMarkdown = (await import('@nance/nance-editor')).setMarkdown + return import('@nance/nance-editor').then((mod) => mod.NanceEditor) + }, + { + ssr: false, + loading: () => , + } +) + +export default function FinalReportEditor({ + projectsWithoutReport, +}: FinalReportEditorProps) { + const { selectedChain } = useContext(ChainContext) + const address = useAddress() + + //Contracts + const { contract: projectContract } = useContract( + PROJECT_ADDRESSES[selectedChain.slug], + ProjectsABI + ) + const { contract: hatsContract } = useContract(HATS_ADDRESS, HatsABI) + + const [signingStatus, setSigningStatus] = useState('idle') + + const { contract: projectsTableContact } = useContract( + PROJECT_TABLE_ADDRESSES[selectedChain.slug], + ProjectTableABI + ) + + const [{ proposalId }, setQuery] = useQueryParams({ proposalId: StringParam }) + const shouldFetch = !!proposalId + const { data } = useProposal( + { space: NANCE_SPACE_NAME, uuid: proposalId! }, + shouldFetch + ) + const [loadedProposal, setLoadedProposal] = useState(undefined) + + useEffect(() => { + if (projectsWithoutReport) { + setLoadedProposal( + projectsWithoutReport.find((p: any) => p.MDP === Number(proposalId)) + ? data?.data + : undefined + ) + } + }, [projectsWithoutReport, data, proposalId]) + + const reportTitle = loadedProposal?.title + ? loadedProposal?.title + ' Final Report' + : '' + + const [selectedProject, setSelectedProject] = useState() + const { isManager } = useProjectData( + projectContract, + hatsContract, + selectedProject + ) + + useEffect(() => { + if (projectsWithoutReport) { + setSelectedProject( + projectsWithoutReport.find((p) => p.MDP === loadedProposal?.proposalId) + ) + } + }, [projectsWithoutReport, loadedProposal]) + + const methods = useForm({ + mode: 'onBlur', + }) + const { handleSubmit } = methods + + const onSubmit: SubmitHandler = async (formData) => { + console.debug('formData', formData) + + if (!reportTitle || !loadedProposal) { + return toast.error('Please select a project that you are a manager of.', { + style: toastStyle, + }) + } + + try { + const markdown = getMarkdown() + if (!markdown) { + throw new Error('No markdown found') + } + const header = `# ${reportTitle}\n\n` + const fileName = `${reportTitle.replace(/\s+/g, '-')}.md` + const file = new File([header + markdown], fileName, { + type: 'text/markdown', + }) + + const { cid: markdownIpfsHash } = await pinBlobOrFile(file) + + const projectsTableName = await projectsTableContact?.call('getTableName') + const statement = `SELECT * FROM ${projectsTableName} WHERE MDP = ${loadedProposal?.proposalId}` + const projectRes = await fetch( + `${TABLELAND_ENDPOINT}?statement=${statement}` + ) + const projectData = await projectRes.json() + const project = projectData[0] + + await projectsTableContact?.call('updateFinalReportIPFS', [ + project.id, + 'ipfs://' + markdownIpfsHash, + ]) + setSelectedProject(undefined) + setMarkdown(FINAL_REPORT_TEMPLATE) + setLoadedProposal(undefined) + setQuery({ proposalId: undefined }) + + toast.success('Final report uploaded successfully.', { + style: toastStyle, + }) + } catch (err) { + console.log(err) + toast.error('Unable to upload final report, please contact support.', { + style: toastStyle, + }) + } + } + + const buttonsDisabled = + !address || signingStatus === 'loading' || !isManager || !selectedProject + + const setProposalId = function (id: string) { + setQuery({ proposalId: id }) + } + + return ( +
+ + +
+
+
+ { + console.debug('setReportTitle', s) + }} + /> +
+ + +
+
+
+ { + const res = await pinBlobOrFile(val) + return res.url + }} + darkMode={true} + onEditorChange={(m) => {}} + /> +
+ +
+ +
+ {/* Submit buttons */} +
+ {/* SUBMIT */} + +
+
+
+
+
+ ) +} diff --git a/ui/components/nance/ProposalEditor.tsx b/ui/components/nance/ProposalEditor.tsx index f5662f0d..0ecb806c 100644 --- a/ui/components/nance/ProposalEditor.tsx +++ b/ui/components/nance/ProposalEditor.tsx @@ -14,7 +14,7 @@ import { getActionsFromBody, trimActionsFromBody, } from '@nance/nance-sdk' -import { usePrivy } from '@privy-io/react-auth' +import { useAddress } from '@thirdweb-dev/react' import { add, differenceInDays, getUnixTime } from 'date-fns' import { StringParam, useQueryParams } from 'next-query-params' import dynamic from 'next/dynamic' @@ -75,6 +75,7 @@ export type ProposalCache = { export default function ProposalEditor() { const router = useRouter() + const address = useAddress() const [signingStatus, setSigningStatus] = useState('idle') const [attachBudget, setAttachBudget] = useState(false) @@ -141,7 +142,7 @@ export default function ProposalEditor() { let proposal = buildProposal(proposalStatus) if (attachBudget) { - const uuid = uuidGen(); + const uuid = uuidGen() const action: Action = { type: 'Request Budget', payload: formData, @@ -166,7 +167,7 @@ export default function ProposalEditor() { const { wallet } = useAccount() const { signProposalAsync } = useSignProposal(wallet) const { trigger } = useProposalUpload(NANCE_SPACE_NAME, loadedProposal?.uuid) - const buttonsDisabled = !wallet?.linked || signingStatus === 'loading' + const buttonsDisabled = !address || signingStatus === 'loading' const buildProposal = (status: ProposalStatus) => { return { diff --git a/ui/components/project/ProjectsDropdown.tsx b/ui/components/project/ProjectsDropdown.tsx new file mode 100644 index 00000000..ad890772 --- /dev/null +++ b/ui/components/project/ProjectsDropdown.tsx @@ -0,0 +1,76 @@ +import { + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, +} from '@headlessui/react' +import { EllipsisVerticalIcon } from '@heroicons/react/24/outline' +import { Fragment } from 'react' +import { Project } from '@/lib/project/useProjectData' + +type ProjectsDropdownProps = { + projects: Project[] | undefined + selectedProject: Project | undefined + setSelectedProject: (project: Project) => void + setProposalId?: (id: string) => void +} + +export default function ProjectsDropdown({ + projects, + selectedProject, + setSelectedProject, + setProposalId, +}: ProjectsDropdownProps) { + return ( + <> + + +
+
+ +
+ {selectedProject?.name || 'Select Project'} +
+
+ + +
+ + {({ focus }) => ( +
+ {projects?.map((aP: any) => ( + + ))} +
+ )} +
+
+
+
+
+ + ) +} diff --git a/ui/const/config.ts b/ui/const/config.ts index 11255958..853da3f9 100644 --- a/ui/const/config.ts +++ b/ui/const/config.ts @@ -118,17 +118,17 @@ export const CITIZEN_TABLE_NAMES: Index = { } export const PROJECT_ADDRESSES: Index = { - arbitrum: '', + arbitrum: '0xB42017d8Beb6758F99eCbEDcB83EC59F8Fa526EA', sepolia: '0x19124F594c3BbCb82078b157e526B278C8E9EfFc', } export const PROJECT_CREATOR_ADDRESSES: Index = { - arbitrum: '', + arbitrum: '0x15F84Ee204a93FE2B80B2B2d30dAEfA154f4429d', sepolia: '0xd1EfE13758b73F2Db9Ed19921eB756fbe4C26E2D', } export const PROJECT_TABLE_ADDRESSES: Index = { - arbitrum: '', + arbitrum: '0xC6620Aa6010c0FFcDd74bf7D3e569Cc4b2e5B5Db', sepolia: '0x17729AFF287d9873F5610c029A5Db814e428e97a', } @@ -204,7 +204,7 @@ export const MOONDAO_HAT_TREE_IDS: Index = { //Projects Sepolia Hat Tree : https://app.hatsprotocol.xyz/trees/11155111/729 //ProjectsArbitrum Hat Tree : export const PROJECT_HAT_TREE_IDS: Index = { - arbitrum: '', + arbitrum: '0x00000045', sepolia: '0x000002d9', } diff --git a/ui/lib/nance/index.ts b/ui/lib/nance/index.ts index 88ea3023..f714094c 100644 --- a/ui/lib/nance/index.ts +++ b/ui/lib/nance/index.ts @@ -1,7 +1,7 @@ -import { v4 } from "uuid"; +import { v4 } from 'uuid' export function uuidGen(): string { - return v4().replaceAll('-', ''); + return v4().replaceAll('-', '') } export function formatNumberUSStyle( @@ -91,3 +91,73 @@ export const TEMPLATE = `\n*Note: Please remove the italicized instructions befo | :---- | :---- | :---- | :---- | | *Send* | *0* | *ETH* | *TBD* | ` + +export const FINAL_REPORT_TEMPLATE = ` +*The title of the project will be included at the top of the file." + +*\*Please read [Projects System v6: Completion](https://docs.moondao.com/Projects/Project-System#completion) before submitting to understand the process of submitting a project final report. When ready, download this doc as a markdown file (File \> Download \> Markdown (.md)) and then upload and submit it at [https://moondao.com/report](https://moondao.com/report)* + +## Original Proposal + +*To be filled out by the Project Lead.* + +**Link to Original Proposal:** *Link to the original proposal* +**Original Abstract:** *Please include the original abstract from the project proposal.* + +## Results + +*To be filled out by the Project Lead.* + +*For each OKR please copy the exact objective and result as appeared on your original proposal. Make a new outline for each objective and key result and keep it in this format.* + +1. **Objective:** *Original objective as it appears in your initial proposal.* + + **Summary:** *Overall discussion of the objective and how it went.* + **Learnings**: *What went well? What went wrong? How could it be improved?* + **Maintenance**: *Create documentation if there is long-term operation of the work that you created so that our operations team can continue supporting the work.* + **Results:** *Please provide the actual results for each key objective.* + 1. **Key Result**: *Original key result as it appears in your initial proposal.* + **Results**: *The actual quantitative result or a link to the work completed.* + 2. **Key Result**: *Original key result as it appears in your initial proposal.* + **Results**: *The actual quantitative result or a link to the work completed.* + + **Grade (Do not fill out \- Exec Leads will review):** *Overall Grade For The Objective* + *Superb \= This is reserved for if the project went incredibly well without any flaws and vastly surpassed all original metrics. Very few projects will meet this grade.* + + *Exceeds Expectations \= Did better than expected. The team surpassed expectations and went above and beyond the original scope of the project.* + + *Meets Expectations \= The project met all its original goals and sufficiently hit all the criteria.* + + *Does Not Meet Expectations \= The project did not achieve its original goal.* + +## Member Contributions + +*To be filled out by each Project Contributor.* + +**@TeamMemberName1:** *A paragraph or two about the work that the member did on the team and a link to the work they did or contributions.* + +**@TeamMemberName2:** *A paragraph or two about the work that the member did on the team and a link to the work they did or contributions.* + +## Reward Distribution (Table A) + +*To be filled out by the Project Lead* + +[*Link to the Coordinape*](https://coordinape.com/)*: Make the Astronauts the admin of the coordinape circle.* + +| Member Name | % of total rewards | Upfront Payment Received | Wallet to receive ETH | +| :---- | :---- | :---- | :---- | +| *@TeamMemberName* | *21%* | *3,000 DAI, 50,000 MOONEY* | | + +## Treasury Transparency (Table B) + +*To be filled out by the Project Lead* + +*Link to Treasury with **unused funds returned to the [MoonDAO Treasury](https://app.safe.global/home?safe=eth:0xce4a1E86a5c47CD677338f53DA22A91d85cab2c9).*** + +*Arbitrum Address: arb1:0xAF26a002d716508b7e375f1f620338442F5470c0* +*Ethereum Address: eth:0xce4a1E86a5c47CD677338f53DA22A91d85cab2c9* + +| Txn Title | Reason | Amount | Recipient | Etherscan Link or Gnosis Link | Deliverable | +| :---- | :---- | :---- | :---- | :---- | :---- | +| Legal Retainer | Retainer for contract lawyer | 3,000 DAI | Lawyers Inc. | **\** | **\** or if nothing to show please include a description of what was delivered. | +` diff --git a/ui/lib/navigation/useNavigation.tsx b/ui/lib/navigation/useNavigation.tsx index 0fda85ce..1ebe8b49 100644 --- a/ui/lib/navigation/useNavigation.tsx +++ b/ui/lib/navigation/useNavigation.tsx @@ -5,7 +5,7 @@ import { FolderIcon, PlusIcon, RocketLaunchIcon, - WrenchScrewdriverIcon, + WrenchScrewdriverIcon, } from '@heroicons/react/24/outline' import { useMemo } from 'react' import IconOrg from '@/components/assets/IconOrg' @@ -30,7 +30,7 @@ export default function useNavigation(citizen: any) { { name: 'Teams', href: '/teams' }, { name: 'Citizens', href: '/citizens' }, { name: 'Map', href: '/map' }, - ] + ], }, { name: 'Projects', @@ -81,6 +81,16 @@ export default function useNavigation(citizen: any) { name: 'Submit Contribution', href: '/contribute', }, + { + name: 'Submit Final Report', + href: '/submit?tag=report', + }, + { + name: 'Get $MOONEY', + href: '/get-mooney', + }, + { name: 'Get Voting Power', href: '/lock' }, + { name: 'Bridge', href: '/bridge' }, ], }, { diff --git a/ui/next.config.js b/ui/next.config.js index b5d99af3..f38e45ba 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -223,6 +223,11 @@ module.exports = nextTranslate({ destination: '/project', permanent: true, }, + { + source: '/report', + destination: '/submit?tag=report', + permanent: true, + }, ] }, webpack: (config, { isServer }) => { diff --git a/ui/pages/submit.tsx b/ui/pages/submit.tsx index 904e8886..94aa99b0 100644 --- a/ui/pages/submit.tsx +++ b/ui/pages/submit.tsx @@ -1,41 +1,53 @@ import { Tab } from '@headlessui/react' import { NanceProvider } from '@nance/nance-hooks' +import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' +import ProjectTableABI from 'const/abis/ProjectTable.json' +import { PROJECT_TABLE_ADDRESSES, TABLELAND_ENDPOINT } from 'const/config' +import { StringParam, useQueryParams } from 'next-query-params' import Image from 'next/image' import Link from 'next/link' -import { useRouter } from 'next/router' import React, { useEffect, useState } from 'react' import { NANCE_API_URL } from '../lib/nance/constants' -import { useShallowQueryRoute } from '@/lib/utils/hooks' +import { Project } from '@/lib/project/useProjectData' +import { initSDK } from '@/lib/thirdweb/thirdweb' import ContributionEditor from '../components/contribution/ContributionEditor' import Container from '../components/layout/Container' import ContentLayout from '../components/layout/ContentLayout' import WebsiteHead from '../components/layout/Head' import { NoticeFooter } from '../components/layout/NoticeFooter' import ProposalEditor from '../components/nance/ProposalEditor' +import FinalReportEditor from '@/components/nance/FinalReportEditor' + +export default function SubmissionPage({ + projectsWithoutReport, +}: { + projectsWithoutReport: Project[] | undefined +}) { + const [{ tag }, setQuery] = useQueryParams({ tag: StringParam }) -const SubmissionPage: React.FC = () => { - const router = useRouter() - const { tag } = router.query - const shallowQueryRoute = useShallowQueryRoute() const [selectedIndex, setSelectedIndex] = useState(0) const title = 'Collaborate with MoonDAO' useEffect(() => { - if (selectedIndex === 1) { - shallowQueryRoute({ tag: 'contribution' }) - } else if (selectedIndex === 0) { - shallowQueryRoute({ tag: 'proposal' }) - } - }, [selectedIndex]) - - useEffect(() => { - if (tag === 'contribution') { + if (tag === 'report') { + setSelectedIndex(2) + } else if (tag === 'contribution') { setSelectedIndex(1) - } else if (!tag || tag === 'proposal') { + } else { setSelectedIndex(0) } }, [tag]) + useEffect(() => { + if (selectedIndex === 2 && tag !== 'report') { + setQuery({ tag: 'report' }, 'replaceIn') + } else if (selectedIndex === 1 && tag !== 'contribution') { + setQuery({ tag: 'contribution' }, 'replaceIn') + } else if (selectedIndex === 0 && tag !== undefined) { + setQuery({ tag: undefined }, 'replaceIn') + } + }, [selectedIndex]) + return ( <> @@ -79,6 +91,18 @@ const SubmissionPage: React.FC = () => { > Submit Contribution + + `rounded-lg py-2.5 font-GoodTimes leading-5 px-5 focus:outline-none + ${ + selected + ? 'bg-gradient-to-r from-[#5757ec] to-[#6b3d79] text-white shadow' + : 'text-white/70 hover:text-white' + }` + } + > + Submit Report + @@ -186,6 +210,16 @@ const SubmissionPage: React.FC = () => { + +
+

+ Submit a final report for your project. +

+
+ +
@@ -197,4 +231,26 @@ const SubmissionPage: React.FC = () => { ) } -export default SubmissionPage +export async function getStaticProps() { + const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + const sdk = initSDK(chain) + + const projectTableContract = await sdk.getContract( + PROJECT_TABLE_ADDRESSES[chain.slug], + ProjectTableABI + ) + const projectTableName = await projectTableContract?.call('getTableName') + + const statement = `SELECT * FROM ${projectTableName} WHERE finalReportIPFS IS ""` + const projectsRes = await fetch( + `${TABLELAND_ENDPOINT}?statement=${statement}` + ) + const projects = await projectsRes.json() + + return { + props: { + projectsWithoutReport: projects, + }, + revalidate: 60, + } +}