From b413baae8ed4c2092a4d22d66bcc66333922fdbd Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 9 Jun 2024 20:37:58 -0700 Subject: [PATCH 1/9] banner states --- src/components/Banner.tsx | 130 +++++++++++++++++++++++++++++++++++-- src/constants/addresses.ts | 2 +- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 191964fd..3bed219d 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -3,12 +3,15 @@ import { Link } from 'react-router-dom'; import classes from '../pages/PageStyles.module.css'; import { useGameManager } from '../hooks/useGameMangers'; -import { GameStatus } from '../types/common'; +import { ContestStatus, GameStatus, VotingStage } from '../types/common'; import { ReactNode } from 'react'; import { useMobile } from '../hooks/useBreakpoint'; +import { useVoting } from '../hooks/useVoting'; +import { secondsToLongDate } from '../utils/time'; export const Banner = () => { const { gm, isLoadingGm, gmError } = useGameManager(); + const { contestStatus, contest, votingStage } = useVoting(); const isMobile = useMobile(); if (isLoadingGm) return ; @@ -119,12 +122,67 @@ export const Banner = () => { ); } - if (gm.currentRound.gameStatus === GameStatus.Completed) { + const roundOver = gm.currentRound.gameStatus === GameStatus.Completed; + + if ( + (roundOver && contestStatus === ContestStatus.Populating) || + (roundOver && contestStatus === ContestStatus.None) + ) { return ( + Preview Portfolios + + } + infoBtn={ + + } + /> + + ); + } + + const voteIsIdle = + roundOver && + contestStatus === ContestStatus.None && + votingStage === VotingStage.None; + + const voteIsSetup = + roundOver && + contestStatus === ContestStatus.Voting && + votingStage === VotingStage.Initiated; + + const voteIsActive = + roundOver && + contestStatus === ContestStatus.Voting && + votingStage === VotingStage.Active; + + const voteIsClosed = + votingStage >= VotingStage.Closed || + contestStatus >= ContestStatus.Finalized; + + if (voteIsIdle || voteIsSetup) { + return ( + + { infoBtn={ + } + /> + + ); + } + + if (voteIsActive) { + return ( + + + Vote Now + + } + infoBtn={ + + } + /> + + ); + } + + if (voteIsClosed) { + return ( + + + See Results + + } + infoBtn={ + } /> diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index e1515687..95acd0cd 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -8,7 +8,7 @@ export const ADDR_TESTNET: Record = { GM_FACTORY: '0x14e32E7893D6A1fA5f852d8B2fE8c57A2aB670ba', GS_FACTORY: '0x8D994BEef251e30C858e44eCE3670feb998CA77a', HATS_POSTER: '0x4F0dc1C7d91d914d921F3C9C188F4454AE260317', - VOTE_CONTEST: '0xF55108489EE3FE27AE60795d5816E0C95683B049', + VOTE_CONTEST: '0x27aa768716cdbAf364fF4023bf11Ed0C144C8bfB', } as const; export const ADDR_PROD: Record = { From b0f99714c9094d66b7a6b9a740aa46f7aafb04c7 Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 9 Jun 2024 21:06:12 -0700 Subject: [PATCH 2/9] big refactor --- src/components/voting/VoteCard.tsx | 141 +++++++++ src/components/voting/VoteResultsPanel.tsx | 182 ++++++++++++ src/pages/Vote.tsx | 323 +-------------------- 3 files changed, 333 insertions(+), 313 deletions(-) create mode 100644 src/components/voting/VoteCard.tsx create mode 100644 src/components/voting/VoteResultsPanel.tsx diff --git a/src/components/voting/VoteCard.tsx b/src/components/voting/VoteCard.tsx new file mode 100644 index 00000000..8d61fca2 --- /dev/null +++ b/src/components/voting/VoteCard.tsx @@ -0,0 +1,141 @@ +import { + Avatar, + Box, + Group, + Paper, + Progress, + Spoiler, + Text, + useMantineTheme, +} from '@mantine/core'; +import { CondensedChoiceData } from '../../pages/Vote'; +import { GsVoter } from '../../queries/getVoters'; +import { useMemo } from 'react'; +import { AddressAvatar } from '../AddressAvatar'; +import { Address, formatEther } from 'viem'; +import { formatBigIntPercentage } from '../../utils/helpers'; +import { IconChevronDown, IconChevronUp } from '@tabler/icons-react'; +import classes from '../feed/FeedStyles.module.css'; + +export const VoteCard = ({ + voter, + choices, + tokenSymbol, +}: { + voter: GsVoter; + choices: CondensedChoiceData[]; + tokenSymbol?: string; +}) => { + const theme = useMantineTheme(); + const colors = [ + theme.colors.blue[5], + theme.colors.violet[5], + theme.colors.pink[5], + ]; + + const totalUserVotes = useMemo(() => { + return voter.votes.reduce((acc, vote) => { + return acc + BigInt(vote?.amount ? vote.amount : 0); + }, 0n); + }, [voter]); + + const consolidated = useMemo(() => { + return choices.map((choice) => { + const vote = voter.votes.find((vote) => choice.id === vote.choice.id); + return { ...choice, vote }; + }); + }, [voter, choices]); + + return ( + + + + + + {consolidated.map((choice, index) => { + return ( + + ); + })} + + ); +}; + +const ShipChoiceVoteBar = ({ + choice, + totalVotes, + voteAmount, + reason, + color, + tokenSymbol, + didVote, +}: { + choice: CondensedChoiceData; + totalVotes: bigint; + voteAmount: bigint; + reason?: string | null; + color: string; + tokenSymbol?: string; + didVote?: boolean; +}) => { + const votePercentage = formatBigIntPercentage(voteAmount, totalVotes); + return ( + + + + + + + {votePercentage}% Voted ({formatEther(voteAmount)}){' '} + {tokenSymbol || ''} + + + + } + showLabel={} + classNames={{ + root: classes.embedTextBox, + control: classes.embedTextControl, + }} + maxHeight={24} + > + {!didVote && ( + + Did not vote + + )} + {didVote && !reason && ( + + No reason given + + )} + {didVote && reason && ( + + {reason} + + )} + + + ); +}; diff --git a/src/components/voting/VoteResultsPanel.tsx b/src/components/voting/VoteResultsPanel.tsx new file mode 100644 index 00000000..c22a2acf --- /dev/null +++ b/src/components/voting/VoteResultsPanel.tsx @@ -0,0 +1,182 @@ +import { + Avatar, + Box, + Divider, + Flex, + Group, + Progress, + Stack, + Text, + useMantineTheme, +} from '@mantine/core'; +import { useVoting } from '../../hooks/useVoting'; +import { ShipsCardUI } from '../../types/ui'; +import { useMemo } from 'react'; +import { MainSection, PageTitle } from '../../layout/Sections'; +import { formatBigIntPercentage } from '../../utils/helpers'; +import { formatEther } from 'viem'; +import { CondensedChoiceData } from '../../pages/Vote'; +import { getContestVoters } from '../../queries/getVoters'; +import { VoteCard } from './VoteCard'; +import { useQuery } from '@tanstack/react-query'; + +export const VoteResultsPanel = ({ ships }: { ships: ShipsCardUI[] }) => { + const { contest, userVotes, tokenData } = useVoting(); + + const theme = useMantineTheme(); + + const consolidated = useMemo(() => { + if (!ships || !userVotes || !contest) return []; + + return ships.map((ship) => { + const shipChoice = contest?.choices.find((c) => c.shipId === ship.id); + + const userVote = userVotes.find((v) => v.choice_id === shipChoice?.id); + + return { ...ship, vote: userVote, choice: shipChoice }; + }); + }, [ships, userVotes, contest]); + + const totals = useMemo(() => { + if (!consolidated || !contest) return null; + + const totalUserVotes = + consolidated && consolidated.length > 0 + ? consolidated.reduce((acc, ship) => { + if (!ship.vote) return acc; + return acc + BigInt(ship.vote.amount); + }, 0n) + : 0n; + const totalVotes = contest?.choices?.length + ? contest?.choices.reduce((acc, choice) => { + return acc + BigInt(choice.voteTally); + }, 0n) + : 0n; + + return { + totalUserVotes, + totalVotes, + }; + }, [consolidated, contest]); + + const condensed = useMemo(() => { + return consolidated?.map((ship) => ({ + shipImg: ship.imgUrl, + shipName: ship.name, + id: ship.choice?.id as string, + })); + }, [consolidated]); + + const colors = [ + theme.colors.blue[5], + theme.colors.violet[5], + theme.colors.pink[5], + ]; + return ( + + + + Your vote has been submitted! + + + + + Your Vote + + {consolidated.map((ship, index) => { + const percentage = totals?.totalUserVotes + ? formatBigIntPercentage( + BigInt(ship.vote?.amount || 0), + totals?.totalUserVotes + ) + : '0'; + const tokenAmount = formatEther(BigInt(ship.vote?.amount || 0)); + + return ( + + + + + {ship.name} + + + + + {Number(percentage)}% Voted ({tokenAmount}{' '} + {tokenData.tokenSymbol}) + + + ); + })} + + + Total:{' '} + + {formatEther(totals?.totalUserVotes || 0n)} {tokenData.tokenSymbol} + + + + + Total Vote Results + + {consolidated?.map((ship, index) => { + const percentage = totals?.totalVotes + ? formatBigIntPercentage( + BigInt(ship.choice?.voteTally), + totals?.totalVotes + ) + : '0'; + const tokenAmount = formatEther(BigInt(ship.choice?.voteTally)); + return ( + + + + + {ship.name} + + + + + {Number(percentage)}% ({tokenAmount} {tokenData.tokenSymbol}) + + + ); + })} + + + Total:{' '} + + {formatEther(totals?.totalVotes || 0n)} {tokenData.tokenSymbol} + + + + + + All Votes + + + + ); +}; + +const AllVotes = ({ choices }: { choices: CondensedChoiceData[] }) => { + const { contest, tokenData } = useVoting(); + const { data: voters } = useQuery({ + queryKey: ['gs-voters'], + queryFn: () => getContestVoters(contest?.id as string), + enabled: !!contest, + }); + + return ( + + {voters?.map((voter) => ( + + ))} + + ); +}; diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index 019e82cd..d22eb21c 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,19 +1,6 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { MainSection, PageTitle } from '../layout/Sections'; -import { - Avatar, - Box, - Divider, - Flex, - Group, - Paper, - Progress, - Spoiler, - Stack, - Stepper, - Text, - useMantineTheme, -} from '@mantine/core'; +import { Flex, Stack, Stepper } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { getShipsPageData } from '../queries/getShipsPage'; import { useLaptop, useMobile, useTablet } from '../hooks/useBreakpoint'; @@ -25,16 +12,16 @@ import { VoteTimesIndicator } from '../components/voting/VoteTimesIndicator'; import { ShipVotingPanel } from '../components/voting/ShipVotingPanel'; import { ConfirmationPanel } from '../components/voting/ConfirmationPanel'; import { useVoting } from '../hooks/useVoting'; -import { ShipsCardUI } from '../types/ui'; -import { formatBigIntPercentage } from '../utils/helpers'; -import { Address, formatEther } from 'viem'; -import classes from '../components/feed/FeedStyles.module.css'; -import { IconChevronDown, IconChevronUp } from '@tabler/icons-react'; -import { GsVoter, getContestVoters } from '../queries/getVoters'; -import { AddressAvatar } from '../components/AddressAvatar'; +import { VoteResultsPanel } from '../components/voting/VoteResultsPanel'; export type VotingFormValues = z.infer; +export type CondensedChoiceData = { + id: string; + shipName: string; + shipImg: string; +}; + export const Vote = () => { const [step, setStep] = useState(0); const { data: ships } = useQuery({ @@ -79,7 +66,7 @@ export const Vote = () => { const hasVotes = userVotes && userVotes.length > 0; if (hasVotes) { - return ; + return ; } const nextStep = () => @@ -139,293 +126,3 @@ export const Vote = () => { ); }; - -const VoteConfirmationPanel = ({ ships }: { ships: ShipsCardUI[] }) => { - const { contest, userVotes, tokenData } = useVoting(); - - const theme = useMantineTheme(); - - const consolidated = useMemo(() => { - if (!ships || !userVotes || !contest) return []; - - return ships.map((ship) => { - const shipChoice = contest?.choices.find((c) => c.shipId === ship.id); - - const userVote = userVotes.find((v) => v.choice_id === shipChoice?.id); - - return { ...ship, vote: userVote, choice: shipChoice }; - }); - }, [ships, userVotes, contest]); - - const totals = useMemo(() => { - if (!consolidated || !contest) return null; - - const totalUserVotes = - consolidated && consolidated.length > 0 - ? consolidated.reduce((acc, ship) => { - if (!ship.vote) return acc; - return acc + BigInt(ship.vote.amount); - }, 0n) - : 0n; - const totalVotes = contest?.choices?.length - ? contest?.choices.reduce((acc, choice) => { - return acc + BigInt(choice.voteTally); - }, 0n) - : 0n; - - return { - totalUserVotes, - totalVotes, - }; - }, [consolidated, contest]); - - const condensed = useMemo(() => { - return consolidated?.map((ship) => ({ - shipImg: ship.imgUrl, - shipName: ship.name, - id: ship.choice?.id as string, - })); - }, [consolidated]); - - const colors = [ - theme.colors.blue[5], - theme.colors.violet[5], - theme.colors.pink[5], - ]; - return ( - - - - Your vote has been submitted! - - - - - Your Vote - - {consolidated.map((ship, index) => { - const percentage = totals?.totalUserVotes - ? formatBigIntPercentage( - BigInt(ship.vote?.amount || 0), - totals?.totalUserVotes - ) - : '0'; - const tokenAmount = formatEther(BigInt(ship.vote?.amount || 0)); - - return ( - - - - - {ship.name} - - - - - {Number(percentage)}% Voted ({tokenAmount}{' '} - {tokenData.tokenSymbol}) - - - ); - })} - - - Total:{' '} - - {formatEther(totals?.totalUserVotes || 0n)} {tokenData.tokenSymbol} - - - - - Total Vote Results - - {consolidated?.map((ship, index) => { - const percentage = totals?.totalVotes - ? formatBigIntPercentage( - BigInt(ship.choice?.voteTally), - totals?.totalVotes - ) - : '0'; - const tokenAmount = formatEther(BigInt(ship.choice?.voteTally)); - return ( - - - - - {ship.name} - - - - - {Number(percentage)}% ({tokenAmount} {tokenData.tokenSymbol}) - - - ); - })} - - - Total:{' '} - - {formatEther(totals?.totalVotes || 0n)} {tokenData.tokenSymbol} - - - - - - All Votes - - - - ); -}; - -type CondensedChoiceData = { - id: string; - shipName: string; - shipImg: string; -}; - -const AllVotes = ({ choices }: { choices: CondensedChoiceData[] }) => { - const { contest, tokenData } = useVoting(); - const { data: voters } = useQuery({ - queryKey: ['gs-voters'], - queryFn: () => getContestVoters(contest?.id as string), - enabled: !!contest, - }); - - return ( - - {voters?.map((voter) => ( - - ))} - - ); -}; - -const VoteCard = ({ - voter, - choices, - tokenSymbol, -}: { - voter: GsVoter; - choices: CondensedChoiceData[]; - tokenSymbol?: string; -}) => { - const theme = useMantineTheme(); - const colors = [ - theme.colors.blue[5], - theme.colors.violet[5], - theme.colors.pink[5], - ]; - - const totalUserVotes = useMemo(() => { - return voter.votes.reduce((acc, vote) => { - return acc + BigInt(vote?.amount ? vote.amount : 0); - }, 0n); - }, [voter]); - - const consolidated = useMemo(() => { - return choices.map((choice) => { - const vote = voter.votes.find((vote) => choice.id === vote.choice.id); - return { ...choice, vote }; - }); - }, [voter, choices]); - - return ( - - - - - - {consolidated.map((choice, index) => { - return ( - - ); - })} - - ); -}; - -const ShipChoiceVoteBar = ({ - choice, - totalVotes, - voteAmount, - reason, - color, - tokenSymbol, - didVote, -}: { - choice: CondensedChoiceData; - totalVotes: bigint; - voteAmount: bigint; - reason?: string | null; - color: string; - tokenSymbol?: string; - didVote?: boolean; -}) => { - const votePercentage = formatBigIntPercentage(voteAmount, totalVotes); - return ( - - - - - - - {votePercentage}% Voted ({formatEther(voteAmount)}){' '} - {tokenSymbol || ''} - - - - } - showLabel={} - classNames={{ - root: classes.embedTextBox, - control: classes.embedTextControl, - }} - maxHeight={24} - > - {!didVote && ( - - Did not vote - - )} - {didVote && !reason && ( - - No reason given - - )} - {didVote && reason && ( - - {reason} - - )} - - - ); -}; From c31f2d050f9ee0cc20612e98fa9e80a6675993d2 Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 9 Jun 2024 22:52:05 -0700 Subject: [PATCH 3/9] refactor vote to top level query, better page load --- src/components/voting/ShipVotingPanel.tsx | 48 +++++-------------- src/pages/Vote.tsx | 58 +++++++++++++++++++++-- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/components/voting/ShipVotingPanel.tsx b/src/components/voting/ShipVotingPanel.tsx index c15bdf39..1dc5dba1 100644 --- a/src/components/voting/ShipVotingPanel.tsx +++ b/src/components/voting/ShipVotingPanel.tsx @@ -1,20 +1,17 @@ import { UseFormReturnType } from '@mantine/form'; import { ShipsCardUI } from '../../types/ui'; import { VotingFormValues } from '../../pages/Vote'; -import { useQuery } from '@tanstack/react-query'; -import { getShipGrants } from '../../queries/getShipGrants'; import { useUserData } from '../../hooks/useUserState'; import { useVoting } from '../../hooks/useVoting'; import { useMemo } from 'react'; -import { getRecentPortfolioReport } from '../../queries/getRecordsByTag'; -import { ADDR } from '../../constants/addresses'; +import { PostedRecord } from '../../queries/getRecordsByTag'; import { formatEther } from 'viem'; import { Avatar, Box, Flex, Paper, Text, useMantineTheme } from '@mantine/core'; import { PortfolioReport } from '../dashboard/ship/PortfolioReport'; import { ContestStatus, ReportStatus, VotingStage } from '../../types/common'; -import { Tag } from '../../constants/tags'; import { VotingFooter } from './VotingFooter'; import { FacilitatorFooter } from './FacilitatorFooter'; +import { DashGrant } from '../../resolvers/grantResolvers'; export const ShipVotingPanel = ({ ship, @@ -22,7 +19,11 @@ export const ShipVotingPanel = ({ index, nextStep, prevStep, + grants, + recentRecord, }: { + grants: DashGrant[] | null; + recentRecord?: PostedRecord | null; ship: ShipsCardUI; form: UseFormReturnType< VotingFormValues, @@ -32,47 +33,20 @@ export const ShipVotingPanel = ({ nextStep: () => void; prevStep: () => void; }) => { - const { - data: grants, - error, - isLoading: isLoadingGrants, - } = useQuery({ - queryKey: [`portfolio-${ship.id}`], - queryFn: () => getShipGrants(ship.id as string), - enabled: !!ship.id, - }); - - const { - contest, - contestStatus, - isLoadingVoting, - refetchGsVotes, - votingStage, - } = useVoting(); + const { contest, contestStatus, refetchGsVotes, votingStage } = useVoting(); const theme = useMantineTheme(); - const { userData, userLoading } = useUserData(); + const { userData } = useUserData(); const shipChoiceId = useMemo(() => { return contest?.choices.find((choice) => choice.shipId === ship.id)?.id; }, [contest?.choices, ship]); - const { data: recentRecord, isLoading: isLoadingRecord } = useQuery({ - queryKey: [`ship-portfolio-${ship.id}`], - queryFn: () => - getRecentPortfolioReport( - `${Tag.ShipSubmitReport}-${ADDR.VOTE_CONTEST}-${ship.id}` - ), - enabled: !!ship.id, - }); - const totalAmount = formatEther( BigInt(ship.amtAllocated) + BigInt(ship.amtAvailable) + BigInt(ship.amtDistributed) ); - const isLoading = - isLoadingRecord || isLoadingVoting || isLoadingGrants || userLoading; return ( @@ -103,14 +77,14 @@ export const ShipVotingPanel = ({ - {contestStatus === ContestStatus.Populating && !isLoading && ( + {contestStatus === ContestStatus.Populating && ( ; @@ -22,11 +35,34 @@ export type CondensedChoiceData = { shipImg: string; }; +const bigVoteQuery = async () => { + const ships = await getShipsPageData(); + + const shipVoteData = await Promise.all( + ships.map(async (ship) => { + const [grants, recentRecord] = await Promise.all([ + getShipGrants(ship.id), + getRecentPortfolioReport( + `${Tag.ShipSubmitReport}-${ADDR.VOTE_CONTEST}-${ship.id}` + ), + ]); + + return { ...ship, grants, recentRecord }; + }) + ); + + return shipVoteData; +}; + export const Vote = () => { const [step, setStep] = useState(0); - const { data: ships } = useQuery({ + const { + data: ships, + isLoading, + error, + } = useQuery({ queryKey: ['ships-page'], - queryFn: getShipsPageData, + queryFn: bigVoteQuery, }); const { userVotes } = useVoting(); @@ -59,6 +95,15 @@ export const Vote = () => { [ships] ); + if (error) { + return ( + + ); + } + if (!ships) { return null; } @@ -69,8 +114,9 @@ export const Vote = () => { return ; } - const nextStep = () => + const nextStep = () => { setStep((current) => (current < 3 ? current + 1 : current)); + }; const prevStep = () => setStep((current) => (current > 0 ? current - 1 : current)); @@ -98,13 +144,17 @@ export const Vote = () => { alignItems: 'center', }} > + {/* */} + {/* */} ))} Date: Sun, 9 Jun 2024 23:21:11 -0700 Subject: [PATCH 4/9] skeleton --- .../dashboard/ship/PortfolioReport.tsx | 13 +---- src/pages/Vote.tsx | 55 +++++++++++++++++-- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/components/dashboard/ship/PortfolioReport.tsx b/src/components/dashboard/ship/PortfolioReport.tsx index e77fc917..39ad09d9 100644 --- a/src/components/dashboard/ship/PortfolioReport.tsx +++ b/src/components/dashboard/ship/PortfolioReport.tsx @@ -19,7 +19,6 @@ import { GAME_TOKEN } from '../../../constants/gameSetup'; import { IconChevronDown, IconChevronUp, - IconExclamationCircle, IconExternalLink, IconSquare, IconSquareCheck, @@ -76,8 +75,6 @@ export const PortfolioReport = ({ onReportSubmit?: () => void; reportData?: ReportData | null; }) => { - const theme = useMantineTheme(); - const form = useForm({ initialValues: defaultValues, validate: zodResolver(portfolioReportSchema), @@ -93,15 +90,7 @@ export const PortfolioReport = ({ ); - if (error || !grants) - return ( - } - description={error?.message || 'Grant Data failed to load'} - bg={theme.colors.pink[8]} - /> - ); + if (error || !grants) return null; if (grants.length === 0) return ( diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index 6ae51579..c5e34614 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; import { MainSection, PageTitle } from '../layout/Sections'; import { + Box, + Divider, Flex, - Loader, Skeleton, Stack, Stepper, Text, - Transition, } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { getShipsPageData } from '../queries/getShipsPage'; @@ -95,6 +95,53 @@ export const Vote = () => { [ships] ); + if (isLoading) { + return ( + + + + + + + Ship Portfolio Report + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + if (error) { return ( { } if (!ships) { - return null; + return ; } const hasVotes = userVotes && userVotes.length > 0; @@ -144,7 +191,6 @@ export const Vote = () => { alignItems: 'center', }} > - {/* */} { nextStep={nextStep} prevStep={prevStep} /> - {/* */} ))} Date: Mon, 10 Jun 2024 12:24:12 -0700 Subject: [PATCH 5/9] prevote --- src/components/voting/PreVoting.tsx | 38 ++++ src/components/voting/VoteResultsPanel.tsx | 74 +++---- src/constants/addresses.ts | 4 +- src/pages/CreateShip.tsx | 7 +- src/pages/Vote.tsx | 217 +++++++++++++-------- 5 files changed, 217 insertions(+), 123 deletions(-) create mode 100644 src/components/voting/PreVoting.tsx diff --git a/src/components/voting/PreVoting.tsx b/src/components/voting/PreVoting.tsx new file mode 100644 index 00000000..08c75f89 --- /dev/null +++ b/src/components/voting/PreVoting.tsx @@ -0,0 +1,38 @@ +import { Group, Paper, Text, useMantineTheme } from '@mantine/core'; +import { MainSection, PageTitle } from '../../layout/Sections'; +import { ShipBadge } from '../RoleBadges'; + +export const PreVoting = () => { + const theme = useMantineTheme(); + return ( + + + + + + + Voting is not open yet! + + + + Stay Tuned! + + + The voting round for Grant Ships is opened after the allocation round. + + + How it works + + + Ships receive funds from a DAO to distribute as grants for projects in + one round. At the round's end, DAO members allocate votes to ships + based on their performance. + + + Ships then receive funds in the next round based on the proportion of + votes they received. + + + + ); +}; diff --git a/src/components/voting/VoteResultsPanel.tsx b/src/components/voting/VoteResultsPanel.tsx index c22a2acf..ad7c776a 100644 --- a/src/components/voting/VoteResultsPanel.tsx +++ b/src/components/voting/VoteResultsPanel.tsx @@ -72,49 +72,55 @@ export const VoteResultsPanel = ({ ships }: { ships: ShipsCardUI[] }) => { theme.colors.violet[5], theme.colors.pink[5], ]; + + const hasUserVoted = userVotes && userVotes.length > 0; + return ( - Your vote has been submitted! + {hasUserVoted ? 'Your vote has been submitted!' : 'Voting is Complete!'} - - - Your Vote - - {consolidated.map((ship, index) => { - const percentage = totals?.totalUserVotes - ? formatBigIntPercentage( - BigInt(ship.vote?.amount || 0), - totals?.totalUserVotes - ) - : '0'; - const tokenAmount = formatEther(BigInt(ship.vote?.amount || 0)); + {hasUserVoted && ( + + + Your Vote + + {consolidated.map((ship, index) => { + const percentage = totals?.totalUserVotes + ? formatBigIntPercentage( + BigInt(ship.vote?.amount || 0), + totals?.totalUserVotes + ) + : '0'; + const tokenAmount = formatEther(BigInt(ship.vote?.amount || 0)); - return ( - - - - - {ship.name} + return ( + + + + + {ship.name} + + + + + {Number(percentage)}% Voted ({tokenAmount}{' '} + {tokenData.tokenSymbol}) - - - - {Number(percentage)}% Voted ({tokenAmount}{' '} - {tokenData.tokenSymbol}) - - - ); - })} - - - Total:{' '} + + ); + })} + + + Total:{' '} + + {formatEther(totals?.totalUserVotes || 0n)}{' '} + {tokenData.tokenSymbol} - {formatEther(totals?.totalUserVotes || 0n)} {tokenData.tokenSymbol} - - + + )} Total Vote Results diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index 95acd0cd..3f2fb7c1 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -8,7 +8,7 @@ export const ADDR_TESTNET: Record = { GM_FACTORY: '0x14e32E7893D6A1fA5f852d8B2fE8c57A2aB670ba', GS_FACTORY: '0x8D994BEef251e30C858e44eCE3670feb998CA77a', HATS_POSTER: '0x4F0dc1C7d91d914d921F3C9C188F4454AE260317', - VOTE_CONTEST: '0x27aa768716cdbAf364fF4023bf11Ed0C144C8bfB', + VOTE_CONTEST: '0x009C84Ba52F109Ed3559FA39a60b5b8A2f167ec0', } as const; export const ADDR_PROD: Record = { @@ -19,7 +19,7 @@ export const ADDR_PROD: Record = { GM_FACTORY: '0xdc9787b869e22256a4f4f49f484586fcff0d351f', GS_FACTORY: '0xb130175b648d4ce92ca6153eaa138cc69eb1cf4c', HATS_POSTER: '0x363a6eFF03cdAbD5Cf4921d9A85eAf7dFd2A7efD', - VOTE_CONTEST: '0x413BAaf73db2330639d93e26d8Fdd909C74A1955', + VOTE_CONTEST: '0x60B753C86D142D7538341B7Fc3Ef6E84499636bB', } as const; export const ADDR: Record = diff --git a/src/pages/CreateShip.tsx b/src/pages/CreateShip.tsx index 9811bc73..42bb4eed 100644 --- a/src/pages/CreateShip.tsx +++ b/src/pages/CreateShip.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useAccount, useWatchContractEvent } from 'wagmi'; +import { useAccount, useChainId, useWatchContractEvent } from 'wagmi'; import Registry from '../abi/Registry.json'; import { ADDR } from '../constants/addresses'; @@ -21,6 +21,7 @@ export type ProfileData = { export const CreateShip = () => { const { address } = useAccount(); + const chainId = useChainId(); const [step, setStep] = useState(0); const [profileData, setProfileData, removeProfileStorage] = useLocalStorage< @@ -34,9 +35,9 @@ export const CreateShip = () => { useWatchContractEvent({ abi: Registry, - address: ADDR.Registry, + address: ADDR.REGISTRY, eventName: 'ProfileCreated', - syncConnectedChain: true, + chainId, pollingInterval: 100, onError: (error) => { console.error('error', error); diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index c5e34614..797967d1 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,9 +1,12 @@ import { useEffect, useState } from 'react'; import { MainSection, PageTitle } from '../layout/Sections'; import { + ActionIcon, Box, - Divider, + Button, Flex, + Group, + Modal, Skeleton, Stack, Stepper, @@ -26,6 +29,14 @@ import { getShipGrants } from '../queries/getShipGrants'; import { getRecentPortfolioReport } from '../queries/getRecordsByTag'; import { Tag } from '../constants/tags'; import { ADDR } from '../constants/addresses'; +import { ContestStatus, GameStatus, VotingStage } from '../types/common'; +import { PreVoting } from '../components/voting/PreVoting'; +import { useGameManager } from '../hooks/useGameMangers'; +import { + IconExclamationCircle, + IconEyeQuestion, + IconQuestionMark, +} from '@tabler/icons-react'; export type VotingFormValues = z.infer; @@ -65,7 +76,9 @@ export const Vote = () => { queryFn: bigVoteQuery, }); - const { userVotes } = useVoting(); + const { userVotes, votingStage, contestStatus } = useVoting(); + const [modalOpen, setModalOpen] = useState(false); + const { currentRound } = useGameManager(); const isLaptop = useLaptop(); @@ -96,50 +109,7 @@ export const Vote = () => { ); if (isLoading) { - return ( - - - - - - - Ship Portfolio Report - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return ; } if (error) { @@ -151,13 +121,24 @@ export const Vote = () => { ); } + const hasVotes = userVotes && userVotes.length > 0; + + if (!currentRound) { + return null; + } + + if ( + currentRound?.gameStatus < GameStatus.Completed && + contestStatus <= ContestStatus.Populating + ) { + return ; + } + if (!ships) { return ; } - const hasVotes = userVotes && userVotes.length > 0; - - if (hasVotes) { + if (hasVotes || votingStage >= VotingStage.Closed) { return ; } @@ -167,57 +148,125 @@ export const Vote = () => { const prevStep = () => setStep((current) => (current > 0 ? current - 1 : current)); + const closeModal = () => setModalOpen(false); + return ( - - - - {ships?.map((ship, index) => ( + + + + + + + {ships?.map((ship, index) => ( + + + + ))} - + - ))} - - - - + + {!isLaptop && ( )} + + We are excited to + ); }; + +const LoadingSkeleton = () => ( + + + + + + + Ship Portfolio Report + + + + + + + + + + + + + + + + + + + + + + + + + + + +); From d2f8e48072948f1d47aac8c3e3c012bbe96ec85b Mon Sep 17 00:00:00 2001 From: jordan Date: Mon, 10 Jun 2024 13:41:52 -0700 Subject: [PATCH 6/9] restructure vote page + help modal --- src/layout/DesktopNav/DesktopNav.tsx | 4 +- src/pages/Vote.tsx | 165 ++++++++++++++++++++------- vite.config.ts | 5 +- 3 files changed, 127 insertions(+), 47 deletions(-) diff --git a/src/layout/DesktopNav/DesktopNav.tsx b/src/layout/DesktopNav/DesktopNav.tsx index 0cdcb5ff..ddbc488d 100644 --- a/src/layout/DesktopNav/DesktopNav.tsx +++ b/src/layout/DesktopNav/DesktopNav.tsx @@ -7,7 +7,7 @@ import { IconInfoCircle, } from '@tabler/icons-react'; import classes from './DesktoNavStyles.module.css'; -import Logo from '../../assets/Logo.svg'; +import Logo from '../../assets/Logo.svg?react'; import { ConnectButton } from './ConnectButton'; import { Link, useLocation } from 'react-router-dom'; @@ -147,7 +147,7 @@ export function DesktopNav() {
- + {!isTablet && ( diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index 797967d1..3fa967c9 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import { MainSection, PageTitle } from '../layout/Sections'; import { - ActionIcon, Box, Button, Flex, @@ -11,6 +10,7 @@ import { Stack, Stepper, Text, + useMantineTheme, } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { getShipsPageData } from '../queries/getShipsPage'; @@ -26,17 +26,19 @@ import { useVoting } from '../hooks/useVoting'; import { VoteResultsPanel } from '../components/voting/VoteResultsPanel'; import { AppAlert } from '../components/UnderContruction'; import { getShipGrants } from '../queries/getShipGrants'; -import { getRecentPortfolioReport } from '../queries/getRecordsByTag'; +import { + PostedRecord, + getRecentPortfolioReport, +} from '../queries/getRecordsByTag'; import { Tag } from '../constants/tags'; import { ADDR } from '../constants/addresses'; import { ContestStatus, GameStatus, VotingStage } from '../types/common'; import { PreVoting } from '../components/voting/PreVoting'; import { useGameManager } from '../hooks/useGameMangers'; -import { - IconExclamationCircle, - IconEyeQuestion, - IconQuestionMark, -} from '@tabler/icons-react'; +import Logo from '../assets/Logo.svg?react'; + +import { IconExclamationCircle } from '@tabler/icons-react'; +import { DashGrant } from '../resolvers/grantResolvers'; export type VotingFormValues = z.infer<typeof votingSchema>; @@ -66,7 +68,6 @@ const bigVoteQuery = async () => { }; export const Vote = () => { - const [step, setStep] = useState(0); const { data: ships, isLoading, @@ -77,37 +78,8 @@ export const Vote = () => { }); const { userVotes, votingStage, contestStatus } = useVoting(); - const [modalOpen, setModalOpen] = useState(false); const { currentRound } = useGameManager(); - const isLaptop = useLaptop(); - - const isTablet = useTablet(); - - const isMobile = useMobile(); - - const form = useForm({ - initialValues: { - ships: [], - } as VotingFormValues, - validate: zodResolver(votingSchema), - validateInputOnBlur: true, - }); - - useEffect( - () => { - if (!ships) return; - const updatedShips = ships?.map((ship) => ({ - shipId: ship.id, - shipPerc: 0, - shipComment: '', - })); - form.setValues((prev) => ({ ...prev, ships: updatedShips })); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ships] - ); - if (isLoading) { return <LoadingSkeleton />; } @@ -142,14 +114,76 @@ export const Vote = () => { return <VoteResultsPanel ships={ships} />; } + return <VotingOpen ships={ships} />; +}; + +const VotingOpen = ({ + ships, +}: { + ships: { + grants: DashGrant[] | null; + recentRecord: PostedRecord | null; + id: string; + name: string; + status: GameStatus; + imgUrl: string; + description: string; + amtAllocated: string; + amtDistributed: string; + amtAvailable: string; + balance: string; + }[]; +}) => { + const form = useForm({ + initialValues: { + ships: [], + } as VotingFormValues, + validate: zodResolver(votingSchema), + validateInputOnBlur: true, + }); + + const [step, setStep] = useState(0); + const [modalOpen, setModalOpen] = useState(false); + + const isLaptop = useLaptop(); + + const isTablet = useTablet(); + + const isMobile = useMobile(); + + useEffect( + () => { + if (!ships) return; + const updatedShips = ships?.map((ship) => ({ + shipId: ship.id, + shipPerc: 0, + shipComment: '', + })); + form.setValues((prev) => ({ ...prev, ships: updatedShips })); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ships] + ); + + useEffect(() => { + const hasSeenHelp = JSON.parse( + localStorage.getItem('has-seen-help') || 'false' + ); + if (!hasSeenHelp) { + setModalOpen(true); + } + }, []); + const nextStep = () => { setStep((current) => (current < 3 ? current + 1 : current)); }; const prevStep = () => setStep((current) => (current > 0 ? current - 1 : current)); - const closeModal = () => setModalOpen(false); - + const closeModal = () => { + localStorage.setItem('has-seen-help', JSON.stringify(true)); + setModalOpen(false); + }; return ( <Flex w="100%"> <MainSection> @@ -218,9 +252,14 @@ export const Vote = () => { opened={modalOpen} onClose={closeModal} centered - title="Welcome to Grant Ships Voting!" + w={'50%'} + title={ + <Text fz="lg" fw={600}> + Grant Ships Voting + </Text> + } > - <Text>We are excited to </Text> + <InfoModalContent closeModal={closeModal} /> </Modal> </Flex> ); @@ -270,3 +309,47 @@ const LoadingSkeleton = () => ( </MainSection> </Flex> ); + +const InfoModalContent = ({ closeModal }: { closeModal: () => void }) => { + const theme = useMantineTheme(); + return ( + <Box> + <Flex w="100%" justify="center" mb="md"> + <Logo height={80} width={80} /> + </Flex> + <Box mb="lg"> + <Text fw={600} mb={'sm'} fz="sm"> + Welcome! + </Text> + <Text fz="sm" c={theme.colors.dark[2]}> + We are excited to have you here for the first "Gaming on Arbitrum" + Voting Round + </Text> + </Box> + <Box mb="lg"> + <Text fz="sm" fw={600} mb={'sm'}> + How to vote + </Text> + <Text fz="sm" c={theme.colors.dark[2]} mb="sm"> + Read through each ship's (grant program) portfolio and evaluate the + projects funded. Leave a comment and an estimated %. When you reach + the final stage, you will be able to review your votes and submit + them. + </Text> + <Text fz="sm" c={theme.colors.dark[2]} mb="sm"></Text> + <Text fz="sm" c={theme.colors.dark[2]} mb="sm"> + The total percentages will determine the funding each ship receives in + the following round. + </Text> + <Text fz="sm" c={theme.colors.dark[2]} mb="sm"> + If you have any questions or need help, please reach out to us on + <a href="https://discord.gg/sqVzFKCf">Discord</a> or{' '} + <a href="https://t.me/grantships">Telegram.</a> + </Text> + <Group justify="flex-end" w="100%"> + <Button onClick={closeModal}>Got it!</Button> + </Group> + </Box> + </Box> + ); +}; diff --git a/vite.config.ts b/vite.config.ts index f179a006..ddd97a87 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,10 +11,7 @@ export default defineConfig({ nodePolyfills({ protocolImports: true, }), - svgr({ - svgrOptions: { exportType: 'default', ref: true }, - include: '**/*.svg', - }), + svgr(), ], define: { // globalThis: 'window', From a76f67e6297e635baef6e6ef81f2a846a6492de5 Mon Sep 17 00:00:00 2001 From: jordan <jordan.lesich@gmail.com> Date: Mon, 10 Jun 2024 13:43:48 -0700 Subject: [PATCH 7/9] fix null pointer bug --- src/resolvers/grantResolvers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvers/grantResolvers.ts b/src/resolvers/grantResolvers.ts index 6f9a878d..67103214 100644 --- a/src/resolvers/grantResolvers.ts +++ b/src/resolvers/grantResolvers.ts @@ -155,7 +155,7 @@ export const resolveMilestones = async ( }; export const resolveMilestoneReviewReason = async (pointer?: string) => { - if (!pointer) { + if (!pointer || pointer === 'NULL') { return null; } From a7e243972b1d3961e35f9b6fa25adbe742c8b880 Mon Sep 17 00:00:00 2001 From: jordan <jordan.lesich@gmail.com> Date: Mon, 10 Jun 2024 14:01:43 -0700 Subject: [PATCH 8/9] additional refinements --- src/components/Banner.tsx | 10 +++------- src/components/voting/ConfirmationPanel.tsx | 12 +++++++++++- src/components/voting/VotingFooter.tsx | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 3bed219d..4edf2cd8 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -212,14 +212,10 @@ export const Banner = () => { return ( <BannerBG> <Innards - statusText="Voting is Live! " - ctaText="Vote Now." + statusText="Grant Ships voting is live! " + ctaText="Cast your vote now." ctaButton={ - <Button - component={Link} - to="create-project" - size={isMobile ? 'xs' : 'sm'} - > + <Button component={Link} to="/vote" size={isMobile ? 'xs' : 'sm'}> Vote Now </Button> } diff --git a/src/components/voting/ConfirmationPanel.tsx b/src/components/voting/ConfirmationPanel.tsx index 00ebb607..96ed36e3 100644 --- a/src/components/voting/ConfirmationPanel.tsx +++ b/src/components/voting/ConfirmationPanel.tsx @@ -267,7 +267,17 @@ export const ConfirmationPanel = ({ </Box> ); })} - <Text fz="md" mb="xs"> + <Text + fz="md" + mb="xs" + c={ + totalPercent < 100 + ? theme.colors.yellow[6] + : exceeds100percent + ? theme.colors.red[7] + : 'inherit' + } + > <Text component="span" fw={600} fz="inherit"> Total Vote:{' '} </Text> diff --git a/src/components/voting/VotingFooter.tsx b/src/components/voting/VotingFooter.tsx index ef74f234..062a58f5 100644 --- a/src/components/voting/VotingFooter.tsx +++ b/src/components/voting/VotingFooter.tsx @@ -36,7 +36,7 @@ export const VotingFooter = ({ <Group align="flex-end" mb="md"> <NumberInput - label="Amount (%)" + label="Estimated Amount (%)" maw={298} min={0} max={100} From 971889a8c1bf080b445b05228fbbb6702ef6267f Mon Sep 17 00:00:00 2001 From: jordan <jordan.lesich@gmail.com> Date: Mon, 10 Jun 2024 14:05:35 -0700 Subject: [PATCH 9/9] typo --- src/pages/Vote.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index 3fa967c9..85ee9dea 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -342,7 +342,7 @@ const InfoModalContent = ({ closeModal }: { closeModal: () => void }) => { the following round. </Text> <Text fz="sm" c={theme.colors.dark[2]} mb="sm"> - If you have any questions or need help, please reach out to us on + If you have any questions or need help, please reach out to us on{' '} <a href="https://discord.gg/sqVzFKCf">Discord</a> or{' '} <a href="https://t.me/grantships">Telegram.</a> </Text>