From 41585a3851dab289400bc401b61eeac34d9eb22e Mon Sep 17 00:00:00 2001 From: namedotget Date: Mon, 30 Dec 2024 18:32:32 -0600 Subject: [PATCH 01/25] Adjusted styling --- ui/components/subscription/Action.tsx | 2 +- ui/components/subscription/CitizenActions.tsx | 2 +- ui/components/subscription/GuestActions.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/components/subscription/Action.tsx b/ui/components/subscription/Action.tsx index 8b4e0fc21..a04c84c16 100644 --- a/ui/components/subscription/Action.tsx +++ b/ui/components/subscription/Action.tsx @@ -15,7 +15,7 @@ export default function Action({ }: ActionProps) { return ( + + + + ) +} diff --git a/ui/const/whitelist.ts b/ui/const/whitelist.ts index 4252206df..fd8cee255 100644 --- a/ui/const/whitelist.ts +++ b/ui/const/whitelist.ts @@ -1,6 +1,7 @@ //https://docs.google.com/spreadsheets/d/1LsYxkI_1alFUD_NxM2a5RngeSRC5e5ElUpP6aR30DiM/edit?gid=0#gid=0 export const blockedTeams: any = [] export const blockedCitizens: any = [48, 72] +export const blockedProjects: any = [] export const featuredTeams: any = [ '6', diff --git a/ui/lib/navigation/useNavigation.tsx b/ui/lib/navigation/useNavigation.tsx index 2a20b5985..5f974fd59 100644 --- a/ui/lib/navigation/useNavigation.tsx +++ b/ui/lib/navigation/useNavigation.tsx @@ -1,6 +1,7 @@ import { BuildingLibraryIcon, ClipboardDocumentListIcon, + DocumentIcon, FolderIcon, PlusIcon, RocketLaunchIcon, @@ -25,6 +26,11 @@ export default function useNavigation(citizen: any) { href: '/network', icon: IconOrg, }, + { + name: 'Projects', + href: '/project', + icon: DocumentIcon, + }, { name: 'Info', icon: FolderIcon, diff --git a/ui/lib/project/useProjectData.tsx b/ui/lib/project/useProjectData.tsx new file mode 100644 index 000000000..38d3b2d6a --- /dev/null +++ b/ui/lib/project/useProjectData.tsx @@ -0,0 +1,39 @@ +import { useAddress } from '@thirdweb-dev/react' +import { useState } from 'react' +import { useHandleRead } from '@/lib/thirdweb/hooks' +import { getAttribute } from '@/lib//utils/nft' + +export default function useProjectData( + projectContract: any, + hatsContract: any, + nft: any +) { + const address = useAddress() + + const [isLoading, setIsLoading] = useState(false) + const [isActive, setIsActive] = useState(false) + const [isManager, setIsManager] = useState(false) + const [subIsValid, setSubIsValid] = useState(true) + const [hatTreeId, setHatTreeId] = useState() + + const { data: adminHatId } = useHandleRead( + projectContract, + 'projectAdminHat', + [nft?.metadata?.id || ''] + ) + + const { data: managerHatId } = useHandleRead( + projectContract, + 'projectManagerHat', + [nft?.metadata?.id || ''] + ) + + return { + isLoading, + isActive, + isManager, + subIsValid, + adminHatId, + managerHatId, + } +} diff --git a/ui/pages/project/[tokenId].tsx b/ui/pages/project/[tokenId].tsx new file mode 100644 index 000000000..d4fbfe53a --- /dev/null +++ b/ui/pages/project/[tokenId].tsx @@ -0,0 +1,475 @@ +import { PencilIcon } from '@heroicons/react/24/outline' +import { + ThirdwebNftMedia, + useAddress, + useContract, + useContractRead, + useSDK, +} from '@thirdweb-dev/react' +import TeamABI from 'const/abis/Team.json' +import { + CITIZEN_ADDRESSES, + TEAM_ADDRESSES, + HATS_ADDRESS, + MOONEY_ADDRESSES, + ZERO_ADDRESS, +} from 'const/config' +import { GetServerSideProps } from 'next' +import Image from 'next/image' +import { useContext, useEffect, useState } from 'react' +import toast from 'react-hot-toast' +import { useSubHats } from '@/lib/hats/useSubHats' +import useProjectData from '@/lib/project/useProjectData' +import ChainContext from '@/lib/thirdweb/chain-context' +import { useChainDefault } from '@/lib/thirdweb/hooks/useChainDefault' +import { useMOONEYBalance } from '@/lib/tokens/mooney-token' +import Address from '@/components/layout/Address' +import Container from '@/components/layout/Container' +import ContentLayout from '@/components/layout/ContentLayout' +import Frame from '@/components/layout/Frame' +import Head from '@/components/layout/Head' +import { NoticeFooter } from '@/components/layout/NoticeFooter' +import SlidingCardMenu from '@/components/layout/SlidingCardMenu' +import GeneralActions from '@/components/subscription/GeneralActions' +import { SubscriptionModal } from '@/components/subscription/SubscriptionModal' +import TeamDonation from '@/components/subscription/TeamDonation' +import TeamManageMembers from '@/components/subscription/TeamManageMembers' +import TeamMembers from '@/components/subscription/TeamMembers' +import TeamMetadataModal from '@/components/subscription/TeamMetadataModal' +import TeamTreasury from '@/components/subscription/TeamTreasury' +import ProjectActions from '@/components/subscription/project/ProjectActions' + +export default function ProjectProfile({ tokenId, nft, imageIpfsLink }: any) { + const sdk = useSDK() + const address = useAddress() + + const { selectedChain } = useContext(ChainContext) + + //Modal states + const [teamMetadataModalEnabled, setTeamMetadataModalEnabled] = + useState(false) + const [teamSubscriptionModalEnabled, setTeamSubscriptionModalEnabled] = + useState(false) + + //Contracts + const { contract: hatsContract } = useContract(HATS_ADDRESS) + const { contract: projectContract } = useContract( + TEAM_ADDRESSES[selectedChain.slug], + TeamABI + ) + const { contract: citizenConract } = useContract( + CITIZEN_ADDRESSES[selectedChain.slug] + ) + const { contract: mooneyContract } = useContract( + MOONEY_ADDRESSES[selectedChain.slug] + ) + const { data: MOONEYBalance } = useMOONEYBalance(mooneyContract, nft?.owner) + + const { + adminHatId, + managerHatId, + isManager, + subIsValid, + isActive, + isLoading: isLoadingProjectData, + } = useProjectData(projectContract, hatsContract, nft) + //Hats + const hats = useSubHats(selectedChain, adminHatId) + + const [nativeBalance, setNativeBalance] = useState(0) + + //Subscription Data + const { data: expiresAt } = useContractRead(projectContract, 'expiresAt', [ + nft?.metadata?.id, + ]) + + // get native balance for multisigj + useEffect(() => { + async function getNativeBalance() { + const provider = sdk?.getProvider() + const balance: any = await provider?.getBalance(nft?.owner as string) + setNativeBalance(+(balance.toString() / 10 ** 18).toFixed(5)) + } + + if (sdk && nft?.owner) { + getNativeBalance() + } + }, [sdk, nft]) + + useChainDefault() + + //Profile Header Section + const ProfileHeader = ( +
+ +
+
+
+
+ {nft?.metadata.image ? ( +
+ +
+ +
+
+ ) : ( + <> + )} +
+
+
+ {subIsValid && isManager && ( + + )} + {nft ? ( +

+ {nft.metadata.name} +

+ ) : ( + <> + )} +
+
+
+ {nft?.metadata.description ? ( +

+ {nft?.metadata.description || ''} +

+ ) : ( + <> + )} + +
+ {isManager || address === nft.owner ? ( + '' + ) : ( +
+ {!isActive && subIsValid && ( + + )} +
+ )} + + {/*Subscription Extension Container*/} + {/* {isManager || address === nft.owner ? ( +
+ {expiresAt && ( +
+
+
+ +
+
+
+ )} +
+ ) : ( + <> + )} */} +
+ {/* {isManager || address === nft.owner ? ( +

+ {'Exp: '} + {new Date(expiresAt?.toString() * 1000).toLocaleString()} +

+ ) : ( + <> + )} */} +
+
+
+
+
+
+
+
+ +
+ ) + + const teamIcon = '/./assets/icon-team.svg' + + return ( + + + {teamSubscriptionModalEnabled && ( + + )} + {teamMetadataModalEnabled && ( + + )} + } + > +
+
+
+ +
+
+ + {/* Header and socials */} + {subIsValid ? ( +
+ {/* Team Actions */} + {/* Team */} + +
+
+
+ Job icon +

Meet the Team

+
+ {isManager && ( +
+ {/* { + window.open( + `https://app.hatsprotocol.xyz/trees/${selectedChain.chainId}/${hatTreeId}` + ) + }} + > + Manage Members + */} + +
+ )} +
+ + +
+ {hats?.[0].id && ( + + )} +
+
+
+ + {/* Mooney and Voting Power */} + {isManager && ( + + )} + {/* General Actions */} + {isManager && } +
+ ) : ( + // Subscription Expired + + {isManager && ( + + )} + + )} +
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + // const tokenId: any = params?.tokenId + + // const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + // const sdk = initSDK(chain) + + // if (tokenId === undefined) { + // return { + // notFound: true, + // } + // } + + // const teamContract = await sdk.getContract( + // TEAM_ADDRESSES[chain.slug], + // TeamABI + // ) + // const nft = await teamContract.erc721.get(tokenId) + + // if ( + // !nft || + // !nft.metadata.uri || + // blockedTeams.includes(Number(nft.metadata.id)) + // ) { + // return { + // notFound: true, + // } + // } + + // const rawMetadataRes = await fetch(nft.metadata.uri) + // const rawMetadata = await rawMetadataRes.json() + // const imageIpfsLink = rawMetadata.image + + const tokenId = '0' + const imageIpfsLink = + 'https://ipfs.io/ipfs/QmQrX8HmgQAgVsWJdcqM4D5X85yo25hSt3jvXWv5Ytf5gG' + + const nft = { + owner: ZERO_ADDRESS, + metadata: { + name: 'Project #1', + description: 'Project #1 Description', + image: + 'https://ipfs.io/ipfs/QmQrX8HmgQAgVsWJdcqM4D5X85yo25hSt3jvXWv5Ytf5gG', + id: '0', + + attributes: [ + { + trait_type: 'website', + value: 'Project Website', + }, + ], + }, + } + + return { + props: { + nft, + tokenId, + imageIpfsLink, + }, + } +} diff --git a/ui/pages/project/index.tsx b/ui/pages/project/index.tsx new file mode 100644 index 000000000..a2d869e11 --- /dev/null +++ b/ui/pages/project/index.tsx @@ -0,0 +1,309 @@ +import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' +import { NFT } from '@thirdweb-dev/react' +import TeamABI from 'const/abis/Team.json' +import { TEAM_ADDRESSES } from 'const/config' +import { blockedProjects } from 'const/whitelist' +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' +import React, { useState, useEffect, useCallback } from 'react' +import { useChainDefault } from '@/lib/thirdweb/hooks/useChainDefault' +import { initSDK } from '@/lib/thirdweb/thirdweb' +import { useShallowQueryRoute } from '@/lib/utils/hooks' +import { getAttribute } from '@/lib/utils/nft' +import Card from '@/components/layout/Card' +import CardGridContainer from '@/components/layout/CardGridContainer' +import CardSkeleton from '@/components/layout/CardSkeleton' +import Container from '@/components/layout/Container' +import ContentLayout from '@/components/layout/ContentLayout' +import Frame from '@/components/layout/Frame' +import Head from '@/components/layout/Head' +import { NoticeFooter } from '@/components/layout/NoticeFooter' +import Search from '@/components/layout/Search' +import StandardButton from '@/components/layout/StandardButton' +import Tab from '@/components/layout/Tab' + +type NetworkProps = { + activeProjects: NFT[] + inactiveProjects: NFT[] +} + +export default function Projects({ + activeProjects, + inactiveProjects, +}: NetworkProps) { + const router = useRouter() + const shallowQueryRoute = useShallowQueryRoute() + + const [input, setInput] = useState('') + function filterBySearch(nfts: NFT[]) { + return nfts.filter((nft) => { + return nft.metadata.name + ?.toString() + .toLowerCase() + .includes(input.toLowerCase()) + }) + } + + const [tab, setTab] = useState('active') + function loadByTab(tab: string) { + if (tab === 'active') { + setCachedNFTs( + input != '' ? filterBySearch(activeProjects) : activeProjects + ) + } else if (tab === 'inactive') { + setCachedNFTs( + input != '' ? filterBySearch(inactiveProjects) : inactiveProjects + ) + } else { + const nfts = + activeProjects?.[0] && inactiveProjects?.[0] + ? [...activeProjects, ...inactiveProjects] + : activeProjects?.[0] + ? activeProjects + : inactiveProjects?.[0] + ? inactiveProjects + : [] + setCachedNFTs(input != '' ? filterBySearch(nfts) : nfts) + } + } + + const handleTabChange = useCallback( + (newTab: string) => { + setTab(newTab) + setPageIdx(1) + shallowQueryRoute({ tab: newTab, page: '1' }) + }, + [shallowQueryRoute] + ) + + const handlePageChange = useCallback( + (newPage: number) => { + setPageIdx(newPage) + shallowQueryRoute({ tab, page: newPage.toString() }) + }, + [shallowQueryRoute, tab] + ) + + const [maxPage, setMaxPage] = useState(1) + + useEffect(() => { + const totalActiveProjects = + input != '' + ? filterBySearch(activeProjects).length + : activeProjects.length + const totalInactiveProjects = + input != '' + ? filterBySearch(inactiveProjects).length + : inactiveProjects.length + + if (tab === 'active') setMaxPage(Math.ceil(totalActiveProjects / 9)) + if (tab === 'inactive') setMaxPage(Math.ceil(totalInactiveProjects / 9)) + }, [tab, input, activeProjects, inactiveProjects]) + + const [cachedNFTs, setCachedNFTs] = useState([]) + + const [pageIdx, setPageIdx] = useState(1) + + useEffect(() => { + const { tab: urlTab, page: urlPage } = router.query + if (urlTab && (urlTab === 'active' || urlTab === 'inactive')) { + setTab(urlTab as string) + } + if (urlPage && !isNaN(Number(urlPage))) { + setPageIdx(Number(urlPage)) + } + }, [router.query]) + + useEffect(() => { + loadByTab(tab) + }, [tab, input, activeProjects, inactiveProjects, router.query]) + + useChainDefault() + + const descriptionSection = ( +
+
MoonDAO Projects.
+ + + +
+
+ +
+ + Active + + + Inactive + +
+ +
+
+
+ ) + + return ( +
+ + + } + mainPadding + mode="compact" + popOverEffect={false} + isProfile + > + <> + + {cachedNFTs?.[0] ? ( + cachedNFTs + ?.slice((pageIdx - 1) * 9, pageIdx * 9) + .map((nft: any, I: number) => { + if (nft.metadata.name !== 'Failed to load NFT metadata') { + const type = nft.metadata.attributes.find( + (attr: any) => attr.trait_type === 'communications' + ) + ? 'team' + : 'citizen' + return ( +
+ +
+ ) + } + }) + ) : ( + <> + {Array.from({ length: 9 }).map((_, i) => ( + + ))} + + )} +
+ +
+ +

+ Page {pageIdx} of {maxPage} +

+ +
+ + +
+
+
+ ) +} + +export async function getStaticProps() { + const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + const sdk = initSDK(chain) + const now = Math.floor(Date.now() / 1000) + + const projectContract = await sdk.getContract( + TEAM_ADDRESSES[chain.slug], + TeamABI + ) + const totalProjects = await projectContract.call('totalSupply') + + const activeProjects = [] + const inactiveProjects = [] + for (let i = 0; i < totalProjects; i++) { + if (!blockedProjects.includes(i)) { + const project = await projectContract.erc721.get(i) + const expiresAt = await projectContract.call('expiresAt', [ + project?.metadata?.id, + ]) + if (expiresAt.toNumber() > now) { + const active = getAttribute( + project.metadata.attributes as any[], + 'active' + )?.value + if (active === '0') { + inactiveProjects.push(project) + } else if (active === '1' || !active) { + activeProjects.push(project) + } + } + } + } + + return { + props: { + activeProjects: activeProjects.reverse(), + inactiveProjects: inactiveProjects.reverse(), + }, + revalidate: 60, + } +} From 47ea56c3342b2196dd1aba3f52a1e3159a36a62f Mon Sep 17 00:00:00 2001 From: namedotget Date: Fri, 3 Jan 2025 11:17:25 -0600 Subject: [PATCH 05/25] wip --- ui/const/config.ts | 16 +++++++-- ui/pages/project/[tokenId].tsx | 59 +++++++++++++++++----------------- ui/pages/project/index.tsx | 31 ++++++++---------- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/ui/const/config.ts b/ui/const/config.ts index 1252d0690..c44a9da86 100644 --- a/ui/const/config.ts +++ b/ui/const/config.ts @@ -117,11 +117,21 @@ export const CITIZEN_TABLE_NAMES: Index = { sepolia: 'CITIZENTABLE_11155111_1671', } +export const PROJECT_ADDRESSES: Index = { + arbitrum: '', + sepolia: '0x9016Ac821055d1E32a5E18d43493faf601b9E756', +} + +export const PROJECT_CREATOR_ADDRESSES: Index = { + arbitrum: '', + sepolia: '0xb1089317f7338860B29fC6d324E460E8FA2333da', +} + export const PROJECT_TABLE_ADDRESSES: Index = { - arbitrum: '0x2E8e4B7DAf62868d3184E691f3Cd5Bd9c069cAe1', - 'arbitrum-sepolia': '0x7876d5a6050fE861B8b0A96FF37B34116C9D0636', - sepolia: '0x4eC886128E970eEef0230a775cA1fD46f9383C27', + arbitrum: '', + sepolia: '0xC391008458004e33bED39EF2c2539857006c0c74', } + export const COMPETITOR_TABLE_ADDRESSES: Index = { sepolia: '0x9057Fff69e8b016a214C4f894430F71dad50b42c', } diff --git a/ui/pages/project/[tokenId].tsx b/ui/pages/project/[tokenId].tsx index d4fbfe53a..9a3c3725a 100644 --- a/ui/pages/project/[tokenId].tsx +++ b/ui/pages/project/[tokenId].tsx @@ -30,6 +30,7 @@ import Frame from '@/components/layout/Frame' import Head from '@/components/layout/Head' import { NoticeFooter } from '@/components/layout/NoticeFooter' import SlidingCardMenu from '@/components/layout/SlidingCardMenu' +import Button from '@/components/subscription/Button' import GeneralActions from '@/components/subscription/GeneralActions' import { SubscriptionModal } from '@/components/subscription/SubscriptionModal' import TeamDonation from '@/components/subscription/TeamDonation' @@ -209,42 +210,42 @@ export default function ProjectProfile({ tokenId, nft, imageIpfsLink }: any) { )} {/*Subscription Extension Container*/} - {/* {isManager || address === nft.owner ? ( -
- {expiresAt && ( -
+ {isManager || address === nft.owner ? ( +
+ {expiresAt && ( +
+
-
{ + setTeamSubscriptionModalEnabled(true) + }} > - -
+ {'Extend Plan'} +
- )} -
- ) : ( - <> - )} */} -
- {/* {isManager || address === nft.owner ? ( -

- {'Exp: '} - {new Date(expiresAt?.toString() * 1000).toLocaleString()} -

+
+ )} +
) : ( <> - )} */} + )} + + {isManager || address === nft.owner ? ( +

+ {'Exp: '} + {new Date(expiresAt?.toString() * 1000).toLocaleString()} +

+ ) : ( + <> + )}
diff --git a/ui/pages/project/index.tsx b/ui/pages/project/index.tsx index a2d869e11..b866d7594 100644 --- a/ui/pages/project/index.tsx +++ b/ui/pages/project/index.tsx @@ -1,10 +1,9 @@ import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' import { NFT } from '@thirdweb-dev/react' import TeamABI from 'const/abis/Team.json' -import { TEAM_ADDRESSES } from 'const/config' +import { PROJECT_ADDRESSES, TEAM_ADDRESSES } from 'const/config' import { blockedProjects } from 'const/whitelist' import Image from 'next/image' -import Link from 'next/link' import { useRouter } from 'next/router' import React, { useState, useEffect, useCallback } from 'react' import { useChainDefault } from '@/lib/thirdweb/hooks/useChainDefault' @@ -20,7 +19,6 @@ import Frame from '@/components/layout/Frame' import Head from '@/components/layout/Head' import { NoticeFooter } from '@/components/layout/NoticeFooter' import Search from '@/components/layout/Search' -import StandardButton from '@/components/layout/StandardButton' import Tab from '@/components/layout/Tab' type NetworkProps = { @@ -269,32 +267,29 @@ export default function Projects({ export async function getStaticProps() { const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia const sdk = initSDK(chain) - const now = Math.floor(Date.now() / 1000) const projectContract = await sdk.getContract( - TEAM_ADDRESSES[chain.slug], + PROJECT_ADDRESSES[chain.slug], TeamABI ) const totalProjects = await projectContract.call('totalSupply') + console.log(totalProjects.toString()) + const activeProjects = [] const inactiveProjects = [] for (let i = 0; i < totalProjects; i++) { if (!blockedProjects.includes(i)) { const project = await projectContract.erc721.get(i) - const expiresAt = await projectContract.call('expiresAt', [ - project?.metadata?.id, - ]) - if (expiresAt.toNumber() > now) { - const active = getAttribute( - project.metadata.attributes as any[], - 'active' - )?.value - if (active === '0') { - inactiveProjects.push(project) - } else if (active === '1' || !active) { - activeProjects.push(project) - } + + const active = getAttribute( + project.metadata.attributes as any[], + 'active' + )?.value + if (active === '0') { + inactiveProjects.push(project) + } else if (active === '1' || active) { + activeProjects.push(project) } } } From 0b5a32f03e0651b5c5829e78034c3dce2a3d428e Mon Sep 17 00:00:00 2001 From: namedotget Date: Fri, 3 Jan 2025 14:33:52 -0600 Subject: [PATCH 06/25] Refactor section card --- .../dashboard/analytics/AnalyticsPage.tsx | 20 +++++-------------- ui/components/layout/SectionCard.tsx | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 ui/components/layout/SectionCard.tsx diff --git a/ui/components/dashboard/analytics/AnalyticsPage.tsx b/ui/components/dashboard/analytics/AnalyticsPage.tsx index 486cbddfb..80aad722e 100644 --- a/ui/components/dashboard/analytics/AnalyticsPage.tsx +++ b/ui/components/dashboard/analytics/AnalyticsPage.tsx @@ -1,22 +1,12 @@ import useTranslation from 'next-translate/useTranslation' import React, { useMemo, useState } from 'react' import { useAssets } from '../../../lib/dashboard/hooks' +import SectionCard from '@/components/layout/SectionCard' import AnalyticsChainSelector from './AnalyticsChainSelector' import { AnalyticsProgress } from './AnalyticsProgress' import AnalyticsSkeleton from './AnalyticsSkeleton' import BarChart from './BarChart' -function Frame(props: any) { - return ( -
- {props.children} -
- ) -} - function Data({ text, value }: any) { return (
@@ -51,15 +41,15 @@ export default function AnalyticsPage({ vMooneyData }: any) { return ( <> - +

{'Governance Power Over Time'}

- - +
+

{'Voting Power Key Figures'} @@ -97,7 +87,7 @@ export default function AnalyticsPage({ vMooneyData }: any) {

- + ) } diff --git a/ui/components/layout/SectionCard.tsx b/ui/components/layout/SectionCard.tsx new file mode 100644 index 000000000..d01c430df --- /dev/null +++ b/ui/components/layout/SectionCard.tsx @@ -0,0 +1,20 @@ +type SectionCardProps = { + id?: string + className?: string + children: React.ReactNode +} + +export default function SectionCard({ + id, + className = '', + children, +}: SectionCardProps) { + return ( +
+ {children} +
+ ) +} From ba77419ea8684e8c62334489c750a7a8e5d03459 Mon Sep 17 00:00:00 2001 From: namedotget Date: Fri, 3 Jan 2025 14:34:28 -0600 Subject: [PATCH 07/25] Started implementing new design for /rewards --- ui/components/nance/RetroactiveRewards.tsx | 126 ++++++++++++++++++++- ui/public/assets/plus-icon.png | Bin 0 -> 453 bytes 2 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 ui/public/assets/plus-icon.png diff --git a/ui/components/nance/RetroactiveRewards.tsx b/ui/components/nance/RetroactiveRewards.tsx index 12de480f8..9c560245a 100644 --- a/ui/components/nance/RetroactiveRewards.tsx +++ b/ui/components/nance/RetroactiveRewards.tsx @@ -1,18 +1,24 @@ -import { Arbitrum, ArbitrumSepolia } from '@thirdweb-dev/chains' +import { PlusCircleIcon } from '@heroicons/react/24/outline' +import { Arbitrum, ArbitrumSepolia, Ethereum } from '@thirdweb-dev/chains' import { useAddress, useContract } from '@thirdweb-dev/react' +import { nativeOnChain } from '@uniswap/smart-order-router' import { DISTRIBUTION_TABLE_ADDRESSES, SNAPSHOT_RETROACTIVE_REWARDS_ID, } from 'const/config' import _ from 'lodash' +import Image from 'next/image' import { useState, useEffect } from 'react' import toast from 'react-hot-toast' import { useCitizens } from '@/lib/citizen/useCitizen' +import { assetImageExtension } from '@/lib/dashboard/dashboard-utils.ts/asset-config' import { useAssets } from '@/lib/dashboard/hooks' import toastStyle from '@/lib/marketplace/marketplace-utils/toastConfig' import { SNAPSHOT_SPACE_NAME } from '@/lib/nance/constants' import { useVotingPowers } from '@/lib/snapshot' import useWindowSize from '@/lib/team/use-window-size' +import { useUniswapTokens } from '@/lib/uniswap/hooks/useUniswapTokens' +import { pregenSwapRoute } from '@/lib/uniswap/pregenSwapRoute' import { getBudget, getPayouts } from '@/lib/utils/rewards' import { computeRewardPercentages } from '@/lib/utils/voting' import Asset from '@/components/dashboard/treasury/balance/Asset' @@ -20,7 +26,8 @@ import Container from '@/components/layout/Container' import ContentLayout from '@/components/layout/ContentLayout' import Head from '@/components/layout/Head' import { NoticeFooter } from '@/components/layout/NoticeFooter' -import StandardButton from '../layout/StandardButton' +import SectionCard from '@/components/layout/SectionCard' +import StandardButton from '@/components/layout/StandardButton' export type Project = { id: string @@ -36,12 +43,53 @@ export type Distribution = { distribution: { [key: string]: number } } +type RewardAssetProps = { + name: string + value: string + usdValue: string + approximateUSD?: boolean +} + export type RetroactiveRewardsProps = { projects: Project[] distributions: Distribution[] refreshRewards: () => void } +function RewardAsset({ + name, + value, + usdValue, + approximateUSD, +}: RewardAssetProps) { + const image = assetImageExtension[name] + ? `/coins/${name}.${assetImageExtension[name]}` + : '/coins/DEFAULT.png' + + return ( +
+ {name} +
+
+

{name}

+

{value}

+
+ {Number(usdValue) > 0 && ( +

{`(${ + approximateUSD ? '~' : '' + }${usdValue})`}

+ )} +
+
+ ) +} + export function RetroactiveRewards({ projects, distributions, @@ -53,7 +101,7 @@ export function RetroactiveRewards({ const userAddress = useAddress() const year = new Date().getFullYear() - const quarter = Math.floor((new Date().getMonth() + 3) / 3) - 1 + const quarter = Math.ceil((new Date().getMonth() + 1) / 3) const [edit, setEdit] = useState(false) const [distribution, setDistribution] = useState<{ [key: string]: number }>( @@ -131,6 +179,22 @@ export function RetroactiveRewards({ year, quarter ) + const [mooneyBudgetUSD, setMooneyBudgetUSD] = useState(0) + const { MOONEY, DAI } = useUniswapTokens(Ethereum) + + useEffect(() => { + async function getMooneyBudgetUSD() { + const route = await pregenSwapRoute(Ethereum, mooneyBudget, MOONEY, DAI) + + const usd = route?.route[0].rawQuote.toString() / 1e18 + setMooneyBudgetUSD(usd) + } + + if (mooneyBudget) { + getMooneyBudgetUSD() + } + }, [mooneyBudget]) + const { projectIdToETHPayout, projectIdToMooneyPayout, @@ -214,7 +278,7 @@ export function RetroactiveRewards({ /> } @@ -223,6 +287,56 @@ export function RetroactiveRewards({ popOverEffect={false} isProfile > + +

{`Q${quarter}: ${year} Rewards`}

+
+ +
+ + + +
+ Create Project + {'Create Project'} +
+
+
+
+ +
+

+ Active Projects +

+ +
+ {projects.map((project, i) => ( +
+
{project.title}
+
+ ))} +
+
+
diff --git a/ui/public/assets/plus-icon.png b/ui/public/assets/plus-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..faaab8c5320988b45a41f11517c10aad4c082424 GIT binary patch literal 453 zcmV;$0XqJPP)>^$6_*b|+{yz}>(byh?>1B&8VQn0$Bd@Ew+) zkwKQC3;=9-XjxCJ9ZLt(C)UXNU=2#Ck(R(_hSj5tHbww@1D+gWs1>j%Di9u=hJ0cOVB>fl!d{BE8SBaetjDM)-BaR zGP9MH8mdbxrq5Itv!V2kmAV?+p;Ispk$E{DJBss2$k?T&a9o>uXhU~kGczwmJC@#8 z{)J6ECnq?*Yv8Ba9_>%OVs4m}P`bl+p7t@aFGBmOwD1O$~ppza-$rW7n z?+bzlKcz v|4_N+ literal 0 HcmV?d00001 From 63778bb8eb93d3eb6b0bb66a8518eeb6885366b0 Mon Sep 17 00:00:00 2001 From: namedotget Date: Mon, 6 Jan 2025 16:33:57 -0600 Subject: [PATCH 08/25] Refactor --- ui/components/layout/NumberStepper.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/components/layout/NumberStepper.tsx b/ui/components/layout/NumberStepper.tsx index 34e3a2761..a676efcec 100644 --- a/ui/components/layout/NumberStepper.tsx +++ b/ui/components/layout/NumberStepper.tsx @@ -15,14 +15,14 @@ export default function NumberStepper({ max, min, }: NumberStepperProps) { - function handleIncrease() { + function increase() { if (max && number + step > max) { return } setNumber(number + step) } - function handleDecrease() { + function decrease() { if (min !== undefined && number - step < min) { return } @@ -36,13 +36,12 @@ export default function NumberStepper({ type="number" value={number} onChange={(e) => setNumber(Number(e.target.value))} - step={0} />
- - - )} - {nft ? ( -

- {nft.metadata.name} -

- ) : ( - <> - )} -
-
{nft?.metadata.description ? (

)} - -

- {isManager || address === nft.owner ? ( - '' - ) : ( -
- {!isActive && subIsValid && ( - - )} -
- )} - - {/*Subscription Extension Container*/} - {isManager || address === nft.owner ? ( -
- {expiresAt && ( -
-
-
- -
-
-
- )} -
- ) : ( - <> - )} -
- {isManager || address === nft.owner ? ( -

- {'Exp: '} - {new Date(expiresAt?.toString() * 1000).toLocaleString()} -

- ) : ( - <> - )} -
-
-
+
+

{`Awarded: ${totalBudget} ETH`}

+ ETH +
) - const teamIcon = '/./assets/icon-team.svg' - return ( - - {teamSubscriptionModalEnabled && ( - - )} - {teamMetadataModalEnabled && ( - - )} + } > @@ -297,115 +149,128 @@ export default function ProjectProfile({ tokenId, nft, imageIpfsLink }: any) { id="page-container" className="animate-fadeIn flex flex-col gap-5 w-full max-w-[1080px]" > -
+ {/* Project Overview */} +
- +
+ + +
+ Attach Final Report + {'Attach Final Report'} +
+
+ + +
+ Star Icon +

Project Overview

+
+
+ +

+ {proposalJSON?.abstract} +

+ +
+ + Report Icon +

{'Review Original Proposal'}

+ +
-
+ - {/* Header and socials */} - {subIsValid ? ( -
- {/* Team Actions */} - {/* Team */} - + +
-
-
- Job icon + Job icon +

Meet the Team

+
+ {isManager && ( +
+ -

Meet the Team

- {isManager && ( -
- {/* { - window.open( - `https://app.hatsprotocol.xyz/trees/${selectedChain.chainId}/${hatTreeId}` - ) - }} - > - Manage Members - */} - -
+ )} +
+ + +
+ {hats?.[0].id && ( + )}
- - -
- {hats?.[0].id && ( - - )} -
-
-
- - {/* Mooney and Voting Power */} - {isManager && ( - - )} - {/* General Actions */} - {isManager && } -
- ) : ( - // Subscription Expired - - {isManager && ( - - )} + +
- )} + {/* Mooney and Voting Power */} + {isManager && ( + + )} +
@@ -413,64 +278,57 @@ export default function ProjectProfile({ tokenId, nft, imageIpfsLink }: any) { } export const getServerSideProps: GetServerSideProps = async ({ params }) => { - // const tokenId: any = params?.tokenId + const tokenId: any = params?.tokenId - // const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia - // const sdk = initSDK(chain) + const chain = Sepolia + const sdk = initSDK(chain) - // if (tokenId === undefined) { - // return { - // notFound: true, - // } - // } - - // const teamContract = await sdk.getContract( - // TEAM_ADDRESSES[chain.slug], - // TeamABI - // ) - // const nft = await teamContract.erc721.get(tokenId) - - // if ( - // !nft || - // !nft.metadata.uri || - // blockedTeams.includes(Number(nft.metadata.id)) - // ) { - // return { - // notFound: true, - // } - // } + if (tokenId === undefined) { + return { + notFound: true, + } + } - // const rawMetadataRes = await fetch(nft.metadata.uri) - // const rawMetadata = await rawMetadataRes.json() - // const imageIpfsLink = rawMetadata.image + const projectContract = await sdk.getContract( + PROJECT_ADDRESSES[chain.slug], + ProjectABI + ) - const tokenId = '0' - const imageIpfsLink = - 'https://ipfs.io/ipfs/QmQrX8HmgQAgVsWJdcqM4D5X85yo25hSt3jvXWv5Ytf5gG' + // const nft = await projectContract.erc721.get(tokenId) const nft = { - owner: ZERO_ADDRESS, metadata: { - name: 'Project #1', - description: 'Project #1 Description', - image: - 'https://ipfs.io/ipfs/QmQrX8HmgQAgVsWJdcqM4D5X85yo25hSt3jvXWv5Ytf5gG', - id: '0', - + name: 'Deprize Development', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', attributes: [ { - trait_type: 'website', - value: 'Project Website', + trait_type: 'active', + value: 'true', + }, + { + trait_type: 'proposalIPFS', + value: + 'ipfs://bafkreifsaljrpcjycsd5fzmpwvo5k2ye7geaeqqcjym4ornafjqwahjmoe', + }, + { + trait_type: 'MDP', + value: '159', }, ], }, } + if (!nft || blockedProjects.includes(Number(tokenId))) { + return { + notFound: true, + } + } + return { props: { nft, tokenId, - imageIpfsLink, }, } } diff --git a/ui/pages/project/index.tsx b/ui/pages/project/index.tsx index b866d7594..1cec9b1c8 100644 --- a/ui/pages/project/index.tsx +++ b/ui/pages/project/index.tsx @@ -1,11 +1,14 @@ import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' -import { NFT } from '@thirdweb-dev/react' +import { NFT, useContract } from '@thirdweb-dev/react' +import HatsABI from 'const/abis/Hats.json' +import ProjectABI from 'const/abis/Project.json' import TeamABI from 'const/abis/Team.json' -import { PROJECT_ADDRESSES, TEAM_ADDRESSES } from 'const/config' +import { HATS_ADDRESS, PROJECT_ADDRESSES, TEAM_ADDRESSES } from 'const/config' import { blockedProjects } from 'const/whitelist' import Image from 'next/image' import { useRouter } from 'next/router' -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useContext } from 'react' +import ChainContext from '@/lib/thirdweb/chain-context' import { useChainDefault } from '@/lib/thirdweb/hooks/useChainDefault' import { initSDK } from '@/lib/thirdweb/thirdweb' import { useShallowQueryRoute } from '@/lib/utils/hooks' @@ -20,6 +23,7 @@ import Head from '@/components/layout/Head' import { NoticeFooter } from '@/components/layout/NoticeFooter' import Search from '@/components/layout/Search' import Tab from '@/components/layout/Tab' +import ProjectCard from '@/components/projects/ProjectCard' type NetworkProps = { activeProjects: NFT[] @@ -30,9 +34,17 @@ export default function Projects({ activeProjects, inactiveProjects, }: NetworkProps) { + const { selectedChain } = useContext(ChainContext) const router = useRouter() const shallowQueryRoute = useShallowQueryRoute() + //Contracts + const { contract: projectContract } = useContract( + PROJECT_ADDRESSES[selectedChain.slug], + ProjectABI + ) + const { contract: hatsContract } = useContract(HATS_ADDRESS, HatsABI) + const [input, setInput] = useState('') function filterBySearch(nfts: NFT[]) { return nfts.filter((nft) => { @@ -179,27 +191,14 @@ export default function Projects({ cachedNFTs ?.slice((pageIdx - 1) * 9, pageIdx * 9) .map((nft: any, I: number) => { - if (nft.metadata.name !== 'Failed to load NFT metadata') { - const type = nft.metadata.attributes.find( - (attr: any) => attr.trait_type === 'communications' - ) - ? 'team' - : 'citizen' - return ( -
- -
- ) - } + return ( + + ) }) ) : ( <> @@ -274,8 +273,6 @@ export async function getStaticProps() { ) const totalProjects = await projectContract.call('totalSupply') - console.log(totalProjects.toString()) - const activeProjects = [] const inactiveProjects = [] for (let i = 0; i < totalProjects; i++) { @@ -294,6 +291,33 @@ export async function getStaticProps() { } } + const nft = { + metadata: { + name: 'Deprize Development', + tokenId: 0, + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + attributes: [ + { + trait_type: 'active', + value: 'true', + }, + { + trait_type: 'proposalIPFS', + value: + 'ipfs://bafkreifsaljrpcjycsd5fzmpwvo5k2ye7geaeqqcjym4ornafjqwahjmoe', + }, + { + trait_type: 'MDP', + value: '159', + }, + ], + }, + } + + activeProjects.push(nft) + inactiveProjects.push(nft) + return { props: { activeProjects: activeProjects.reverse(), From 57906978b30d46fe0177baf1b2b998598c5d3cc8 Mon Sep 17 00:00:00 2001 From: namedotget Date: Tue, 7 Jan 2025 13:45:16 -0600 Subject: [PATCH 13/25] Fixed input unfocus bug --- ui/components/layout/NumberStepper.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/components/layout/NumberStepper.tsx b/ui/components/layout/NumberStepper.tsx index a676efcec..2ebb8aac6 100644 --- a/ui/components/layout/NumberStepper.tsx +++ b/ui/components/layout/NumberStepper.tsx @@ -1,4 +1,5 @@ import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline' +import { useRef } from 'react' type NumberStepperProps = { number: number @@ -15,11 +16,14 @@ export default function NumberStepper({ max, min, }: NumberStepperProps) { + const inputRef = useRef(null) + function increase() { if (max && number + step > max) { return } setNumber(number + step) + inputRef.current?.focus() } function decrease() { @@ -27,15 +31,19 @@ export default function NumberStepper({ return } setNumber(number - step) + inputRef.current?.focus() } return (
setNumber(Number(e.target.value))} + step={0} />