Skip to content

Commit

Permalink
feat: add upload contract UI
Browse files Browse the repository at this point in the history
  • Loading branch information
marslavish committed Sep 15, 2024
1 parent ef87061 commit 43867de
Show file tree
Hide file tree
Showing 18 changed files with 580 additions and 77 deletions.
16 changes: 12 additions & 4 deletions examples/chain-template/components/common/Header/ChainDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Box, Combobox, Skeleton, Stack, Text } from '@interchain-ui/react';
import { useStarshipChains, useDetectBreakpoints } from '@/hooks';
import { chainStore, useChainStore } from '@/contexts';
import { chainOptions } from '@/config';
import { getSignerOptions } from '@/utils';

export const ChainDropdown = () => {
const { selectedChain } = useChainStore();
Expand All @@ -18,12 +19,19 @@ export const ChainDropdown = () => {
const { addChains, getChainLogo } = useManager();

useEffect(() => {
if (starshipChains) {
// @ts-ignore
addChains(starshipChains.chains, starshipChains.assets);
if (
starshipChains?.chains.length &&
starshipChains?.assets.length &&
!isChainsAdded
) {
addChains(
starshipChains.chains,
starshipChains.assets,
getSignerOptions(),
);
setIsChainsAdded(true);
}
}, [starshipChains]);
}, [starshipChains, isChainsAdded]);

const chains = isChainsAdded
? chainOptions.concat(starshipChains?.chains ?? [])
Expand Down
24 changes: 4 additions & 20 deletions examples/chain-template/components/common/Radio/Radio.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,14 @@
}

.radio:checked {
border: 1px solid #7310ff;
}

.radio:checked::before {
content: '';
display: block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #7310ff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-color: #7310ff;
border-width: 4px;
}

.radioDark {
border: 1px solid #343c44;
border-color: #323a42;
}

.radioDark:checked {
border: 1px solid #ab6fff;
}

.radioDark:checked::before {
background: #ab6fff;
border-color: #ab6fff;
}
38 changes: 31 additions & 7 deletions examples/chain-template/components/common/Radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Box, useTheme } from '@interchain-ui/react';
import { Box, Icon, IconName, Text, useTheme } from '@interchain-ui/react';
import styles from './Radio.module.css';

type RadioProps = {
children: React.ReactNode;
value: string;
icon?: IconName | React.ReactNode;
name?: string;
checked?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
Expand All @@ -14,21 +15,30 @@ export const Radio = ({
value,
checked,
name,
icon,
onChange,
}: RadioProps) => {
const { theme } = useTheme();
const color = checked ? '$purple600' : '$blackAlpha600';

return (
<Box
as="label"
color="$blackAlpha600"
fontSize="14px"
fontWeight="500"
lineHeight="22px"
display="flex"
flexDirection="column"
alignItems="center"
gap="8px"
justifyContent="center"
flex="1 1 auto"
gap="6px"
cursor="pointer"
position="relative"
width="100%"
height="100px"
borderRadius="4px"
borderWidth="1px"
borderStyle="solid"
borderColor={checked ? '$purple600' : '$blackAlpha200'}
padding="10px"
>
<input
type="radio"
Expand All @@ -39,8 +49,22 @@ export const Radio = ({
className={`${styles.radio} ${
theme === 'dark' ? styles.radioDark : ''
}`}
style={{
position: 'absolute',
top: '6px',
right: '6px',
}}
/>
{children}
<Box color={color}>
{typeof icon === 'string' ? (
<Icon name={icon as IconName} color={color} size="$lg" />
) : (
icon
)}
</Box>
<Text color={color} fontSize="14px" fontWeight="500">
{children}
</Text>
</Box>
);
};
21 changes: 21 additions & 0 deletions examples/chain-template/components/contract/BackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Box, Icon, Text } from '@interchain-ui/react';

export const BackButton = ({ onClick }: { onClick: () => void }) => {
return (
<Box
display="flex"
gap="6px"
alignItems="center"
cursor="pointer"
attributes={{ onClick }}
>
<Icon
name="arrowRightLine"
attributes={{ transform: 'rotate(180deg)' }}
/>
<Text color="$text" fontSize="16px" fontWeight="500">
Back
</Text>
</Box>
);
};
18 changes: 18 additions & 0 deletions examples/chain-template/components/contract/CreateFromUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box } from '@interchain-ui/react';
import { UploadContract } from './UploadContract';
import { BackButton } from './BackButton';

type CreateFromUploadProps = {
onBack: () => void;
};

export const CreateFromUpload = ({ onBack }: CreateFromUploadProps) => {
return (
<Box position="relative">
<Box position="absolute" top="0" left="0">
<BackButton onClick={onBack} />
</Box>
<UploadContract show />
</Box>
);
};
47 changes: 47 additions & 0 deletions examples/chain-template/components/contract/InputField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box, Text } from '@interchain-ui/react';

const InputField = ({
children,
title,
required = false,
}: {
title: string;
children: React.ReactNode;
required?: boolean;
}) => {
return (
<Box display="flex" flexDirection="column" gap="10px">
<Text color="$blackAlpha600" fontSize="16px" fontWeight="700">
{title}{' '}
{required && (
<Text as="span" color="$red600" fontSize="16px">
*
</Text>
)}
</Text>
{children}
</Box>
);
};

const Description = ({
children,
intent = 'default',
}: {
children: string;
intent?: 'error' | 'default';
}) => {
return (
<Text
color={intent === 'error' ? '$red600' : '$blackAlpha500'}
fontSize="12px"
fontWeight="500"
>
{children}
</Text>
);
};

InputField.Description = Description;

export { InputField };
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useEffect } from 'react';
import { Box, Text, TextField } from '@interchain-ui/react';
import { HiOutlineTrash } from 'react-icons/hi';
import { LuPlus } from 'react-icons/lu';
import { cosmwasm } from 'interchain-query';
import { useChain } from '@cosmos-kit/react';
import { GrGroup } from 'react-icons/gr';
import { MdOutlineHowToVote } from 'react-icons/md';
import { MdChecklistRtl } from 'react-icons/md';

import { Button, Radio, RadioGroup } from '../common';
import { InputField } from './InputField';
import { validateChainAddress } from '@/utils';
import { useChainStore } from '@/contexts';

export const AccessType = cosmwasm.wasm.v1.AccessType;

export type Permission = (typeof AccessType)[keyof typeof AccessType];

export type Address = {
value: string;
isValid?: boolean;
errorMsg?: string | null;
};

type Props = {
addresses: Address[];
permission: Permission;
setAddresses: (addresses: Address[]) => void;
setPermission: (permission: Permission) => void;
};

export const InstantiatePermissionRadio = ({
addresses,
permission,
setAddresses,
setPermission,
}: Props) => {
const { selectedChain } = useChainStore();
const { chain } = useChain(selectedChain);

const onAddAddress = () => {
setAddresses([...addresses, { value: '' }]);
};

const onDeleteAddress = (index: number) => {
const newAddresses = [...addresses];
newAddresses.splice(index, 1);
setAddresses(newAddresses);
};

const onAddressChange = (value: string, index: number) => {
const newAddresses = [...addresses];
newAddresses[index].value = value;
setAddresses(newAddresses);
};

useEffect(() => {
if (permission !== AccessType.ACCESS_TYPE_ANY_OF_ADDRESSES) return;

const newAddresses = addresses.map((addr, index) => {
const isDuplicate =
addresses.findIndex((a) => a.value === addr.value) !== index;

const errorMsg = isDuplicate
? 'Address already exists'
: validateChainAddress(addr.value, chain.bech32_prefix);

return {
...addr,
isValid: !!addr.value && !errorMsg,
errorMsg: addr.value ? errorMsg : null,
};
});

setAddresses(newAddresses);
}, [JSON.stringify(addresses.map((addr) => addr.value))]);

return (
<>
<RadioGroup
direction="row"
name="instantiate_permission"
value={permission.toString()}
onChange={(val) => {
setPermission(Number(val) as Permission);
}}
>
<Radio
icon={<GrGroup size="22px" />}
value={AccessType.ACCESS_TYPE_EVERYBODY.toString()}
>
Everybody
</Radio>
<Radio
icon={<MdOutlineHowToVote size="22px" />}
value={AccessType.ACCESS_TYPE_NOBODY.toString()}
>
Governance only
</Radio>
<Radio
icon={<MdChecklistRtl size="22px" />}
value={AccessType.ACCESS_TYPE_ANY_OF_ADDRESSES.toString()}
>
Approved addresses
</Radio>
</RadioGroup>

{permission === AccessType.ACCESS_TYPE_ANY_OF_ADDRESSES && (
<InputField title="Addresses">
{addresses.map(({ value, errorMsg }, index) => (
<Box display="flex" gap="10px" key={index}>
<Box width="$full">
<TextField
id={`address-${index}`}
value={value}
onChange={(e) => onAddressChange(e.target.value, index)}
attributes={{ width: '100%' }}
intent={errorMsg ? 'error' : 'default'}
autoComplete="off"
/>
{errorMsg && (
<Text
color="$red600"
fontSize="12px"
fontWeight="500"
wordBreak="break-all"
attributes={{ mt: '6px' }}
>
{errorMsg}
</Text>
)}
</Box>
<Button
leftIcon={<HiOutlineTrash size="18px" />}
px="10px"
disabled={addresses.length === 1}
onClick={() => onDeleteAddress(index)}
/>
</Box>
))}
<Button
leftIcon={<LuPlus size="20px" />}
width="$fit"
px="10px"
onClick={onAddAddress}
>
Add More Address
</Button>
</InputField>
)}
</>
);
};
Loading

0 comments on commit 43867de

Please sign in to comment.