Skip to content

Commit

Permalink
Merge pull request #315 from DAOmasons/badges
Browse files Browse the repository at this point in the history
Badges
  • Loading branch information
jordanlesich authored Aug 30, 2024
2 parents 43c7f3e + 739307b commit 6bca358
Show file tree
Hide file tree
Showing 44 changed files with 45,038 additions and 27,698 deletions.
1,427 changes: 1,419 additions & 8 deletions src/.graphclient/index.ts

Large diffs are not rendered by default.

1,093 changes: 1,087 additions & 6 deletions src/.graphclient/schema.graphql

Large diffs are not rendered by default.

65,345 changes: 37,706 additions & 27,639 deletions src/.graphclient/sources/grant-ships/introspectionSchema.ts

Large diffs are not rendered by default.

1,093 changes: 1,087 additions & 6 deletions src/.graphclient/sources/grant-ships/schema.graphql

Large diffs are not rendered by default.

1,040 changes: 1,040 additions & 0 deletions src/.graphclient/sources/grant-ships/types.ts

Large diffs are not rendered by default.

459 changes: 459 additions & 0 deletions src/abi/ScaffoldShaman.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/components/AddressAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {
Avatar,
Group,
MantineSize,
MantineSpacing,
StyleProp,
Text,
Tooltip,
} from '@mantine/core';
import React, { ComponentProps } from 'react';
import { ComponentProps } from 'react';
import { Address, isAddress } from 'viem';
import { useEnsAvatar, useEnsName } from 'wagmi';
import { ensConfig } from '../utils/config';
Expand All @@ -21,6 +22,7 @@ export const AddressAvatar = ({
fz,
displayText = true,
withTooltip = false,
gap,
canCopy,
}: {
address: Address;
Expand All @@ -30,6 +32,7 @@ export const AddressAvatar = ({
displayText?: boolean;
hideText?: boolean;
canCopy?: boolean;
gap?: MantineSpacing;
}) => {
const { data: ensName } = useEnsName({
address,
Expand All @@ -50,6 +53,7 @@ export const AddressAvatar = ({

return (
<Group
gap={gap}
style={{
cursor: canCopy ? 'pointer' : 'default',
}}
Expand Down
2 changes: 1 addition & 1 deletion src/components/AvatarPickerIPFS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Text,
useMantineTheme,
} from '@mantine/core';
import { IconCameraPlus, IconPencil, IconUser } from '@tabler/icons-react';
import { IconCameraPlus } from '@tabler/icons-react';
import { pinFileToIPFS } from '../utils/ipfs/pin';
import { ReactNode, useEffect, useState } from 'react';
import { getGatewayUrl } from '../utils/ipfs/get';
Expand Down
4 changes: 2 additions & 2 deletions src/components/PlayerAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMemo } from 'react';

type DisplayParams = {
gap: string | number;
fontSize: StyleProp<number | MantineSize | (string & {})> | undefined;
fontSize: StyleProp<number | MantineSize | (string & object)> | undefined;
avatarSize: number;
displayBadge: boolean;
};
Expand Down Expand Up @@ -60,7 +60,7 @@ export const PlayerAvatar = ({
return <FacilitatorBadge size={18} />;
}
return null;
}, [playerType]);
}, [playerType, display]);

const { gap, fontSize, avatarSize } = displayParams[display];

Expand Down
6 changes: 6 additions & 0 deletions src/components/Typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ export const Bold = ({ children }: { children: ReactNode }) => (
{children}
</Text>
);

export const Italic = ({ children }: { children: ReactNode }) => (
<Text component="span" fw={'inherit'} fz="inherit" fs="italic">
{children}
</Text>
);
273 changes: 273 additions & 0 deletions src/components/dashboard/facilitator/BadgeMintDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import {
Avatar,
Box,
Button,
Divider,
Group,
NumberInput,
Stack,
Text,
Textarea,
TextInput,
useMantineTheme,
} from '@mantine/core';
import {
BadgeManager,
ResolvedTemplate,
} from '../../../queries/getBadgeManager';
import { PageDrawer } from '../../PageDrawer';
import { TxButton } from '../../TxButton';
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { useForm, zodResolver } from '@mantine/form';
import { z } from 'zod';
import { isAddress, parseEther } from 'viem';
import { useTx } from '../../../hooks/useTx';
import ScaffoldShaman from '../../../abi/ScaffoldShaman.json';
import { BADGE_SHAMAN } from '../../../constants/addresses';
import { useState } from 'react';
import { pinJSONToIPFS } from '../../../utils/ipfs/pin';
import { notifications } from '@mantine/notifications';
import { reasonSchema } from '../../../utils/ipfs/metadataValidation';

const badgeMintFormSchema = z.object({
badges: z.array(
z.object({
comment: z.string(),
recipientAddress: z
.string()
.min(1, { message: 'Send address is required' })
.refine((val) => isAddress(val), { message: 'Invalid address' }),
amount: z.number(),
})
),
});

export const BadgeMintDrawer = ({
opened,
onClose,
shaman,
onPollSuccess,
selectedTemplate,
}: {
selectedTemplate: ResolvedTemplate;
shaman?: BadgeManager;
opened: boolean;
onClose: () => void;
onPollSuccess: () => void;
}) => {
const [numBadges, setNumBadges] = useState([undefined]);
const [isPinning, setIsPinning] = useState(false);
const theme = useMantineTheme();
const { tx } = useTx();

const form = useForm({
initialValues: {
badges: [
{
recipientAddress: '',
comment: '',
amount: 0,
},
],
},
validateInputOnBlur: true,
validate: zodResolver(badgeMintFormSchema),
});

const mintBadge = async () => {
try {
if (!shaman) return;

const badgeIds: bigint[] = [];
const amounts: bigint[] = [];
const comments: [bigint, string][] = [];
const recipients: string[] = [];

setIsPinning(true);

await Promise.all(
numBadges.map(async (_, i) => {
const badgeFormData = form.values.badges[i];

if (!badgeFormData) {
setIsPinning(false);
return;
}

const recipientAddress = badgeFormData.recipientAddress;
if (!isAddress(recipientAddress)) {
form.setFieldError(
`badges.${i}.recipientAddress`,
'Invalid address'
);
setIsPinning(false);
return;
}

badgeIds.push(selectedTemplate.badgeId);
amounts.push(parseEther(badgeFormData.amount.toString()) || 0n);

if (badgeFormData.comment.length > 0) {
const validated = reasonSchema.safeParse({
reason: badgeFormData.comment,
});

if (!validated.success) {
notifications.show({
title: 'Error',
message: validated.error.message,
color: 'red',
});
setIsPinning(false);
return;
}

const ipfsRes = await pinJSONToIPFS({
reason: badgeFormData.comment || '',
});

if (ipfsRes.IpfsHash[0] !== 'Q') {
notifications.show({
title: 'IPFS Error',
message: ipfsRes.IpfsHash[1],
color: 'red',
});
setIsPinning(false);
return;
}

comments.push([1n, ipfsRes.IpfsHash] || '');
} else {
comments.push([0n, 'NULL']);
}
recipients.push(badgeFormData.recipientAddress);
})
);

onClose();

setIsPinning(false);

const args = [badgeIds, amounts, comments, recipients];

tx({
writeContractParams: {
abi: ScaffoldShaman,
functionName: 'applyBadges',
address: BADGE_SHAMAN,
args,
},
writeContractOptions: {
onPollSuccess() {
onPollSuccess?.();
},
},
});
} catch (error) {
console.error(error);

notifications.show({
title: 'Error',
message: (error as Error).message,
color: 'red',
});
setIsPinning(false);
}
};

return (
<PageDrawer opened={opened} onClose={onClose} closeOnBack>
<Box pb="xl">
<Text mb="lg" fz="lg" fw={600}>
Create a Badge
</Text>
</Box>
<Group align="start" justify="space-between" mb="xl">
<Avatar
bg={theme.colors.dark[5]}
src={selectedTemplate.templateMetadata.imgUrl}
size={240}
radius={'sm'}
pos="relative"
/>
<TxButton
disabled={form.isValid() === false}
loading={isPinning}
leftSection={<IconPlus />}
onClick={mintBadge}
>
Mint Badge
</TxButton>
</Group>
<Stack mb="xl">
{numBadges.map((_, i) => (
<Stack gap={'sm'} key={`badge-section-${i}`}>
<Group align="end" justify="space-between">
<TextInput
label="Recipient Address"
placeholder="0x..."
maw={280}
w={'100%'}
required
{...form.getInputProps(`badges.${i}.recipientAddress`)}
/>
{i === numBadges.length - 1 && (
<Button
variant="secondary"
leftSection={<IconTrash size={18} />}
onClick={() => {
setNumBadges(numBadges.filter((_, j) => j !== i));
form.setValues({
badges: form.values.badges.filter((_, j) => j !== i),
});
}}
>
Delete Badge
</Button>
)}
</Group>
<Textarea
label="Comment"
placeholder="Enter a comment to accompany the badge (optional)"
autosize
minRows={4}
maxRows={8}
{...form.getInputProps(`badges.${i}.comment`)}
mb={selectedTemplate.hasFixedAmount ? 'md' : 'sm'}
/>
{selectedTemplate.hasFixedAmount === false && (
<NumberInput
label="Amount"
placeholder={`Enter the amount of ${selectedTemplate.isVotingToken ? shaman?.sharesToken.symbol : shaman?.lootToken.symbol} to mint to the recipient`}
required
{...form.getInputProps(`badges.${i}.amount`)}
/>
)}
<Divider />
</Stack>
))}
</Stack>
<Group justify="center" pb="xl">
<Button
leftSection={<IconPlus />}
variant="secondary"
onClick={() => {
form.setValues({
badges: [
...form.values.badges,
{
recipientAddress: '',
comment: '',
amount: 0,
},
],
});
setNumBadges([...numBadges, undefined]);
}}
>
Add Badge
</Button>
</Group>
</PageDrawer>
);
};
Loading

0 comments on commit 6bca358

Please sign in to comment.