diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 027a78d..098c328 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -149,6 +149,7 @@ type ItemProp @entity { type FieldProp @entity { id: ID! + position: BigInt! type: String! label: String! description: String! diff --git a/subgraph/src/Curate.ts b/subgraph/src/Curate.ts index 8f780f3..ad2835b 100644 --- a/subgraph/src/Curate.ts +++ b/subgraph/src/Curate.ts @@ -1,5 +1,5 @@ /* eslint-disable prefer-const */ -import { json, log } from "@graphprotocol/graph-ts"; +import { BigInt, json, log } from "@graphprotocol/graph-ts"; import { Item, Request, Registry, FieldProp } from "../generated/schema"; import { @@ -219,6 +219,7 @@ export function handleListMetadataSet(event: ListMetadataSet): void { let fieldPropId = registry.id + "@" + checkedLabel; let fieldProp = new FieldProp(fieldPropId); + fieldProp.position = BigInt.fromI32(i); fieldProp.type = JSONValueToMaybeString(_type); fieldProp.label = JSONValueToMaybeString(label); fieldProp.description = JSONValueToMaybeString(description); diff --git a/web/src/components/ActionButton/Modal/ChallengeItemModal.tsx b/web/src/components/ActionButton/Modal/ChallengeItemModal.tsx index 5a23433..9a9b794 100644 --- a/web/src/components/ActionButton/Modal/ChallengeItemModal.tsx +++ b/web/src/components/ActionButton/Modal/ChallengeItemModal.tsx @@ -1,11 +1,8 @@ -import React, { useMemo, useRef, useState } from "react"; +import React, { useMemo, useState } from "react"; import styled from "styled-components"; -import { useClickAway } from "react-use"; -import { Overlay } from "components/Overlay"; import Header from "./Header"; import Buttons from "./Buttons"; import DepositRequired from "./DepositRequired"; -import { StyledModal } from "./StyledModal"; import Info from "./Info"; import { prepareWriteCurateV2, @@ -19,8 +16,9 @@ import { wrapWithToast } from "utils/wrapWithToast"; import { IBaseModal } from "."; import EvidenceUpload, { Evidence } from "./EvidenceUpload"; import { uploadFileToIPFS } from "utils/uploadFileToIPFS"; +import Modal from "components/Modal"; -const ReStyledModal = styled(StyledModal)` +const ReStyledModal = styled(Modal)` gap: 32px; `; @@ -46,8 +44,6 @@ const ChallengeItemModal: React.FC = ({ challengeType, refetch, }) => { - const containerRef = useRef(null); - useClickAway(containerRef, () => toggleModal()); const { address } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); @@ -111,52 +107,50 @@ const ChallengeItemModal: React.FC = ({ }, [depositRequired, userBalance, isEvidenceUploading, isEvidenceValid]); return ( - <> - - -
- - - - { - setIsChallengingItem(true); - - const evidenceFile = new File([JSON.stringify(evidence)], "evidence.json", { - type: "application/json", - }); - - uploadFileToIPFS(evidenceFile) - .then(async (res) => { - if (res.status === 200 && walletClient) { - const response = await res.json(); - const fileURI = response["cids"][0]; - - const { request } = await prepareWriteCurateV2({ - //@ts-ignore - address: registryAddress, - functionName: "challengeRequest", - args: [itemId as `0x${string}`, fileURI], - value: depositRequired, - }); - - wrapWithToast(async () => await walletClient.writeContract(request), publicClient).then((res) => { + +
+ + + + { + setIsChallengingItem(true); + + const evidenceFile = new File([JSON.stringify(evidence)], "evidence.json", { + type: "application/json", + }); + + uploadFileToIPFS(evidenceFile) + .then(async (res) => { + if (res.status === 200 && walletClient) { + const response = await res.json(); + const fileURI = response["cids"][0]; + + const { request } = await prepareWriteCurateV2({ + //@ts-ignore + address: registryAddress, + functionName: "challengeRequest", + args: [itemId as `0x${string}`, fileURI], + value: depositRequired, + }); + + wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then((res) => { console.log({ res }); refetch(); toggleModal(); - }); - } - }) - .catch((err) => console.log(err)) - .finally(() => setIsChallengingItem(false)); - }} - /> - - + }) + .finally(() => setIsChallengingItem(false)); + } + }) + .catch((err) => console.log(err)); + }} + /> + ); }; diff --git a/web/src/components/ActionButton/Modal/EvidenceUpload.tsx b/web/src/components/ActionButton/Modal/EvidenceUpload.tsx index 1927073..8d9494d 100644 --- a/web/src/components/ActionButton/Modal/EvidenceUpload.tsx +++ b/web/src/components/ActionButton/Modal/EvidenceUpload.tsx @@ -6,6 +6,7 @@ import LabeledInput from "components/LabeledInput"; import { responsiveSize } from "styles/responsiveSize"; import { OPTIONS as toastOptions } from "utils/wrapWithToast"; import { uploadFileToIPFS } from "utils/uploadFileToIPFS"; +import { SUPPORTED_FILE_TYPES } from "src/consts"; const Container = styled.div` width: 100%; @@ -69,7 +70,7 @@ const EvidenceUpload: React.FC = ({ setEvidence, setIsEvidenceU }, [title, description, fileURI]); const handleFileUpload = (file: File) => { - if (file?.type !== "application/pdf") { + if (!SUPPORTED_FILE_TYPES.includes(file?.type)) { toast.error("File type not supported", toastOptions); return; } diff --git a/web/src/components/ActionButton/Modal/RemoveModal.tsx b/web/src/components/ActionButton/Modal/RemoveModal.tsx index 3d91c72..42a1407 100644 --- a/web/src/components/ActionButton/Modal/RemoveModal.tsx +++ b/web/src/components/ActionButton/Modal/RemoveModal.tsx @@ -1,11 +1,8 @@ -import React, { useMemo, useRef, useState } from "react"; +import React, { useMemo, useState } from "react"; import styled from "styled-components"; -import { useClickAway } from "react-use"; -import { Overlay } from "components/Overlay"; import Header from "./Header"; import Buttons from "./Buttons"; import DepositRequired from "./DepositRequired"; -import { StyledModal } from "./StyledModal"; import Info from "./Info"; import { IBaseModal } from "."; import { useAccount, useBalance, usePublicClient, useWalletClient } from "wagmi"; @@ -18,8 +15,9 @@ import { useArbitrationCost } from "hooks/useArbitrationCostFromKlerosCore"; import { wrapWithToast } from "utils/wrapWithToast"; import EvidenceUpload, { Evidence } from "./EvidenceUpload"; import { uploadFileToIPFS } from "utils/uploadFileToIPFS"; +import Modal from "components/Modal"; -const ReStyledModal = styled(StyledModal)` +const ReStyledModal = styled(Modal)` gap: 32px; `; @@ -32,8 +30,6 @@ const alertMessage = (isItem: boolean) => ` Make sure you read and understand the Policy before proceeding.`; const RemoveModal: React.FC = ({ toggleModal, isItem, registryAddress, itemId, refetch }) => { - const containerRef = useRef(null); - useClickAway(containerRef, () => toggleModal()); const { address } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); @@ -87,52 +83,50 @@ const RemoveModal: React.FC = ({ toggleModal, isItem, registryAddr }, [depositRequired, userBalance, isEvidenceUploading, isEvidenceValid]); return ( - <> - - -
- - - - { - setIsRemovingItem(true); - - const evidenceFile = new File([JSON.stringify(evidence)], "evidence.json", { - type: "application/json", - }); - - uploadFileToIPFS(evidenceFile) - .then(async (res) => { - if (res.status === 200 && walletClient) { - const response = await res.json(); - const fileURI = response["cids"][0]; - - const { request } = await prepareWriteCurateV2({ - //@ts-ignore - address: registryAddress, - functionName: "removeItem", - args: [itemId as `0x${string}`, fileURI], - value: depositRequired, - }); - - wrapWithToast(async () => await walletClient.writeContract(request), publicClient).then((res) => { + +
+ + + + { + setIsRemovingItem(true); + + const evidenceFile = new File([JSON.stringify(evidence)], "evidence.json", { + type: "application/json", + }); + + uploadFileToIPFS(evidenceFile) + .then(async (res) => { + if (res.status === 200 && walletClient) { + const response = await res.json(); + const fileURI = response["cids"][0]; + + const { request } = await prepareWriteCurateV2({ + //@ts-ignore + address: registryAddress, + functionName: "removeItem", + args: [itemId as `0x${string}`, fileURI], + value: depositRequired, + }); + + wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then((res) => { console.log({ res }); refetch(); toggleModal(); - }); - } - }) - .catch((err) => console.log(err)) - .finally(() => setIsRemovingItem(false)); - }} - /> - - + }) + .finally(() => setIsRemovingItem(false)); + } + }) + .catch((err) => console.log(err)); + }} + /> + ); }; diff --git a/web/src/components/ActionButton/Modal/ResubmitModal.tsx b/web/src/components/ActionButton/Modal/ResubmitModal.tsx index 8c55b46..803eabd 100644 --- a/web/src/components/ActionButton/Modal/ResubmitModal.tsx +++ b/web/src/components/ActionButton/Modal/ResubmitModal.tsx @@ -1,11 +1,8 @@ -import React, { useMemo, useRef, useState } from "react"; +import React, { useMemo, useState } from "react"; import styled from "styled-components"; -import { useClickAway } from "react-use"; -import { Overlay } from "components/Overlay"; import Header from "./Header"; import Buttons from "./Buttons"; import DepositRequired from "./DepositRequired"; -import { StyledModal } from "./StyledModal"; import Info from "./Info"; import { IBaseModal } from "."; import { useAccount, useBalance, usePublicClient } from "wagmi"; @@ -18,8 +15,9 @@ import { import { useArbitrationCost } from "hooks/useArbitrationCostFromKlerosCore"; import { wrapWithToast } from "utils/wrapWithToast"; import { useItemDetailsQuery } from "hooks/queries/useItemDetailsQuery"; +import Modal from "components/Modal"; -const ReStyledModal = styled(StyledModal)` +const ReStyledModal = styled(Modal)` gap: 32px; `; @@ -32,8 +30,6 @@ const alertMessage = (isItem: boolean) => ` Make sure you read and understand the Policy before proceeding.`; const ResubmitModal: React.FC = ({ toggleModal, isItem, registryAddress, itemId, refetch }) => { - const containerRef = useRef(null); - useClickAway(containerRef, () => toggleModal()); const { address } = useAccount(); const publicClient = usePublicClient(); const [isResubmittingItem, setIsResubmittingItem] = useState(false); @@ -79,38 +75,35 @@ const ResubmitModal: React.FC = ({ toggleModal, isItem, registryAd const { writeAsync: resubmitItem } = useCurateV2AddItem(config); return ( - <> - - -
- - - { - if (!resubmitItem) return; - setIsResubmittingItem(true); - wrapWithToast( - async () => - await resubmitItem().then((response) => { - return response.hash; - }), - publicClient - ) - .then((res) => { - console.log({ res }); - refetch(); - toggleModal(); - }) - .catch(() => {}) - .finally(() => setIsResubmittingItem(false)); - }} - /> - - + +
+ + + { + if (!resubmitItem) return; + setIsResubmittingItem(true); + wrapWithToast( + async () => + await resubmitItem().then((response) => { + return response.hash; + }), + publicClient + ) + .then((res) => { + console.log({ res }); + refetch(); + toggleModal(); + }) + .catch(() => {}) + .finally(() => setIsResubmittingItem(false)); + }} + /> + ); }; diff --git a/web/src/components/ActionButton/Modal/StyledModal.tsx b/web/src/components/ActionButton/Modal/StyledModal.tsx deleted file mode 100644 index d4d2744..0000000 --- a/web/src/components/ActionButton/Modal/StyledModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import styled from "styled-components"; - -export const StyledModal = styled.div` - display: flex; - position: fixed; - top: 10vh; - left: 50%; - transform: translateX(-50%); - max-height: 80vh; - overflow-y: auto; - - z-index: 10; - flex-direction: column; - align-items: center; - width: 86vw; - max-width: 600px; - border-radius: 3px; - border: 1px solid ${({ theme }) => theme.stroke}; - background-color: ${({ theme }) => theme.whiteBackground}; - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06); - padding: 32px 32px 32px 36px; -`; diff --git a/web/src/components/ActionButton/index.tsx b/web/src/components/ActionButton/index.tsx index bb01f96..cd6d04f 100644 --- a/web/src/components/ActionButton/index.tsx +++ b/web/src/components/ActionButton/index.tsx @@ -1,5 +1,5 @@ import { Button } from "@kleros/ui-components-library"; -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { Address } from "viem"; import { COURT_SITE } from "consts/index"; import { Status } from "consts/status"; @@ -10,6 +10,7 @@ import { useToggle } from "react-use"; import Modal, { getModalButtonText } from "./Modal"; import ExecuteButton from "./ExecuteButton"; import { useCurateV2ChallengePeriodDuration } from "hooks/contracts/generated"; +import { useQueryClient } from "@tanstack/react-query"; const StyledKlerosIcon = styled(KlerosIcon)` path { @@ -22,10 +23,11 @@ interface IActionButton { registryAddress: Address; itemId: string; isItem: boolean; - refetch: () => void; } -const ActionButton: React.FC = ({ status, registryAddress, itemId, isItem, refetch }) => { +const ActionButton: React.FC = ({ status, registryAddress, itemId, isItem }) => { + const queryClient = useQueryClient(); + const [isModalOpen, toggleModal] = useToggle(false); const { data: requests, isLoading } = useItemRequests(`${itemId}@${registryAddress}`); @@ -40,6 +42,11 @@ const ActionButton: React.FC = ({ status, registryAddress, itemId return !latestRequest.resolved && Date.now() / 1000 - latestRequest.submissionTime > challengePeriodDuration; }, [latestRequest, challengePeriodDuration]); + const refetch = useCallback(() => { + queryClient.invalidateQueries(["refetchOnBlock", `itemDetailsQuery${itemId}@${registryAddress}`]); + queryClient.invalidateQueries(["refetchOnBlock", `registryDetailsQuery${registryAddress}`]); + }, [itemId, registryAddress, queryClient]); + let ButtonComponent: JSX.Element | null = useMemo(() => { if (status === Status.Disputed) return ( diff --git a/web/src/components/InformationCard/index.tsx b/web/src/components/InformationCard/index.tsx deleted file mode 100644 index 0d91112..0000000 --- a/web/src/components/InformationCard/index.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled, { css } from "styled-components"; -import { responsiveSize } from "styles/responsiveSize"; -import Skeleton from "react-loading-skeleton"; -import { Card, Copiable } from "@kleros/ui-components-library"; -import EtherscanIcon from "svgs/icons/etherscan.svg"; -import { Status } from "consts/status"; -import { DEFAULT_CHAIN, SUPPORTED_CHAINS } from "consts/chains"; -import { getIpfsUrl } from "utils/getIpfsUrl"; -import { isUndefined } from "utils/index"; -import AliasDisplay from "components/RegistryInfo/AliasDisplay"; -import { getStatusColor, getStatusLabel } from "components/RegistryCard/StatusBanner"; -import { Policies } from "./Policies"; -import { DEFAULT_LIST_LOGO } from "consts/index"; -import { landscapeStyle } from "styles/landscapeStyle"; -import ActionButton from "../ActionButton"; -import { Address } from "viem"; - -const StyledCard = styled(Card)` - display: flex; - width: 100%; - height: auto; - flex-direction: column; - margin-bottom: 64px; -`; - -const StatusContainer = styled.div<{ status: Status }>` - display: flex; - .dot { - ::before { - content: ""; - display: inline-block; - height: 8px; - width: 8px; - border-radius: 50%; - margin-right: 8px; - } - } - ${({ theme, status }) => { - const [frontColor] = getStatusColor(status, theme); - return ` - .front-color { - color: ${frontColor}; - } - .dot { - ::before { - background-color: ${frontColor}; - } - } - `; - }}; -`; - -const TopInfo = styled.div` - display: flex; - justify-content: space-between; - flex-wrap: wrap; - gap: 12px; - padding: ${responsiveSize(20, 24)} ${responsiveSize(24, 32)} 12px ${responsiveSize(24, 32)}; - ${landscapeStyle( - () => css` - flex-wrap: nowrap; - ` - )} -`; - -const LogoAndTitle = styled.div` - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; -`; - -const TopLeftInfo = styled.div` - display: flex; - flex-direction: column; - gap: ${responsiveSize(8, 16)} 0; -`; - -const TopRightInfo = styled.div` - display: flex; - flex-direction: row; - gap: 32px; - flex-wrap: wrap; - flex-shrink: 0; - align-items: start; - padding-top: 20px; - ${landscapeStyle( - () => css` - gap: 0 ${responsiveSize(24, 32, 900)}; - ` - )} -`; - -const StyledEtherscanIcon = styled(EtherscanIcon)` - display: flex; - height: 16px; - width: 16px; -`; - -const StyledLogo = styled.img<{ isListView: boolean }>` - width: ${({ isListView }) => (isListView ? "40px" : "125px")}; - height: ${({ isListView }) => (isListView ? "40px" : "125px")}; - object-fit: contain; - margin-bottom: ${({ isListView }) => (isListView ? "0px" : "8px")}; -`; - -const StyledTitle = styled.h1` - margin: 0; -`; - -const StyledP = styled.p` - color: ${({ theme }) => theme.secondaryText}; - margin: 0; -`; - -const StyledLabel = styled.label` - color: ${({ theme }) => theme.primaryBlue}; -`; - -const Divider = styled.hr` - border: none; - height: 1px; - background-color: ${({ theme }) => theme.stroke}; - margin: ${responsiveSize(20, 28)} ${responsiveSize(24, 32)}; -`; - -const BottomInfo = styled.div` - display: flex; - padding: 0 ${responsiveSize(24, 32)} 12px ${responsiveSize(24, 32)}; - flex-wrap: wrap; - gap: 20px; - justify-content: space-between; -`; - -const SkeletonLogo = styled(Skeleton)` - width: 125px; - height: 125px; - border-radius: 62.5px; - margin-bottom: 8px; -`; - -const SkeletonTitle = styled(Skeleton)` - width: 180px; - height: 30px; -`; - -const SkeletonDescription = styled(Skeleton)` - width: 90%; - height: 21px; -`; - -interface IInformationCard { - id?: string; - title?: string; - logoURI: string; - description?: string; - status: Status; - registerer: string; - policyURI: string; - explorerAddress?: Address; - itemId: string; - registryAddress: Address; - className?: string; - refetch: () => {}; -} - -const InformationCard: React.FC = ({ - id, - title, - logoURI, - description, - registerer, - status, - policyURI, - explorerAddress, - className, - itemId, - registryAddress, - refetch = () => {}, -}) => { - const [imageSrc, setImageSrc] = useState(getIpfsUrl(logoURI ?? "")); - useEffect(() => setImageSrc(getIpfsUrl(logoURI)), [logoURI]); - - return ( - - - - - {isUndefined(logoURI) ? ( - - ) : ( - setImageSrc(getIpfsUrl(DEFAULT_LIST_LOGO))} - alt="List Img" - isListView={false} - /> - )} - {isUndefined(title) ? : {title}} - - {isUndefined(description) ? : {description}} - - - - Registry Address - - {explorerAddress ? ( - - - - ) : null} - - - - - - - - - - - - - - - ); -}; - -export default InformationCard; diff --git a/web/src/components/ItemInformationCard/FieldsDisplay.tsx b/web/src/components/InformationCards/ItemInformationCard/FieldsDisplay.tsx similarity index 97% rename from web/src/components/ItemInformationCard/FieldsDisplay.tsx rename to web/src/components/InformationCards/ItemInformationCard/FieldsDisplay.tsx index 909831d..9ab53d2 100644 --- a/web/src/components/ItemInformationCard/FieldsDisplay.tsx +++ b/web/src/components/InformationCards/ItemInformationCard/FieldsDisplay.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; import { ItemDetailsFragment } from "src/graphql/graphql"; -import ItemField from "../ItemCard/ItemField"; +import ItemField from "components/ItemCard/ItemField"; const FieldsContainer = styled.div` display: flex; diff --git a/web/src/components/ItemInformationCard/index.tsx b/web/src/components/InformationCards/ItemInformationCard/index.tsx similarity index 81% rename from web/src/components/ItemInformationCard/index.tsx rename to web/src/components/InformationCards/ItemInformationCard/index.tsx index 3d1afc3..b2c4961 100644 --- a/web/src/components/ItemInformationCard/index.tsx +++ b/web/src/components/InformationCards/ItemInformationCard/index.tsx @@ -1,17 +1,17 @@ import React from "react"; -import styled, { css } from "styled-components"; -import { responsiveSize } from "styles/responsiveSize"; import { Card, Copiable } from "@kleros/ui-components-library"; +import styled, { css } from "styled-components"; +import Skeleton from "react-loading-skeleton"; import AliasDisplay from "components/RegistryInfo/AliasDisplay"; -import { ItemDetailsFragment } from "src/graphql/graphql"; -import { Policies } from "../InformationCard/Policies"; +import ActionButton from "components/ActionButton"; +import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner"; +import { responsiveSize } from "styles/responsiveSize"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { Policies } from "../RegistryInformationCard/Policies"; import FieldsDisplay from "./FieldsDisplay"; -import StatusDisplay from "./StatusDisplay"; -import Skeleton from "react-loading-skeleton"; -import ActionButton from "../ActionButton"; +import StatusDisplay from "../StatusDisplay"; +import { ItemDetailsQuery } from "src/graphql/graphql"; import { Address } from "viem"; -import { mapFromSubgraphStatus } from "../RegistryCard/StatusBanner"; -import { landscapeStyle } from "styles/landscapeStyle"; const StyledCard = styled(Card)` display: flex; @@ -49,7 +49,12 @@ const TopRightInfo = styled.div` align-items: start; gap: 48px; padding-top: 20px; - flex-shrink: 0; + flex-shrink: 1; + ${landscapeStyle( + () => css` + flex-shrink: 0; + ` + )} `; const StyledLabel = styled.label` @@ -72,11 +77,11 @@ const BottomInfo = styled.div` justify-content: space-between; `; -interface IItemInformationCard extends ItemDetailsFragment { +type ItemDetails = NonNullable; +interface IItemInformationCard extends ItemDetails { className?: string; policyURI: string; registryAddress: Address; - refetch: () => void; } const ItemInformationCard: React.FC = ({ @@ -88,7 +93,7 @@ const ItemInformationCard: React.FC = ({ itemID, props, registryAddress, - refetch = () => {}, + latestRequestSubmissionTime, }) => { return ( <> @@ -99,7 +104,7 @@ const ItemInformationCard: React.FC = ({ Item Id - + @@ -117,7 +122,6 @@ const ItemInformationCard: React.FC = ({ itemId: itemID, registryAddress, isItem: true, - refetch, }} /> diff --git a/web/src/components/InformationCard/Policies.tsx b/web/src/components/InformationCards/RegistryInformationCard/Policies.tsx similarity index 92% rename from web/src/components/InformationCard/Policies.tsx rename to web/src/components/InformationCards/RegistryInformationCard/Policies.tsx index 24f1fd6..ff1c345 100644 --- a/web/src/components/InformationCard/Policies.tsx +++ b/web/src/components/InformationCards/RegistryInformationCard/Policies.tsx @@ -70,7 +70,11 @@ export const Policies: React.FC = ({ policyURI, isItem }) => { {!isItem ? ( <> {parentRegistryDetails ? ( - + Curation Policy diff --git a/web/src/components/InformationCards/RegistryInformationCard/TopInfo.tsx b/web/src/components/InformationCards/RegistryInformationCard/TopInfo.tsx new file mode 100644 index 0000000..2d69e54 --- /dev/null +++ b/web/src/components/InformationCards/RegistryInformationCard/TopInfo.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { Copiable } from "@kleros/ui-components-library"; +import Skeleton from "react-loading-skeleton"; +import { RegistryDetails } from "context/RegistryDetailsContext"; +import styled, { css } from "styled-components"; +import StatusDisplay from "../StatusDisplay"; +import { DEFAULT_CHAIN, SUPPORTED_CHAINS } from "src/consts/chains"; +import { landscapeStyle } from "src/styles/landscapeStyle"; +import { responsiveSize } from "src/styles/responsiveSize"; +import { isUndefined } from "src/utils"; +import { DEFAULT_LIST_LOGO } from "src/consts"; +import { getIpfsUrl } from "utils/getIpfsUrl"; +import EtherscanIcon from "svgs/icons/etherscan.svg"; + +const TopInfoContainer = styled.div` + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding: ${responsiveSize(20, 24)} ${responsiveSize(24, 32)} 12px ${responsiveSize(24, 32)}; + ${landscapeStyle( + () => css` + flex-wrap: nowrap; + ` + )} +`; + +const LogoAndTitle = styled.div` + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +`; + +const TopLeftInfo = styled.div` + display: flex; + flex-direction: column; + gap: ${responsiveSize(8, 16)} 0; +`; + +const TopRightInfo = styled.div` + display: flex; + flex-direction: row; + gap: 32px; + flex-wrap: wrap; + flex-shrink: 1; + align-items: start; + padding-top: 20px; + ${landscapeStyle( + () => css` + flex-shrink: 0; + gap: 0 ${responsiveSize(24, 32, 900)}; + ` + )} +`; + +const StyledEtherscanIcon = styled(EtherscanIcon)` + display: flex; + height: 16px; + width: 16px; +`; + +const StyledLogo = styled.img<{ isListView: boolean }>` + width: ${({ isListView }) => (isListView ? "40px" : "125px")}; + height: ${({ isListView }) => (isListView ? "40px" : "125px")}; + object-fit: contain; + margin-bottom: ${({ isListView }) => (isListView ? "0px" : "8px")}; +`; + +const StyledTitle = styled.h1` + margin: 0; +`; + +const StyledP = styled.p` + color: ${({ theme }) => theme.secondaryText}; + margin: 0; +`; + +const StyledLabel = styled.label` + color: ${({ theme }) => theme.primaryBlue}; +`; + +const SkeletonLogo = styled(Skeleton)` + width: 125px; + height: 125px; + border-radius: 62.5px; + margin-bottom: 8px; +`; + +const SkeletonTitle = styled(Skeleton)` + width: 180px; + height: 30px; +`; + +const SkeletonDescription = styled(Skeleton)` + width: 90%; + height: 21px; +`; + +interface ITopInfo + extends Pick< + RegistryDetails, + "description" | "title" | "logoURI" | "status" | "disputed" | "id" | "latestRequestSubmissionTime" + > { + registryAddress: string; +} + +const TopInfo: React.FC = ({ + id, + title, + description, + logoURI, + status, + disputed, + latestRequestSubmissionTime, + registryAddress, +}) => { + const [imageSrc, setImageSrc] = useState(getIpfsUrl(logoURI ?? "")); + useEffect(() => setImageSrc(getIpfsUrl(logoURI ?? "")), [logoURI]); + return ( + + + + {isUndefined(logoURI) ? ( + + ) : ( + setImageSrc(getIpfsUrl(DEFAULT_LIST_LOGO))} + alt="List Img" + isListView={false} + /> + )} + {isUndefined(title) ? : {title}} + + {isUndefined(description) ? : {description}} + + + + Registry Address + + {id ? ( + + + + ) : null} + + + + ); +}; + +export default TopInfo; diff --git a/web/src/components/InformationCards/RegistryInformationCard/index.tsx b/web/src/components/InformationCards/RegistryInformationCard/index.tsx new file mode 100644 index 0000000..54ff3fe --- /dev/null +++ b/web/src/components/InformationCards/RegistryInformationCard/index.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import styled from "styled-components"; +import { responsiveSize } from "styles/responsiveSize"; +import { Card, Copiable } from "@kleros/ui-components-library"; +import AliasDisplay from "components/RegistryInfo/AliasDisplay"; +import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner"; +import { Policies } from "./Policies"; +import ActionButton from "../../ActionButton"; +import TopInfo from "./TopInfo"; +import { RegistryDetails } from "context/RegistryDetailsContext"; + +const StyledCard = styled(Card)` + display: flex; + width: 100%; + height: auto; + flex-direction: column; + margin-bottom: 64px; +`; + +const Divider = styled.hr` + border: none; + height: 1px; + background-color: ${({ theme }) => theme.stroke}; + margin: ${responsiveSize(20, 28)} ${responsiveSize(24, 32)}; +`; + +const BottomInfo = styled.div` + display: flex; + padding: 0 ${responsiveSize(24, 32)} 12px ${responsiveSize(24, 32)}; + flex-wrap: wrap; + gap: 20px; + justify-content: space-between; +`; + +interface IInformationCard + extends Pick< + RegistryDetails, + | "title" + | "description" + | "logoURI" + | "disputed" + | "registerer" + | "status" + | "policyURI" + | "latestRequestSubmissionTime" + > { + id: string; + itemId: string; + parentRegistryAddress: string; + className?: string; +} + +const RegistryInformationCard: React.FC = ({ + id, + title, + logoURI, + description, + registerer, + status, + disputed, + policyURI, + className, + itemId, + parentRegistryAddress, + latestRequestSubmissionTime, +}) => ( + + + + + + + + + + + +); + +export default RegistryInformationCard; diff --git a/web/src/components/InformationCards/StatusDisplay.tsx b/web/src/components/InformationCards/StatusDisplay.tsx new file mode 100644 index 0000000..f754f44 --- /dev/null +++ b/web/src/components/InformationCards/StatusDisplay.tsx @@ -0,0 +1,80 @@ +import React, { useMemo } from "react"; +import { getStatusColor, getStatusLabel, mapFromSubgraphStatus } from "../RegistryCard/StatusBanner"; +import styled, { css, useTheme } from "styled-components"; +import { Status } from "consts/status"; +import { Status as SubgraphStatus } from "src/graphql/graphql"; +import { useCurateV2ChallengePeriodDuration } from "hooks/contracts/generated"; +import { useCountdown } from "hooks/useCountdown"; +import { secondsToDayHourMinute } from "utils/date"; + +const StatusContainer = styled.div` + display: flex; + gap: 4px 8px; + flex-wrap: wrap; +`; + +const StyledLabel = styled.label<{ frontColor: string; withDot?: boolean }>` + display: flex; + align-items: center; + color: ${({ frontColor }) => frontColor}; + ${({ withDot, frontColor }) => + withDot + ? css` + ::before { + content: ""; + display: inline-block; + height: 8px; + width: 8px; + border-radius: 50%; + margin-right: 8px; + background-color: ${frontColor}; + flex-shrink: 0; + } + ` + : null} +`; +interface IStatusDisplay { + status: SubgraphStatus; + disputed: boolean; + registryAddress: string; + latestRequestSubmissionTime: string; +} +const StatusDisplay: React.FC = ({ + status, + disputed, + registryAddress, + latestRequestSubmissionTime, +}) => { + const theme = useTheme(); + + const processedStatus = mapFromSubgraphStatus(status, disputed); + const [frontColor] = getStatusColor(processedStatus, theme); + + const { data: challengePeriodDuration } = useCurateV2ChallengePeriodDuration({ + //@ts-ignore + address: registryAddress, + enabled: [Status.ClearingPending, Status.RegistrationPending].includes(processedStatus), + }); + + const challengePeriodDeadline = useMemo(() => { + if (!challengePeriodDuration || !latestRequestSubmissionTime) return 0; + + return parseInt(challengePeriodDuration.toString()) + parseInt(latestRequestSubmissionTime, 10); + }, [latestRequestSubmissionTime, challengePeriodDuration]); + + const countdown = useCountdown(challengePeriodDeadline); + + const showCountdown = + countdown && countdown > 0 && [Status.ClearingPending, Status.RegistrationPending].includes(processedStatus); + + return ( + + + {getStatusLabel(processedStatus)} + + {showCountdown ? secondsToDayHourMinute(countdown) : ""} + + ); +}; + +export default StatusDisplay; diff --git a/web/src/components/ItemCard/ItemField/LongTextField.tsx b/web/src/components/ItemCard/ItemField/LongTextField.tsx new file mode 100644 index 0000000..f964ec4 --- /dev/null +++ b/web/src/components/ItemCard/ItemField/LongTextField.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import styled from "styled-components"; +import WithHelpTooltip from "components/WithHelpTooltip"; +import TruncatedText from "components/TruncatedText"; +import { Overlay } from "components/Overlay"; +import { Textarea } from "@kleros/ui-components-library"; +import { useToggle } from "react-use"; +import Modal from "components/Modal"; + +const Container = styled.p` + margin: 0px; +`; + +const StyledModal = styled(Modal)` + padding: 0; +`; + +const TextDisplay = styled(Textarea)` + width: 100%; + height: 80vh; +`; + +const StyledLabel = styled.label` + color: ${({ theme }) => theme.primaryBlue}; + cursor: pointer; +`; + +const LongTextFullDisplay: React.FC<{ text: string; toggleModal: () => void }> = ({ text, toggleModal }) => ( + <> + + + + + +); + +export interface ILongTextField { + value: string; + detailed?: boolean; + label?: string; +} + +const LongTextField: React.FC = ({ value, detailed, label }) => { + const [isModalOpen, toggleModal] = useToggle(false); + return ( + <> + + {detailed ? ( + + +   [Read More] + + ) : ( + + )} + + {isModalOpen && } + + ); +}; + +export default LongTextField; diff --git a/web/src/components/ItemCard/ItemField/index.tsx b/web/src/components/ItemCard/ItemField/index.tsx index 23e6beb..43b35e2 100644 --- a/web/src/components/ItemCard/ItemField/index.tsx +++ b/web/src/components/ItemCard/ItemField/index.tsx @@ -8,6 +8,7 @@ import FileField, { IFileField } from "./FileField"; import BooleanField, { IBooleanField } from "./BooleanField"; import NumberField, { INumberField } from "./NumberField"; import ChainField, { IChainField } from "./ChainField"; +import LongTextField, { ILongTextField } from "./LongTextField"; type ItemDetails = ItemDetailsFragment["props"][number]; @@ -60,6 +61,12 @@ const ItemField: React.FC = ({ detailed, type, ...props }) => { FieldComponent = ; break; } + case "longText": { + const { value, label } = props as ILongTextField; + + FieldComponent = ; + break; + } default: { const { value, label } = props as ITextField; FieldComponent = ; diff --git a/web/src/components/ItemInformationCard/StatusDisplay.tsx b/web/src/components/ItemInformationCard/StatusDisplay.tsx deleted file mode 100644 index bf23bfa..0000000 --- a/web/src/components/ItemInformationCard/StatusDisplay.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { getStatusColor, getStatusLabel, mapFromSubgraphStatus } from "../RegistryCard/StatusBanner"; -import styled from "styled-components"; -import { Status } from "consts/status"; -import { Status as SubgraphStatus } from "src/graphql/graphql"; - -const StatusContainer = styled.div<{ status: Status; isList: boolean }>` - display: flex; - .dot { - ::before { - content: ""; - display: inline-block; - height: 8px; - width: 8px; - border-radius: 50%; - margin-right: 8px; - } - } - ${({ theme, status }) => { - const [frontColor] = getStatusColor(status, theme); - return ` - .front-color { - color: ${frontColor}; - } - .dot { - ::before { - background-color: ${frontColor}; - } - } - `; - }}; -`; -interface IStatusDisplay { - status: SubgraphStatus; - disputed: boolean; -} -const StatusDisplay: React.FC = ({ status, disputed }) => { - return ( - - - - ); -}; - -export default StatusDisplay; diff --git a/web/src/components/Modal.tsx b/web/src/components/Modal.tsx new file mode 100644 index 0000000..2286f84 --- /dev/null +++ b/web/src/components/Modal.tsx @@ -0,0 +1,44 @@ +import React, { useRef } from "react"; +import styled from "styled-components"; +import { Overlay } from "./Overlay"; +import { useClickAway } from "react-use"; + +const StyledModal = styled.div` + display: flex; + position: fixed; + top: 10vh; + left: 50%; + transform: translateX(-50%); + max-height: 80vh; + overflow-y: auto; + + z-index: 10; + flex-direction: column; + align-items: center; + width: 86vw; + max-width: 600px; + border-radius: 3px; + border: 1px solid ${({ theme }) => theme.stroke}; + background-color: ${({ theme }) => theme.whiteBackground}; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06); + padding: 32px 32px 32px 36px; +`; + +const Modal: React.FC<{ children: React.ReactNode; toggleModal: () => void; className?: string }> = ({ + children, + toggleModal, + className, +}) => { + const containerRef = useRef(null); + useClickAway(containerRef, () => toggleModal()); + return ( + <> + + + {children} + + + ); +}; + +export default Modal; diff --git a/web/src/components/RegistryCard/RegistryInfo.tsx b/web/src/components/RegistryCard/RegistryInfo.tsx index f9b7b21..8f8adda 100644 --- a/web/src/components/RegistryCard/RegistryInfo.tsx +++ b/web/src/components/RegistryCard/RegistryInfo.tsx @@ -9,6 +9,7 @@ import { Status } from "consts/status"; import { getIpfsUrl } from "utils/getIpfsUrl"; import StatusBanner from "./StatusBanner"; import { DEFAULT_LIST_LOGO } from "consts/index"; +import TruncatedText from "../TruncatedText"; const Container = styled.div<{ isListView: boolean }>` height: calc(100% - 45px); @@ -63,16 +64,6 @@ const StyledLabel = styled.label` color: ${({ theme }) => theme.secondaryText}; `; -const StyledTitle = styled.h3` - font-weight: 400; - margin: 0px; -`; - -const TruncatedTitle = ({ text, maxLength }) => { - const truncatedText = text?.length <= maxLength ? text : text?.slice(0, maxLength) + "…"; - return {truncatedText}; -}; - const StyledButton = styled(Button)` background-color: transparent; padding: 0; @@ -100,19 +91,22 @@ const SkeletonLogo = styled(Skeleton)<{ isListView: boolean }>` `; interface IListInfo { - title: string; + title?: string; totalItems: number; - logoURI: string; - chainId: number; + logoURI?: string; status: Status; isListView?: boolean; } -const ListInfo: React.FC = ({ title, totalItems, logoURI, chainId, status, isListView = false }) => { +const ListInfo: React.FC = ({ title, totalItems, logoURI, status, isListView = false }) => { const [imageLoaded, setImageLoaded] = useState(false); - const [imageSrc, setImageSrc] = useState(getIpfsUrl(logoURI)); + const [imageSrc, setImageSrc] = useState(getIpfsUrl(logoURI ?? DEFAULT_LIST_LOGO)); + + useEffect(() => { + if (!logoURI) return; + setImageSrc(getIpfsUrl(logoURI)); + }, [logoURI]); - useEffect(() => setImageSrc(getIpfsUrl(logoURI)), [logoURI]); return ( {!imageLoaded ? : null} @@ -124,7 +118,7 @@ const ListInfo: React.FC = ({ title, totalItems, logoURI, chainId, st onError={() => setImageSrc(getIpfsUrl(DEFAULT_LIST_LOGO))} style={{ display: imageLoaded ? "block" : "none" }} /> - + {totalItems} items {isListView && } {isListView && } diff --git a/web/src/components/RegistryCard/index.tsx b/web/src/components/RegistryCard/index.tsx index 9d11bfa..9f83ca2 100644 --- a/web/src/components/RegistryCard/index.tsx +++ b/web/src/components/RegistryCard/index.tsx @@ -6,6 +6,8 @@ import { landscapeStyle } from "styles/landscapeStyle"; import StatusBanner from "./StatusBanner"; import RegistryInfo from "./RegistryInfo"; import { useNavigateAndScrollTop } from "hooks/useNavigateAndScrollTop"; +import { GetRegistriesByIdsQuery } from "src/graphql/graphql"; +import { Status } from "src/consts/status"; const StyledCard = styled(Card)` width: 100%; @@ -24,21 +26,15 @@ const StyledListItem = styled(Card)` )} `; -type List = [number]; +type List = GetRegistriesByIdsQuery["registries"][number]; interface IListCard extends List { overrideIsListView?: boolean; + itemId?: string; + totalItems: number; + status: Status; } -const RegistryCard: React.FC = ({ - id, - itemId, - title, - logoURI, - totalItems, - status, - chainId, - overrideIsListView, -}) => { +const RegistryCard: React.FC = ({ id, itemId, title, logoURI, totalItems, status, overrideIsListView }) => { const { isListView } = useIsListView(); const navigateAndScrollTop = useNavigateAndScrollTop(); @@ -48,15 +44,15 @@ const RegistryCard: React.FC = ({ <> {!isListView || overrideIsListView ? ( navigateAndScrollTop(`/lists/${registryAddressAndItemId}/display/1/desc/all`)}> - - + + ) : ( navigateAndScrollTop(`/lists/${registryAddressAndItemId}/display/desc/all`)} > - + )} diff --git a/web/src/components/Search.tsx b/web/src/components/Search.tsx index a699609..fe24877 100644 --- a/web/src/components/Search.tsx +++ b/web/src/components/Search.tsx @@ -95,6 +95,7 @@ const Search: React.FC<{ isList?: Boolean }> = ({ isList }) => { = ({ text, maxLength, className }) => { + const truncatedText = text?.length <= maxLength ? text : text?.slice(0, maxLength) + "…"; + return {truncatedText}; +}; + +export default TruncatedText; diff --git a/web/src/consts/filters.ts b/web/src/consts/filters.ts index fda45d5..af03b60 100644 --- a/web/src/consts/filters.ts +++ b/web/src/consts/filters.ts @@ -1,8 +1,11 @@ -import { Status } from "src/graphql/graphql"; +import { Item_Filter, Status } from "src/graphql/graphql"; -export const List_filters = { +const keys = ["Pending", "Disputed", "Included", "Removed", "Active"] as const; + +export const List_filters: Record<(typeof keys)[number], Item_Filter> = { Pending: { status_in: [Status.RegistrationRequested, Status.ClearingRequested], disputed: false }, Disputed: { disputed: true }, Included: { status: Status.Registered }, Removed: { status: Status.Absent }, + Active: { status_not_in: [Status.Absent] }, }; diff --git a/web/src/consts/index.ts b/web/src/consts/index.ts index 5346b39..394e3c8 100644 --- a/web/src/consts/index.ts +++ b/web/src/consts/index.ts @@ -21,3 +21,5 @@ export const DEFAULT_LIST_LOGO = "ipfs://QmWfxEmfEWwM6LDgER2Qp2XZpK1MbDtNp7uGqCS export const CURATION_POLICY = `${IPFS_GATEWAY}/ipfs/QmWciZMi8mBJg34FapRHK4Yh7a6UqmxrpcKQ3KRNMXzjfx`; export const COURT_SITE = "https://dev--kleros-v2.netlify.app/#/cases"; + +export const SUPPORTED_FILE_TYPES = ["application/pdf", "text/rtf", "text/markdown", "text/plain"]; diff --git a/web/src/context/RegistryDetailsContext.tsx b/web/src/context/RegistryDetailsContext.tsx index d79e6e5..2880294 100644 --- a/web/src/context/RegistryDetailsContext.tsx +++ b/web/src/context/RegistryDetailsContext.tsx @@ -1,8 +1,8 @@ import React, { createContext, useContext, useState, ReactNode } from "react"; -import { ItemDetailsFragment } from "src/graphql/graphql"; +import { ItemDetailsQuery } from "src/graphql/graphql"; import { RegistryDetailsQuery } from "hooks/queries/useRegistryDetailsQuery"; -type RegistryDetails = RegistryDetailsQuery["registry"] & ItemDetailsFragment; +export type RegistryDetails = RegistryDetailsQuery["registry"] & ItemDetailsQuery["item"]; interface RegistryDetailsContextType { registryDetails: RegistryDetails | null; setRegistryDetails: (details: RegistryDetails | null) => void; diff --git a/web/src/context/SubmitListContext.tsx b/web/src/context/SubmitListContext.tsx index 4c8898f..f745d6d 100644 --- a/web/src/context/SubmitListContext.tsx +++ b/web/src/context/SubmitListContext.tsx @@ -21,7 +21,17 @@ export interface IList { challengePeriodDuration: number; } -export const FieldTypes = ["text", "address", "image", "link", "number", "boolean", "file", "chain"] as const; +export const FieldTypes = [ + "text", + "address", + "image", + "link", + "number", + "boolean", + "file", + "chain", + "longText", +] as const; export type ListField = { id: number; label: string; diff --git a/web/src/hooks/queries/useItemDetailsQuery.ts b/web/src/hooks/queries/useItemDetailsQuery.ts index 87ec55a..c5e2e8a 100644 --- a/web/src/hooks/queries/useItemDetailsQuery.ts +++ b/web/src/hooks/queries/useItemDetailsQuery.ts @@ -17,6 +17,7 @@ const itemDetailsQuery = graphql(` key2 key3 key4 + latestRequestSubmissionTime latestChallenger { id } diff --git a/web/src/hooks/queries/useRegistryDetailsQuery.ts b/web/src/hooks/queries/useRegistryDetailsQuery.ts index 99a64c6..b91e148 100644 --- a/web/src/hooks/queries/useRegistryDetailsQuery.ts +++ b/web/src/hooks/queries/useRegistryDetailsQuery.ts @@ -35,7 +35,7 @@ export const registryFragment = graphql(` numberOfRegistered numberOfDisputed - fieldProps { + fieldProps(orderBy: position) { label description type diff --git a/web/src/hooks/useCountdown.ts b/web/src/hooks/useCountdown.ts new file mode 100644 index 0000000..ef11b81 --- /dev/null +++ b/web/src/hooks/useCountdown.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; + +import { getTimeLeft } from "utils/date"; +import { isUndefined } from "utils/index"; + +export function useCountdown(deadline?: number): number | undefined { + const [counter, setCounter] = useState(); + useEffect(() => { + if (typeof deadline !== "undefined") { + const timeLeft = getTimeLeft(deadline); + setCounter(timeLeft); + } + }, [deadline]); + useEffect(() => { + if (!isUndefined(counter) && counter > 0) { + const timeout = setTimeout(() => setCounter(counter - 1), 1000); + return () => clearTimeout(timeout); + } else return; + }, [counter]); + return counter; +} diff --git a/web/src/pages/AllLists/ItemDisplay/index.tsx b/web/src/pages/AllLists/ItemDisplay/index.tsx index 34a8290..5ad46e6 100644 --- a/web/src/pages/AllLists/ItemDisplay/index.tsx +++ b/web/src/pages/AllLists/ItemDisplay/index.tsx @@ -3,24 +3,19 @@ import { useParams } from "react-router-dom"; import History from "components/HistoryDisplay"; import { useItemDetailsQuery } from "queries/useItemDetailsQuery"; import { useRegistryDetailsQuery } from "queries/useRegistryDetailsQuery"; -import ItemInformationCard from "components/ItemInformationCard"; +import ItemInformationCard from "components/InformationCards/ItemInformationCard"; const ItemDisplay: React.FC = () => { const { itemId } = useParams(); const [, listAddress] = itemId?.split("@"); - const { data: itemDetails, refetch: refetchItemDetails } = useItemDetailsQuery(itemId); - const { data: registryDetails, refetch: refetchRegistryDetails } = useRegistryDetailsQuery(listAddress); + const { data: itemDetails } = useItemDetailsQuery(itemId); + const { data: registryDetails } = useRegistryDetailsQuery(listAddress); - const refetch = () => { - refetchItemDetails(); - refetchRegistryDetails(); - }; return (
diff --git a/web/src/pages/AllLists/RegistriesFetcher.tsx b/web/src/pages/AllLists/RegistriesFetcher.tsx index d4391c6..32d02b9 100644 --- a/web/src/pages/AllLists/RegistriesFetcher.tsx +++ b/web/src/pages/AllLists/RegistriesFetcher.tsx @@ -84,6 +84,8 @@ const RegistriesFetcher: React.FC = () => { return mainCurate.registry.numberOfAbsent; case JSON.stringify(List_filters.Pending): return mainCurate.registry.numberOfPending; + case JSON.stringify(List_filters.Active): + return mainCurate.registry.totalItems - mainCurate.registry.numberOfAbsent; default: return mainCurate.registry.totalItems; } diff --git a/web/src/pages/AllLists/RegistryDetails/List/index.tsx b/web/src/pages/AllLists/RegistryDetails/List/index.tsx index 0327683..a33a4bf 100644 --- a/web/src/pages/AllLists/RegistryDetails/List/index.tsx +++ b/web/src/pages/AllLists/RegistryDetails/List/index.tsx @@ -57,6 +57,8 @@ const List: React.FC = ({ registryAddress }) => { return registry.numberOfAbsent; case JSON.stringify(List_filters.Pending): return registry.numberOfPending; + case JSON.stringify(List_filters.Active): + return registry.totalItems - registry.numberOfAbsent; default: return registry.totalItems; } diff --git a/web/src/pages/AllLists/RegistryDetails/Tabs.tsx b/web/src/pages/AllLists/RegistryDetails/Tabs.tsx index 1020f4a..3b1e2b8 100644 --- a/web/src/pages/AllLists/RegistryDetails/Tabs.tsx +++ b/web/src/pages/AllLists/RegistryDetails/Tabs.tsx @@ -4,6 +4,8 @@ import { useNavigate, useLocation, useParams } from "react-router-dom"; import { Tabs as TabsComponent } from "@kleros/ui-components-library"; import PaperIcon from "assets/svgs/icons/paper.svg"; import HistoryIcon from "assets/svgs/icons/history.svg"; +import { encodeListURIFilter } from "utils/uri"; +import { List_filters } from "consts/filters"; const StyledTabs = styled(TabsComponent)` width: 100%; @@ -22,7 +24,7 @@ const TABS = [ text: "List", value: 0, Icon: PaperIcon, - path: "list/1/desc/all", + path: `list/1/desc/${encodeListURIFilter(List_filters.Active)}`, identifier: "list", }, { diff --git a/web/src/pages/AllLists/RegistryDetails/index.tsx b/web/src/pages/AllLists/RegistryDetails/index.tsx index ae753e1..780a68d 100644 --- a/web/src/pages/AllLists/RegistryDetails/index.tsx +++ b/web/src/pages/AllLists/RegistryDetails/index.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from "react"; import { Navigate, Route, Routes, useParams } from "react-router-dom"; -import InformationCard from "components/InformationCard"; +import RegistryInformationCard from "components/InformationCards/RegistryInformationCard"; import Tabs from "./Tabs"; import List from "./List"; import History from "components/HistoryDisplay"; -import { useRegistryDetailsContext } from "context/RegistryDetailsContext"; +import { RegistryDetails as RegistryDetailsType, useRegistryDetailsContext } from "context/RegistryDetailsContext"; import { useRegistryDetailsQuery } from "queries/useRegistryDetailsQuery"; import { useItemDetailsQuery } from "queries/useItemDetailsQuery"; -import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner"; +import { List_filters } from "consts/filters"; const RegistryDetails: React.FC = () => { const { id } = useParams(); @@ -15,15 +15,8 @@ const RegistryDetails: React.FC = () => { const [listAddress, itemId] = id?.split("-"); const [, registryAddress] = itemId.split("@"); - const { data: itemDetails, refetch: refetchItemDetails } = useItemDetailsQuery(itemId?.toLowerCase()); - const { data: registryDetails, refetch: refetchRegistryDetails } = useRegistryDetailsQuery( - listAddress?.toLowerCase() - ); - - const refetch = () => { - refetchItemDetails(); - refetchRegistryDetails(); - }; + const { data: itemDetails } = useItemDetailsQuery(itemId?.toLowerCase()); + const { data: registryDetails } = useRegistryDetailsQuery(listAddress?.toLowerCase()); const { title, @@ -35,6 +28,7 @@ const RegistryDetails: React.FC = () => { disputed, setRegistryDetails, itemID: registryAsitemId, + latestRequestSubmissionTime, } = useRegistryDetailsContext(); useEffect(() => { @@ -42,33 +36,32 @@ const RegistryDetails: React.FC = () => { setRegistryDetails({ ...registryDetails.registry, ...itemDetails.item, - registerer: registryDetails?.registry?.registerer, - }); + } as RegistryDetailsType); } }, [itemDetails, registryDetails, setRegistryDetails]); return (
- } /> } /> - } /> + } />
); diff --git a/web/src/pages/AllLists/StyledBreadcrumb.tsx b/web/src/pages/AllLists/StyledBreadcrumb.tsx index 0108ad5..89d8095 100644 --- a/web/src/pages/AllLists/StyledBreadcrumb.tsx +++ b/web/src/pages/AllLists/StyledBreadcrumb.tsx @@ -5,6 +5,8 @@ import { useLocation, useNavigate } from "react-router-dom"; import HomeIcon from "svgs/icons/home.svg"; import { useRegistryDetailsQuery } from "hooks/queries/useRegistryDetailsQuery"; import { useLocalStorage } from "hooks/useLocalStorage"; +import { encodeListURIFilter } from "utils/uri"; +import { List_filters } from "consts/filters"; const StyledBreadcrumb = styled(BreadcrumbBase)` margin-bottom: 32px; @@ -45,8 +47,8 @@ const Breadcrumb: React.FC = () => { const breadcrumbItems = useMemo(() => { const baseItems = [ - { text: , value: "/lists/display/1/desc/all" }, - { text: "All Lists", value: "/lists/display/1/desc/all" }, + { text: , value: "/" }, + { text: "All Lists", value: `/lists/display/1/desc/${encodeListURIFilter(List_filters.Active)}` }, ]; switch (page) { case "allLists": diff --git a/web/src/pages/Home/Highlights/index.tsx b/web/src/pages/Home/Highlights/index.tsx index 0eeadae..92d1734 100644 --- a/web/src/pages/Home/Highlights/index.tsx +++ b/web/src/pages/Home/Highlights/index.tsx @@ -1,12 +1,9 @@ import React, { useMemo } from "react"; import styled from "styled-components"; -import { BREAKPOINT_LANDSCAPE } from "styles/landscapeStyle"; -import { useWindowSize } from "react-use"; import { Button } from "@kleros/ui-components-library"; import Header from "./Header"; import RegistryCard from "components/RegistryCard"; -import { SkeletonRegistryCard, SkeletonRegistryListItem } from "components/StyledSkeleton"; -import { useIsListView } from "context/IsListViewProvider"; +import { SkeletonRegistryCard } from "components/StyledSkeleton"; import { DEFAULT_CHAIN } from "consts/chains"; import { isUndefined } from "utils/index"; import { listOfListsAddresses } from "utils/listOfListsAddresses"; @@ -15,6 +12,9 @@ import { useItemsQuery } from "queries/useItemsQuery"; import { useRegistriesByIdsQuery } from "queries/useRegistriesByIdsQuery"; import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner"; import { sortRegistriesByIds } from "utils/sortRegistriesByIds"; +import { Status } from "src/graphql/graphql"; +import { List_filters } from "consts/filters"; +import { encodeListURIFilter } from "utils/uri"; const Container = styled.div` width: 100%; @@ -32,24 +32,16 @@ const GridContainer = styled.div` gap: var(--gap); `; -const ListContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - gap: 8px; -`; - const StyledButton = styled(Button)` margin: 0 auto; `; const HighlightedLists = () => { const navigateAndScrollTop = useNavigateAndScrollTop(); - const { isListView } = useIsListView(); - const { width } = useWindowSize(); - const screenIsBig = useMemo(() => width > BREAKPOINT_LANDSCAPE, [width]); + const { data: itemsData, isLoading: isItemsDataLoading } = useItemsQuery(0, 6, { registry: listOfListsAddresses[DEFAULT_CHAIN], + ...List_filters.Active, }); const registryIds = useMemo( @@ -87,35 +79,24 @@ const HighlightedLists = () => { return (
- {isListView && screenIsBig ? ( - - {registriesLoading - ? [...Array(6)].map((_, i) => ) - : combinedListsData?.map((registry, i) => ( - - ))} - - ) : ( - - {registriesLoading - ? [...Array(6)].map((_, i) => ) - : combinedListsData?.map((registry, i) => ( - - ))} - - )} + + {registriesLoading + ? [...Array(6)].map((_, i) => ) + : combinedListsData?.map((registry, i) => ( + + ))} + navigateAndScrollTop("/lists/display/1/desc/all")} + onClick={() => navigateAndScrollTop(`/lists/display/1/desc/${encodeListURIFilter(List_filters.Active)}`)} text="Show All" variant="secondary" /> diff --git a/web/src/pages/SubmitItem/ItemField/FieldInput/FileInput.tsx b/web/src/pages/SubmitItem/ItemField/FieldInput/FileInput.tsx index 15b087b..424573a 100644 --- a/web/src/pages/SubmitItem/ItemField/FieldInput/FileInput.tsx +++ b/web/src/pages/SubmitItem/ItemField/FieldInput/FileInput.tsx @@ -7,6 +7,7 @@ import styled, { css } from "styled-components"; import { FileUploader } from "@kleros/ui-components-library"; import { responsiveSize } from "styles/responsiveSize"; import { landscapeStyle } from "styles/landscapeStyle"; +import { SUPPORTED_FILE_TYPES } from "src/consts"; const StyledFileUploader = styled(FileUploader)` width: 84vw; @@ -22,7 +23,7 @@ const StyledFileUploader = styled(FileUploader)` `; const FileInput: React.FC = ({ fieldProp, handleWrite }) => { const handleFileUpload = (file: File) => { - if (file?.type !== "application/pdf") { + if (!SUPPORTED_FILE_TYPES.includes(file?.type)) { toast.error("File type not supported", toastOptions); return; } diff --git a/web/src/pages/SubmitItem/ItemField/FieldInput/LongTextInput.tsx b/web/src/pages/SubmitItem/ItemField/FieldInput/LongTextInput.tsx new file mode 100644 index 0000000..6a12691 --- /dev/null +++ b/web/src/pages/SubmitItem/ItemField/FieldInput/LongTextInput.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { IFieldInput } from "."; +import { Textarea } from "@kleros/ui-components-library"; +import styled, { css } from "styled-components"; +import { responsiveSize } from "styles/responsiveSize"; +import { landscapeStyle } from "styles/landscapeStyle"; + +const StyledField = styled(Textarea)` + width: 80vw; + margin-bottom: ${responsiveSize(68, 40)}; + height: fit-content; + textarea { + resize: vertical; + } + + input { + font-size: 16px; + } + + svg { + margin-top: 8px; + } + small { + margin-top: 6px; + } + + ${landscapeStyle( + () => css` + width: ${responsiveSize(200, 720)}; + ` + )}; +`; + +const LongTextInput: React.FC = ({ fieldProp, handleWrite }) => { + const handleChange = (event: React.ChangeEvent) => { + handleWrite(event.target.value); + }; + return ( + + ); +}; + +export default LongTextInput; diff --git a/web/src/pages/SubmitItem/ItemField/FieldInput/index.tsx b/web/src/pages/SubmitItem/ItemField/FieldInput/index.tsx index a851a15..96fdbd1 100644 --- a/web/src/pages/SubmitItem/ItemField/FieldInput/index.tsx +++ b/web/src/pages/SubmitItem/ItemField/FieldInput/index.tsx @@ -1,4 +1,4 @@ -import { RegistryDetailsQuery } from "src/graphql/graphql"; +import { RegistryDetailsFragment } from "src/graphql/graphql"; import AddressInput from "./AddressInput"; import React from "react"; import LinkInput from "./LinkInput"; @@ -8,8 +8,9 @@ import FileInput from "./FileInput"; import ImageInput from "./ImageInput"; import BooleanInput from "./BooleanInput"; import ChainInput from "./ChainInput"; +import LongTextInput from "./LongTextInput"; -type FieldProp = NonNullable["fieldProps"][number] & { value?: string }; +type FieldProp = NonNullable["fieldProps"][number] & { value?: string }; export interface IFieldInput { fieldProp: FieldProp; handleWrite: (value: string) => void; @@ -50,6 +51,10 @@ const FieldInput: React.FC = ({ fieldProp, handleWrite }) => { FieldComponent = ; break; } + case "longText": { + FieldComponent = ; + break; + } default: { FieldComponent = ; break; diff --git a/web/src/pages/SubmitItem/ItemField/index.tsx b/web/src/pages/SubmitItem/ItemField/index.tsx index 88df20a..a1904f9 100644 --- a/web/src/pages/SubmitItem/ItemField/index.tsx +++ b/web/src/pages/SubmitItem/ItemField/index.tsx @@ -7,6 +7,7 @@ import { useRegistryDetailsContext } from "context/RegistryDetailsContext"; import { useParams } from "react-router-dom"; import Skeleton from "react-loading-skeleton"; import FieldInput from "./FieldInput"; +import { isUndefined } from "src/utils"; const Container = styled.div` display: flex; @@ -31,7 +32,8 @@ const ItemField: React.FC = () => { if (!itemField) return; const prevFields = fields ?? {}; - if (!prevFields.values?.[itemField.label]) prevFields.columns.push(itemField); + if (isUndefined(prevFields.values?.[itemField.label])) prevFields.columns.push(itemField); + prevFields.values = { ...fields.values, [itemField.label]: val }; setFields({ ...prevFields }); }; @@ -40,7 +42,7 @@ const ItemField: React.FC = () => { {itemField ? : <StyledSkeleton width={100} height={40} />} {itemField ? ( - <FieldInput fieldProp={{ ...itemField, value }} handleWrite={handleWrite} /> + <FieldInput key={id} fieldProp={{ ...itemField, value }} handleWrite={handleWrite} /> ) : ( <StyledSkeleton width={200} height={100} /> )} diff --git a/web/src/pages/SubmitItem/Preview/ListDisplay.tsx b/web/src/pages/SubmitItem/Preview/ListDisplay.tsx index a8774e6..dfeef33 100644 --- a/web/src/pages/SubmitItem/Preview/ListDisplay.tsx +++ b/web/src/pages/SubmitItem/Preview/ListDisplay.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; -import ItemInformationCard from "components/ItemInformationCard"; +import ItemInformationCard from "components/InformationCards/ItemInformationCard"; import { useSubmitItemContext } from "context/SubmitItemContext"; import { ItemDetailsFragment, Status } from "src/graphql/graphql"; @@ -38,7 +38,6 @@ const ListDisplay: React.FC<IListDisplay> = ({}) => { policyURI="/ipfs/QmSxGYpXHBWBGvGnBeZD1pFxh8fRHj4Z7o3fBzrGiqNx4v/tokens-policy.pdf" status={Status.RegistrationRequested} id="1" - chainId={42161} disputed={false} /> </Container> diff --git a/web/src/pages/SubmitList/ItemParameters/ItemPreview/ItemDisplay.tsx b/web/src/pages/SubmitList/ItemParameters/ItemPreview/ItemDisplay.tsx index e803947..301140d 100644 --- a/web/src/pages/SubmitList/ItemParameters/ItemPreview/ItemDisplay.tsx +++ b/web/src/pages/SubmitList/ItemParameters/ItemPreview/ItemDisplay.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; -import ItemInformationCard from "components/ItemInformationCard"; +import ItemInformationCard from "components/InformationCards/ItemInformationCard"; import { useSubmitListContext } from "context/SubmitListContext"; import { constructItemWithMockValues } from "utils/submitListUtils"; @@ -28,7 +28,7 @@ const ItemDisplay: React.FC<IItemDisplay> = ({}) => { return ( <Container> <StyledP>Check how the item is displayed on the Item page:</StyledP> - <StyledItemInformationCard {...item} policyURI={listMetadata.policyURI} /> + <StyledItemInformationCard {...item} policyURI={listMetadata.policyURI ?? ""} /> </Container> ); }; diff --git a/web/src/pages/SubmitList/ListParameters/ListPreview/ListPageDisplay.tsx b/web/src/pages/SubmitList/ListParameters/ListPreview/ListPageDisplay.tsx index 1427b3d..d8459d8 100644 --- a/web/src/pages/SubmitList/ListParameters/ListPreview/ListPageDisplay.tsx +++ b/web/src/pages/SubmitList/ListParameters/ListPreview/ListPageDisplay.tsx @@ -1,10 +1,9 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; -import InformationCard from "components/InformationCard"; +import RegistryInformationCard from "components/InformationCards/RegistryInformationCard"; import { useSubmitListContext } from "context/SubmitListContext"; import { Status } from "src/graphql/graphql"; -import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner"; const Container = styled.div` display: flex; @@ -17,7 +16,7 @@ const StyledP = styled.p` margin: 0; `; -const StyledInformationCard = styled(InformationCard)` +const StyledInformationCard = styled(RegistryInformationCard)` margin: 0px; `; @@ -29,10 +28,14 @@ const ListPageDisplay: React.FC = () => { <Container> <StyledP>Check how the list is displayed on the List page:</StyledP> <StyledInformationCard + id="" + parentRegistryAddress="" + registerer={{ id: "" }} + itemId="" title={previewData.title} description={previewData.description} - chainId={421614} - status={mapFromSubgraphStatus(Status.Registered, false)} + status={Status.Registered} + disputed={false} logoURI={previewData.logoURI} policyURI="https://cdn.kleros.link/ipfs/QmSxGYpXHBWBGvGnBeZD1pFxh8fRHj4Z7o3fBzrGiqNx4v/tokens-policy.pdf" /> diff --git a/web/src/pages/SubmitList/ListParameters/Policy.tsx b/web/src/pages/SubmitList/ListParameters/Policy.tsx index 5b8839c..72091b5 100644 --- a/web/src/pages/SubmitList/ListParameters/Policy.tsx +++ b/web/src/pages/SubmitList/ListParameters/Policy.tsx @@ -9,6 +9,7 @@ import { useSubmitListContext } from "context/SubmitListContext"; import { toast } from "react-toastify"; import { OPTIONS as toastOptions } from "utils/wrapWithToast"; import { uploadFileToIPFS } from "utils/uploadFileToIPFS"; +import { SUPPORTED_FILE_TYPES } from "src/consts"; const Container = styled.div` display: flex; @@ -40,7 +41,7 @@ const Policy: React.FC = () => { const { listMetadata, setListMetadata, setIsPolicyUploading } = useSubmitListContext(); const handleFileUpload = (file: File) => { - if (file?.type !== "application/pdf") { + if (!SUPPORTED_FILE_TYPES.includes(file?.type)) { toast.error("File type not supported", toastOptions); return; } diff --git a/web/src/utils/submitListUtils.ts b/web/src/utils/submitListUtils.ts index 62ee481..db99267 100644 --- a/web/src/utils/submitListUtils.ts +++ b/web/src/utils/submitListUtils.ts @@ -177,6 +177,8 @@ const getMockValueForType = (type: string) => { switch (type) { case "text": return "Ethereum"; + case "longText": + return "Lorem ipsum dolor sit amet, consectetur amet"; case "address": return "0x922911F4f80a569a4425fa083456239838F7F003"; case "link":