From 83a4bb95c4fc599c3756ff7ce7c5b490ba83cbf9 Mon Sep 17 00:00:00 2001 From: "Carina.Akaia.io" Date: Sun, 29 Dec 2024 11:02:59 +0000 Subject: [PATCH] wip --- src/common/api/indexer/hooks.ts | 4 +- src/common/contracts/core/voting/hooks.ts | 10 ++-- src/common/types.ts | 2 +- .../_shared/account/hooks/social-profile.ts | 4 +- src/entities/pot/components/PotTimeline.tsx | 10 ++-- src/entities/pot/hooks/feature-flags.ts | 5 +- src/entities/pot/hooks/lifecycle.ts | 11 ++-- src/entities/pot/hooks/tags.ts | 6 +-- .../voting-round/components/Leaderboard.tsx} | 14 +++--- .../voting-round/components/WinnerList.tsx | 50 +++++++++++++++++++ src/entities/voting-round/hooks/results.ts | 4 +- src/entities/voting-round/hooks/rounds.ts | 8 ++- src/entities/voting-round/index.ts | 1 + .../voting-round/model/round-results.ts | 20 ++++---- src/entities/voting-round/types.ts | 2 +- .../pot-application/hooks/clearance.ts | 8 +-- .../components/ConfigurationForm.tsx | 4 +- src/layout/pot/components/PotLayoutHero.tsx | 38 ++++++++++---- src/layout/pot/hooks/tab-navigation.ts | 34 ++++++++----- src/pages/pot/[potId]/payouts.tsx | 4 ++ src/pages/pot/[potId]/settings.tsx | 4 +- 21 files changed, 169 insertions(+), 74 deletions(-) rename src/{layout/pot/components/PotVotingLeaderboard.tsx => entities/voting-round/components/Leaderboard.tsx} (87%) create mode 100644 src/entities/voting-round/components/WinnerList.tsx diff --git a/src/common/api/indexer/hooks.ts b/src/common/api/indexer/hooks.ts index 53e3eb0a..b23affbe 100644 --- a/src/common/api/indexer/hooks.ts +++ b/src/common/api/indexer/hooks.ts @@ -1,5 +1,5 @@ import { isAccountId, isEthereumAddress } from "@/common/lib"; -import { ByAccountId, ByListId, type ConditionalExecution } from "@/common/types"; +import { ByAccountId, ByListId, type ConditionalActivation } from "@/common/types"; import * as generatedClient from "./internal/client.generated"; import { INDEXER_CLIENT_CONFIG } from "./internal/config"; @@ -42,7 +42,7 @@ export const useAccounts = (params?: generatedClient.V1AccountsRetrieveParams) = /** * https://test-dev.potlock.io/api/schema/swagger-ui/#/v1/v1_accounts_retrieve_2 */ -export const useAccount = ({ accountId, enabled = true }: ByAccountId & ConditionalExecution) => { +export const useAccount = ({ accountId, enabled = true }: ByAccountId & ConditionalActivation) => { const queryResult = generatedClient.useV1AccountsRetrieve2(accountId, { ...INDEXER_CLIENT_CONFIG, swr: { enabled: enabled && isAccountId(accountId) && !isEthereumAddress(accountId) }, diff --git a/src/common/contracts/core/voting/hooks.ts b/src/common/contracts/core/voting/hooks.ts index 5b369b50..2822eda4 100644 --- a/src/common/contracts/core/voting/hooks.ts +++ b/src/common/contracts/core/voting/hooks.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import type { ByPotId } from "@/common/api/indexer"; -import { ByAccountId, type ConditionalExecution } from "@/common/types"; +import { ByAccountId, type ConditionalActivation } from "@/common/types"; import { AccountId, ElectionId } from "./interfaces"; import { votingContractClient } from "./singleton.client"; @@ -10,10 +10,10 @@ export interface ByElectionId { electionId: ElectionId; } -type BasicElectionQueryKey = ByElectionId & ConditionalExecution; +type BasicElectionQueryKey = ByElectionId & ConditionalActivation; -export const useElections = () => - useSWR(["get_elections"], () => votingContractClient.get_elections({})); +export const useElections = ({ enabled = true }: ConditionalActivation | undefined = {}) => + useSWR(["get_elections"], () => (!enabled ? undefined : votingContractClient.get_elections({}))); export const useActiveElections = () => useSWR(["get_active_elections"], () => votingContractClient.get_active_elections()); @@ -100,7 +100,7 @@ export const useUniqueVoters = ({ electionId, enabled = true }: BasicElectionQue !enabled ? undefined : votingContractClient.get_unique_voters({ election_id }), ); -export const usePotElections = ({ potId }: ByPotId) => { +export const usePotElections = ({ potId, enabled = true }: ByPotId & ConditionalActivation) => { const { data: elections, isLoading } = useElections(); return { diff --git a/src/common/types.ts b/src/common/types.ts index e474f863..13a4d814 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -73,7 +73,7 @@ export type U128String = string; export type ClientConfig = { swr?: SWRConfiguration }; -export interface ConditionalExecution { +export interface ConditionalActivation { enabled?: boolean; } diff --git a/src/entities/_shared/account/hooks/social-profile.ts b/src/entities/_shared/account/hooks/social-profile.ts index 35845e41..eb3590c2 100644 --- a/src/entities/_shared/account/hooks/social-profile.ts +++ b/src/entities/_shared/account/hooks/social-profile.ts @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useRegistration } from "@/common/_deprecated/useRegistration"; import { NEARSocialUserProfile, getSocialProfile } from "@/common/contracts/social"; -import type { ByAccountId, ConditionalExecution } from "@/common/types"; +import type { ByAccountId, ConditionalActivation } from "@/common/types"; import { ACCOUNT_PROFILE_COVER_IMAGE_PLACEHOLDER_SRC, @@ -14,7 +14,7 @@ import { export const useAccountSocialProfile = ({ accountId, enabled = true, -}: ByAccountId & ConditionalExecution) => { +}: ByAccountId & ConditionalActivation) => { const [profile, setProfile] = useState(undefined); const [isReady, setProfileReady] = useState(false); diff --git a/src/entities/pot/components/PotTimeline.tsx b/src/entities/pot/components/PotTimeline.tsx index e41f4f5a..cefc0350 100644 --- a/src/entities/pot/components/PotTimeline.tsx +++ b/src/entities/pot/components/PotTimeline.tsx @@ -68,17 +68,21 @@ const Loader = styled.div` `; export type PotTimelineProps = ByPotId & { - hasVoting?: boolean; + hasProportionalFundingMechanism?: boolean; classNames?: { root?: string; }; }; -export const PotTimeline: React.FC = ({ potId, classNames, hasVoting }) => { +export const PotTimeline: React.FC = ({ + potId, + classNames, + hasProportionalFundingMechanism, +}) => { const [isMobileMenuActive, setIsMobileMenuActive] = useState(false); const toggleMobileMenu = useCallback(() => setIsMobileMenuActive((isActive) => !isActive), []); - const lifecycle = usePotLifecycle({ potId, hasVoting }); + const lifecycle = usePotLifecycle({ potId, hasProportionalFundingMechanism }); const showActiveState = (lifecycle.currentStage === undefined ? 0 : lifecycle.stages.indexOf(lifecycle.currentStage)) * diff --git a/src/entities/pot/hooks/feature-flags.ts b/src/entities/pot/hooks/feature-flags.ts index 3bfddab1..d236fa35 100644 --- a/src/entities/pot/hooks/feature-flags.ts +++ b/src/entities/pot/hooks/feature-flags.ts @@ -11,7 +11,10 @@ export const usePotFeatureFlags = ({ potId }: ByPotId) => { ); return useMemo( - () => ({ isPotExtensionConfigLoading, hasVoting: (elections?.length ?? 0) > 0 }), + () => ({ + isPotExtensionConfigLoading, + hasProportionalFundingMechanism: (elections?.length ?? 0) > 0, + }), [isPotExtensionConfigLoading, elections?.length], ); }; diff --git a/src/entities/pot/hooks/lifecycle.ts b/src/entities/pot/hooks/lifecycle.ts index 9fe7a85f..f1c1848a 100644 --- a/src/entities/pot/hooks/lifecycle.ts +++ b/src/entities/pot/hooks/lifecycle.ts @@ -12,9 +12,12 @@ import { PotLifecycleStageTagEnum } from "../types"; */ const getDateTime = (date: string) => new Date(date).getTime(); -export type PotLifecycleCalculationInputs = ByPotId & { hasVoting?: boolean }; +export type PotLifecycleCalculationInputs = ByPotId & { hasProportionalFundingMechanism?: boolean }; -export const usePotLifecycle = ({ potId, hasVoting }: PotLifecycleCalculationInputs) => { +export const usePotLifecycle = ({ + potId, + hasProportionalFundingMechanism, +}: PotLifecycleCalculationInputs) => { const { data: pot } = indexer.usePot({ potId }); const date = new Date(); @@ -44,7 +47,7 @@ export const usePotLifecycle = ({ potId, hasVoting }: PotLifecycleCalculationInp { tag: PotLifecycleStageTagEnum.Matching, - label: `${hasVoting ? "Voting" : "Matching"} round`, + label: `${hasProportionalFundingMechanism ? "Voting" : "Matching"} round`, daysLeft: public_round_end_ms, started: now >= public_round_start_ms, completed: now > public_round_end_ms, @@ -79,7 +82,7 @@ export const usePotLifecycle = ({ potId, hasVoting }: PotLifecycleCalculationInp }, ]; } else return []; - }, [hasVoting, now, pot]); + }, [hasProportionalFundingMechanism, now, pot]); const currentStage = useMemo( () => stages.find((stage) => stage.started && !stage.completed), diff --git a/src/entities/pot/hooks/tags.ts b/src/entities/pot/hooks/tags.ts index b8be1426..81a8e34a 100644 --- a/src/entities/pot/hooks/tags.ts +++ b/src/entities/pot/hooks/tags.ts @@ -11,7 +11,7 @@ import { usePotFeatureFlags } from "./feature-flags"; export const usePotTags = ({ potId }: ByPotId) => { const { data: pot } = indexer.usePot({ potId }); - const { hasVoting } = usePotFeatureFlags({ potId }); + const { hasProportionalFundingMechanism } = usePotFeatureFlags({ potId }); return useMemo(() => { if (pot) { @@ -54,7 +54,7 @@ export const usePotTags = ({ potId }: ByPotId) => { backgroundColor: "#F7FDE8", borderColor: "#9ADD33", textColor: "#192C07", - text: `${daysUntil(publicRoundEndMs)} left to ${hasVoting ? "vote" : "donate"}`, + text: `${daysUntil(publicRoundEndMs)} left to ${hasProportionalFundingMechanism ? "vote" : "donate"}`, preElementsProps: { colorOuter: "#D7F5A1", colorInner: "#9ADD33", animate: true }, visibility: publicRoundOpen, }, @@ -96,5 +96,5 @@ export const usePotTags = ({ potId }: ByPotId) => { }, ]; } else return []; - }, [hasVoting, pot]); + }, [hasProportionalFundingMechanism, pot]); }; diff --git a/src/layout/pot/components/PotVotingLeaderboard.tsx b/src/entities/voting-round/components/Leaderboard.tsx similarity index 87% rename from src/layout/pot/components/PotVotingLeaderboard.tsx rename to src/entities/voting-round/components/Leaderboard.tsx index abe9ae92..80aa1101 100644 --- a/src/layout/pot/components/PotVotingLeaderboard.tsx +++ b/src/entities/voting-round/components/Leaderboard.tsx @@ -8,30 +8,30 @@ import { LabeledIcon } from "@/common/ui/components"; import { cn } from "@/common/ui/utils"; import { AccountHandle, AccountProfilePicture } from "@/entities/_shared/account"; import { TokenIcon } from "@/entities/_shared/token"; -import { useVotingRoundResults } from "@/entities/voting-round"; -export type PotVotingLeaderboardProps = ByPotId & {}; +import { useVotingRoundResults } from "../hooks/results"; -export const PotVotingLeaderboard: React.FC = ({ potId }) => { +export type VotingRoundLeaderboardProps = ByPotId & {}; + +export const VotingRoundLeaderboard: React.FC = ({ potId }) => { const votingRoundResults = useVotingRoundResults({ potId }); const leadingPositionAccountIds = useMemo( () => - values(votingRoundResults?.candidates ?? {}) + values(votingRoundResults?.winners ?? {}) .sort( (candidateA, candidateB) => candidateB.accumulatedWeight - candidateA.accumulatedWeight, ) .slice(0, 3) .map(({ accountId }) => accountId), - [votingRoundResults?.candidates], + [votingRoundResults?.winners], ); return votingRoundResults === undefined ? null : (
{leadingPositionAccountIds.map((accountId, index) => { - const { accumulatedWeight, estimatedPayoutAmount } = - votingRoundResults.candidates[accountId]; + const { accumulatedWeight, estimatedPayoutAmount } = votingRoundResults.winners[accountId]; return (
= ({ data }) => { + const { height: windowHeight } = useWindowSize(); + + return ( +
+
+
+ {"Rank"} +
+ +
+ {"Projects"} +
+ +
+
+ + {"Votes"} + +
+ +
+ + {"Pool Allocation"} + +
+
+
+ + +
+ {/* {data.map((winner) => ( + + ))} */} +
+
+
+ ); +}; diff --git a/src/entities/voting-round/hooks/results.ts b/src/entities/voting-round/hooks/results.ts index 811efea0..17287a07 100644 --- a/src/entities/voting-round/hooks/results.ts +++ b/src/entities/voting-round/hooks/results.ts @@ -10,8 +10,8 @@ import { useVotingRound } from "./rounds"; export const useVotingRoundResults = ({ potId }: VotingRoundKey) => { const { data: pot } = indexer.usePot({ potId }); - const { hasVoting } = usePotFeatureFlags({ potId }); - const votingRound = useVotingRound({ potId }); + const { hasProportionalFundingMechanism } = usePotFeatureFlags({ potId }); + const votingRound = useVotingRound({ enabled: hasProportionalFundingMechanism, potId }); const { data: votes } = votingContractHooks.useElectionVotes({ enabled: votingRound !== undefined, diff --git a/src/entities/voting-round/hooks/rounds.ts b/src/entities/voting-round/hooks/rounds.ts index 5a79f069..d179c1a5 100644 --- a/src/entities/voting-round/hooks/rounds.ts +++ b/src/entities/voting-round/hooks/rounds.ts @@ -1,12 +1,16 @@ import { useMemo } from "react"; import { votingContractHooks } from "@/common/contracts/core/voting"; +import type { ConditionalActivation } from "@/common/types"; import type { VotingRound, VotingRoundKey } from "../types"; // TODO: Figure out a way to know exactly which ONE election to pick ( Pots V2 milestone ) -export const useVotingRound = ({ potId }: VotingRoundKey): VotingRound | undefined => { - const { elections } = votingContractHooks.usePotElections({ potId }); +export const useVotingRound = ({ + potId, + enabled = true, +}: VotingRoundKey & ConditionalActivation): VotingRound | undefined => { + const { elections } = votingContractHooks.usePotElections({ enabled, potId }); return useMemo(() => { const election = elections?.at(0); diff --git a/src/entities/voting-round/index.ts b/src/entities/voting-round/index.ts index 8d301ec2..5fd36ff6 100644 --- a/src/entities/voting-round/index.ts +++ b/src/entities/voting-round/index.ts @@ -5,6 +5,7 @@ export * from "./types"; export * from "./components/badges"; export * from "./components/CandidateList"; export * from "./components/HistoryEntry"; +export * from "./components/Leaderboard"; export * from "./components/RuleList"; export * from "./components/VoteWeightBreakdown"; diff --git a/src/entities/voting-round/model/round-results.ts b/src/entities/voting-round/model/round-results.ts index 0ba7e32d..9caa5588 100644 --- a/src/entities/voting-round/model/round-results.ts +++ b/src/entities/voting-round/model/round-results.ts @@ -7,21 +7,21 @@ import { PRICES_REQUEST_CONFIG, intearPricesClient } from "@/common/api/intear-p import { is_human } from "@/common/contracts/core/sybil"; import { AccountId, type ElectionId, Vote } from "@/common/contracts/core/voting"; import { ftClient } from "@/common/contracts/tokens/ft"; +import type { ByAccountId } from "@/common/types"; import { VOTING_ROUND_CONFIG_MPDAO } from "./hardcoded"; -import type { VoterProfile, VotingRoundCandidateResult } from "../types"; +import type { VoterProfile, VotingRoundWinner } from "../types"; -type VotingRoundCandidateIntermediateResult = { - accountId: string; +type VotingRoundWinnerIntermediateData = ByAccountId & { accumulatedWeight: Big; }; -type VotingRoundCandidateResultRegistry = { - candidates: Record; +type VotingRoundWinnerRegistry = { + winners: Record; }; interface VotingRoundResultsState { - resultsCache: Record; + resultsCache: Record; updateResults: (params: { electionId: number; @@ -155,7 +155,7 @@ export const useRoundResultsStore = create()( // First pass: Calculate accumulated weights for each candidate const intermediateResults = Object.entries(votesByCandidate).reduce< - Record + Record >((acc, [candidateAccountId, candidateVotes]) => { // Calculate total weight for this candidate const accumulatedWeight = candidateVotes.reduce((sum, vote) => { @@ -172,7 +172,7 @@ export const useRoundResultsStore = create()( return acc; }, {}); - // Calculate total accumulated weight across all candidates + // Calculate total accumulated weight across all winners const totalAccumulatedWeight = Object.values(intermediateResults).reduce( (sum, result) => sum.add(result.accumulatedWeight), Big(0), @@ -180,7 +180,7 @@ export const useRoundResultsStore = create()( // Second pass: Calculate estimated payouts using total accumulated weight const candidateResults = Object.entries(intermediateResults).reduce< - VotingRoundCandidateResultRegistry["candidates"] + VotingRoundWinnerRegistry["winners"] >((acc, [candidateAccountId, result]) => { acc[candidateAccountId as AccountId] = { ...result, @@ -200,7 +200,7 @@ export const useRoundResultsStore = create()( [electionId]: { totalVoteCount: votes.length, - candidates: candidateResults, + winners: candidateResults, }, }, })); diff --git a/src/entities/voting-round/types.ts b/src/entities/voting-round/types.ts index 68b4078b..8f227cc8 100644 --- a/src/entities/voting-round/types.ts +++ b/src/entities/voting-round/types.ts @@ -53,7 +53,7 @@ export type VotingRound = ByElectionId & { election: Election }; export type VotingRoundKey = ByPotId; -export type VotingRoundCandidateResult = ByAccountId & { +export type VotingRoundWinner = ByAccountId & { accumulatedWeight: number; estimatedPayoutAmount: number; }; diff --git a/src/features/pot-application/hooks/clearance.ts b/src/features/pot-application/hooks/clearance.ts index 0641c4de..a2ab7645 100644 --- a/src/features/pot-application/hooks/clearance.ts +++ b/src/features/pot-application/hooks/clearance.ts @@ -19,8 +19,8 @@ import { POT_APPLICATION_REQUIREMENTS_MPDAO } from "../constants"; */ export const usePotApplicationUserClearance = ({ potId, - hasVoting, -}: ByPotId & { hasVoting?: boolean }): ClearanceCheckResult => { + hasProportionalFundingMechanism, +}: ByPotId & { hasProportionalFundingMechanism?: boolean }): ClearanceCheckResult => { const { staking } = POT_APPLICATION_REQUIREMENTS_MPDAO; const { data: pot } = indexer.usePot({ potId }); @@ -45,7 +45,7 @@ export const usePotApplicationUserClearance = ({ ] : []), - ...(hasVoting + ...(hasProportionalFundingMechanism ? [ { title: `An equivalent of ${staking.minAmountUsd} USD staked in NEAR on ${staking.platformName}`, @@ -73,7 +73,7 @@ export const usePotApplicationUserClearance = ({ error: null, }; }, [ - hasVoting, + hasProportionalFundingMechanism, isAccountInfoLoading, isVerifiedPublicGoodsProvider, pot?.sybil_wrapper_provider, diff --git a/src/features/proportional-funding/components/ConfigurationForm.tsx b/src/features/proportional-funding/components/ConfigurationForm.tsx index f658ffb9..9ae9bdc4 100644 --- a/src/features/proportional-funding/components/ConfigurationForm.tsx +++ b/src/features/proportional-funding/components/ConfigurationForm.tsx @@ -17,7 +17,7 @@ export type ProportionalFundingConfigurationFormProps = ByPotId & { export const ProportionalFundingConfigurationForm: React.FC< ProportionalFundingConfigurationFormProps > = ({ potId, footerContent, className }) => { - const { hasVoting } = usePotFeatureFlags({ potId }); + const { hasProportionalFundingMechanism } = usePotFeatureFlags({ potId }); const form = useForm({ resolver: zodResolver(votingConfigurationSchema) }); @@ -34,7 +34,7 @@ export const ProportionalFundingConfigurationForm: React.FC< */} - {hasVoting ? null : ( + {hasProportionalFundingMechanism ? null : (
diff --git a/src/layout/pot/components/PotLayoutHero.tsx b/src/layout/pot/components/PotLayoutHero.tsx index 4e2d6cda..b51fc2f9 100644 --- a/src/layout/pot/components/PotLayoutHero.tsx +++ b/src/layout/pot/components/PotLayoutHero.tsx @@ -20,11 +20,10 @@ import { usePotFeatureFlags, usePotLifecycle, } from "@/entities/pot"; +import { VotingRoundLeaderboard } from "@/entities/voting-round"; import { DonateToPotProjects } from "@/features/donation"; import { usePotApplicationUserClearance } from "@/features/pot-application"; -import { PotVotingLeaderboard } from "./PotVotingLeaderboard"; - export type PotLayoutHeroProps = ByPotId & { onApplyClick?: () => void; onChallengePayoutsClick?: () => void; @@ -38,10 +37,15 @@ export const PotLayoutHero: React.FC = ({ onFundMatchingPoolClick, }) => { const { data: pot } = indexer.usePot({ potId }); - const { hasVoting } = usePotFeatureFlags({ potId }); + const { hasProportionalFundingMechanism } = usePotFeatureFlags({ potId }); const { isSignedIn, accountId } = useSession(); - const applicationClearance = usePotApplicationUserClearance({ potId, hasVoting }); - const lifecycle = usePotLifecycle({ potId, hasVoting }); + + const applicationClearance = usePotApplicationUserClearance({ + potId, + hasProportionalFundingMechanism, + }); + + const lifecycle = usePotLifecycle({ potId, hasProportionalFundingMechanism }); const isApplicationPeriodOngoing = useMemo( () => lifecycle.currentStage?.tag === PotLifecycleStageTagEnum.Application, @@ -81,9 +85,19 @@ export const PotLayoutHero: React.FC = ({ donationStats ); } else { - return hasVoting ? : donationStats; + return hasProportionalFundingMechanism ? ( + + ) : ( + donationStats + ); } - }, [applicationClearance.requirements, hasVoting, isApplicationPeriodOngoing, pot, potId]); + }, [ + applicationClearance.requirements, + hasProportionalFundingMechanism, + isApplicationPeriodOngoing, + pot, + potId, + ]); return (
= ({ {pot ? ( ) : ( @@ -189,10 +203,14 @@ export const PotLayoutHero: React.FC = ({
{canApply && applicationClearance.isEveryRequirementSatisfied && ( - + )} - {hasVoting ? null : <>{canDonate && }} + {hasProportionalFundingMechanism ? null : ( + <>{canDonate && } + )} {canFund && (