kleros/kleros-v2

View on GitHub
web-devtools/src/app/(main)/ruler/SelectArbitrable.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import styled, { css } from "styled-components";

import { Address, PublicClient } from "viem";
import { usePublicClient } from "wagmi";

import { Copiable, DropdownSelect, Field } from "@kleros/ui-components-library";

import { useRulerContext } from "context/RulerContext";
import { shortenAddress } from "utils/shortenAddress";
import { klerosCoreAddress } from "hooks/contracts/generated";
import { DEFAULT_CHAIN } from "consts/chains";
import { landscapeStyle } from "styles/landscapeStyle";
import { validateAddress } from "utils/validateAddressOrEns";

const Container = styled.div`
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: 16px;
  align-items: center;
  margin: 16px 0;
  padding: 8px 16px;
  border-radius: 3px;
  background-color: ${({ theme }) => theme.klerosUIComponentsWhiteBackground};
`;

const AddressContainer = styled.div`
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
`;

const Arbitrables = styled.div`
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
`;
const StyledLabel = styled.label``;

const SelectContainer = styled.div`
  position: relative;
`;

const StyledField = styled(Field)`
  width: auto;
  ${landscapeStyle(
    () => css`
      min-width: 250px;
    `
  )}
`;

const StyledDropdown = styled(DropdownSelect)`
  position: absolute;
  z-index: 0;
  top: 40px;
  left: 0;
  width: 100%;
  > button {
    display: none;
  }
  > div {
    z-index: 1;
    width: 100%;
    > div {
      width: 100%;
    }
  }
  .simplebar-content {
    > div {
      width: 100%;
    }
  }
`;

const ErrorMessage = styled.div`
  color: red;
  font-size: 14px;
  margin-top: 4px;
`;

const SelectArbitrable: React.FC = () => {
  const { arbitrable, setArbitrable, knownArbitrables } = useRulerContext();
  const publicClient = usePublicClient({ chainId: 1 }) as PublicClient;
  const ref = useRef<HTMLDivElement>(null);
  const [isClient, setIsClient] = useState(false);
  const [inputValue, setInputValue] = useState("");
  const [error, setError] = useState<string | null>(null);

  // hydration workaround, local storage is inevitably going to be different, so knownArbitrables will be different
  // server and client side
  useEffect(() => {
    setIsClient(true);
  }, []);

  const items = useMemo(
    () =>
      !isClient ? [] : knownArbitrables.map((arbitrable) => ({ text: shortenAddress(arbitrable), value: arbitrable })),
    [isClient, knownArbitrables]
  );

  const openDropdown = useCallback(() => {
    if (!ref.current || knownArbitrables.length === 0) return;

    const child = ref.current.firstElementChild?.firstChild as HTMLButtonElement;
    child.click();
  }, [knownArbitrables, ref]);

  const handleInputChange = useCallback(
    async (value: string) => {
      setInputValue(value);
      setError(null);

      if (value) {
        const isValid = await validateAddress(value, publicClient);
        if (isValid) {
          setArbitrable(value as Address);
        } else {
          setError("Invalid address or ENS name");
        }
      } else {
        setArbitrable("" as Address);
      }
    },
    [publicClient, setArbitrable]
  );

  return (
    <Container>
      <AddressContainer>
        <StyledLabel>Ruler Address:</StyledLabel>
        <Copiable copiableContent={klerosCoreAddress[DEFAULT_CHAIN]} info="Ruler Address">
          <StyledLabel>{shortenAddress(klerosCoreAddress[DEFAULT_CHAIN])}</StyledLabel>
        </Copiable>
      </AddressContainer>
      <Arbitrables suppressHydrationWarning={true}>
        <StyledLabel>Arbitrable:</StyledLabel>
        <SelectContainer ref={ref}>
          <StyledDropdown
            defaultValue={arbitrable}
            items={items}
            callback={(val) => handleInputChange(val.toString())}
          />
          <StyledField
            value={inputValue}
            placeholder="Enter Arbitrable"
            onChange={(e) => handleInputChange(e.target.value)}
            onClick={openDropdown}
          />
          {error && <ErrorMessage>{error}</ErrorMessage>}
        </SelectContainer>
      </Arbitrables>
    </Container>
  );
};

export default SelectArbitrable;