diff --git a/CHANGELOG.md b/CHANGELOG.md index d7777aae9..b561451ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,21 +40,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - [#102](https://github.com/alleslabs/celatone-frontend/pull/102) Add quick menu in overview and add highlighted in left sidebar +- [#125](https://github.com/alleslabs/celatone-frontend/pull/125) Add connect wallet alert in instantiate page +- [#126](https://github.com/alleslabs/celatone-frontend/pull/126) Add port id copier for IBC port id +- [#76](https://github.com/alleslabs/celatone-frontend/pull/76) Add Public projects page - [#116](https://github.com/alleslabs/celatone-frontend/pull/116) Support Terra2.0 mainnet and testnet - [#94](https://github.com/alleslabs/celatone-frontend/pull/94) Add unsupported assets in contract details page -- [#72](https://github.com/alleslabs/celatone-frontend/pull/72) Fix general wording and grammar -- [#110](https://github.com/alleslabs/celatone-frontend/pull/110) Fix proposal detail rendering -- [#109](https://github.com/alleslabs/celatone-frontend/pull/109) Fix incorrect rendering of zero value badges - [#106](https://github.com/alleslabs/celatone-frontend/pull/106) Add sort alphabetically to query and execute shortcuts - [#88](https://github.com/alleslabs/celatone-frontend/pull/88) Add code snippet for query and execute - [#107](https://github.com/alleslabs/celatone-frontend/pull/107) Remove osmosis mainnet from chain list - [#99](https://github.com/alleslabs/celatone-frontend/pull/99) Validate label and codeId field in instantiate page - [#103](https://github.com/alleslabs/celatone-frontend/pull/103) Add check mark to selected network - [#92](https://github.com/alleslabs/celatone-frontend/pull/92) Create select contract component for admin and migrate pages -- [#101](https://github.com/alleslabs/celatone-frontend/pull/101) Fix incorrect truncating of proposal id in contract detail's migration table -- [#100](https://github.com/alleslabs/celatone-frontend/pull/100) Fix contract instantiated time parsing - [#97](https://github.com/alleslabs/celatone-frontend/pull/97) Change label style to always afloat -- [#96](https://github.com/alleslabs/celatone-frontend/pull/96) Fix incorrect instantiated block height explorer link - [#95](https://github.com/alleslabs/celatone-frontend/pull/95) Add network to url path - [#89](https://github.com/alleslabs/celatone-frontend/pull/89) Update feedback link - [#90](https://github.com/alleslabs/celatone-frontend/pull/90) Add update admin (`/admin`) and migrate (`/migrate`) page routes @@ -63,20 +60,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#81](https://github.com/alleslabs/celatone-frontend/pull/81) Can scroll on side bar with fix deploy new contract button - [#86](https://github.com/alleslabs/celatone-frontend/pull/86) Add transactions table in contract details page - [#74](https://github.com/alleslabs/celatone-frontend/pull/74) Add tokens rendering for contract details page -- [#87](https://github.com/alleslabs/celatone-frontend/pull/87) Fix funds didn't microfy before sending tx - [#85](https://github.com/alleslabs/celatone-frontend/pull/85) Add sending asset in execute contract page - [#84](https://github.com/alleslabs/celatone-frontend/pull/84) Contract proposals table ui and wireup - [#82](https://github.com/alleslabs/celatone-frontend/pull/82) Add all codes page - [#83](https://github.com/alleslabs/celatone-frontend/pull/83) Add invalid code state - [#73](https://github.com/alleslabs/celatone-frontend/pull/73) Wireup migration table - [#77](https://github.com/alleslabs/celatone-frontend/pull/77) Wireup code info section in code details page -- [#80](https://github.com/alleslabs/celatone-frontend/pull/80) Fix the misalignment of state in the PastTx page - [#70](https://github.com/alleslabs/celatone-frontend/pull/70) Change default token denom on contract detail - [#78](https://github.com/alleslabs/celatone-frontend/pull/78) Ignore building step when branch is not main - [#62](https://github.com/alleslabs/celatone-frontend/pull/62) Add footer - [#71](https://github.com/alleslabs/celatone-frontend/pull/71) Add search bar at the top (currently support only contract address and code id) - [#69](https://github.com/alleslabs/celatone-frontend/pull/69) Add execute table in contract details page -- [#68](https://github.com/alleslabs/celatone-frontend/pull/63) Refactor past txs link props and make sure navigation works - [#65](https://github.com/alleslabs/celatone-frontend/pull/60) Create instantiate button component - [#64](https://github.com/alleslabs/celatone-frontend/pull/64) Add contract not exist page - [#63](https://github.com/alleslabs/celatone-frontend/pull/63) Add code id explorer link and code table row navigation @@ -102,6 +96,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#114](https://github.com/alleslabs/celatone-frontend/pull/114) Handle wallet connection cases in instantiate button +- [#115](https://github.com/alleslabs/celatone-frontend/pull/115) (Contract Details Page) Show no admin and correctly handle explorer link by address type +- [#68](https://github.com/alleslabs/celatone-frontend/pull/68) Refactor past txs link props and make sure navigation works - [#64](https://github.com/alleslabs/celatone-frontend/pull/64) Add address validation functions for contract and user addresses - [#52](https://github.com/alleslabs/celatone-frontend/pull/52) Create a component for disconnected State and apply to contract, code, past tx - [#56](https://github.com/alleslabs/celatone-frontend/pull/56) Refactor offchain form component by not receiving nameField and descriptionField @@ -109,8 +106,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +- [#122](https://github.com/alleslabs/celatone-frontend/pull/122) Fix unknown code upload block height +- [#121](https://github.com/alleslabs/celatone-frontend/pull/121) Fix code snippet for query axios +- [#119](https://github.com/alleslabs/celatone-frontend/pull/119) Fix searching and project ordering in public projects page +- [#118](https://github.com/alleslabs/celatone-frontend/pull/118) Fix floating tooltip when scrolling out of copy button - [#111](https://github.com/alleslabs/celatone-frontend/pull/111) Fix recent activities navigation and instantiate encode/decode - [#105](https://github.com/alleslabs/celatone-frontend/pull/105) Propoerly show instantiator of code contracts and contract in the instantiated list +- [#72](https://github.com/alleslabs/celatone-frontend/pull/72) Fix general wording and grammar +- [#110](https://github.com/alleslabs/celatone-frontend/pull/110) Fix proposal detail rendering +- [#109](https://github.com/alleslabs/celatone-frontend/pull/109) Fix incorrect rendering of zero value badges +- [#101](https://github.com/alleslabs/celatone-frontend/pull/101) Fix incorrect truncating of proposal id in contract detail's migration table +- [#100](https://github.com/alleslabs/celatone-frontend/pull/100) Fix contract instantiated time parsing +- [#96](https://github.com/alleslabs/celatone-frontend/pull/96) Fix incorrect instantiated block height explorer link +- [#87](https://github.com/alleslabs/celatone-frontend/pull/87) Fix funds didn't microfy before sending tx +- [#80](https://github.com/alleslabs/celatone-frontend/pull/80) Fix the misalignment of state in the PastTx page - [#42](https://github.com/alleslabs/celatone-frontend/pull/42) Properly show CTAs on contract-list page and edit zero/disconnected state - [#45](https://github.com/alleslabs/celatone-frontend/pull/45) Add chain ID and code details to contract detail data loader diff --git a/src/env.ts b/src/env.ts index 1e3ff1966..f41c44393 100644 --- a/src/env.ts +++ b/src/env.ts @@ -79,3 +79,13 @@ export const getChainApiPath = (chainName: string) => { return undefined; } }; +// TODO to handle testnet separately later +export const getMainnetApiPath = (chainId: string) => { + switch (chainId) { + case "osmo-test-4": + case "osmosis": + return "osmosis-1"; + default: + return undefined; + } +}; diff --git a/src/lib/app-provider/contexts/app.tsx b/src/lib/app-provider/contexts/app.tsx index 4856063b6..ffaaa5fc3 100644 --- a/src/lib/app-provider/contexts/app.tsx +++ b/src/lib/app-provider/contexts/app.tsx @@ -14,7 +14,11 @@ import { } from "lib/app-fns/explorer"; import { LoadingOverlay } from "lib/components/LoadingOverlay"; import { DEFAULT_ADDRESS, getChainNameByNetwork } from "lib/data"; -import { useCodeStore, useContractStore } from "lib/hooks"; +import { + useCodeStore, + useContractStore, + usePublicProjectStore, +} from "lib/hooks"; import type { ChainGasPrice, Token, U } from "lib/types"; import { formatUserKey } from "lib/utils"; @@ -64,6 +68,7 @@ export const AppProvider = ({ const { currentChainName, currentChainRecord, setCurrentChain } = useWallet(); const { setCodeUserKey, isCodeUserKeyExist } = useCodeStore(); const { setContractUserKey, isContractUserKeyExist } = useContractStore(); + const { setProjectUserKey, isProjectUserKeyExist } = usePublicProjectStore(); const chainGasPrice = useMemo(() => { if ( @@ -111,8 +116,9 @@ export const AppProvider = ({ const userKey = formatUserKey(currentChainName, DEFAULT_ADDRESS); setCodeUserKey(userKey); setContractUserKey(userKey); + setProjectUserKey(userKey); } - }, [currentChainName, setCodeUserKey, setContractUserKey]); + }, [currentChainName, setCodeUserKey, setContractUserKey, setProjectUserKey]); useEffect(() => { /** @@ -128,7 +134,11 @@ export const AppProvider = ({ }, [router.query.network, setCurrentChain]); const AppContent = observer(() => { - if (isCodeUserKeyExist() && isContractUserKeyExist()) + if ( + isCodeUserKeyExist() && + isContractUserKeyExist() && + isProjectUserKeyExist() + ) return ( {children} ); diff --git a/src/lib/components/AppLink.tsx b/src/lib/components/AppLink.tsx index 57399fe47..ff7332bf8 100644 --- a/src/lib/components/AppLink.tsx +++ b/src/lib/components/AppLink.tsx @@ -18,7 +18,9 @@ export const AppLink = ({ } > {typeof children === "string" ? ( - {children} + + {children} + ) : ( children )} diff --git a/src/lib/components/Copier.tsx b/src/lib/components/Copier.tsx index a032f66d1..424c6fe96 100644 --- a/src/lib/components/Copier.tsx +++ b/src/lib/components/Copier.tsx @@ -1,13 +1,21 @@ import { CopyIcon } from "@chakra-ui/icons"; +import type { LayoutProps } from "@chakra-ui/react"; import { Tooltip, useClipboard } from "@chakra-ui/react"; import { useEffect } from "react"; interface CopierProps { value: string; ml?: string; + className?: string; + display?: LayoutProps["display"]; } -export const Copier = ({ value, ml = "8px" }: CopierProps) => { +export const Copier = ({ + value, + ml = "8px", + className, + display = "flex", +}: CopierProps) => { const { onCopy, hasCopied, setValue } = useClipboard(value); useEffect(() => setValue(value), [value, setValue]); @@ -21,17 +29,20 @@ export const Copier = ({ value, ml = "8px" }: CopierProps) => { arrowSize={8} bg="primary.dark" > - { - e.stopPropagation(); - onCopy(); - }} - /> +
+ { + e.stopPropagation(); + onCopy(); + }} + /> +
); }; diff --git a/src/lib/components/ExplorerLink.tsx b/src/lib/components/ExplorerLink.tsx index 866cacf48..f41d07091 100644 --- a/src/lib/components/ExplorerLink.tsx +++ b/src/lib/components/ExplorerLink.tsx @@ -139,7 +139,6 @@ export const ExplorerLink = ({ return ( @@ -161,13 +163,12 @@ export const ExplorerLink = ({ isEllipsis={textFormat === "ellipsis"} maxWidth={maxWidth} /> - - - + )} diff --git a/src/lib/components/button/InstantiateButton.tsx b/src/lib/components/button/InstantiateButton.tsx index 21b4c4f74..a4a7be2ec 100644 --- a/src/lib/components/button/InstantiateButton.tsx +++ b/src/lib/components/button/InstantiateButton.tsx @@ -23,27 +23,35 @@ const StyledIcon = chakra(Icon, { const getInstantiateButtonProps = ( isAllowed: boolean, - isDisabled: boolean + isUnknown: boolean, + isWalletConnected: boolean ): { tooltipLabel: string; variant: string; icon: JSX.Element | undefined; } => { + if (isUnknown) { + return { + tooltipLabel: "", + variant: "outline-gray", + icon: undefined, + }; + } if (isAllowed) { return { - tooltipLabel: isDisabled - ? "You need to connect wallet to instantiate" - : "You can instantiate without opening proposal", + tooltipLabel: isWalletConnected + ? "You can instantiate without opening proposal" + : "You need to connect wallet to instantiate contract", variant: "outline-primary", icon: , }; } return { - tooltipLabel: isDisabled - ? "" - : "Instantiate through proposal only (Coming Soon)", + tooltipLabel: isWalletConnected + ? "Instantiate through proposal only (Coming Soon)" + : "You need to connect wallet to open instantiate proposal", variant: "outline-gray", - icon: isDisabled ? undefined : , + icon: , }; }; @@ -53,7 +61,7 @@ export const InstantiateButton = ({ codeId, ...buttonProps }: InstantiateButtonProps) => { - const { address } = useWallet(); + const { address, isWalletConnected } = useWallet(); const navigate = useInternalNavigate(); const goToInstantiate = () => navigate({ pathname: "/instantiate", query: { "code-id": codeId } }); @@ -61,12 +69,18 @@ export const InstantiateButton = ({ const isAllowed = permissionAddresses.includes(address as HumanAddr) || instantiatePermission === InstantiatePermission.EVERYBODY; - const isDisabled = - instantiatePermission === InstantiatePermission.UNKNOWN || !address; + + /** + * @todos use isDisabled when proposal flow is done + */ + // const isDisabled = + // instantiatePermission === InstantiatePermission.UNKNOWN || + // !isWalletConnected; const { tooltipLabel, variant, icon } = getInstantiateButtonProps( isAllowed, - isDisabled + instantiatePermission === InstantiatePermission.UNKNOWN, + isWalletConnected ); return ( @@ -79,7 +93,7 @@ export const InstantiateButton = ({ > + + ); + + return ( + + + {!filteredPublicProjects.length ? ( + + ) : ( + + {filteredPublicProjects.map((item) => ( + + ))} + + )} + + ); +}); diff --git a/src/lib/pages/public-project/components/BookmarkButton.tsx b/src/lib/pages/public-project/components/BookmarkButton.tsx new file mode 100644 index 000000000..3efb2c11f --- /dev/null +++ b/src/lib/pages/public-project/components/BookmarkButton.tsx @@ -0,0 +1,131 @@ +import { Flex, Icon, Button, useToast, Text } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; +import type { CSSProperties, MouseEvent } from "react"; +import { useCallback } from "react"; +import type { IconType } from "react-icons"; +import { MdBookmark, MdBookmarkBorder, MdCheckCircle } from "react-icons/md"; + +import { usePublicProjectStore } from "lib/hooks"; +import type { Option, Detail } from "lib/types"; + +interface DetailProps { + details: Option; + slug: string; + hasText?: boolean; +} + +const buttonTextProps: CSSProperties = { + padding: "6px 16px", + minWidth: "auto", + height: "auto", + borderRadius: "4px", +}; + +const buttonIconProps: CSSProperties = { + padding: "2px", + minWidth: "fit-content", + height: "fit-content", + borderRadius: "full", +}; + +const toastIcon = ( + +); + +interface StyledButtonProps { + hasText: boolean; + actionText: string; + icon: IconType; + iconColor: string; + action: (e: MouseEvent) => void; + variant: string; +} + +const StyledButton = ({ + hasText, + actionText, + icon, + action, + variant, + iconColor, +}: StyledButtonProps) => ( + +); + +export const BookmarkButton = observer( + ({ details, slug, hasText = true }: DetailProps) => { + const { isPublicProjectSaved, savePublicProject, removePublicProject } = + usePublicProjectStore(); + const toast = useToast({ + status: "success", + duration: 5000, + isClosable: false, + position: "bottom-right", + icon: toastIcon, + }); + + const handleSave = useCallback(() => { + savePublicProject({ + name: details?.name || "", + slug, + logo: details?.logo || "", + }); + toast({ + title: `Bookmarked \u2018${details?.name}\u2019 successfully`, + }); + }, [slug, details, savePublicProject, toast]); + + const handleRemove = useCallback(() => { + removePublicProject(slug); + toast({ + title: `\u2018${details?.name}\u2019 is removed from bookmark`, + }); + }, [slug, details, removePublicProject, toast]); + + return ( + + {isPublicProjectSaved(slug) ? ( + { + e.stopPropagation(); + handleRemove(); + }} + /> + ) : ( + { + if (details) { + e.stopPropagation(); + handleSave(); + } + }} + /> + )} + + ); + } +); diff --git a/src/lib/pages/public-project/components/CodesTable.tsx b/src/lib/pages/public-project/components/CodesTable.tsx new file mode 100644 index 000000000..258032a2a --- /dev/null +++ b/src/lib/pages/public-project/components/CodesTable.tsx @@ -0,0 +1,153 @@ +// TODO combine with codestable in codelist +import { + Button, + TableContainer, + Td, + Table, + Thead, + Tr, + Th, + Tbody, + Icon, + Text, + Flex, + Box, + Tag, +} from "@chakra-ui/react"; +import { matchSorter } from "match-sorter"; +import { useMemo, useState } from "react"; +import { MdBookmarkBorder, MdHowToVote, MdSearchOff } from "react-icons/md"; + +import { useInternalNavigate } from "lib/app-provider"; +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TextInput } from "lib/components/forms"; +import { EmptyState } from "lib/components/state/EmptyState"; +import type { Code } from "lib/types/projects"; + +interface CodesTableProps { + codes: Code[]; + hasSearchInput?: boolean; +} +export const CodesTable = ({ + codes = [], + hasSearchInput = true, +}: CodesTableProps) => { + const navigate = useInternalNavigate(); + const [searchKeyword, setSearchKeyword] = useState(""); + const filteredCodes = useMemo(() => { + return matchSorter(codes, searchKeyword, { + keys: ["id", "description"], + }); + }, [codes, searchKeyword]); + return ( + + {hasSearchInput && ( + + + + )} + {!filteredCodes.length ? ( + + + + ) : ( + + + + th": { + textTransform: "capitalize", + borderColor: "divider.main", + }, + }} + > + + + + + + + + + {/* TODO Link code id and row to code detail */} + {filteredCodes.map((code) => ( + td": { borderColor: "divider.main" }, + }} + _hover={{ + bg: "gray.900", + }} + onClick={() => navigate({ pathname: `/code/${code.id}` })} + cursor="pointer" + > + + + + + + + + ))} + +
Code IDCode Description + Contracts + UploaderPermission +
+ + + {code.description} + + todo + + todo + + {/* TODO: add condition for permission tag */} + + Nobody + + {/* + OnlyAddress + + + AnyOfAddresses + + + Everybody + */} + + + + {/* TODO save code */} + + +
+
+ )} +
+ ); +}; diff --git a/src/lib/pages/public-project/components/ContractsTable.tsx b/src/lib/pages/public-project/components/ContractsTable.tsx new file mode 100644 index 000000000..5009b4440 --- /dev/null +++ b/src/lib/pages/public-project/components/ContractsTable.tsx @@ -0,0 +1,168 @@ +import { InfoIcon } from "@chakra-ui/icons"; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Flex, + Button, + Box, + Text, + Tooltip, +} from "@chakra-ui/react"; +import { matchSorter } from "match-sorter"; +import { useMemo, useState } from "react"; +import { MdSearchOff } from "react-icons/md"; + +import { useInternalNavigate } from "lib/app-provider"; +import { AppLink } from "lib/components/AppLink"; +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TextInput } from "lib/components/forms"; +import { EmptyState } from "lib/components/state/EmptyState"; +import type { Contract } from "lib/types"; + +interface ContractsTableProps { + contracts: Contract[]; + hasSearchInput?: boolean; +} + +export const ContractsTable = ({ + contracts = [], + hasSearchInput = true, +}: ContractsTableProps) => { + const navigate = useInternalNavigate(); + const [searchKeyword, setSearchKeyword] = useState(""); + const filteredContracts = useMemo(() => { + return matchSorter(contracts, searchKeyword, { + keys: ["name", "contractAddress", "description"], + }); + }, [contracts, searchKeyword]); + + return ( + + {hasSearchInput && ( + + + + )} + {!filteredContracts.length ? ( + + + + ) : ( + + + + th": { + borderColor: "divider.main", + textTransform: "capitalize", + }, + }} + > + + + + + + + + {filteredContracts?.map((item) => ( + td": { borderColor: "divider.main" }, + }} + onClick={() => + navigate({ pathname: `/contract/${item.contractAddress}` }) + } + cursor="pointer" + > + + + {/* TODO Instantiator Info */} + + + + ))} + +
Contract AddressContract NameInstantiated by +
+ + + + + + {item.name} + + {item.description && ( + + + + )} + + + + TODO + + + + + + + + + {/* TODO save contract */} + {/* + } + /> */} + +
+
+ )} +
+ ); +}; diff --git a/src/lib/pages/public-project/components/DetailHeader.tsx b/src/lib/pages/public-project/components/DetailHeader.tsx new file mode 100644 index 000000000..cac2aace7 --- /dev/null +++ b/src/lib/pages/public-project/components/DetailHeader.tsx @@ -0,0 +1,83 @@ +import { + Box, + Breadcrumb, + BreadcrumbItem, + Text, + Flex, + Heading, + Image, +} from "@chakra-ui/react"; +import { MdChevronRight } from "react-icons/md"; + +import { AppLink } from "lib/components/AppLink"; +import type { Option, Detail } from "lib/types"; + +import { BookmarkButton } from "./BookmarkButton"; +import { SocialMedia } from "./SocialMedia"; + +interface DetailProps { + details: Option; + slug: string; +} +export const DetailHeader = ({ details, slug }: DetailProps) => { + return ( + + } + > + + + Public Projects + + + + + {details?.name} + + + + + + + Celatone + + {details?.name} + + + + {details?.description} + + + + + + + + + ); +}; diff --git a/src/lib/pages/public-project/components/PublicProjectCard.tsx b/src/lib/pages/public-project/components/PublicProjectCard.tsx new file mode 100644 index 000000000..b5744ac3e --- /dev/null +++ b/src/lib/pages/public-project/components/PublicProjectCard.tsx @@ -0,0 +1,91 @@ +import { Flex, Text, Image, Box } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; +import { useClampText } from "use-clamp-text"; + +import { useInternalNavigate } from "lib/app-provider"; +import type { PublicProjectInfo } from "lib/types"; + +import { BookmarkButton } from "./BookmarkButton"; +import { SocialMedia } from "./SocialMedia"; + +interface PublicProjectCardProps { + item: PublicProjectInfo["details"]; + slug: string; +} + +export const PublicProjectCard = observer( + ({ item, slug }: PublicProjectCardProps) => { + const navigate = useInternalNavigate(); + const handleOnClick = () => { + navigate({ pathname: `/public-project/${slug}` }); + }; + + const [ref, { clampedText }] = useClampText({ + text: item?.description || "", + ellipsis: "...", + lines: 3, + }); + + return ( + + + + + + Celatone + + {item.name} + + + + + } + variant="body3" + color="text.primary" + pt={3} + > + {clampedText} + + + + + + ); + } +); diff --git a/src/lib/pages/public-project/components/SocialMedia.tsx b/src/lib/pages/public-project/components/SocialMedia.tsx new file mode 100644 index 000000000..7ad8e7ef3 --- /dev/null +++ b/src/lib/pages/public-project/components/SocialMedia.tsx @@ -0,0 +1,85 @@ +import { Flex, Link, Icon } from "@chakra-ui/react"; +import { + FaTwitter, + FaGithub, + FaTelegram, + FaDiscord, + FaInfo, +} from "react-icons/fa"; +import { MdLanguage } from "react-icons/md"; + +import type { Option, Detail } from "lib/types"; + +export const renderSocial = (name: string) => { + switch (name) { + case "twitter": + return FaTwitter; + case "telegram": + return FaTelegram; + case "discord": + return FaDiscord; + default: + return FaInfo; + } +}; + +interface SocialMediaProps { + details: Option; +} +export const SocialMedia = ({ details }: SocialMediaProps) => { + if (!details) return null; + return ( + e.stopPropagation()} + > + {details.website && ( + // todos: create ExternalLink component later + + + + )} + {details.github && ( + // todos: create ExternalLink component later + + + + )} + {details.socials.length && + details.socials.map( + (social) => + social.url !== "" && ( + // todos: create ExternalLink component later + + + + ) + )} + + ); +}; diff --git a/src/lib/pages/public-project/data.ts b/src/lib/pages/public-project/data.ts new file mode 100644 index 000000000..8730c777e --- /dev/null +++ b/src/lib/pages/public-project/data.ts @@ -0,0 +1,23 @@ +import { useRouter } from "next/router"; + +import { usePublicProjectBySlugQuery } from "lib/services/publicProject"; +import { getFirstQueryParam } from "lib/utils"; + +// TODO: +// 1. contract -> get instantiator +// 2. code -> get uploader +// 3. code -> get contract amount that instantiated from this code ID +// 4. code -> get permission and render the right tag (already has UI) + +export const usePublicData = () => { + const router = useRouter(); + const projectSlug = getFirstQueryParam(router.query.slug); + const { data: projectInfo } = usePublicProjectBySlugQuery(projectSlug); + + return { + publicCodes: projectInfo?.codes || [], + publicContracts: projectInfo?.contracts || [], + projectDetail: projectInfo?.details, + slug: projectSlug, + }; +}; diff --git a/src/lib/pages/public-project/index.tsx b/src/lib/pages/public-project/index.tsx new file mode 100644 index 000000000..fc08cb1a1 --- /dev/null +++ b/src/lib/pages/public-project/index.tsx @@ -0,0 +1,18 @@ +import { Flex, Heading } from "@chakra-ui/react"; + +import PageContainer from "lib/components/PageContainer"; + +import { AllProject } from "./components/AllProject"; + +export const AllPublicProjectsPage = () => ( + + + + + Public Projects + + + + + +); diff --git a/src/lib/pages/public-project/slug.tsx b/src/lib/pages/public-project/slug.tsx new file mode 100644 index 000000000..a1ae687dd --- /dev/null +++ b/src/lib/pages/public-project/slug.tsx @@ -0,0 +1,145 @@ +import { + Box, + Heading, + Tabs, + TabList, + TabPanels, + Flex, + TabPanel, + Button, + Icon, +} from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; +import { MdExpandMore } from "react-icons/md"; + +import { CustomTab } from "lib/components/CustomTab"; +import { EmptyState } from "lib/components/state/EmptyState"; + +import { CodesTable } from "./components/CodesTable"; +import { ContractsTable } from "./components/ContractsTable"; +import { DetailHeader } from "./components/DetailHeader"; +import { usePublicData } from "./data"; + +export const ProjectDetail = observer(() => { + const [tabIndex, setTabIndex] = useState(0); + const { publicCodes, publicContracts, projectDetail, slug } = usePublicData(); + return ( + + + + + setTabIndex(0)} + > + Overview + + setTabIndex(1)} + isDisabled={!publicCodes.length} + count={publicCodes.length} + > + Codes + + setTabIndex(2)} + isDisabled={!publicContracts.length} + count={publicContracts.length} + > + Contracts + + + + + + + Codes + + {publicCodes.length ? ( + + + {publicCodes.length > 5 ?? ( + + + + )} + + ) : ( + + + + )} + + Contracts + + {publicContracts.length ? ( + + + {publicContracts.length > 5 ?? ( + + + + )} + + ) : ( + + + + )} + + + + + + + + + + + ); +}); diff --git a/src/lib/services/codeService.ts b/src/lib/services/codeService.ts index fa7c49ba7..3fefa6be2 100644 --- a/src/lib/services/codeService.ts +++ b/src/lib/services/codeService.ts @@ -133,7 +133,7 @@ export const useCodeInfoByCodeId = ( proposal: codes_by_pk.code_proposals[0] ? { proposalId: codes_by_pk.code_proposals[0].proposal_id, - height: codes_by_pk.code_proposals[0].block?.height ?? 0, + height: codes_by_pk.code_proposals[0].block?.height, created: parseDateDefault( codes_by_pk.code_proposals[0].block?.timestamp ), diff --git a/src/lib/services/publicProject.ts b/src/lib/services/publicProject.ts new file mode 100644 index 000000000..ce456b641 --- /dev/null +++ b/src/lib/services/publicProject.ts @@ -0,0 +1,68 @@ +import { useWallet } from "@cosmos-kit/react"; +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { useCallback } from "react"; + +import { CELATONE_API_ENDPOINT, getChainApiPath, getMainnetApiPath } from "env"; +import type { + Option, + RawContract, + RawPublicProjectInfo, + PublicProjectInfo, + Contract, +} from "lib/types"; + +const parseContract = (raw: RawContract): Contract => ({ + contractAddress: raw.address, + description: raw.description, + name: raw.name, + slug: raw.slug, +}); + +export const usePublicProjectsQuery = () => { + const { currentChainRecord } = useWallet(); + + const queryFn = useCallback(async () => { + if (!currentChainRecord) throw new Error("No chain selected"); + + return axios + .get( + `${CELATONE_API_ENDPOINT}/projects/${getChainApiPath( + currentChainRecord.chain.chain_name + )}/${getMainnetApiPath(currentChainRecord.chain.chain_id)}` + ) + .then(({ data: projects }) => + projects.map((project) => ({ + ...project, + contracts: project.contracts.map(parseContract), + })) + ); + }, [currentChainRecord]); + + return useQuery(["public_project"], queryFn, { + keepPreviousData: true, + }); +}; + +export const usePublicProjectBySlugQuery = (slug: Option) => { + const { currentChainRecord } = useWallet(); + const queryFn = useCallback(async (): Promise> => { + if (!slug) throw new Error("No project selected"); + if (!currentChainRecord) throw new Error("No chain selected"); + return axios + .get( + `${CELATONE_API_ENDPOINT}/projects/${getChainApiPath( + currentChainRecord.chain.chain_name + )}/${getMainnetApiPath(currentChainRecord.chain.chain_id)}/${slug}` + ) + .then(({ data: project }) => ({ + ...project, + contracts: project.contracts.map(parseContract), + })); + }, [currentChainRecord, slug]); + + return useQuery(["public_project_by_slug"], queryFn, { + keepPreviousData: true, + enabled: !!slug, + }); +}; diff --git a/src/lib/stores/project.ts b/src/lib/stores/project.ts new file mode 100644 index 000000000..5066a91b0 --- /dev/null +++ b/src/lib/stores/project.ts @@ -0,0 +1,65 @@ +import { makeAutoObservable } from "mobx"; +import { isHydrated, makePersistable } from "mobx-persist-store"; + +import type { Dict } from "lib/types"; + +export interface PublicProject { + name: string; + slug: string; + logo: string; +} + +export class PublicProjectStore { + private userKey: string; + + publicProjects: Dict; // user key + + constructor() { + this.userKey = ""; + this.publicProjects = {}; + + makeAutoObservable(this, {}, { autoBind: true }); + + makePersistable(this, { + name: "PublicProjectStore", + properties: ["publicProjects"], + }); + } + + get isHydrated(): boolean { + return isHydrated(this); + } + + isProjectUserKeyExist(): boolean { + return !!this.userKey; + } + + setProjectUserKey(userKey: string) { + this.userKey = userKey; + } + + getSavedPublicProjects(): PublicProject[] { + return this.publicProjects[this.userKey] ?? []; + } + + isPublicProjectSaved(slug: string): boolean { + const publicProjectByUserKey = this.getSavedPublicProjects(); + + return publicProjectByUserKey.findIndex((x) => x.slug === slug) > -1; + } + + savePublicProject(newProject: PublicProject): void { + if (!this.isPublicProjectSaved(newProject.slug)) { + this.publicProjects[this.userKey] = [ + ...this.getSavedPublicProjects(), + newProject, + ]; + } + } + + removePublicProject(slug: string): void { + this.publicProjects[this.userKey] = this.publicProjects[ + this.userKey + ]?.filter((each) => each.slug !== slug); + } +} diff --git a/src/lib/stores/root.ts b/src/lib/stores/root.ts index 6418bbd9b..5b7f32e53 100644 --- a/src/lib/stores/root.ts +++ b/src/lib/stores/root.ts @@ -1,13 +1,17 @@ import { CodeStore } from "./code"; import { ContractStore } from "./contract"; +import { PublicProjectStore } from "./project"; export class RootStore { codeStore: CodeStore; contractStore: ContractStore; + publicProjectStore: PublicProjectStore; + constructor() { this.codeStore = new CodeStore(); this.contractStore = new ContractStore(); + this.publicProjectStore = new PublicProjectStore(); } } diff --git a/src/lib/types/code.ts b/src/lib/types/code.ts index 031d9477a..a0c2b742e 100644 --- a/src/lib/types/code.ts +++ b/src/lib/types/code.ts @@ -21,7 +21,7 @@ export interface CodeInfo extends CodeLocalInfo { interface CodeProposal { proposalId: number; - height: number; + height: Option; created: Date; } diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 26d7fae8c..d59b71ff3 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -6,3 +6,4 @@ export * from "./LVPair"; export * from "./rpc"; export * from "./wallet"; export * from "./contract"; +export * from "./projects"; diff --git a/src/lib/types/projects.ts b/src/lib/types/projects.ts new file mode 100644 index 000000000..834b1134c --- /dev/null +++ b/src/lib/types/projects.ts @@ -0,0 +1,58 @@ +import type { AssetInfo, ContractAddr, HumanAddr } from "lib/types"; + +export interface Account { + address: HumanAddr; + description: string; + name: string; + slug: string; +} + +export interface Code { + description: string; + id: number; + name: string; + slug: string; +} + +export interface RawContract { + address: ContractAddr; + description: string; + name: string; + slug: string; +} + +export interface Social { + name: string; + url: string; +} + +export interface Detail { + github: string; + logo: string; + name: string; + socials: Social[]; + website: string; + description: string; +} + +export interface RawPublicProjectInfo { + accounts: Account[]; + assets: AssetInfo; + codes: Code[]; + contracts: RawContract[]; + details: Detail; + slug: string; +} + +export interface Contract extends Omit { + contractAddress: string; +} + +export interface PublicProjectInfo { + accounts: Account[]; + assets: AssetInfo; + codes: Code[]; + contracts: Contract[]; + details: Detail; + slug: string; +} diff --git a/src/pages/[network]/public-project/[slug].tsx b/src/pages/[network]/public-project/[slug].tsx new file mode 100644 index 000000000..21ef00b5f --- /dev/null +++ b/src/pages/[network]/public-project/[slug].tsx @@ -0,0 +1,3 @@ +import { ProjectDetail } from "lib/pages/public-project/slug"; + +export default ProjectDetail; diff --git a/src/pages/[network]/public-project/index.tsx b/src/pages/[network]/public-project/index.tsx new file mode 100644 index 000000000..8308e5555 --- /dev/null +++ b/src/pages/[network]/public-project/index.tsx @@ -0,0 +1,3 @@ +import { AllPublicProjectsPage } from "lib/pages/public-project"; + +export default AllPublicProjectsPage; diff --git a/src/pages/public-project/[slug].tsx b/src/pages/public-project/[slug].tsx new file mode 100644 index 000000000..21ef00b5f --- /dev/null +++ b/src/pages/public-project/[slug].tsx @@ -0,0 +1,3 @@ +import { ProjectDetail } from "lib/pages/public-project/slug"; + +export default ProjectDetail; diff --git a/src/pages/public-project/index.tsx b/src/pages/public-project/index.tsx new file mode 100644 index 000000000..8308e5555 --- /dev/null +++ b/src/pages/public-project/index.tsx @@ -0,0 +1,3 @@ +import { AllPublicProjectsPage } from "lib/pages/public-project"; + +export default AllPublicProjectsPage;