From 3e2b100cc93f77512b700afce6b3b21c3f8f3304 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:50:55 +0100 Subject: [PATCH 1/5] feat(web): add parameters field, improve styling, small mapping adjustments --- .../utils/actionTypeValidators.ts | 2 +- .../dataMappings/utils/createResultObject.ts | 22 ++- web/src/pages/DisputeTemplateView.tsx | 154 +++++++++++++++--- 3 files changed, 152 insertions(+), 26 deletions(-) diff --git a/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts b/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts index c40b9fdf8..b95296043 100644 --- a/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts +++ b/kleros-sdk/src/dataMappings/utils/actionTypeValidators.ts @@ -9,7 +9,7 @@ import { } from "./actionTypes"; export const validateSubgraphMapping = (mapping: ActionMapping) => { - if ((mapping as SubgraphMapping).endpoint !== undefined) { + if ((mapping as SubgraphMapping).endpoint === undefined) { throw new Error("Invalid mapping for graphql action."); } return mapping as SubgraphMapping; diff --git a/kleros-sdk/src/dataMappings/utils/createResultObject.ts b/kleros-sdk/src/dataMappings/utils/createResultObject.ts index cc910c063..184965e4f 100644 --- a/kleros-sdk/src/dataMappings/utils/createResultObject.ts +++ b/kleros-sdk/src/dataMappings/utils/createResultObject.ts @@ -1,12 +1,26 @@ // Can this be replaced by Mustache ? export const createResultObject = (sourceData, seek, populate) => { const result = {}; + seek.forEach((key, idx) => { - let foundValue; - if (typeof sourceData !== "object" || key === "0") { - foundValue = sourceData; + let foundValue = sourceData; + + if (key.includes(".")) { + const keyParts = key.split("."); + for (const part of keyParts) { + if (foundValue[part] !== undefined) { + foundValue = foundValue[part]; + } else { + foundValue = undefined; + break; + } + } } else { - foundValue = sourceData[key]; + if (typeof sourceData !== "object" || key === "0") { + foundValue = sourceData; + } else { + foundValue = sourceData[key]; + } } console.log(`Seek key: ${key}, Found value:`, foundValue); diff --git a/web/src/pages/DisputeTemplateView.tsx b/web/src/pages/DisputeTemplateView.tsx index 171f156e2..164d9107d 100644 --- a/web/src/pages/DisputeTemplateView.tsx +++ b/web/src/pages/DisputeTemplateView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import styled from "styled-components"; -import { Textarea } from "@kleros/ui-components-library"; +import { Field, Textarea } from "@kleros/ui-components-library"; import PolicyIcon from "svgs/icons/policy.svg"; import ReactMarkdown from "components/ReactMarkdown"; import { INVALID_DISPUTE_DATA_ERROR, IPFS_GATEWAY } from "consts/index"; @@ -69,7 +69,7 @@ const LinkContainer = styled.div` justify-content: space-between; `; -const Wrapper = styled.div` +const LongTextSections = styled.div` min-height: calc(100vh - 144px); margin: 24px; display: flex; @@ -77,24 +77,61 @@ const Wrapper = styled.div` `; const StyledTextArea = styled(Textarea)` - width: 50%; + width: 30vw; height: calc(100vh - 300px); `; +const StyledForm = styled.form` + display: flex; + flex-direction: column; + justify-content: center; + margin-top: 24px; + margin-left: 24px; +`; + +const StyledRow = styled.div` + display: flex; + flex-direction: row; + gap: 24px; +`; + +const StyledP = styled.p` + font-style: italic; +`; + +const StyledHeader = styled.h1` + margin-left: 24px; + margin-top 24px; +`; + +const LongText = styled.div` + display: flex; + flex-direction: column; + width: auto; +`; + const DisputeTemplateView: React.FC = () => { const [disputeDetails, setDisputeDetails] = useState(undefined); const [disputeTemplateInput, setDisputeTemplateInput] = useState(""); const [dataMappingsInput, setDataMappingsInput] = useState(""); - // TODO: add some input fields for the IArbitrableV2.DisputeRequest event which is available to the SDK in a real case - // - arbitrable (= the address which emitted DisputeRequest) - // - the DisputeRequest event params: arbitrator, arbitrableDisputeID, externalDisputeID, templateId, templateUri - const arbitrable = "0xdaf749DABE7be6C6894950AE69af35c20a00ABd9"; + const [arbitrator, setArbitrator] = useState(""); + const [arbitrable, setArbitrable] = useState(""); + const [arbitrableDisputeID, setArbitrableDisputeID] = useState(""); + const [externalDisputeID, setExternalDisputeID] = useState(""); + const [templateID, setTemplateID] = useState(""); + const [templateUri, setTemplateUri] = useState(""); useEffect(() => { configureSDK({ apiKey: alchemyApiKey }); + const initialContext = { + arbitrator: arbitrator, arbitrable: arbitrable, + arbitrableDisputeID: parseInt(arbitrableDisputeID), + externalDisputeID: parseInt(externalDisputeID), + templateID: parseInt(templateID), + templateUri: templateUri, }; if (!disputeTemplateInput || !dataMappingsInput) return; @@ -113,22 +150,97 @@ const DisputeTemplateView: React.FC = () => { }; fetchData(); - }, [disputeTemplateInput, dataMappingsInput, arbitrable]); + }, [ + disputeTemplateInput, + dataMappingsInput, + arbitrable, + arbitrator, + arbitrableDisputeID, + externalDisputeID, + templateID, + templateUri, + ]); return ( - - setDisputeTemplateInput(e.target.value)} - placeholder="Enter dispute template" - /> - setDataMappingsInput(e.target.value)} - placeholder="Enter data mappings" - /> - - + <> + Dispute Preview + + Dispute Request event parameters + +

Arbitrator

+ setArbitrator(e.target.value)} + placeholder="Enter arbitrator address" + /> +
+ +

Arbitrable

+ setArbitrable(e.target.value)} + placeholder="Enter arbitrable address" + /> +
+ +

ArbitrableDisputeID

+ setArbitrableDisputeID(e.target.value)} + placeholder="Enter arbitrableDisputeID" + /> +
+ +

ExternalDisputeID

+ setExternalDisputeID(e.target.value)} + placeholder="Enter externalDisputeID" + /> +
+ +

TemplateID

+ setTemplateID(e.target.value)} + placeholder="Enter templateID" + /> +
+ +

TemplateUri

+ setTemplateUri(e.target.value)} + placeholder="Enter templateUri" + /> +
+
+ + +

Template

+ setDisputeTemplateInput(e.target.value)} + placeholder="Enter dispute template" + /> +
+ +

Data Mapping

+ setDataMappingsInput(e.target.value)} + placeholder="Enter data mappings" + /> +
+ +
+ ); }; From 7c9c465aac804e95acfc1b70335e26e782f60308 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Thu, 1 Feb 2024 13:49:01 +0100 Subject: [PATCH 2/5] feat(web): add debounce, skeleton while loading --- web/src/pages/DisputeTemplateView.tsx | 101 ++++++++++++++++++-------- 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/web/src/pages/DisputeTemplateView.tsx b/web/src/pages/DisputeTemplateView.tsx index 164d9107d..aa2c0147a 100644 --- a/web/src/pages/DisputeTemplateView.tsx +++ b/web/src/pages/DisputeTemplateView.tsx @@ -9,6 +9,8 @@ import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActio import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate"; import { Answer, DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; import { alchemyApiKey } from "context/Web3Provider"; +import { useDebounce } from "react-use"; +import Skeleton from "react-loading-skeleton"; const Container = styled.div` width: 50%; @@ -110,11 +112,10 @@ const LongText = styled.div` width: auto; `; -const DisputeTemplateView: React.FC = () => { +const DisputeTemplateView = () => { const [disputeDetails, setDisputeDetails] = useState(undefined); const [disputeTemplateInput, setDisputeTemplateInput] = useState(""); const [dataMappingsInput, setDataMappingsInput] = useState(""); - const [arbitrator, setArbitrator] = useState(""); const [arbitrable, setArbitrable] = useState(""); const [arbitrableDisputeID, setArbitrableDisputeID] = useState(""); @@ -122,45 +123,85 @@ const DisputeTemplateView: React.FC = () => { const [templateID, setTemplateID] = useState(""); const [templateUri, setTemplateUri] = useState(""); + const [debouncedArbitrator, setDebouncedArbitrator] = useState(arbitrator); + const [debouncedArbitrable, setDebouncedArbitrable] = useState(arbitrable); + const [debouncedArbitrableDisputeID, setDebouncedArbitrableDisputeID] = useState(arbitrableDisputeID); + const [debouncedExternalDisputeID, setDebouncedExternalDisputeID] = useState(externalDisputeID); + const [debouncedTemplateID, setDebouncedTemplateID] = useState(templateID); + const [debouncedTemplateUri, setDebouncedTemplateUri] = useState(templateUri); + const [loading, setLoading] = useState(false); + + useDebounce(() => setDebouncedArbitrator(arbitrator), 350, [arbitrator]); + useDebounce(() => setDebouncedArbitrable(arbitrable), 350, [arbitrable]); + useDebounce(() => setDebouncedArbitrableDisputeID(arbitrableDisputeID), 350, [arbitrableDisputeID]); + useDebounce(() => setDebouncedExternalDisputeID(externalDisputeID), 350, [externalDisputeID]); + useDebounce(() => setDebouncedTemplateID(templateID), 350, [templateID]); + useDebounce(() => setDebouncedTemplateUri(templateUri), 350, [templateUri]); + useEffect(() => { configureSDK({ apiKey: alchemyApiKey }); - const initialContext = { - arbitrator: arbitrator, - arbitrable: arbitrable, - arbitrableDisputeID: parseInt(arbitrableDisputeID), - externalDisputeID: parseInt(externalDisputeID), - templateID: parseInt(templateID), - templateUri: templateUri, - }; + let isFetchDataScheduled = false; + + const scheduleFetchData = () => { + if (!isFetchDataScheduled) { + isFetchDataScheduled = true; - if (!disputeTemplateInput || !dataMappingsInput) return; + setLoading(true); - const fetchData = async () => { - try { - const parsedMappings = JSON.parse(dataMappingsInput); - const data = await executeActions(parsedMappings, initialContext); - const finalDisputeDetails = populateTemplate(disputeTemplateInput, data); - setDisputeDetails(finalDisputeDetails); - console.log("finalTemplate: ", finalDisputeDetails); - } catch (e) { - console.error(e); - setDisputeDetails(undefined); + setTimeout(() => { + const initialContext = { + arbitrator: debouncedArbitrator, + arbitrable: debouncedArbitrable, + arbitrableDisputeID: debouncedArbitrableDisputeID, + externalDisputeID: debouncedExternalDisputeID, + templateID: debouncedTemplateID, + templateUri: debouncedTemplateUri, + }; + + const fetchData = async () => { + try { + const parsedMappings = JSON.parse(dataMappingsInput); + const data = await executeActions(parsedMappings, initialContext); + const finalDisputeDetails = populateTemplate(disputeTemplateInput, data); + setDisputeDetails(finalDisputeDetails); + } catch (e) { + console.error(e); + setDisputeDetails(undefined); + } finally { + setLoading(false); + } + }; + + fetchData(); + + isFetchDataScheduled = false; + }, 350); } }; - fetchData(); + if ( + disputeTemplateInput || + dataMappingsInput || + debouncedArbitrator || + debouncedArbitrable || + debouncedArbitrableDisputeID || + debouncedExternalDisputeID || + debouncedTemplateID || + debouncedTemplateUri + ) { + scheduleFetchData(); + } }, [ disputeTemplateInput, dataMappingsInput, - arbitrable, - arbitrator, - arbitrableDisputeID, - externalDisputeID, - templateID, - templateUri, + debouncedArbitrator, + debouncedArbitrable, + debouncedArbitrableDisputeID, + debouncedExternalDisputeID, + debouncedTemplateID, + debouncedTemplateUri, ]); - return ( <> Dispute Preview @@ -238,7 +279,7 @@ const DisputeTemplateView: React.FC = () => { placeholder="Enter data mappings" /> - + {loading ? : } ); From 80cf7a79ea278358de4bf022ef4d8b7c895a9508 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 1 Feb 2024 23:12:46 +0000 Subject: [PATCH 3/5] chore: escrow template and mappings example --- .../example4/DisputeMappings.json.mustache | 40 +++++++++++++++++++ .../example4/DisputeTemplate.json.mustache | 40 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeMappings.json.mustache create mode 100644 kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeTemplate.json.mustache diff --git a/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeMappings.json.mustache b/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeMappings.json.mustache new file mode 100644 index 000000000..6c7832dcf --- /dev/null +++ b/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeMappings.json.mustache @@ -0,0 +1,40 @@ +[ + { + "type": "graphql", + "endpoint": "https://api.thegraph.com/subgraphs/name/kemuru/escrow-v2-devnet", + "query": "query GetTransaction($transactionId: ID!) { escrow(id: $transactionId) { transactionUri buyer seller amount asset deadline } }", + "variables": { + "transactionId": "{{externalDisputeID}}" + }, + "seek": [ + "escrow.transactionUri", + "escrow.buyer", + "escrow.seller", + "escrow.amount", + "escrow.asset", + "escrow.deadline" + ], + "populate": [ + "transactionUri", + "address", + "sendingRecipientAddress", + "amount", + "asset", + "deadline" + ] + }, + { + "type": "fetch/ipfs/json", + "ipfsUri": "{{transactionUri}}", + "seek": [ + "title", + "description", + "extraDescriptionUri" + ], + "populate": [ + "escrowTitle", + "deliverableText", + "extraDescriptionUri" + ] + } +] \ No newline at end of file diff --git a/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeTemplate.json.mustache b/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeTemplate.json.mustache new file mode 100644 index 000000000..b305e190d --- /dev/null +++ b/kleros-sdk/config/v2-disputetemplate/reality/example4/DisputeTemplate.json.mustache @@ -0,0 +1,40 @@ +{ + "title": "{{escrowTitle}}", + "description": "{{deliverableText}}", + "question": "Which party abided by the terms of the contract?", + "answers": [ + { + "title": "Refund the Buyer", + "description": "Select this to return the funds to the Buyer." + }, + { + "title": "Pay the Seller", + "description": "Select this to release the funds to the Seller." + } + ], + "policyURI": "ipfs://TODO", + "attachment": { + "label": "Transaction Terms", + "uri": "{{extraDescriptionUri}}" + }, + "frontendUrl": "https://escrow-v2.kleros.builders/#/myTransactions/{{externalDisputeID}}", + "arbitrableChainID": "421614", + "arbitrableAddress": "{{arbitrator}}", + "arbitratorChainID": "421614", + "arbitratorAddress": "{{arbitrable}}", + "metadata": { + "buyer": "{{address}}", + "seller": "{{sendingRecipientAddress}}", + "amount": "{{sendingQuantity}}", + "asset": "{{asset}}", + "deadline": "{{deadline}}", + "transactionUri": "{{transactionUri}}" + }, + "category": "Escrow", + "specification": "KIPXXX", + "aliases": { + "Buyer": "{{address}}", + "Seller": "{{sendingRecipientAddress}}" + }, + "version": "1.0" +} \ No newline at end of file From dafa3001a441e38a1571e2bb63d25cad0a4ebb73 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 2 Feb 2024 00:36:33 +0000 Subject: [PATCH 4/5] feat: styling and hidden link --- web/src/hooks/useContractAddress.tsx | 15 ++++- web/src/layout/Header/TestnetBanner.tsx | 4 +- web/src/layout/Header/navbar/Explore.tsx | 11 ++++ web/src/pages/DisputeTemplateView.tsx | 71 ++++++++++-------------- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/web/src/hooks/useContractAddress.tsx b/web/src/hooks/useContractAddress.tsx index 474ce3e78..ad0cb8ecf 100644 --- a/web/src/hooks/useContractAddress.tsx +++ b/web/src/hooks/useContractAddress.tsx @@ -1,7 +1,16 @@ import { Abi, PublicClient } from "viem"; import { usePublicClient } from "wagmi"; import { GetContractArgs, GetContractResult } from "wagmi/actions"; -import { getPinakionV2, pinakionV2ABI, getWeth, getPnkFaucet, wethABI, pnkFaucetABI } from "./contracts/generated"; +import { + getPinakionV2, + pinakionV2ABI, + getWeth, + getPnkFaucet, + wethABI, + pnkFaucetABI, + klerosCoreABI, + getKlerosCore, +} from "./contracts/generated"; type Config = Omit, "abi" | "address"> & { chainId?: any; @@ -25,3 +34,7 @@ export const useWETHAddress = () => { export const usePNKFaucetAddress = () => { return useContractAddress(getPnkFaucet)?.address; }; + +export const useKlerosCoreAddress = () => { + return useContractAddress(getKlerosCore)?.address; +}; diff --git a/web/src/layout/Header/TestnetBanner.tsx b/web/src/layout/Header/TestnetBanner.tsx index e0ed68e3a..3e9edc4dc 100644 --- a/web/src/layout/Header/TestnetBanner.tsx +++ b/web/src/layout/Header/TestnetBanner.tsx @@ -9,11 +9,11 @@ const Container = styled.div` background-color: ${({ theme }) => theme.tintPurple}; color: ${({ theme }) => theme.primaryText}; - padding: 6px 2px; + padding: 5px 2px; ${landscapeStyle( () => css` - padding: 8px 10px; + padding: 5px 10px; ` )} `; diff --git a/web/src/layout/Header/navbar/Explore.tsx b/web/src/layout/Header/navbar/Explore.tsx index dccdf73a1..6cfb9a4d8 100644 --- a/web/src/layout/Header/navbar/Explore.tsx +++ b/web/src/layout/Header/navbar/Explore.tsx @@ -48,6 +48,10 @@ const StyledLink = styled(Link)<{ isActive: boolean }>` )}; `; +const HiddenLink = styled(StyledLink)<{ isActive: boolean }>` + color: ${({ theme }) => theme.primaryPurple}; +`; + const links = [ { to: "/", text: "Home" }, { to: "/cases/display/1/desc/all", text: "Cases" }, @@ -73,6 +77,13 @@ const Explore: React.FC = () => { ))} + + X + ); }; diff --git a/web/src/pages/DisputeTemplateView.tsx b/web/src/pages/DisputeTemplateView.tsx index aa2c0147a..b6d945dc3 100644 --- a/web/src/pages/DisputeTemplateView.tsx +++ b/web/src/pages/DisputeTemplateView.tsx @@ -8,12 +8,12 @@ import { configureSDK } from "@kleros/kleros-sdk/src/sdk"; import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActions"; import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate"; import { Answer, DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { useKlerosCoreAddress } from "hooks/useContractAddress"; import { alchemyApiKey } from "context/Web3Provider"; import { useDebounce } from "react-use"; import Skeleton from "react-loading-skeleton"; const Container = styled.div` - width: 50%; height: auto; display: flex; flex-direction: column; @@ -81,6 +81,7 @@ const LongTextSections = styled.div` const StyledTextArea = styled(Textarea)` width: 30vw; height: calc(100vh - 300px); + font-family: "Roboto Mono", monospace; `; const StyledForm = styled.form` @@ -98,11 +99,10 @@ const StyledRow = styled.div` `; const StyledP = styled.p` - font-style: italic; + font-family: "Roboto Mono", monospace; `; -const StyledHeader = styled.h1` - margin-left: 24px; +const StyledHeader = styled.h2` margin-top 24px; `; @@ -113,14 +113,15 @@ const LongText = styled.div` `; const DisputeTemplateView = () => { + const klerosCoreAddress = useKlerosCoreAddress(); const [disputeDetails, setDisputeDetails] = useState(undefined); const [disputeTemplateInput, setDisputeTemplateInput] = useState(""); const [dataMappingsInput, setDataMappingsInput] = useState(""); - const [arbitrator, setArbitrator] = useState(""); - const [arbitrable, setArbitrable] = useState(""); - const [arbitrableDisputeID, setArbitrableDisputeID] = useState(""); - const [externalDisputeID, setExternalDisputeID] = useState(""); - const [templateID, setTemplateID] = useState(""); + const [arbitrator, setArbitrator] = useState(klerosCoreAddress as string); + const [arbitrable, setArbitrable] = useState("0x10f7A6f42Af606553883415bc8862643A6e63fdA"); // Escrow devnet + const [arbitrableDisputeID, setArbitrableDisputeID] = useState("0"); + const [externalDisputeID, setExternalDisputeID] = useState("0"); + const [templateID, setTemplateID] = useState("0"); const [templateUri, setTemplateUri] = useState(""); const [debouncedArbitrator, setDebouncedArbitrator] = useState(arbitrator); @@ -204,67 +205,51 @@ const DisputeTemplateView = () => { ]); return ( <> - Dispute Preview - Dispute Request event parameters + Dispute Request event parameters -

Arbitrator

- setArbitrator(e.target.value)} - placeholder="Enter arbitrator address" - /> + {"{{ arbitrator }}"} + setArbitrator(e.target.value)} placeholder="0x..." />
-

Arbitrable

- setArbitrable(e.target.value)} - placeholder="Enter arbitrable address" - /> + {"{{ arbitrable }}"} + setArbitrable(e.target.value)} placeholder="0x..." />
-

ArbitrableDisputeID

+ {"{{ arbitrableDisputeID }}"} setArbitrableDisputeID(e.target.value)} - placeholder="Enter arbitrableDisputeID" + placeholder="0" />
-

ExternalDisputeID

+ {"{{ externalDisputeID }}"} setExternalDisputeID(e.target.value)} - placeholder="Enter externalDisputeID" + placeholder="0" />
-

TemplateID

- setTemplateID(e.target.value)} - placeholder="Enter templateID" - /> + {"{{ templateID }}"} + setTemplateID(e.target.value)} placeholder="0" />
-

TemplateUri

+ {"{{ templateUri }}"} setTemplateUri(e.target.value)} - placeholder="Enter templateUri" + placeholder="ipfs://... (optional)" />
-

Template

+ Template setDisputeTemplateInput(e.target.value)} @@ -272,14 +257,18 @@ const DisputeTemplateView = () => { />
-

Data Mapping

+ Data Mapping setDataMappingsInput(e.target.value)} placeholder="Enter data mappings" />
- {loading ? : } + + Dispute Preview +
+ {loading ? : } +
); From b41e91b5f68fd901e9bc9e3c19ad12945949aff1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 2 Feb 2024 00:52:31 +0000 Subject: [PATCH 5/5] feat: better hidden link --- web/src/layout/Header/navbar/Explore.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/web/src/layout/Header/navbar/Explore.tsx b/web/src/layout/Header/navbar/Explore.tsx index 6cfb9a4d8..812862c45 100644 --- a/web/src/layout/Header/navbar/Explore.tsx +++ b/web/src/layout/Header/navbar/Explore.tsx @@ -49,7 +49,7 @@ const StyledLink = styled(Link)<{ isActive: boolean }>` `; const HiddenLink = styled(StyledLink)<{ isActive: boolean }>` - color: ${({ theme }) => theme.primaryPurple}; + color: ${({ isActive, theme }) => (isActive ? theme.primaryText : theme.primaryPurple)}; `; const links = [ @@ -77,13 +77,15 @@ const Explore: React.FC = () => { ))} - - X - + + + Dev + + ); };