diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 191964fd..4edf2cd8 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/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/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 = ({ ); })} - + Total Vote:{' '} 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/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 && ( { + 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..ad7c776a --- /dev/null +++ b/src/components/voting/VoteResultsPanel.tsx @@ -0,0 +1,188 @@ +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], + ]; + + const hasUserVoted = userVotes && userVotes.length > 0; + + return ( + + + + {hasUserVoted ? 'Your vote has been submitted!' : 'Voting is Complete!'} + + + {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} + + + + + {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/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 = ({ = { GM_FACTORY: '0x14e32E7893D6A1fA5f852d8B2fE8c57A2aB670ba', GS_FACTORY: '0x8D994BEef251e30C858e44eCE3670feb998CA77a', HATS_POSTER: '0x4F0dc1C7d91d914d921F3C9C188F4454AE260317', - VOTE_CONTEST: '0xF55108489EE3FE27AE60795d5816E0C95683B049', + VOTE_CONTEST: '0x009C84Ba52F109Ed3559FA39a60b5b8A2f167ec0', } as const; export const ADDR_PROD: Record = { 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/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 019e82cd..85ee9dea 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,14 +1,12 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { MainSection, PageTitle } from '../layout/Sections'; import { - Avatar, Box, - Divider, + Button, Flex, Group, - Paper, - Progress, - Spoiler, + Modal, + Skeleton, Stack, Stepper, Text, @@ -25,31 +23,117 @@ 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'; +import { AppAlert } from '../components/UnderContruction'; +import { getShipGrants } from '../queries/getShipGrants'; +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 Logo from '../assets/Logo.svg?react'; + +import { IconExclamationCircle } from '@tabler/icons-react'; +import { DashGrant } from '../resolvers/grantResolvers'; export type VotingFormValues = z.infer<typeof votingSchema>; +export type CondensedChoiceData = { + id: string; + shipName: string; + 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(); + const { userVotes, votingStage, contestStatus } = useVoting(); + const { currentRound } = useGameManager(); - const isLaptop = useLaptop(); + if (isLoading) { + return <LoadingSkeleton />; + } - const isTablet = useTablet(); + if (error) { + return ( + <AppAlert + title="Error loading Ships" + description={error.message || 'Unknown error message'} + /> + ); + } - const isMobile = useMobile(); + const hasVotes = userVotes && userVotes.length > 0; + + if (!currentRound) { + return null; + } + + if ( + currentRound?.gameStatus < GameStatus.Completed && + contestStatus <= ContestStatus.Populating + ) { + return <PreVoting />; + } + + if (!ships) { + return <AppAlert title="No Ships Found" description="No ships found" />; + } + if (hasVotes || votingStage >= VotingStage.Closed) { + 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: [], @@ -58,6 +142,15 @@ export const Vote = () => { 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; @@ -72,360 +165,191 @@ export const Vote = () => { [ships] ); - if (!ships) { - return null; - } - - const hasVotes = userVotes && userVotes.length > 0; - - if (hasVotes) { - return <VoteConfirmationPanel ships={ships} />; - } + useEffect(() => { + const hasSeenHelp = JSON.parse( + localStorage.getItem('has-seen-help') || 'false' + ); + if (!hasSeenHelp) { + setModalOpen(true); + } + }, []); - const nextStep = () => + const nextStep = () => { setStep((current) => (current < 3 ? current + 1 : current)); + }; const prevStep = () => setStep((current) => (current > 0 ? current - 1 : current)); + const closeModal = () => { + localStorage.setItem('has-seen-help', JSON.stringify(true)); + setModalOpen(false); + }; return ( <Flex w="100%"> <MainSection> - <VoteAffix formValues={form.values} /> - <PageTitle title="Vote" /> - <Stepper - active={step} - maw={600} - miw={300} - size="xs" - w={'100%'} - mt={'lg'} - mb="xl" - onStepClick={setStep} - > - {ships?.map((ship, index) => ( + <Box pos="relative"> + <VoteAffix formValues={form.values} /> + + <PageTitle title="Vote" /> + <Button + variant="light" + pos="absolute" + top={0} + right={0} + rightSection={<IconExclamationCircle size={20} />} + onClick={() => setModalOpen(true)} + > + Help + </Button> + <Stepper + active={step} + maw={600} + miw={300} + size="xs" + w={'100%'} + mt={'lg'} + mb="xl" + onStepClick={setStep} + > + {ships?.map((ship, index) => ( + <Stepper.Step + key={ship.id} + mih={isTablet ? 36 : undefined} + label={isMobile ? undefined : `Ship ${index + 1}`} + style={{ + alignItems: 'center', + }} + > + <ShipVotingPanel + ship={ship} + form={form} + index={index} + grants={ship.grants} + recentRecord={ship.recentRecord} + nextStep={nextStep} + prevStep={prevStep} + /> + </Stepper.Step> + ))} <Stepper.Step - key={ship.id} + label={isMobile ? undefined : 'Final'} mih={isTablet ? 36 : undefined} - label={isMobile ? undefined : `Ship ${index + 1}`} style={{ alignItems: 'center', }} > - <ShipVotingPanel - ship={ship} - form={form} - index={index} - nextStep={nextStep} - prevStep={prevStep} - /> + <ConfirmationPanel ships={ships} form={form} /> </Stepper.Step> - ))} - <Stepper.Step - label={isMobile ? undefined : 'Final'} - mih={isTablet ? 36 : undefined} - style={{ - alignItems: 'center', - }} - > - <ConfirmationPanel ships={ships} form={form} /> - </Stepper.Step> - </Stepper> + </Stepper> + </Box> </MainSection> {!isLaptop && ( <Stack gap={'md'} mt={72} w={270}> <VoteTimesIndicator /> </Stack> )} + <Modal + opened={modalOpen} + onClose={closeModal} + centered + w={'50%'} + title={ + <Text fz="lg" fw={600}> + Grant Ships Voting + </Text> + } + > + <InfoModalContent closeModal={closeModal} /> + </Modal> </Flex> ); }; -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 ( - <MainSection maw={850}> +const LoadingSkeleton = () => ( + <Flex w="100%"> + <MainSection> <PageTitle title="Vote" /> - <Text fz={32} fw={600} mt="xl"> - Your vote has been submitted! - </Text> - <Flex w="100%" justify="space-between" wrap="wrap" mt={40}> - <Stack w={350} gap="lg" mb={40}> - <Text fz="xl" fw={500}> - Your Vote - </Text> - {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 ( - <Box key={`total_v_${ship.id}`}> - <Group gap="xs" mb="sm"> - <Avatar size={32} src={ship.imgUrl} /> - <Text fz="md" fw={600}> - {ship.name} - </Text> - </Group> - <Progress value={Number(percentage)} color={colors[index]} /> - <Text fz="sm" mt="xs"> - {Number(percentage)}% Voted ({tokenAmount}{' '} - {tokenData.tokenSymbol}) - </Text> - </Box> - ); - })} - <Text fz="sm" mt="xs"> - <Text fz="sm" component="span" fw={600}> - Total:{' '} - </Text> - {formatEther(totals?.totalUserVotes || 0n)} {tokenData.tokenSymbol} - </Text> - </Stack> - <Stack w={350} mb={40} gap="lg"> - <Text fz="xl" fw={500}> - Total Vote Results - </Text> - {consolidated?.map((ship, index) => { - const percentage = totals?.totalVotes - ? formatBigIntPercentage( - BigInt(ship.choice?.voteTally), - totals?.totalVotes - ) - : '0'; - const tokenAmount = formatEther(BigInt(ship.choice?.voteTally)); - return ( - <Box key={`total_v_${ship.id}`}> - <Group gap="xs" mb="sm"> - <Avatar size={32} src={ship.imgUrl} /> - <Text fz="md" fw={600}> - {ship.name} - </Text> - </Group> - <Progress value={Number(percentage)} color={colors[index]} /> - <Text fz="sm" mt="xs"> - {Number(percentage)}% ({tokenAmount} {tokenData.tokenSymbol}) - </Text> - </Box> - ); - })} - <Text fz="sm" mt="xs"> - <Text fz="sm" component="span" fw={600}> - Total:{' '} - </Text> - {formatEther(totals?.totalVotes || 0n)} {tokenData.tokenSymbol} + <Stepper + active={0} + maw={600} + miw={300} + size="xs" + w={'100%'} + mt={'lg'} + mb="xl" + > + <Stepper.Step label="Ship 1"> + <Text fz="xl" fw={600} mb="md"> + Ship Portfolio Report </Text> - </Stack> - </Flex> - <Divider /> - <Text fz="xl" my="xl" fw={500}> - All Votes - </Text> - <AllVotes choices={condensed} /> + <Skeleton w={'100%'} h={120} mb="xl" /> + <Skeleton h={16} w="50%" mb={'md'} /> + <Skeleton h={89} w="100%" mb="xl" /> + <Skeleton h={16} w="50%" mb={'md'} /> + <Flex h={69} align="center"> + <Skeleton circle h={32} w={32} mr="sm" /> + <Skeleton h={20} w="70%" /> + </Flex> + <Skeleton h={1} w="100%" /> + <Flex h={69} align="center"> + <Skeleton circle h={32} w={32} mr="sm" /> + <Skeleton h={20} w="70%" /> + </Flex> + <Skeleton h={1} w="100%" /> + <Flex h={69} align="center"> + <Skeleton circle h={32} w={32} mr="sm" /> + <Skeleton h={20} w="70%" /> + </Flex> + <Skeleton h={1} w="100%" /> + </Stepper.Step> + <Stepper.Step label="Ship 2" /> + <Stepper.Step label="Ship 3" /> + <Stepper.Step label="Final" /> + </Stepper> </MainSection> - ); -}; - -type CondensedChoiceData = { - id: string; - shipName: string; - shipImg: string; -}; + </Flex> +); -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 ( - <Flex w={'100%'} wrap="wrap" justify={'space-between'}> - {voters?.map((voter) => ( - <VoteCard - key={voter.id} - voter={voter} - choices={choices} - tokenSymbol={tokenData?.tokenSymbol || undefined} - /> - ))} - </Flex> - ); -}; - -const VoteCard = ({ - voter, - choices, - tokenSymbol, -}: { - voter: GsVoter; - choices: CondensedChoiceData[]; - tokenSymbol?: string; -}) => { +const InfoModalContent = ({ closeModal }: { closeModal: () => void }) => { 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 ( - <Paper w={350} mb="lg" bg={theme.colors.dark[6]} p={'lg'}> - <Box mb="xl"> - <AddressAvatar address={voter.id as Address} size={36} /> + <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> - - {consolidated.map((choice, index) => { - return ( - <ShipChoiceVoteBar - tokenSymbol={tokenSymbol} - key={`${voter.id}-${choice.id}`} - choice={{ - shipImg: choice.shipImg, - shipName: choice.shipName, - id: choice.id, - }} - totalVotes={totalUserVotes} - reason={choice?.vote?.reason} - voteAmount={BigInt(choice.vote?.amount || 0)} - color={colors[index]} - didVote={!!choice.vote} - /> - ); - })} - </Paper> - ); -}; - -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 ( - <Box mb="md"> - <Group w={'100%'} mb="sm" align="flex-end"> - <Avatar size={32} src={choice.shipImg} /> - <Box maw={250} w="100%"> - <Progress - value={Number(votePercentage)} - color={color} - opacity={0.7} - mb={2} - /> - <Text fz="xs"> - {votePercentage}% Voted ({formatEther(voteAmount)}){' '} - {tokenSymbol || ''} - </Text> - </Box> - </Group> - <Spoiler - mb={'xs'} - maw={300} - hideLabel={<IconChevronUp stroke={1} />} - showLabel={<IconChevronDown stroke={1} />} - classNames={{ - root: classes.embedTextBox, - control: classes.embedTextControl, - }} - maxHeight={24} - > - {!didVote && ( - <Text fz="sm" className="ws-pre-wrap"> - Did not vote - </Text> - )} - {didVote && !reason && ( - <Text fz="sm" className="ws-pre-wrap"> - No reason given - </Text> - )} - {didVote && reason && ( - <Text fz="sm" className="ws-pre-wrap"> - {reason} - </Text> - )} - </Spoiler> </Box> ); }; 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; } 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',