diff --git a/examples/telescope-authz/components/authz/AuthzSection.tsx b/examples/telescope-authz/components/authz/AuthzSection.tsx new file mode 100644 index 000000000..3e5ae2c6a --- /dev/null +++ b/examples/telescope-authz/components/authz/AuthzSection.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { ChainName } from 'cosmos-kit'; +import { useChain } from '@cosmos-kit/react'; +import { Box, Button, Tabs, Text } from '@interchain-ui/react'; + +import { Grants } from './Grants'; +import { GrantModal } from './GrantModal'; + +export const AuthzSection = ({ chainName }: { chainName: ChainName }) => { + const [isOpen, setIsOpen] = useState(false); + const [activeTab, setActiveTab] = useState(0); + const { address } = useChain(chainName); + + if (!address) { + return ( + + Please connect your wallet to view and create grants + + ); + } + + return ( + + + setActiveTab(tabId)} + attributes={{ width: '$min' }} + /> + + + + + + setIsOpen(false)} + chainName={chainName} + /> + + ); +}; diff --git a/examples/telescope-authz/components/authz/CustomizationField.tsx b/examples/telescope-authz/components/authz/CustomizationField.tsx new file mode 100644 index 000000000..7021bb5da --- /dev/null +++ b/examples/telescope-authz/components/authz/CustomizationField.tsx @@ -0,0 +1,132 @@ +import { Dispatch, SetStateAction, useState } from 'react'; +import { Box, NumberField, SelectButton, Text } from '@interchain-ui/react'; + +import { useValidators } from '@/hooks'; +import { Permission, PermissionId } from '@/configs'; +import { SelectValidatorsModal } from './SelectValidatorsModal'; +import { AccessList } from './GrantModal'; + +// ============================================== + +type SendCustomizationProps = { + value: number | undefined; + onChange: (value: string) => void; +}; + +const SendCustomization = ({ value, onChange }: SendCustomizationProps) => { + return ( + { + // @ts-ignore + onChange(e.target.value); + }} + formatOptions={{ + maximumFractionDigits: 6, + }} + /> + ); +}; + +// ============================================== + +type DelegateCustomizationProps = { + value: number | undefined; + onChange: (value: string) => void; + chainName: string; + accessList: AccessList; + setAccessList: Dispatch>; +}; + +const DelegateCustomization = ({ + value, + onChange, + chainName, + accessList, + setAccessList, +}: DelegateCustomizationProps) => { + const [isOpen, setIsOpen] = useState(false); + + const { data } = useValidators(chainName); + + const validatorNames = data + ? accessList.addresses.map( + (address) => data.find((v) => v.address === address)!.name + ) + : []; + + return ( + <> + { + // @ts-ignore + onChange(e.target.value); + }} + formatOptions={{ + maximumFractionDigits: 6, + }} + /> + setIsOpen(true)} + /> + 0 ? 'block' : 'none'} + mt="$2" + px="$2" + > + + + {accessList.type === 'allowList' ? 'Allow List' : 'Deny List'} + :  + + {validatorNames.join(', ')} + + + setIsOpen(false)} + /> + + ); +}; + +// ============================================== + +type CustomizationFieldProps = + | ({ + permissionType: typeof Permission['Send']; + } & SendCustomizationProps) + | ({ + permissionType: typeof Permission['Delegate']; + } & DelegateCustomizationProps); + +export const CustomizationField = ({ + permissionType, + ...rest +}: CustomizationFieldProps): JSX.Element | null => { + const fields: Partial> = { + send: + permissionType === 'send' ? ( + + ) : null, + delegate: + permissionType === 'delegate' ? ( + + ) : null, + }; + + return fields[permissionType] ?? null; +}; diff --git a/examples/telescope-authz/components/authz/GrantCard.tsx b/examples/telescope-authz/components/authz/GrantCard.tsx new file mode 100644 index 000000000..a9e70ff1c --- /dev/null +++ b/examples/telescope-authz/components/authz/GrantCard.tsx @@ -0,0 +1,185 @@ +import Link from 'next/link'; +import Image from 'next/image'; +import { useState } from 'react'; +import { + Box, + Button, + IconButton, + Stack, + Text, + TextField, +} from '@interchain-ui/react'; +import { useChain } from '@cosmos-kit/react'; + +import { + getChainLogoByChainName, + PrettyGrant, + PrettyPermission, +} from '@/utils'; +import { useAuthzContext } from '@/context'; +import { useAuthzTx, useGrants } from '@/hooks'; +import { getCoin, permissionNameToRouteMap } from '@/configs'; + +import styles from '@/styles/custom.module.css'; + +type GrantCardProps = { + role: 'granter' | 'grantee'; + grant: PrettyGrant; + chainName: string; + onViewDetails: () => void; +}; + +export const GrantCard = ({ + role, + grant, + chainName, + onViewDetails, +}: GrantCardProps) => { + const [isCopied, setIsCopied] = useState(false); + const [isRevoking, setIsRevoking] = useState(false); + const [revokingPermission, setRevokingPermission] = + useState(); + + const { chain } = useChain(chainName); + const { refetch } = useGrants(chainName); + const { setPermission } = useAuthzContext(); + const { authzTx, createRevokeMsg } = useAuthzTx(chainName); + + const { address, permissions } = grant; + + const isGranter = role === 'granter'; + const token = getCoin(chainName); + + const copy = (text: string) => { + if (isCopied) return; + + navigator.clipboard + .writeText(text) + .then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 800); + }) + .catch((error) => { + console.error('Failed to copy:', error); + }); + }; + + const handleRevoke = (permission: PrettyPermission) => { + setIsRevoking(true); + + authzTx({ + msgs: [createRevokeMsg(permission)], + onSuccess: () => { + refetch(); + }, + onComplete: () => { + setIsRevoking(false); + }, + }); + }; + + return ( + + + {token.name} + + {chain.pretty_name} + + + + + + + copy(address)} + /> + + + + + Permissions + + + + {permissions.map((permission) => + isGranter ? ( + + ) : permissionNameToRouteMap[permission.name] ? ( + + + + ) : ( + + ) + )} + + + + + ); +}; diff --git a/examples/telescope-authz/components/authz/GrantDetailsModal.tsx b/examples/telescope-authz/components/authz/GrantDetailsModal.tsx new file mode 100644 index 000000000..592498149 --- /dev/null +++ b/examples/telescope-authz/components/authz/GrantDetailsModal.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { BasicModal, Box, Button } from '@interchain-ui/react'; + +import { useAuthzTx, useGrants } from '@/hooks'; +import { PrettyGrant, PrettyPermission } from '@/utils'; +import { PermissionDetailCard } from './PermissionDetailCard'; + +type GrantDetailsModalProps = { + grant: PrettyGrant; + chainName: string; + role: 'granter' | 'grantee'; + isOpen: boolean; + onClose: () => void; +}; + +export const GrantDetailsModal = ({ + role, + grant, + isOpen, + onClose, + chainName, +}: GrantDetailsModalProps) => { + const { permissions } = grant; + const isGranter = role === 'granter'; + + const [isRevoking, setIsRevoking] = useState(false); + const [revokingPermission, setRevokingPermission] = + useState(); + + const { refetch } = useGrants(chainName); + const { authzTx, createRevokeMsg } = useAuthzTx(chainName); + + const handleRevoke = (permissions: PrettyPermission[]) => { + setIsRevoking(true); + + authzTx({ + msgs: permissions.map(createRevokeMsg), + onSuccess: () => { + refetch(); + onClose(); + }, + onComplete: () => { + setIsRevoking(false); + setRevokingPermission(undefined); + }, + }); + }; + + return ( + + + + {permissions.map((permission) => ( + { + handleRevoke([permission]); + setRevokingPermission(permission); + }} + isRevoking={ + isRevoking && permission.name === revokingPermission?.name + } + chainName={chainName} + permission={permission} + /> + ))} + + + {isGranter && ( + + )} + + + ); +}; diff --git a/examples/telescope-authz/components/authz/GrantModal.tsx b/examples/telescope-authz/components/authz/GrantModal.tsx new file mode 100644 index 000000000..d96671def --- /dev/null +++ b/examples/telescope-authz/components/authz/GrantModal.tsx @@ -0,0 +1,292 @@ +import { useState } from 'react'; +import { ChainName } from 'cosmos-kit'; +import { + BasicModal, + Box, + TextField, + Button, + Popover, + PopoverTrigger, + PopoverContent, + SelectButton, + ListItem, + Stack, + FieldLabel, +} from '@interchain-ui/react'; +import { coin } from '@cosmjs/amino'; +import { useChain } from '@cosmos-kit/react'; +import { IoMdCalendar } from 'react-icons/io'; +import Calendar from 'react-calendar'; +import dayjs from 'dayjs'; + +import { + getExponent, + PermissionId, + PermissionItem, + permissions, +} from '@/configs'; +import { AuthorizationType } from '@/src/codegen/cosmos/staking/v1beta1/authz'; +import { GrantMsg, useAuthzTx, useGrants } from '@/hooks'; +import { getTokenByChainName, shiftDigits } from '@/utils'; +import { CustomizationField } from './CustomizationField'; +import { AddressInput } from '@/components'; + +import styles from '@/styles/custom.module.css'; + +export type AccessList = { + type: 'allowList' | 'denyList'; + addresses: string[]; +}; + +type GrantModalProps = { + isOpen: boolean; + onClose: () => void; + chainName: ChainName; +}; + +export const GrantModal = ({ isOpen, onClose, chainName }: GrantModalProps) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + + const [granteeAddress, setGranteeAddress] = useState(''); + const [addressErrorMsg, setAddressErrorMsg] = useState(''); + const [expiryDate, setExpiryDate] = useState(null); + const [selectedPermission, setSelectedPermission] = + useState(null); + + const [sendLimit, setSendLimit] = useState(undefined); + const [delegateLimit, setDelegateLimit] = useState( + undefined + ); + const [accessList, setAccessList] = useState({ + type: 'allowList', + addresses: [], + }); + + const [isGranting, setIsGranting] = useState(false); + + const { refetch } = useGrants(chainName); + const { address } = useChain(chainName); + const { authzTx, createGrantMsg } = useAuthzTx(chainName); + + const token = getTokenByChainName(chainName); + const exponent = getExponent(chainName); + const denom = token.base; + + const onModalClose = () => { + setGranteeAddress(''); + setExpiryDate(null); + setSelectedPermission(null); + setSendLimit(undefined); + setDelegateLimit(undefined); + setIsGranting(false); + setAccessList({ type: 'allowList', addresses: [] }); + onClose(); + }; + + const onGrantClick = () => { + if (!address || !granteeAddress || !expiryDate || !selectedPermission) + return; + + setIsGranting(true); + + const sendMsg: GrantMsg = { + grantType: 'send', + customize: sendLimit + ? { + spendLimit: [coin(shiftDigits(sendLimit, exponent), denom)], + } + : undefined, + }; + + const delegateMsg: GrantMsg = { + grantType: 'delegate', + customize: + delegateLimit || accessList.addresses.length > 0 + ? { + authorizationType: AuthorizationType.AUTHORIZATION_TYPE_DELEGATE, + maxTokens: coin(shiftDigits(delegateLimit, exponent), denom), + [accessList.type]: { address: accessList.addresses }, + } + : undefined, + }; + + const grantMsg: Record = { + send: sendMsg, + delegate: delegateMsg, + vote: { grantType: 'vote' }, + 'claim-rewards': { grantType: 'claim-rewards' }, + }; + + const msg = createGrantMsg({ + grantee: granteeAddress, + granter: address, + expiration: expiryDate, + ...grantMsg[selectedPermission.id], + }); + + authzTx({ + msgs: [msg], + onSuccess: () => { + refetch(); + onModalClose(); + }, + onComplete: () => { + setIsGranting(false); + }, + }); + }; + + return ( + + + + + + + + + + + {}} + /> + + + + {permissions.map((p) => ( + { + setSelectedPermission(p); + setIsDropdownOpen(false); + }, + }} + > + {p.name} + + ))} + + + + + {selectedPermission?.id === 'send' && ( + { + if (!val) { + setSendLimit(undefined); + return; + } + setSendLimit(Number(val)); + }} + /> + )} + + {selectedPermission?.id === 'delegate' && ( + { + if (!val) { + setDelegateLimit(undefined); + return; + } + setDelegateLimit(Number(val)); + }} + /> + )} + + + + + + + + + + + + { + if (Array.isArray(val)) { + setExpiryDate(val[1]); + return; + } + setExpiryDate(val); + }} + onClickDay={() => { + setIsCalendarOpen(false); + }} + /> + + + + + + + + + + + ); +}; diff --git a/examples/telescope-authz/components/authz/Grants.tsx b/examples/telescope-authz/components/authz/Grants.tsx new file mode 100644 index 000000000..76c0151ae --- /dev/null +++ b/examples/telescope-authz/components/authz/Grants.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { Box, Spinner, Text } from '@interchain-ui/react'; + +import { useGrants } from '@/hooks'; +import { PrettyGrant } from '@/utils'; +import { GrantCard } from './GrantCard'; +import { GrantDetailsModal } from './GrantDetailsModal'; + +type GrantsProps = { + role: 'granter' | 'grantee'; + chainName: string; +}; + +export const Grants = ({ chainName, role }: GrantsProps) => { + const [isOpen, setIsOpen] = useState(false); + const [viewingGrant, setViewingGrant] = useState(); + const { data, isLoading } = useGrants(chainName); + + const isGranter = role === 'granter'; + const grants = isGranter ? data?.granterGrants : data?.granteeGrants; + + return ( + + {isLoading ? ( + + ) : grants && grants.length > 0 ? ( + + {grants.map((grant) => ( + { + setIsOpen(true); + setViewingGrant(grant); + }} + /> + ))} + + ) : ( + + {isGranter + ? "You haven't granted any permission yet" + : "You don't have any grants"} + + )} + + {viewingGrant && ( + setIsOpen(false)} + /> + )} + + ); +}; diff --git a/examples/telescope-authz/components/authz/LoginInfoBanner.tsx b/examples/telescope-authz/components/authz/LoginInfoBanner.tsx new file mode 100644 index 000000000..7644b375b --- /dev/null +++ b/examples/telescope-authz/components/authz/LoginInfoBanner.tsx @@ -0,0 +1,39 @@ +import { useChain } from '@cosmos-kit/react'; +import { Box, Icon, Text } from '@interchain-ui/react'; + +type LoginInfoBannerProps = { + loginAddress: string; + chainName: string; +}; + +export const LoginInfoBanner = ({ + loginAddress, + chainName, +}: LoginInfoBannerProps) => { + const { isWalletConnected } = useChain(chainName); + + if (!isWalletConnected) return null; + + return ( + + + + You are now logged in as  + + {loginAddress} + + + + ); +}; diff --git a/examples/telescope-authz/components/authz/PermissionDetailCard.tsx b/examples/telescope-authz/components/authz/PermissionDetailCard.tsx new file mode 100644 index 000000000..a58804b57 --- /dev/null +++ b/examples/telescope-authz/components/authz/PermissionDetailCard.tsx @@ -0,0 +1,130 @@ +import Link from 'next/link'; +import { Box, Button, Icon, Skeleton, Text } from '@interchain-ui/react'; + +import { useValidators } from '@/hooks'; +import { permissionNameToRouteMap } from '@/configs'; +import { getAttributePairs, PrettyGrant } from '@/utils'; +import { useAuthzContext } from '@/context'; + +type PermissionDetailCardProps = { + role: 'granter' | 'grantee'; + onRevoke: () => void; + isRevoking: boolean; + chainName: string; + permission: PrettyGrant['permissions'][0]; +}; + +export const PermissionDetailCard = ({ + role, + onRevoke, + isRevoking, + chainName, + permission, +}: PermissionDetailCardProps) => { + const { name, expiration, expiry, authorization } = permission; + const isGranter = role === 'granter'; + + const { setPermission } = useAuthzContext(); + const { data, isLoading } = useValidators(chainName, { fetchLogos: false }); + const attributes = getAttributePairs(authorization, data || []); + + return ( + + + {isGranter ? ( + + + {name} + + + + ) : permissionNameToRouteMap[name] ? ( + + setPermission(permission) }} + > + + {name} + + + + + ) : ( + + {name} + + )} + + + + {expiration && } + {attributes.map((attr) => ( + + ))} + + + ); +}; + +type PermissionAttributeProps = { + label: string; + value: string; + isLoading?: boolean; +}; + +const PermissionAttribute = ({ + label, + value, + isLoading = false, +}: PermissionAttributeProps) => { + return ( + + + {label} + + {isLoading ? ( + + ) : ( + + {value} + + )} + + ); +}; diff --git a/examples/telescope-authz/components/authz/SelectValidatorsModal.tsx b/examples/telescope-authz/components/authz/SelectValidatorsModal.tsx new file mode 100644 index 000000000..04ee042ae --- /dev/null +++ b/examples/telescope-authz/components/authz/SelectValidatorsModal.tsx @@ -0,0 +1,161 @@ +import { Dispatch, SetStateAction, useMemo, useState } from 'react'; +import { + BasicModal, + Box, + Button, + GovernanceRadio, + GovernanceRadioGroup, + GridColumn, + Spinner, + Stack, + Text, + ValidatorList, + ValidatorNameCell, +} from '@interchain-ui/react'; + +import { useValidators } from '@/hooks'; +import { ExtendedValidator as Validator } from '@/utils'; +import { AccessList } from './GrantModal'; + +type SelectValidatorsModalProps = { + isOpen: boolean; + onClose: () => void; + chainName: string; + accessList: AccessList; + setAccessList: Dispatch>; +}; + +type ListType = 'allowList' | 'denyList'; + +export const SelectValidatorsModal = ({ + isOpen, + onClose, + chainName, + accessList, + setAccessList, +}: SelectValidatorsModalProps) => { + const { data, isLoading } = useValidators(chainName); + const listType = accessList.type; + + const columns: GridColumn[] = useMemo(() => { + return [ + { + id: 'validator', + label: 'Validator', + width: '196px', + align: 'left', + render: (validator: Validator) => ( + + ), + }, + { + id: 'action', + width: '126px', + align: 'right', + render: (validator: Validator) => ( + + {accessList.addresses.includes(validator.address) ? ( + + ) : ( + <> + + + )} + + ), + }, + ]; + }, [chainName, accessList]); + + return ( + + + {isLoading ? ( + + ) : data && data.length > 0 ? ( + + + { + setAccessList((prev) => ({ + ...prev, + type: selected as ListType, + })); + }} + > + + + Allow List + + Deny List + + + + + + + ) : ( + + No Validators Found + + )} + + + ); +}; diff --git a/examples/telescope-authz/components/authz/index.ts b/examples/telescope-authz/components/authz/index.ts new file mode 100644 index 000000000..fe0758403 --- /dev/null +++ b/examples/telescope-authz/components/authz/index.ts @@ -0,0 +1,2 @@ +export * from './AuthzSection'; +export * from './LoginInfoBanner'; diff --git a/examples/telescope-authz/components/claim-rewards/ClaimRewardsSection.tsx b/examples/telescope-authz/components/claim-rewards/ClaimRewardsSection.tsx new file mode 100644 index 000000000..27dea72cf --- /dev/null +++ b/examples/telescope-authz/components/claim-rewards/ClaimRewardsSection.tsx @@ -0,0 +1,50 @@ +import { useChain } from '@cosmos-kit/react'; +import { ChainName } from 'cosmos-kit'; +import { Box, Spinner, Text } from '@interchain-ui/react'; + +import { useStakingData } from '@/hooks'; +import Overview from './Overview'; + +export const ClaimRewardsSection = ({ + chainName, +}: { + chainName: ChainName; +}) => { + const { isWalletConnected } = useChain(chainName); + const { data, isLoading, refetch } = useStakingData(chainName); + + return ( + + {!isWalletConnected ? ( + + + Please connect the wallet + + + ) : isLoading || !data ? ( + + + + ) : ( + + )} + + ); +}; diff --git a/examples/telescope-authz/components/claim-rewards/Overview.tsx b/examples/telescope-authz/components/claim-rewards/Overview.tsx new file mode 100644 index 000000000..4ba470c26 --- /dev/null +++ b/examples/telescope-authz/components/claim-rewards/Overview.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { + Box, + StakingAssetHeader, + StakingClaimHeader, +} from '@interchain-ui/react'; +import { ChainName } from 'cosmos-kit'; + +import { getCoin } from '@/configs'; +import { Prices, useAuthzTx } from '@/hooks'; +import { + sum, + calcDollarValue, + isGreaterThanZero, + type ParsedRewards as Rewards, +} from '@/utils'; +import { MsgWithdrawDelegatorReward } from '@/src/codegen/cosmos/distribution/v1beta1/tx'; +import { useAuthzContext } from '@/context'; + +const Overview = ({ + balance, + rewards, + staked, + updateData, + chainName, + prices, +}: { + balance: string; + rewards: Rewards; + staked: string; + updateData: () => void; + chainName: ChainName; + prices: Prices; +}) => { + const [isClaiming, setIsClaiming] = useState(false); + + const { permission } = useAuthzContext(); + const { authzTx, createExecMsg } = useAuthzTx(chainName); + + const totalAmount = sum(balance, staked, rewards?.total ?? 0); + const coin = getCoin(chainName); + + const onClaimRewardClick = () => { + if (!permission) return; + + setIsClaiming(true); + + const { grantee, granter, expiration } = permission; + + const msgs = rewards.byValidators.map(({ validatorAddress }) => + MsgWithdrawDelegatorReward.toProtoMsg({ + delegatorAddress: granter, + validatorAddress, + }) + ); + + authzTx({ + msgs: [createExecMsg({ msgs, grantee })], + execExpiration: expiration, + onSuccess: () => { + updateData(); + }, + onComplete: () => { + setIsClaiming(false); + }, + }); + }; + + return ( + <> + + + + + + + + + ); +}; + +export default Overview; diff --git a/examples/telescope-authz/components/claim-rewards/index.ts b/examples/telescope-authz/components/claim-rewards/index.ts new file mode 100644 index 000000000..68a0b4a8a --- /dev/null +++ b/examples/telescope-authz/components/claim-rewards/index.ts @@ -0,0 +1 @@ +export * from './ClaimRewardsSection'; diff --git a/examples/telescope-authz/components/common/AddressInput.tsx b/examples/telescope-authz/components/common/AddressInput.tsx new file mode 100644 index 000000000..1d3bad918 --- /dev/null +++ b/examples/telescope-authz/components/common/AddressInput.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { useChain } from '@cosmos-kit/react'; +import { fromBech32 } from '@cosmjs/encoding'; +import { TextField, Text, Box, BoxProps } from '@interchain-ui/react'; + +type AddressInputProps = { + chainName: string; + address: string; + onAddressChange: (address: string) => void; + mb?: BoxProps['mb']; + label?: string; + placeholder?: string; + onInvalidAddress?: (error: string) => void; +}; + +export const AddressInput = ({ + chainName, + address, + onAddressChange, + label, + mb, + onInvalidAddress, + placeholder, +}: AddressInputProps) => { + const { chain } = useChain(chainName); + + const errorMessage = useMemo(() => { + let errorMsg = ''; + + if (!address) { + onInvalidAddress && onInvalidAddress(errorMsg); + return errorMsg; + } + + try { + const res = fromBech32(address); + if (!address.startsWith(chain.bech32_prefix)) { + errorMsg = `Invalid address: Unexpected prefix (expected: ${chain.bech32_prefix}, actual: ${res.prefix})`; + } + } catch (error) { + errorMsg = 'Invalid address'; + } finally { + onInvalidAddress && onInvalidAddress(errorMsg); + return errorMsg; + } + }, [address]); + + return ( + + onAddressChange(e.target.value)} + label={label} + placeholder={placeholder} + attributes={{ mb: errorMessage ? '$2' : '0' }} + intent={errorMessage ? 'error' : 'default'} + /> + + {errorMessage && {errorMessage}} + + ); +}; diff --git a/examples/telescope-authz/components/common/Footer.tsx b/examples/telescope-authz/components/common/Footer.tsx new file mode 100644 index 000000000..6dbfd8dcd --- /dev/null +++ b/examples/telescope-authz/components/common/Footer.tsx @@ -0,0 +1,166 @@ +import { + Box, + Link, + Text, + Icon, + Stack, + Divider, + useColorModeValue, +} from '@interchain-ui/react'; +import { dependencies, products, Project } from '@/configs'; + +function Product({ name, desc, link }: Project) { + return ( + + + + {name} → + + + {desc} + + + + ); +} + +function Dependency({ name, desc, link }: Project) { + return ( + + + + + + + + + {name} + + + {desc} + + + + + ); +} + +export function Footer() { + return ( + <> + + {products.map((product) => ( + + ))} + + + {dependencies.map((dependency) => ( + + ))} + + + + + + Built with + + Cosmology + + + + ); +} diff --git a/examples/telescope-authz/components/common/Header.tsx b/examples/telescope-authz/components/common/Header.tsx new file mode 100644 index 000000000..b573280b2 --- /dev/null +++ b/examples/telescope-authz/components/common/Header.tsx @@ -0,0 +1,63 @@ +import { + Box, + Button, + Icon, + Text, + useTheme, + useColorModeValue, +} from '@interchain-ui/react'; + +const stacks = ['CosmosKit', 'Next.js']; + +export function Header() { + const { theme, setTheme } = useTheme(); + + const toggleColorMode = () => { + setTheme(theme === 'light' ? 'dark' : 'light'); + }; + + return ( + <> + + + + + + + Create Cosmos App + + + + Welcome to  + + + {stacks.join(' + ')} + + + + + ); +} diff --git a/examples/telescope-authz/components/common/Layout.tsx b/examples/telescope-authz/components/common/Layout.tsx new file mode 100644 index 000000000..ff3365d5e --- /dev/null +++ b/examples/telescope-authz/components/common/Layout.tsx @@ -0,0 +1,19 @@ +import Head from 'next/head'; +import { Container } from '@interchain-ui/react'; +import { Header } from './Header'; +import { Footer } from './Footer'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + Create Cosmos App + + + +
+ {children} +