diff --git a/src/app/pool/[chain]/[id]/page.tsx b/src/app/pool/[chain]/[id]/page.tsx index 57ff537..92cf290 100644 --- a/src/app/pool/[chain]/[id]/page.tsx +++ b/src/app/pool/[chain]/[id]/page.tsx @@ -1,6 +1,13 @@ import PoolDetailPage from "@/components/Pool/PoolDetail"; -import { IPoolDetailResponse, TPoolDetail } from "@/components/Pool/types"; +import { + IPoolDetailResponse, + Status, + StrategyId, + TPoolDetail, + TRfpStrategy, +} from "@/components/Pool/types"; import { getPoolDetailDataQuery, graphqlEndpoint } from "@/utils/query"; +import { fetchIpfsMetadata } from "@/utils/utils"; import { request } from "graphql-request"; export default async function PoolDetail({ @@ -8,31 +15,108 @@ export default async function PoolDetail({ }: { params: { chain: string; id: string }; }) { + // ============ todo: replace dummy data with real data ============ + const rfpStrategyDetails: TRfpStrategy = { + useRegistryAnchor: true, + metadataRequired: true, + acceptedRecipient: "0x1234567890123456789012345678901234567890", + maxBid: 2000000000000000, + upcomingMilsetoneId: 1, + recipients: [ + { + recipient: "0x1234567890123456789012345678901234567890", + useRegistryAnchor: true, + recipientAddress: "0x1234567890123456789012345678901234567890", + proposalBid: 1000000000000000, + recipientStatus: Status.Accepted, + }, + { + recipient: "0x1234567890123456789012345678901234567890", + useRegistryAnchor: true, + recipientAddress: "0x1234567890123456789012345678901234567890", + proposalBid: 1000000000000000, + recipientStatus: Status.Pending, + }, + ], + milestones: [ + { + amountPercentage: 10, + metadata: { + protocol: 1, + pointer: + "bafkreigwiljyskihuaeyjsedoei3taprwbbheldxig25lhoqvw2kpcf4bu", + }, + metadataObj: await fetchIpfsMetadata({ + protocol: 1, + pointer: + "bafkreigwiljyskihuaeyjsedoei3taprwbbheldxig25lhoqvw2kpcf4bu", + }), + milestoneStatus: Status.Pending, + }, + { + amountPercentage: 10, + metadata: { + protocol: 1, + pointer: + "bafkreigwiljyskihuaeyjsedoei3taprwbbheldxig25lhoqvw2kpcf4bu", + }, + metadataObj: await fetchIpfsMetadata({ + protocol: 1, + pointer: + "bafkreigwiljyskihuaeyjsedoei3taprwbbheldxig25lhoqvw2kpcf4bu", + }), + milestoneStatus: Status.Pending, + }, + ], + distributions: [ + { + acceptedRecipient: "0x1234567890123456789012345678901234567890", + recipientAddress: "0x1234567890123456789012345678901234567890", + amount: 1000000000000000, + sender: "0x1234567890123456789012345678901234567890", + }, + { + acceptedRecipient: "0x1234567890123456789012345678901234567890", + recipientAddress: "0x1234567890123456789012345678901234567890", + amount: 1000000000000000, + sender: "0x1234567890123456789012345678901234567890", + }, + ], + votes: [ + { + voter: "0x1234567890123456789012345678901234567890", + recipientId: "0x1234567890123456789012345678901234567890", + timestamp: 1234567890, + }, + { + voter: "0x1234567890123456789012345678901234567890", + recipientId: "0x1234567890123456789012345678901234567890", + timestamp: 1234567890, + }, + ], + }; + const response: IPoolDetailResponse = await request( graphqlEndpoint, getPoolDetailDataQuery, { chainId: params.chain, poolId: params.id, - } + }, ); const { pool }: { pool: TPoolDetail } = response; - let poolMetadata = "{}"; - - try { - const response = await fetch( - `https://gitcoin.mypinata.cloud/ipfs/${pool.metadataPointer}`, - ); + if (pool.poolId === "42") + pool.strategyDetails = { + strategyId: StrategyId.RFPCommittee, + details: rfpStrategyDetails, + }; - // Check if the response status is OK (200) - if (response.ok) { - poolMetadata = await response.text(); - } - } catch (error) { - console.error(error); - } + const metadataObj: Object = await fetchIpfsMetadata({ + pointer: pool.metadataPointer, + protocol: pool.metadataProtocol, + }); - return ; + return ; } diff --git a/src/app/profile/[chain]/[id]/page.tsx b/src/app/profile/[chain]/[id]/page.tsx index b49c91f..654a1e1 100644 --- a/src/app/profile/[chain]/[id]/page.tsx +++ b/src/app/profile/[chain]/[id]/page.tsx @@ -4,6 +4,7 @@ import { TProfileDetail, } from "@/components/Registry/types"; import { getProfileDetailDataQuery, graphqlEndpoint } from "@/utils/query"; +import { fetchIpfsMetadata } from "@/utils/utils"; import request from "graphql-request"; export default async function ProfileDetailPage({ @@ -21,18 +22,14 @@ export default async function ProfileDetailPage({ ); const profile: TProfileDetail = profileDetails.profile; - - const response = await fetch( - `https://gitcoin.mypinata.cloud/ipfs/${profile.metadataPointer}`, - ); - - let metadata = ""; - - if (response.ok) metadata = await response.text(); + const metadataObj: Object = await fetchIpfsMetadata({ + pointer: profile.metadataPointer, + protocol: profile.metadataProtocol, + }); return (
- +
); } diff --git a/src/components/List.tsx b/src/components/List.tsx new file mode 100644 index 0000000..bf16a48 --- /dev/null +++ b/src/components/List.tsx @@ -0,0 +1,35 @@ +import { useMediaQuery } from "@/hooks/useMediaQuery"; +import { TListProps } from "@/types/types"; + +const List = (props: {data: TListProps[]}) => { + const isMobile = useMediaQuery(768); + const py = isMobile ? "py-2" : "py-6"; + + return ( +
+
+ {props.data.map((d, index) => ( +
+
+ {d.label} +
+
+ {d.value} +
+
+ ))} +
+
+ ); +}; + +export default List; diff --git a/src/components/Metadata.tsx b/src/components/Metadata.tsx new file mode 100644 index 0000000..e3fb27b --- /dev/null +++ b/src/components/Metadata.tsx @@ -0,0 +1,43 @@ +import { Metadata } from "@/types/types"; +import JsonView from "@uiw/react-json-view"; +import { TbExternalLink } from "react-icons/tb"; +import { truncatedString } from "./Address"; + +const Metadata = ({ + isMobile, + metadata, + metadataObj, + shortenTexteAfterLength = 120, + collapsed = 2, +}: { + isMobile: boolean; + metadata: Metadata; + metadataObj: Object; + shortenTexteAfterLength?: number; + collapsed?: number; +}) => { + return ( +
+
+ {isMobile ? truncatedString(metadata.pointer) : metadata.pointer} + + + +
+
+ +
+
+ ); +}; + +export default Metadata; diff --git a/src/components/Pool/PoolDetail.tsx b/src/components/Pool/PoolDetail.tsx index d49bbcb..efd5d37 100644 --- a/src/components/Pool/PoolDetail.tsx +++ b/src/components/Pool/PoolDetail.tsx @@ -1,33 +1,123 @@ "use client"; -import { convertChainIdToNetworkName } from "@/utils/utils"; +import { amountString, convertChainIdToNetworkName } from "@/utils/utils"; import { AddressResponsive, truncatedString } from "../Address"; -import { TPoolDetail } from "./types"; -import { MetadataProtocol } from "@/types/types"; -import { TbExternalLink } from "react-icons/tb"; -import JsonView from "@uiw/react-json-view"; +import { StrategyId, TPoolDetail } from "./types"; +import { MetadataProtocol, TListProps } from "@/types/types"; import { ethers } from "ethers"; import Link from "next/link"; import { getNetworks } from "@/utils/networks"; import { useMediaQuery } from "@/hooks/useMediaQuery"; +import List from "../List"; +import Metadata from "../Metadata"; +import RfpDetails from "./Strategies/RfpDetails"; const PoolDetailPage = ({ pool, - poolMetadata, + metadataObj, }: { pool: TPoolDetail; - poolMetadata: string; + metadataObj: Object; }) => { - let metadataObj; - try { - metadataObj = JSON.parse(poolMetadata); - } catch (error) { - metadataObj = { - error: "Error parsing metadata", - }; - } - const isMobile = useMediaQuery(768); - const py = isMobile ? "py-2" : "py-6"; + const isMobile = useMediaQuery(768); + + const listProps: TListProps[] = [ + { + label: "Strategy", + value: ( + + ), + }, + { + label: "Network", + value: convertChainIdToNetworkName(Number(pool.chainId)), + }, + { + label: "Token", + value: ( + + ), + }, + { + label: "Amount", + value: amountString( + pool.amount, + pool.tokenMetadata, + Number(pool.chainId), + ), + }, + { + label: "Creator", + value: ( + + ), + }, + { + label: "Profile", + value: ( + <> + {pool.profile.name} + + +
+ + {isMobile + ? truncatedString(pool.profile.profileId) + : pool.profile.profileId}{" "} + + + + ), + }, + { + label: "Created at", + value: new Date(pool.createdAt).toLocaleString(), + }, + { + label: "Updated at", + value: new Date(pool.updatedAt).toLocaleString(), + }, + { + label: "Metadata (" + MetadataProtocol[pool.metadataProtocol] + ")", + value: ( + + ), + }, + ]; + + const renderDetails = () => { + if (!pool.strategyDetails) return null; + + switch (pool.strategyDetails.strategyId) { + case StrategyId.RFPSimple: + case StrategyId.RFPCommittee: + return ( + + ); + default: + return null; + } + }; return (
@@ -48,124 +138,8 @@ const PoolDetailPage = ({
-
-
-
-
- Strategy -
-
- -
-
-
-
- Network -
-
- {convertChainIdToNetworkName(Number(pool.chainId))} -
-
-
-
- Token -
-
- -
-
-
-
- Amount -
-
- {ethers.formatUnits( - pool.amount ?? 0, - pool.tokenMetadata.decimals ?? 18, - )}{" "} - {pool.tokenMetadata.symbol ?? - getNetworks()[Number(pool.chainId)].symbol} -
-
-
-
- Creator -
-
- -
-
-
-
- Profile -
-
- {pool.profile.name} - - -
- - {isMobile - ? truncatedString(pool.profile.profileId) - : pool.profile.profileId}{" "} - - -
-
-
-
- Created at -
-
- {new Date(pool.createdAt).toLocaleString()} -
-
-
-
- Updated at -
-
- {new Date(pool.updatedAt).toLocaleString()} -
-
-
-
- Metadata ({MetadataProtocol[pool.metadataProtocol]}){" "} -
-
-
- {isMobile - ? truncatedString(pool.metadataPointer) - : pool.metadataPointer} - - - -
-
-
-
-
- -
-
+ + {renderDetails()} ); }; diff --git a/src/components/Pool/Strategies/RfpDetails.tsx b/src/components/Pool/Strategies/RfpDetails.tsx new file mode 100644 index 0000000..6048516 --- /dev/null +++ b/src/components/Pool/Strategies/RfpDetails.tsx @@ -0,0 +1,186 @@ +import { TListProps, TTableData } from "@/types/types"; +import { Status, TRfpStrategy, TTokenMetadata } from "../types"; +import List from "@/components/List"; +import { Address, AddressResponsive } from "@/components/Address"; +import { amountString } from "@/utils/utils"; +import Table from "@/components/Table"; +import Metadata from "@/components/Metadata"; +import { useMediaQuery } from "@/hooks/useMediaQuery"; + +const RfpDetails = ({ + details, + tokenMetadata, + chainId, +}: { + details: TRfpStrategy; + tokenMetadata: TTokenMetadata; + chainId: number; +}) => { + const isMobile = useMediaQuery(768); + const recipientsTable: TTableData = { + headers: [ + "Recipient", + "Recipient Address", + "Proposal Bid", + "Status", + "Registry Anchor", + ], + rows: details.recipients.map((r) => [ + // eslint-disable-next-line react/jsx-key +
, + // eslint-disable-next-line react/jsx-key +
, + amountString(r.proposalBid, tokenMetadata, chainId), + r.recipientStatus.toString(), + r.useRegistryAnchor ? "Yes" : "No", + ]), + }; + + const milestonesTable: TTableData = { + headers: ["Amount", "Metadata", "Status"], + rows: details.milestones.map((m) => [ + m.amountPercentage.toString() + "%", + // eslint-disable-next-line react/jsx-key + , + Status[m.milestoneStatus], + ]), + }; + + const distributionsTable: TTableData = { + headers: ["Recipient", "Recipient Address", "Amount", "Sender"], + rows: details.distributions.map((r) => [ + // eslint-disable-next-line react/jsx-key +
, + // eslint-disable-next-line react/jsx-key +
, + amountString(r.amount, tokenMetadata, chainId), + // eslint-disable-next-line react/jsx-key +
, + ]), + }; + + const voteTable: TTableData = { + headers: ["Voter", "Recipient", "Timestamp"], + rows: details.votes.map((v) => [ + // eslint-disable-next-line react/jsx-key +
, + // eslint-disable-next-line react/jsx-key +
, + // eslint-disable-next-line react/jsx-key + new Date(v.timestamp).toLocaleString(), + ]), + }; + + const listProps: TListProps[] = [ + { + label: "Use Registry Anchor", + value: details.useRegistryAnchor ? "Yes" : "No", + }, + { + label: "Metadata Required", + value: details.metadataRequired ? "Yes" : "No", + }, + { + label: "Accepted Recipient", + value: ( + // eslint-disable-next-line react/jsx-key + + ), + }, + { + label: "Max Bid", + value: amountString(details.maxBid, tokenMetadata, chainId), + }, + { + label: "Upcoming Milestone Id", + value: details.upcomingMilsetoneId.toString(), + }, + ]; + + return ( +
+
+

+ RFP Strategy Details +

+
+
+ +
+ + {details.recipients.length > 0 && ( +
+

+ Recipients +

+ + + )} + + {details.milestones.length > 0 && ( +
+

+ Milestones +

+
+ + )} + + {details.distributions.length > 0 && ( +
+

+ Distributions +

+
+ + )} + {details.votes.length > 0 && ( +
+

+ Votes +

+
+ + )} + + ); +}; + +export default RfpDetails; diff --git a/src/components/Pool/types.ts b/src/components/Pool/types.ts index 47b62da..46dd381 100644 --- a/src/components/Pool/types.ts +++ b/src/components/Pool/types.ts @@ -1,3 +1,4 @@ +import { Metadata } from "@/types/types"; import { TProfile } from "../Registry/types"; export type TPool = { @@ -15,6 +16,10 @@ export type TPoolDetail = TPool & { tokenMetadata: TTokenMetadata; updatedAt: string; createdAt: string; + strategyDetails?: { + strategyId: StrategyId; + details: TRfpStrategy; + }; }; export type TTokenMetadata = { @@ -30,3 +35,63 @@ export interface IPoolsResponse { export interface IPoolDetailResponse { pool: TPoolDetail; } + +export enum Status { + "None", + "Pending", + "Accepted", + "Rejected", + "Appealed", + "Cancelled", +} + +export enum StrategyId { + "None", + RFPSimple = "0xb87f34c0968bd74d43a6a5b72831a5ea733a4783a026b9fc9b1d17adf51214d2", + RFPCommittee = "0x414f2ea9b91b8ee2e35a380fa0af0e14079832cc93530a61a4893b3dbf0a9aba", + QVSimple = "0xed28ce0387d1786c1a38404047e9eecc4d1dcaeff695b867e912483e36c3d770", + DonationVotingMerkleDistributionDirectTransfer = "0xc5263e972c91d7ff40708bc71239a2b6cbc8768704e210ca3069e2e11fc195df", + DonationVotingMerkleDistributionVault = "0xecc48557f4826bd1181a4495232d6d07f248ef9cc0a650e64520f6c9f7458a8c", +} + +// ==================== RFP ==================== + +export type TRfpRecipient = { + recipient: string; + useRegistryAnchor: boolean; + recipientAddress: string; + proposalBid: number; + recipientStatus: Status; +}; + +export type TRfpMilestone = { + amountPercentage: number; + metadata: Metadata; + metadataObj: Object; + milestoneStatus: Status; +}; + +export type TRfpDistribution = { + acceptedRecipient: string; + recipientAddress: string; + amount: number; + sender: string; +}; + +export type RfpVote = { + voter: string; + recipientId: string; + timestamp: number; +}; + +export type TRfpStrategy = { + useRegistryAnchor: boolean; + metadataRequired: boolean; + acceptedRecipient: string; + maxBid: number; + upcomingMilsetoneId: number; + recipients: TRfpRecipient[]; + milestones: TRfpMilestone[]; + distributions: TRfpDistribution[]; + votes: RfpVote[]; +}; diff --git a/src/components/Registry/ProfileDetail.tsx b/src/components/Registry/ProfileDetail.tsx index 10581cf..d0d13dd 100644 --- a/src/components/Registry/ProfileDetail.tsx +++ b/src/components/Registry/ProfileDetail.tsx @@ -3,30 +3,95 @@ import { convertChainIdToNetworkName } from "@/utils/utils"; import { AddressResponsive, truncatedString } from "../Address"; import { TProfileDetail } from "./types"; -import { MetadataProtocol } from "@/types/types"; -import { TbExternalLink } from "react-icons/tb"; -import JsonView from "@uiw/react-json-view"; +import { MetadataProtocol, TListProps } from "@/types/types"; import Link from "next/link"; import Pool from "../Pool/Pool"; import { useMediaQuery } from "@/hooks/useMediaQuery"; +import List from "../List"; +import Metadata from "../Metadata"; const ProfileDetail = ({ profile, - metadata, + metadataObj, }: { profile: TProfileDetail; - metadata: string; + metadataObj: Object; }) => { - let metadataObj; - try { - metadataObj = JSON.parse(metadata ?? ""); - } catch (error) { - metadataObj = { - error: "Error parsing metadata", - }; - } const isMobile = useMediaQuery(768); - const py = isMobile ? "py-2" : "py-6"; + + const listProps: TListProps[] = [ + { + label: "Network", + value: convertChainIdToNetworkName(profile.chainId), + }, + { + label: "Nonce", + value: profile.nonce.toString(), + }, + { + label: "Anchor", + value: ( + + ), + }, + { + label: "Creator", + value: ( + + ), + }, + { + label: "Owner", + value: ( + + ), + }, + { + label: "Members", + value: ( +
    + {profile.role.roleAccounts.map((account, index) => ( +
  • +
    +
    + + + +
    +
    +
  • + ))} +
+ ), + }, + { + label: "Created at", + value: new Date(profile.createdAt).toLocaleString(), + }, + { + label: "Updated at", + value: new Date(profile.updatedAt).toLocaleString(), + }, + { + label: `Metadata (${MetadataProtocol[profile.metadataProtocol]})`, + value: , + }, + ]; return (
@@ -47,128 +112,9 @@ const ProfileDetail = ({
-
-
-
-
- Network -
-
- {convertChainIdToNetworkName(profile.chainId)} -
-
-
-
- Nonce -
-
- {profile.nonce} -
-
-
-
- Anchor -
-
- -
-
-
-
- Creator -
-
- -
-
-
-
- Owner -
-
- -
-
-
-
- Members -
-
-
    - {profile.role.roleAccounts.map((account, index) => ( -
  • -
    -
    - - - -
    -
    -
  • - ))} -
-
-
-
-
- Created at -
-
- {new Date(profile.createdAt).toLocaleString()} -
-
-
-
- Updated at -
-
- {new Date(profile.updatedAt).toLocaleString()} -
-
-
-
- Metadata ({MetadataProtocol[profile.metadataProtocol]}){" "} -
-
-
- {isMobile - ? truncatedString(profile.metadataPointer) - : profile.metadataPointer} - - - -
-
-
-
-
- -
-
+ + + {profile.pools.length > 0 && ( <>
diff --git a/src/components/Table.tsx b/src/components/Table.tsx index a520f07..297836c 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -15,12 +15,14 @@ const Table = ({ description, rowsPerPage = 10, showPagination, + showBorder = true, }: { data: TTableData; header: string | undefined | ""; description: string | undefined | ""; rowsPerPage?: number; showPagination?: boolean; + showBorder?: boolean; }) => { const [currentPage, setCurrentPage] = useState(1); const totalPages = Math.ceil(data.rows.length / rowsPerPage); @@ -44,13 +46,11 @@ const Table = ({ const endRow = startRow + rowsPerPage; const currentRows = data.rows.slice(startRow, endRow); - console.log("isMobile", isMobile); - return ( <>
@@ -70,7 +70,13 @@ const Table = ({ )}
-
+
{!isMobile ? (
diff --git a/src/types/types.ts b/src/types/types.ts index 3d88eba..d95b99c 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -86,3 +86,9 @@ export type TFunctionArgs = { type: string; value: string; }; + +export type TListProps = { + label: string; + value: string | React.JSX.Element; +}; + diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4bfec22..90e0e67 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,7 @@ +import { Metadata, MetadataProtocol } from "@/types/types"; import { getNetworks } from "./networks"; import { ethers } from "ethers"; +import { TTokenMetadata } from "@/components/Pool/types"; const networks = getNetworks(); @@ -27,11 +29,57 @@ export const convertBytesToShortString = (address: string) => { return address.slice(0, 6) + "..." + address.slice(-4); }; - export const convertAddressToShortString = (address: string) => { return address.slice(0, 6) + "..." + address.slice(-4); }; export const copy = (data: string) => { navigator.clipboard.writeText(data); -}; \ No newline at end of file +}; + +export const amountString = ( + amount: number, + tokenMetadata: TTokenMetadata, + chainId: number, +): string => { + return ( + ethers.formatUnits(amount, tokenMetadata.decimals ?? 18) + " " + + (tokenMetadata.symbol ?? getNetworks()[Number(chainId)].symbol) + ); +}; + +export const fetchIpfsMetadata = async ( + metadata: Metadata, +): Promise => { + let metadataObj: Object = {}; + + if (metadata.protocol === MetadataProtocol.IPFS) { + try { + const response = await fetch( + `https://gitcoin.mypinata.cloud/ipfs/${metadata.pointer}`, + ); + // Check if the response status is OK (200) + if (response.ok) { + const data = await response.text(); + try { + let metadataObjJson = JSON.parse(data ?? ""); + metadataObj = metadataObjJson; + } catch (error) { + metadataObj = { + error: "Error parsing metadata", + }; + } + } else { + metadataObj = { + error: "Error fetching metadata", + }; + } + } catch (error) { + metadataObj = { + error: "Error fetching metadata", + }; + } + } + + return metadataObj; +};