Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Garage: show proposal votes on collector profiles #917

Merged
merged 9 commits into from
Aug 16, 2023
36 changes: 36 additions & 0 deletions garage/src/hooks/useOwnerVotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import { selectOwnerVotes } from "../utils/queries";
import { useTablelandConnection } from "./useTablelandConnection";
import { ProposalWithOptions } from "../types";

export interface Vote {
proposal: Pick<ProposalWithOptions, "id" | "name" | "options">;
ft: number;
choices: { optionId: string; weight: number; comment?: string }[];
}

export const useOwnerVotes = (owner?: string) => {
const { db } = useTablelandConnection();

const [votes, setVotes] = useState<Vote[]>();

useEffect(() => {
if (!owner) return;

let isCancelled = false;

db.prepare(selectOwnerVotes(owner))
.all<Vote>()
.then(({ results }) => {
if (isCancelled) return;

setVotes(results);
});

return () => {
isCancelled = true;
};
}, [owner, setVotes]);

return { votes };
};
9 changes: 5 additions & 4 deletions garage/src/hooks/useRigImageUrls.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Rig } from "../types";

const ipfsGatewayBaseUrl = "https://nftstorage.link";

const ipfsUriToGatewayUrl = (ipfsUri: string): string => {
const cidAndPath = ipfsUri.match(/^ipfs:\/\/(.*)$/)![1];
return `${ipfsGatewayBaseUrl}/ipfs/${cidAndPath}`;
const match = ipfsUri.match(/^ipfs:\/\/([a-zA-Z0-9]*)\/(.*)$/);
if (!match) return "";

const [, cid, path] = match;
return `https://${cid}.ipfs.nftstorage.link/${path}`;
};

interface RigImageUrls {
Expand Down
15 changes: 12 additions & 3 deletions garage/src/pages/OwnerDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { useOwnedRigs } from "../../hooks/useOwnedRigs";
import { useOwnerPilots } from "../../hooks/useOwnerPilots";
import { useOwnerActivity } from "../../hooks/useOwnerActivity";
import { useOwnerFTRewards } from "../../hooks/useOwnerFTRewards";
import { useOwnerVotes } from "../../hooks/useOwnerVotes";
import { useNFTsCached } from "../../components/NFTsContext";
import { TOPBAR_HEIGHT } from "../../Topbar";
import { RigsGrid } from "./modules/RigsInventory";
import { ActivityLog } from "./modules/Activity";
import { Pilots } from "./modules/Pilots";
import { FTRewards } from "./modules/FTRewards";
import { Votes } from "./modules/Votes";
import { prettyNumber } from "../../utils/fmt";
import { isValidAddress } from "../../utils/types";

Expand Down Expand Up @@ -45,6 +47,7 @@ export const OwnerDetails = () => {
const { events } = useOwnerActivity(owner);
const { nfts } = useNFTsCached(pilots);
const { rewards } = useOwnerFTRewards(owner);
const { votes } = useOwnerVotes(owner);

const { data: ens } = useEnsName({
address: isValidAddress(owner) ? owner : undefined,
Expand Down Expand Up @@ -91,17 +94,23 @@ export const OwnerDetails = () => {
width="100%"
align={{ base: "stretch", lg: "start" }}
>
<VStack align="top" spacing={GRID_GAP}>
<VStack align="top" spacing={GRID_GAP} flexGrow="1">
<RigsGrid
rigs={rigs}
nfts={nfts}
{...MODULE_PROPS}
gap={GRID_GAP}
flexGrow="1"
/>
<FTRewards rewards={rewards} {...MODULE_PROPS} flexGrow="1" />
<Votes votes={votes} {...MODULE_PROPS} />
<FTRewards rewards={rewards} {...MODULE_PROPS} />
</VStack>
<VStack flexShrink="0" align="top" spacing={GRID_GAP}>
<VStack
flexShrink="0"
align="top"
spacing={GRID_GAP}
minWidth={{ lg: "300px", xl: "360px" }}
>
<Pilots pilots={pilots} nfts={nfts} {...MODULE_PROPS} />
<ActivityLog events={events} {...MODULE_PROPS} />
</VStack>
Expand Down
4 changes: 2 additions & 2 deletions garage/src/pages/OwnerDetails/modules/RigsInventory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface RigsGridProps extends React.ComponentProps<typeof Box> {
export const RigsGrid = ({ rigs, nfts, gap, p, ...props }: RigsGridProps) => {
return (
<Box p={p} {...props}>
<Heading pb={p}>Rigs {rigs?.length && ` (${rigs.length})`}</Heading>
<Heading pb={p}>Rigs ({rigs?.length ?? 0})</Heading>
<Grid
templateColumns={{
base: "repeat(2, 1fr)",
Expand Down Expand Up @@ -60,7 +60,7 @@ export const RigsGrid = ({ rigs, nfts, gap, p, ...props }: RigsGridProps) => {
})}
</Grid>
{rigs?.length === 0 && (
<Text variant="emptyState" pt={8}>
<Text variant="emptyState" pt={4}>
This wallet doesn't own any Rigs.
</Text>
)}
Expand Down
111 changes: 111 additions & 0 deletions garage/src/pages/OwnerDetails/modules/Votes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from "react";
import {
Box,
Flex,
Heading,
Spinner,
Table,
Tbody,
Thead,
Th,
Text,
Tr,
Td,
VStack,
Show,
useBreakpointValue,
} from "@chakra-ui/react";
import { prettyNumber } from "../../../utils/fmt";
import { Vote } from "../../../hooks/useOwnerVotes";
import { Link } from "react-router-dom";

interface VotesProps extends React.ComponentProps<typeof Box> {
votes?: Vote[];
}

const truncateChoiceString = (s: string, l: number = 80) =>
s.slice(0, l) + (s.length > l ? "..." : "");

const noBorderBottom = { borderBottom: "none" };

export const Votes = ({ votes, p, ...props }: VotesProps) => {
const isMobile = useBreakpointValue({
base: true,
sm: false,
});

const mainRowColAttrs = isMobile ? noBorderBottom : {};

return (
<VStack align="stretch" spacing={4} pt={p} {...props}>
<Heading px={p}>Votes</Heading>
<Table variant="simple">
<Thead>
<Tr>
<Th pl={p} maxWidth={{ base: "100%", sm: "230px" }}>
Proposal
</Th>
<Show above="sm">
<Th>Choices</Th>
</Show>
<Th pr={p} isNumeric>
Voting Power
</Th>
</Tr>
</Thead>
<Tbody>
{votes &&
votes.map(({ choices, proposal, ft }, index) => {
const { options } = proposal;

const optionLookupMap = Object.fromEntries(
options.map(({ id, description }) => [id, description])
);
const choiceString = choices
.map(
({ optionId, weight }) =>
`${weight}% for ${optionLookupMap[optionId]}`
)
.join(", ");
return (
<React.Fragment key={`vote-${index}`}>
<Tr>
<Td pl={p} maxWidth="230px" {...mainRowColAttrs}>
<Link to={`/proposals/${proposal.id}`}>
{proposal.name}
</Link>
</Td>
<Show above="sm">
<Td title={choiceString}>
{truncateChoiceString(choiceString)}
</Td>
</Show>
<Td pr={p} isNumeric {...mainRowColAttrs}>
{prettyNumber(ft)}
</Td>
</Tr>
<Show below="sm">
<Tr>
<Td px={p} pt={0} colSpan={2} title={choiceString}>
{truncateChoiceString(choiceString)}
</Td>
</Tr>
</Show>
</React.Fragment>
);
})}
</Tbody>
</Table>
{!votes && (
<Flex justify="center" p={p}>
<Spinner />
</Flex>
)}
{votes?.length === 0 && (
<Text px={p} py={4} variant="emptyState">
This wallet has not voted in any proposals yet.
</Text>
)}
</VStack>
);
};
31 changes: 31 additions & 0 deletions garage/src/utils/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const {
ftRewardsTable,
lookupsTable,
pilotSessionsTable,
proposalsTable,
optionsTable,
ftSnapshotTable,
votesTable,
} = deployment;

const IMAGE_IPFS_URI_SELECT = `'ipfs://'||renders_cid||'/'||(SELECT value from ${lookupsTable} WHERE label = 'image_full_name')`;
Expand Down Expand Up @@ -446,3 +450,30 @@ export const selectFilteredRigs = (
export const selectOwnerFTRewards = (owner: string) => {
return `SELECT block_num as "blockNum", recipient, reason, amount FROM ${ftRewardsTable} WHERE recipient = '${owner}' ORDER BY block_num DESC`;
};

export const selectOwnerVotes = (owner: string) => {
return `
SELECT
(SELECT json_object(
'name', name,
'id', id,
'options', (
SELECT json_group_array(json_object('id', id, 'description', description))
FROM ${optionsTable} WHERE proposal_id = proposal_id
)
) FROM ${proposalsTable} WHERE id = proposal_id
) as "proposal",
(SELECT ft
FROM ${ftSnapshotTable}
WHERE proposal_id = proposal_id AND lower(address) = lower(votes.address)
) as "ft",
json_group_array(json_object(
'optionId', option_id, 'weight', weight, 'comment', comment
)) as "choices"
FROM
${votesTable} as "votes"
WHERE
lower(votes.address) = lower('${owner}') AND votes.weight > 0
GROUP BY
proposal_id`;
};
Loading