react-icons/fa#FaEye TypeScript Examples

The following examples show how to use react-icons/fa#FaEye. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: AnnotationList.tsx    From slim with Apache License 2.0 6 votes vote down vote up
render (): React.ReactNode {
    const items = this.props.rois.map((roi, index) => (
      <AnnotationItem
        key={roi.uid}
        roi={roi}
        index={index}
        isVisible={this.props.visibleRoiUIDs.includes(roi.uid)}
        onVisibilityChange={this.props.onVisibilityChange}
      />
    ))

    return (
      <>
        <div style={{ paddingLeft: '14px', paddingTop: '7px', paddingBottom: '7px' }}>
          <Switch
            size='small'
            onChange={this.handleVisibilityChange}
            checked={this.props.visibleRoiUIDs.length > 0}
            checkedChildren={<FaEye />}
            unCheckedChildren={<FaEyeSlash />}
          />
        </div>
        <Menu
          selectedKeys={this.props.selectedRoiUIDs}
          onSelect={this.handleMenuItemSelection}
          onClick={this.handleMenuItemSelection}
        >
          {items}
        </Menu>
      </>
    )
  }
Example #2
Source File: Icons.tsx    From crosshare with GNU Affero General Public License v3.0 6 votes vote down vote up
CheckOrReveal = ({
  x,
  y,
  reveal,
}: {
  x: number;
  y: number;
  reveal: boolean;
}) => {
  if (reveal) {
    return (
      <FaEye x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
    );
  }
  return (
    <FaCheck x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
  );
}
Example #3
Source File: VideoTeaser.tsx    From 3Speak-app with GNU General Public License v3.0 5 votes vote down vote up
export function VideoTeaser(props: any) {
  const [video_info, setVideoInfo] = useState<any>({})
  const [thumbnail, setThumbnail] = useState('')
  const reflink = useMemo(() => {
    return Reflink.parse(props.reflink)
  }, [])

  useEffect(() => {
    const load = async () => {
      setVideoInfo(await AccountService.permalinkToVideoInfo(props.reflink))
      setThumbnail(await VideoService.getThumbnailURL(props.reflink))
    }

    void load()
  }, [])

  return (
    <div className="video-card-list">
      <div className="teaser_holder video-card-image">
        <div className="card-label">
          {(() => {
            const pattern = DateTime.compile('mm:ss')
            return DateTime.format(new Date(video_info.meta.duration * 1000), pattern)
          })()}
        </div>
        <a href={`#/watch/${props.reflink}`}>
          <img className="img-fluid bg-dark" src={thumbnail} alt="" />
        </a>
      </div>
      <span className="video-card-body">
        <div className="video-title">
          <a
            href={`#/watch/${props.reflink}`}
            style={{ textOverflow: 'ellipsis', overflow: 'nowrap' }}
          >
            {video_info.title}
          </a>
        </div>
        <div className="video-page">
          <a href={`#/user/${reflink.source.value}:${reflink.root}`}>{reflink.root}</a>
        </div>
        <div className="video-view">
          <FaEye /> Unknown views
          <span>
            <FaCalendarAlt />
            {(() => {
              if (video_info.creation) {
                const dateBest = convert(
                  (new Date(new Date().toUTCString()) as any) / 1 -
                    Date.parse(video_info.creation) / 1,
                )
                  .from('ms')
                  .toBest()
                if (Math.round(dateBest.val) >= 2) {
                  return `${Math.round(dateBest.val)} ${dateBest.plural} ago`
                } else {
                  return `${Math.round(dateBest.val)} ${dateBest.singular} ago`
                }
              }
            })()}
          </span>
        </div>
      </span>
    </div>
  )
}
Example #4
Source File: IPMenu.tsx    From iplocate with MIT License 5 votes vote down vote up
IPMenu: React.FC<Props> = (props) => {
  const { ips, onSetCurrentIp, onToggleIpVisible, onRemoveIp } = props;

  if (ips.length === 0) return null;

  return (
    <Wrapper>
      {ips.map((ip) => (
        <Row key={ip.traits.ipAddress}>
          <RowText onClick={() => onSetCurrentIp(ip)}>
            {ip.traits.ipAddress}
          </RowText>
          <div>
            <RowAction
              onClick={() => onRemoveIp(ip)}
              aria-label="remove ip address"
            >
              <FaTrash />
            </RowAction>
            {ip.hidden ? (
              <RowAction
                onClick={() => onToggleIpVisible(ip)}
                aria-label="toggle ip visibility on"
              >
                <FaEyeSlash />
              </RowAction>
            ) : (
              <RowAction
                onClick={() => onToggleIpVisible(ip)}
                aria-label="toggle ip visibility off"
              >
                <FaEye />
              </RowAction>
            )}
          </div>
        </Row>
      ))}
    </Wrapper>
  );
}
Example #5
Source File: notebook-post.tsx    From portfolio with MIT License 4 votes vote down vote up
NotebookPost: React.SFC<PostProps> = () => {
  const textColor = useColorModeValue("gray.500", "gray.200");
  const post = articles[4];

  return (
    <>
      <VStack mt={0} mb={6} spacing={1} align="start">
        <Heading as="h1" fontSize="3xl" lineHeight="shorter" fontWeight="bold">
          {post.title}
        </Heading>
        <Divider
          orientation="horizontal"
          opacity={1}
          borderBottomWidth={0}
          height={"1px"}
          bg={"gray.200"}
        />
      </VStack>
      <Flex
        justifyContent={"space-between"}
        flexDirection={["column", "row", "row"]}
      >
        <HStack spacing={2} isInline>
          <Text fontSize="sm" fontWeight="400" color={textColor}>
            {post.published}
          </Text>
          <Text fontSize="sm" fontWeight="400" color={textColor}>
            •
          </Text>
          <Tooltip hasArrow label="Views" placement="top">
            <Flex alignItems="center">
              <Text
                fontSize="sm"
                noOfLines={1}
                fontWeight="400"
                align="left"
                color={textColor}
              >
                {post.views}
              </Text>
              <Icon as={FaEye} ml={1} color={textColor} />
            </Flex>
          </Tooltip>

          <Text fontSize="sm" fontWeight="600" color={textColor}>
            •
          </Text>
          <Tooltip hasArrow label="Read time" placement="top">
            <Text
              fontSize="sm"
              noOfLines={1}
              fontWeight="400"
              align="left"
              color={textColor}
            >
              {post.readTime}
            </Text>
          </Tooltip>
        </HStack>
        <HStack spacing={1} alignItems="center">
          {post.tags.map(tag => (
            <Tag
              size="sm"
              padding="0 3px"
              key={tag}
              colorScheme={getTagColor(tag)}
            >
              {tag}
            </Tag>
          ))}
        </HStack>
      </Flex>
      <HStack align="end" mt={5}>
        <Link href={post.live} isExternal>
          <Button
            ml={2}
            variant="outline"
            size={["sm"]}
            color={useColorModeValue("green.600", "green.200")}
            bg={useColorModeValue("white", "gray.800")}
            leftIcon={<BiLinkExternal size={18} />}
          >
            Demo
          </Button>
        </Link>
        <Link href={post.github_url} isExternal>
          <Button
            ml={2}
            variant="outline"
            size={["sm"]}
            color={useColorModeValue("green.600", "green.200")}
            bg={useColorModeValue("white", "gray.800")}
            leftIcon={<FiGithub size={18} />}
          >
            Github link
          </Button>
        </Link>
      </HStack>

      <Box height={["35vh", "45vh", "55vh", "70vh"]} marginTop={5}>
        <Carousel images={post.images} />
      </Box>
      <VStack spacing={5} align={"start"} mt={6}>
        <Header fontSize={"xl"} mt={0} mb={0}>
          What will you learn?
        </Header>
        <Box fontSize="md">
          <UnorderedList textAlign="left" paddingLeft={5} m={0}>
            <ListItem>How to create a CRUD app with react</ListItem>
            <ListItem>How to create a responsive app using ChakraUi</ListItem>
            <ListItem>How to use animations with framer-motion</ListItem>
            <ListItem>How to create slider with framer-motion</ListItem>
          </UnorderedList>
        </Box>
      </VStack>
      <VStack spacing={5} align={"start"} mt={6}>
        <Header fontSize={"xl"} mt={0} mb={0}>
          Built with
        </Header>
        <Box fontSize="md">
          <UnorderedList textAlign="left" paddingLeft={5} m={0}>
            <ListItem>
              Programming language -{" "}
              <Link
                href="https://www.typescriptlang.org/"
                isExternal
                color={"blue.500"}
              >
                Typescript
              </Link>
            </ListItem>
            <ListItem>
              Front-end library -{" "}
              <Link
                href="https://github.com/facebook/react/"
                isExternal
                color={"blue.500"}
              >
                React
              </Link>
            </ListItem>
            <ListItem>
              UI components -{" "}
              <Link href="https://chakra-ui.com/" isExternal color={"blue.500"}>
                Chakra-ui
              </Link>
            </ListItem>
            <ListItem>
              Animation library -{" "}
              <Link
                href="https://www.framer.com/motion/"
                isExternal
                color={"blue.500"}
              >
                Framer motion
              </Link>
            </ListItem>
            <ListItem>
              Notes display -{" "}
              <Link
                href="https://github.com/tsuyoshiwada/react-stack-grid"
                isExternal
                color={"blue.500"}
              >
                react-stack-grid
              </Link>
            </ListItem>
            <ListItem>
              Forms Validation -{" "}
              <Link
                href="https://react-hook-form.com/"
                isExternal
                color={"blue.500"}
              >
                React hook form
              </Link>
            </ListItem>
            <ListItem>
              Icons -{" "}
              <Link
                href="https://react-icons.github.io/react-icons/"
                isExternal
                color={"blue.500"}
              >
                React icons
              </Link>
            </ListItem>
            <ListItem>
              Images placeholder -{" "}
              <Link href="https://blurha.sh/" isExternal color={"blue.500"}>
                blurhash
              </Link>
            </ListItem>
            <ListItem>
              Progressive image loading -{" "}
              <Link
                href="https://github.com/FormidableLabs/react-progressive-image"
                isExternal
                color={"blue.500"}
              >
                react-progressive-image
              </Link>
            </ListItem>
          </UnorderedList>
        </Box>
      </VStack>
    </>
  );
}
Example #6
Source File: post-card.tsx    From portfolio with MIT License 4 votes vote down vote up
PostCard: React.SFC<PostCardProps> = ({ article }) => {
  const textColor = useColorModeValue("gray.500", "gray.200");
  const devIcon = useColorModeValue(dev, dev2);

  return (
    <CardTransition>
      <VStack
        spacing={1}
        p={4}
        isExternal
        _hover={{ shadow: "md", textDecoration: "none" }}
        borderWidth="1px"
        position="relative"
        rounded="md"
        bg={useColorModeValue("white", "gray.800")}
        align="left"
      >
        {article.external ? (
          <Tooltip hasArrow label="Dev.to" placement="top">
            <Image
              src={devIcon}
              width="2rem"
              height="2rem"
              position="absolute"
              color="#cbd5e0"
              right="0.5rem"
              top="-14px"
            />
          </Tooltip>
        ) : (
          <Tooltip hasArrow label="mahmad.me" placement="top">
            <Box position="absolute" color="#cbd5e0" right="0.5rem" top="-14px">
              <Badge ml="1" variant="solid" colorScheme="blackAlpha">
                Website
              </Badge>
            </Box>
          </Tooltip>
        )}
        <Heading fontSize="lg" align="left" mt={0}>
          {article.external ? (
            <Text as={Link} href={article.link} target="_blank">
              {article.title}
            </Text>
          ) : (
            <Link as={NavLink} to={article.link}>
              {article.title}
            </Link>
          )}
          {article.isNew && (
            <Badge
              ml="1"
              mb="1"
              colorScheme="green"
              fontSize="0.7em"
              lineHeight={1.5}
            >
              New
            </Badge>
          )}
        </Heading>
        <HStack spacing={2} isInline>
          <Tooltip hasArrow label="Published" placement="top">
            <Text fontSize="sm" fontWeight="400" color={textColor}>
              {article.published}
            </Text>
          </Tooltip>
          <Text fontSize="sm" fontWeight="400" color={textColor}>
            •
          </Text>
          <Tooltip hasArrow label="Views" placement="top">
            <Flex alignItems="center">
              <Text
                fontSize="sm"
                noOfLines={1}
                fontWeight="400"
                align="left"
                color={textColor}
              >
                {article.views}
              </Text>
              <Icon as={FaEye} ml={1} color={textColor} />
            </Flex>
          </Tooltip>
          <Text fontSize="sm" fontWeight="600" color={textColor}>
            •
          </Text>
          <Tooltip hasArrow label="Read time" placement="top">
            <Text
              fontSize="sm"
              noOfLines={1}
              fontWeight="400"
              align="left"
              color={textColor}
            >
              {article.readTime}
            </Text>
          </Tooltip>
          <HStack spacing={1} alignItems="center" d={["none", "none", "flex"]}>
            {article.tags.map(tag => (
              <Tag
                size="sm"
                padding="0 3px"
                key={tag}
                colorScheme={getTagColor(tag)}
              >
                {tag}
              </Tag>
            ))}
          </HStack>
        </HStack>
        <HStack spacing={1} alignItems="center" d={["flex", "flex", "none"]}>
          {article.tags.map(tag => (
            <Tag
              size="sm"
              padding="0 3px"
              key={tag}
              colorScheme={getTagColor(tag)}
            >
              {tag}
            </Tag>
          ))}
        </HStack>
        <Text align="left" fontSize="md" noOfLines={4} color={textColor}>
          {article.desc}
        </Text>
      </VStack>
    </CardTransition>
  );
}
Example #7
Source File: InputField.tsx    From hub with Apache License 2.0 4 votes vote down vote up
InputField = forwardRef((props: Props, ref: Ref<RefInputField>) => {
  const input = useRef<HTMLInputElement>(null);
  const [isValid, setIsValid] = useState<boolean | null>(null);
  const [inputValue, setInputValue] = useState(props.value || '');
  const [invalidText, setInvalidText] = useState(!isUndefined(props.invalidText) ? props.invalidText.default : '');
  const [isCheckingAvailability, setIsCheckingAvailability] = useState(false);
  const [isCheckingPwdStrength, setIsCheckingPwdStrength] = useState(false);
  const [pwdStrengthError, setPwdStrengthError] = useState<string | null>(null);
  const [activeType, setActiveType] = useState<string>(props.type);
  const [validateTimeout, setValidateTimeout] = useState<NodeJS.Timeout | null>(null);

  useImperativeHandle(ref, () => ({
    checkIsValid(): Promise<boolean> {
      return isValidField();
    },
    reset: () => {
      setInputValue('');
    },
    getValue(): string {
      return inputValue;
    },
    checkValidity(): boolean {
      return input.current ? input.current!.checkValidity() : true;
    },
    updateValue(newValue: string): void {
      setInputValue(newValue);
    },
  }));

  const checkValidity = (): boolean => {
    let isInputValid = true;
    if (input.current) {
      isInputValid = input.current!.checkValidity();
      if (!isInputValid && !isUndefined(props.invalidText)) {
        let errorTxt = props.invalidText.default;
        const validityState: ValidityState | undefined = input.current.validity;
        if (!isUndefined(validityState)) {
          if (validityState.typeMismatch && !isUndefined(props.invalidText.typeMismatch)) {
            errorTxt = props.invalidText.typeMismatch;
          } else if (validityState.tooShort && !isUndefined(props.invalidText.tooShort)) {
            errorTxt = props.invalidText.tooShort;
          } else if (validityState.patternMismatch && !isUndefined(props.invalidText.patternMismatch)) {
            errorTxt = props.invalidText.patternMismatch;
          } else if (validityState.typeMismatch && !isUndefined(props.invalidText.typeMismatch)) {
            errorTxt = props.invalidText.typeMismatch;
          } else if (validityState.rangeUnderflow && !isUndefined(props.invalidText.rangeUnderflow)) {
            errorTxt = props.invalidText.rangeUnderflow;
          } else if (validityState.rangeOverflow && !isUndefined(props.invalidText.rangeOverflow)) {
            errorTxt = props.invalidText.rangeOverflow;
          } else if (validityState.customError && !isUndefined(props.invalidText.customError)) {
            if (!isUndefined(props.excludedValues) && props.excludedValues.includes(input.current.value)) {
              errorTxt = props.invalidText.excluded;
            } else {
              errorTxt = props.invalidText.customError;
            }
          }
        }
        setInvalidText(errorTxt);
      }
      setIsValid(isInputValid);
      if (!isUndefined(props.setValidationStatus)) {
        props.setValidationStatus(false);
      }
    }
    return isInputValid;
  };

  const isValidField = async (): Promise<boolean> => {
    if (input.current) {
      const value = input.current!.value;
      if (value !== '') {
        if (!isUndefined(props.excludedValues) && props.excludedValues.includes(value)) {
          input.current!.setCustomValidity('Value is excluded');
        } else if (!isUndefined(props.checkAvailability) && !props.checkAvailability.excluded.includes(value)) {
          setIsCheckingAvailability(true);
          try {
            const isAvailable = await API.checkAvailability({
              resourceKind: props.checkAvailability.resourceKind,
              value: value,
            });
            if (!isNull(input.current)) {
              if (isAvailable) {
                input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? 'Already taken' : '');
              } else {
                input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? '' : 'Resource is not valid');
              }
            }
          } catch {
            if (!isNull(input.current)) {
              input.current!.setCustomValidity(props.checkAvailability!.isAvailable ? 'Already taken' : '');
            }
          }
          setIsCheckingAvailability(false);
        } else if (props.checkPasswordStrength) {
          setIsCheckingPwdStrength(true);
          try {
            await API.checkPasswordStrength(value);
            if (!isNull(input.current)) {
              input.current!.setCustomValidity('');
              setPwdStrengthError(null);
            }
          } catch (e: any) {
            if (!isNull(input.current) && e.message) {
              setPwdStrengthError(e.message);
              input.current!.setCustomValidity(e.message);
            }
          }
          setIsCheckingPwdStrength(false);
        } else {
          if (!isNull(input.current)) {
            input.current!.setCustomValidity('');
          }
        }
      }
    }
    return checkValidity();
  };

  const handleOnBlur = (): void => {
    if (!isUndefined(props.validateOnBlur) && props.validateOnBlur && input.current) {
      cleanTimeout(); // On blur we clean timeout if it's necessary
      isValidField();
    }
  };

  const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
    setInputValue(e.target.value);
    if (!isUndefined(props.onChange)) {
      props.onChange(e);
    }
  };

  const cleanTimeout = () => {
    if (!isNull(validateTimeout)) {
      clearTimeout(validateTimeout);
      setValidateTimeout(null);
    }
  };

  useEffect(() => {
    const isInputFocused = input.current === document.activeElement;
    if (isInputFocused && !isUndefined(props.validateOnChange) && props.validateOnChange) {
      cleanTimeout();
      setValidateTimeout(
        setTimeout(() => {
          isValidField();
        }, VALIDATION_DELAY)
      );
    }

    return () => {
      if (validateTimeout) {
        clearTimeout(validateTimeout);
      }
    };
  }, [inputValue]); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className={`${props.smallBottomMargin ? 'mb-3' : 'mb-4'} position-relative ${props.className}`}>
      {!isUndefined(props.label) && (
        <label htmlFor={props.name} className={`form-label fw-bold ${styles.label}`}>
          <span className="fw-bold">{props.label}</span>
          {!isUndefined(props.labelLegend) && <>{props.labelLegend}</>}
        </label>
      )}

      <input
        data-testid={`${props.name}Input`}
        ref={input}
        type={activeType}
        id={props.name}
        name={props.name}
        value={inputValue}
        className={classnames('form-control', props.inputClassName, { 'is-invalid': !isNull(isValid) && !isValid })}
        placeholder={props.placeholder}
        required={props.required}
        minLength={props.minLength}
        maxLength={props.maxLength}
        min={props.min}
        max={props.max}
        pattern={props.pattern}
        autoComplete={props.autoComplete}
        readOnly={props.readOnly || false}
        onChange={handleOnChange}
        onBlur={handleOnBlur}
        onKeyDown={props.onKeyDown}
        autoFocus={props.autoFocus}
        disabled={props.disabled}
        spellCheck="false"
      />

      {props.type === 'password' && props.visiblePassword && (
        <button
          type="button"
          className={classnames('btn btn-link position-absolute bottom-0', styles.revealBtn, {
            'text-muted': activeType === 'password',
            'text-secondary': activeType !== 'password',
          })}
          onClick={() => setActiveType(activeType === 'password' ? 'text' : 'password')}
          aria-label={`${activeType === 'password' ? 'Hide' : 'Show'} password`}
        >
          {activeType === 'password' ? <FaEyeSlash /> : <FaEye />}
        </button>
      )}

      {(isCheckingAvailability || isCheckingPwdStrength) && (
        <div className={`position-absolute ${styles.spinner}`}>
          <span className="spinner-border spinner-border-sm text-primary" />
        </div>
      )}

      {!isUndefined(props.validText) && (
        <div className={`valid-feedback mt-0 ${styles.inputFeedback}`}>{props.validText}</div>
      )}

      {!isUndefined(invalidText) && isNull(pwdStrengthError) && (
        <div className={`invalid-feedback mt-0 ${styles.inputFeedback}`}>{invalidText}</div>
      )}

      {!isNull(pwdStrengthError) && (
        <div className={`invalid-feedback mt-0 ${styles.inputPwdStrengthError}`}>
          {capitalizeFirstLetter(pwdStrengthError)}
        </div>
      )}

      {!isUndefined(props.additionalInfo) && <div className="alert p-0 mt-4">{props.additionalInfo}</div>}
    </div>
  );
})
Example #8
Source File: AnnotationGroupItem.tsx    From slim with Apache License 2.0 4 votes vote down vote up
render (): React.ReactNode {
    const identifier = `Annotation Group ${this.props.annotationGroup.number}`
    const attributes: Array<{ name: string, value: string }> = [
      {
        name: 'Label',
        value: this.props.annotationGroup.label
      },
      {
        name: 'Algorithm Name',
        value: this.props.annotationGroup.algorithmName
      },
      {
        name: 'Property category',
        value: this.props.annotationGroup.propertyCategory.CodeMeaning
      },
      {
        name: 'Property type',
        value: this.props.annotationGroup.propertyType.CodeMeaning
      }
    ]

    const index = this.props.metadata.AnnotationGroupSequence.findIndex(
      item => (item.AnnotationGroupUID === this.props.annotationGroup.uid)
    )
    const item = this.props.metadata.AnnotationGroupSequence[index]
    const measurementsSequence = item.MeasurementsSequence ?? []

    const measurementOptions = measurementsSequence.map(measurementItem => {
      const name = measurementItem.ConceptNameCodeSequence[0]
      const key = `${name.CodingSchemeDesignator}-${name.CodeValue}`
      return (
        <Select.Option
          key={key}
          value={key}
          dropdownMatchSelectWidth={false}
          size='small'
          disabled={!this.props.isVisible}
        >
          {name.CodeMeaning}
        </Select.Option>
      )
    })

    const settings = (
      <div>
        <Row justify='start' align='middle' gutter={[8, 8]}>
          <Col span={6}>
            Opacity
          </Col>
          <Col span={12}>
            <Slider
              range={false}
              min={0}
              max={1}
              step={0.01}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
          <Col span={6}>
            <InputNumber
              min={0}
              max={1}
              size='small'
              step={0.1}
              style={{ width: '65px' }}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
        </Row>
        <Divider plain>
          Exploration
        </Divider>
        <Row justify='start' align='middle' gutter={[8, 8]}>
          <Col span={8}>
            Measurement
          </Col>
          <Col span={16}>
            <Select
              style={{ minWidth: '65px', width: '90%' }}
              onSelect={this.handleMeasurementSelection}
              key='annotation-group-measurements'
              defaultValue={undefined}
            >
              {measurementOptions}
            </Select>
          </Col>
        </Row>
      </div>
    )

    const {
      annotationGroup,
      defaultStyle,
      isVisible,
      metadata,
      onVisibilityChange,
      onStyleChange,
      ...otherProps
    } = this.props
    return (
      <Menu.Item
        style={{ height: '100%', paddingLeft: '3px' }}
        key={this.props.annotationGroup.uid}
        {...otherProps}
      >
        <Space align='start'>
          <div style={{ paddingLeft: '14px' }}>
            <Space direction='vertical' align='end'>
              <Switch
                size='small'
                onChange={this.handleVisibilityChange}
                checked={this.props.isVisible}
                checkedChildren={<FaEye />}
                unCheckedChildren={<FaEyeSlash />}
              />
              <Popover
                placement='left'
                content={settings}
                overlayStyle={{ width: '350px' }}
                title='Display Settings'
              >
                <Button
                  type='primary'
                  shape='circle'
                  icon={<SettingOutlined />}
                />
              </Popover>
            </Space>
          </div>
          <Description
            header={identifier}
            attributes={attributes}
            selectable
            hasLongValues
          />
        </Space>
      </Menu.Item>
    )
  }
Example #9
Source File: AnnotationItem.tsx    From slim with Apache License 2.0 4 votes vote down vote up
render (): React.ReactNode {
    const identifier = `ROI ${this.props.index + 1}`
    const attributes: Array<{ name: string, value: string }> = []
    /**
     * This hack is required for Menu.Item to work properly:
     * https://github.com/react-component/menu/issues/142
     */
    const { isVisible, onVisibilityChange, ...otherProps } = this.props
    this.props.roi.evaluations.forEach((
      item: (
        dcmjs.sr.valueTypes.TextContentItem |
        dcmjs.sr.valueTypes.CodeContentItem
      )
    ) => {
      const nameValue = item.ConceptNameCodeSequence[0].CodeValue
      const nameMeaning = item.ConceptNameCodeSequence[0].CodeMeaning
      const name = `${nameMeaning}`
      if (item.ValueType === dcmjs.sr.valueTypes.ValueTypes.CODE) {
        const codeContentItem = item as dcmjs.sr.valueTypes.CodeContentItem
        const valueMeaning = codeContentItem.ConceptCodeSequence[0].CodeMeaning
        // For consistency with Segment and Annotation Group
        if (nameValue === '276214006') {
          attributes.push({
            name: 'Property category',
            value: `${valueMeaning}`
          })
        } else if (nameValue === '121071') {
          attributes.push({
            name: 'Property type',
            value: `${valueMeaning}`
          })
        } else if (nameValue === '111001') {
          attributes.push({
            name: 'Algorithm Name',
            value: `${valueMeaning}`
          })
        } else {
          attributes.push({
            name: name,
            value: `${valueMeaning}`
          })
        }
      } else if (item.ValueType === dcmjs.sr.valueTypes.ValueTypes.TEXT) {
        const textContentItem = item as dcmjs.sr.valueTypes.TextContentItem
        attributes.push({
          name: name,
          value: textContentItem.TextValue
        })
      }
    })
    this.props.roi.measurements.forEach(item => {
      const nameMeaning = item.ConceptNameCodeSequence[0].CodeMeaning
      const name = `${nameMeaning}`
      const seq = item.MeasuredValueSequence[0]
      const value = seq.NumericValue.toPrecision(6)
      const unit = seq.MeasurementUnitsCodeSequence[0].CodeValue
      attributes.push({
        name: name,
        value: `${value} ${unit}`
      })
    })
    return (
      <Space align='start'>
        <div style={{ paddingLeft: '14px' }}>
          <Switch
            size='small'
            onChange={this.handleVisibilityChange}
            checked={this.props.isVisible}
            checkedChildren={<FaEye />}
            unCheckedChildren={<FaEyeSlash />}
          />
        </div>
        <Menu.Item
          style={{ height: '100%', paddingLeft: '3px' }}
          key={this.props.roi.uid}
          {...otherProps}
        >
          <Description
            header={identifier}
            attributes={attributes}
            selectable
            hasLongValues
          />
        </Menu.Item>
      </Space>
    )
  }
Example #10
Source File: MappingItem.tsx    From slim with Apache License 2.0 4 votes vote down vote up
render (): React.ReactNode {
    const identifier = `Mapping ${this.props.mapping.number}`
    const attributes: Array<{ name: string, value: string }> = [
      {
        name: 'Label',
        value: this.props.mapping.label
      }
    ]

    const settings = (
      <div>
        <Row justify='center' align='middle'>
          <Col span={6}>
            Opacity
          </Col>
          <Col span={12}>
            <Slider
              range={false}
              min={0}
              max={1}
              step={0.01}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
          <Col span={6}>
            <InputNumber
              min={0}
              max={1}
              size='small'
              step={0.1}
              style={{ width: '65px' }}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
        </Row>
      </div>
    )

    /**
     * This hack is required for Menu.Item to work properly:
     * https://github.com/react-component/menu/issues/142
     */
    const {
      defaultStyle,
      isVisible,
      mapping,
      metadata,
      onVisibilityChange,
      onStyleChange,
      ...otherProps
    } = this.props
    return (
      <Menu.Item
        style={{ height: '100%', paddingLeft: '3px' }}
        key={this.props.mapping.uid}
        {...otherProps}
      >
        <Space align='start'>
          <div style={{ paddingLeft: '14px' }}>
            <Space direction='vertical' align='end' size={100}>
              <Space direction='vertical' align='end'>
                <Switch
                  size='small'
                  onChange={this.handleVisibilityChange}
                  checked={this.props.isVisible}
                  checkedChildren={<FaEye />}
                  unCheckedChildren={<FaEyeSlash />}
                />
                <Popover
                  placement='left'
                  content={settings}
                  overlayStyle={{ width: '350px' }}
                  title='Display Settings'
                >
                  <Button
                    type='primary'
                    shape='circle'
                    icon={<SettingOutlined />}
                  />
                </Popover>
              </Space>
            </Space>
          </div>
          <Description
            header={identifier}
            attributes={attributes}
            selectable
            hasLongValues
          />
        </Space>
      </Menu.Item>
    )
  }
Example #11
Source File: SegmentItem.tsx    From slim with Apache License 2.0 4 votes vote down vote up
render (): React.ReactNode {
    const attributes: Array<{ name: string, value: string }> = [
      {
        name: 'Property Category',
        value: this.props.segment.propertyCategory.CodeMeaning
      },
      {
        name: 'Property Type',
        value: this.props.segment.propertyType.CodeMeaning
      },
      {
        name: 'Algorithm Name',
        value: this.props.segment.algorithmName
      }
    ]

    const settings = (
      <div>
        <Row justify='center' align='middle'>
          <Col span={6}>
            Opacity
          </Col>
          <Col span={12}>
            <Slider
              range={false}
              min={0}
              max={1}
              step={0.01}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
          <Col span={6}>
            <InputNumber
              min={0}
              max={1}
              size='small'
              step={0.1}
              style={{ width: '65px' }}
              value={this.state.currentStyle.opacity}
              onChange={this.handleOpacityChange}
            />
          </Col>
        </Row>
      </div>
    )

    /**
     * This hack is required for Menu.Item to work properly:
     * https://github.com/react-component/menu/issues/142
     */
    const {
      defaultStyle,
      isVisible,
      segment,
      metadata,
      onVisibilityChange,
      onStyleChange,
      ...otherProps
    } = this.props
    return (
      <Menu.Item
        style={{ height: '100%', paddingLeft: '3px' }}
        key={this.props.segment.uid}
        {...otherProps}
      >
        <Space align='start'>
          <div style={{ paddingLeft: '14px' }}>
            <Space direction='vertical' align='end'>
              <Switch
                size='small'
                onChange={this.handleVisibilityChange}
                checked={this.props.isVisible}
                checkedChildren={<FaEye />}
                unCheckedChildren={<FaEyeSlash />}
              />
              <Popover
                placement='left'
                content={settings}
                overlayStyle={{ width: '350px' }}
                title='Display Settings'
              >
                <Button
                  type='primary'
                  shape='circle'
                  icon={<SettingOutlined />}
                />
              </Popover>
            </Space>
          </div>
          <Description
            header={this.props.segment.label}
            attributes={attributes}
            selectable
            hasLongValues
          />
        </Space>
      </Menu.Item>
    )
  }
Example #12
Source File: SlideViewer.tsx    From slim with Apache License 2.0 4 votes vote down vote up
render (): React.ReactNode {
    const rois: dmv.roi.ROI[] = []
    const segments: dmv.segment.Segment[] = []
    const mappings: dmv.mapping.ParameterMapping[] = []
    const annotationGroups: dmv.annotation.AnnotationGroup[] = []
    rois.push(...this.volumeViewer.getAllROIs())
    segments.push(...this.volumeViewer.getAllSegments())
    mappings.push(...this.volumeViewer.getAllParameterMappings())
    annotationGroups.push(...this.volumeViewer.getAllAnnotationGroups())

    const openSubMenuItems = ['specimens', 'opticalpaths', 'annotations']

    let report: React.ReactNode
    const dataset = this.state.generatedReport
    if (dataset !== undefined) {
      report = <Report dataset={dataset} />
    }

    let annotationMenuItems: React.ReactNode
    if (rois.length > 0) {
      annotationMenuItems = (
        <AnnotationList
          rois={rois}
          selectedRoiUIDs={this.state.selectedRoiUIDs}
          visibleRoiUIDs={this.state.visibleRoiUIDs}
          onSelection={this.handleAnnotationSelection}
          onVisibilityChange={this.handleAnnotationVisibilityChange}
        />
      )
    }

    const findingOptions = this.findingOptions.map(finding => {
      return (
        <Select.Option
          key={finding.CodeValue}
          value={finding.CodeValue}
        >
          {finding.CodeMeaning}
        </Select.Option>
      )
    })

    const geometryTypeOptionsMapping: { [key: string]: React.ReactNode } = {
      point: <Select.Option key='point' value='point'>Point</Select.Option>,
      circle: <Select.Option key='circle' value='circle'>Circle</Select.Option>,
      box: <Select.Option key='box' value='box'>Box</Select.Option>,
      polygon: <Select.Option key='polygon' value='polygon'>Polygon</Select.Option>,
      line: <Select.Option key='line' value='line'>Line</Select.Option>,
      freehandpolygon: (
        <Select.Option key='freehandpolygon' value='freehandpolygon'>
          Polygon (freehand)
        </Select.Option>
      ),
      freehandline: (
        <Select.Option key='freehandline' value='freehandline'>
          Line (freehand)
        </Select.Option>
      )
    }

    const selections: React.ReactNode[] = [
      (
        <Select
          style={{ minWidth: 130 }}
          onSelect={this.handleAnnotationFindingSelection}
          key='annotation-finding'
          defaultActiveFirstOption
        >
          {findingOptions}
        </Select>
      )
    ]

    const selectedFinding = this.state.selectedFinding
    if (selectedFinding !== undefined) {
      const key = _buildKey(selectedFinding)
      this.evaluationOptions[key].forEach(evaluation => {
        const evaluationOptions = evaluation.values.map(code => {
          return (
            <Select.Option
              key={code.CodeValue}
              value={code.CodeValue}
              label={evaluation.name}
            >
              {code.CodeMeaning}
            </Select.Option>
          )
        })
        selections.push(
          <>
            {evaluation.name.CodeMeaning}
            <Select
              style={{ minWidth: 130 }}
              onSelect={this.handleAnnotationEvaluationSelection}
              allowClear
              onClear={this.handleAnnotationEvaluationClearance}
              defaultActiveFirstOption={false}
            >
              {evaluationOptions}
            </Select>
          </>
        )
      })
      const geometryTypeOptions = this.geometryTypeOptions[key].map(name => {
        return geometryTypeOptionsMapping[name]
      })
      selections.push(
        <Select
          style={{ minWidth: 130 }}
          onSelect={this.handleAnnotationGeometryTypeSelection}
          key='annotation-geometry-type'
        >
          {geometryTypeOptions}
        </Select>
      )
      selections.push(
        <Checkbox
          onChange={this.handleAnnotationMeasurementActivation}
          key='annotation-measurement'
        >
          measure
        </Checkbox>
      )
    }

    const specimenMenu = (
      <Menu.SubMenu key='specimens' title='Specimens'>
        <SpecimenList
          metadata={this.props.slide.volumeImages[0]}
          showstain={false}
        />
      </Menu.SubMenu>
    )

    const equipmentMenu = (
      <Menu.SubMenu key='equipment' title='Equipment'>
        <Equipment metadata={this.props.slide.volumeImages[0]} />
      </Menu.SubMenu>
    )

    const defaultOpticalPathStyles: {
      [identifier: string]: {
        opacity: number
        color?: number[]
        limitValues?: number[]
      }
    } = {}
    const opticalPathMetadata: {
      [identifier: string]: dmv.metadata.VLWholeSlideMicroscopyImage[]
    } = {}
    const opticalPaths = this.volumeViewer.getAllOpticalPaths()
    opticalPaths.sort((a, b) => {
      if (a.identifier < b.identifier) {
        return -1
      } else if (a.identifier > b.identifier) {
        return 1
      }
      return 0
    })
    opticalPaths.forEach(opticalPath => {
      const identifier = opticalPath.identifier
      const metadata = this.volumeViewer.getOpticalPathMetadata(identifier)
      opticalPathMetadata[identifier] = metadata
      const style = this.volumeViewer.getOpticalPathStyle(identifier)
      defaultOpticalPathStyles[identifier] = style
    })
    const opticalPathMenu = (
      <Menu.SubMenu key='opticalpaths' title='Optical Paths'>
        <OpticalPathList
          metadata={opticalPathMetadata}
          opticalPaths={opticalPaths}
          defaultOpticalPathStyles={defaultOpticalPathStyles}
          visibleOpticalPathIdentifiers={this.state.visibleOpticalPathIdentifiers}
          activeOpticalPathIdentifiers={this.state.activeOpticalPathIdentifiers}
          onOpticalPathVisibilityChange={this.handleOpticalPathVisibilityChange}
          onOpticalPathStyleChange={this.handleOpticalPathStyleChange}
          onOpticalPathActivityChange={this.handleOpticalPathActivityChange}
          selectedPresentationStateUID={this.state.selectedPresentationStateUID}
        />
      </Menu.SubMenu>
    )

    let presentationStateMenu
    console.log('DEBUG: ', this.state.presentationStates)
    if (this.state.presentationStates.length > 0) {
      const presentationStateOptions = this.state.presentationStates.map(
        presentationState => {
          return (
            <Select.Option
              key={presentationState.SOPInstanceUID}
              value={presentationState.SOPInstanceUID}
              dropdownMatchSelectWidth={false}
              size='small'
            >
              {presentationState.ContentDescription}
            </Select.Option>
          )
        }
      )
      presentationStateMenu = (
        <Menu.SubMenu key='presentationStates' title='Presentation States'>
          <Space align='center' size={20} style={{ padding: '14px' }}>
            <Select
              style={{ minWidth: 200, maxWidth: 200 }}
              onSelect={this.handlePresentationStateSelection}
              key='presentation-states'
              defaultValue={this.props.selectedPresentationStateUID}
              value={this.state.selectedPresentationStateUID}
            >
              {presentationStateOptions}
            </Select>
            <Tooltip title='Reset'>
              <Btn
                icon={<UndoOutlined />}
                type='primary'
                onClick={this.handlePresentationStateReset}
              />
            </Tooltip>
          </Space>
        </Menu.SubMenu>
      )
    }

    let segmentationMenu
    if (segments.length > 0) {
      const defaultSegmentStyles: {
        [segmentUID: string]: {
          opacity: number
        }
      } = {}
      const segmentMetadata: {
        [segmentUID: string]: dmv.metadata.Segmentation[]
      } = {}
      const segments = this.volumeViewer.getAllSegments()
      segments.forEach(segment => {
        defaultSegmentStyles[segment.uid] = this.volumeViewer.getSegmentStyle(
          segment.uid
        )
        segmentMetadata[segment.uid] = this.volumeViewer.getSegmentMetadata(
          segment.uid
        )
      })
      segmentationMenu = (
        <Menu.SubMenu key='segmentations' title='Segmentations'>
          <SegmentList
            segments={segments}
            metadata={segmentMetadata}
            defaultSegmentStyles={defaultSegmentStyles}
            visibleSegmentUIDs={this.state.visibleSegmentUIDs}
            onSegmentVisibilityChange={this.handleSegmentVisibilityChange}
            onSegmentStyleChange={this.handleSegmentStyleChange}
          />
        </Menu.SubMenu>
      )
      openSubMenuItems.push('segmentations')
    }

    let parametricMapMenu
    if (mappings.length > 0) {
      const defaultMappingStyles: {
        [mappingUID: string]: {
          opacity: number
        }
      } = {}
      const mappingMetadata: {
        [mappingUID: string]: dmv.metadata.ParametricMap[]
      } = {}
      mappings.forEach(mapping => {
        defaultMappingStyles[mapping.uid] = this.volumeViewer.getParameterMappingStyle(
          mapping.uid
        )
        mappingMetadata[mapping.uid] = this.volumeViewer.getParameterMappingMetadata(
          mapping.uid
        )
      })
      parametricMapMenu = (
        <Menu.SubMenu key='parmetricmaps' title='Parametric Maps'>
          <MappingList
            mappings={mappings}
            metadata={mappingMetadata}
            defaultMappingStyles={defaultMappingStyles}
            visibleMappingUIDs={this.state.visibleMappingUIDs}
            onMappingVisibilityChange={this.handleMappingVisibilityChange}
            onMappingStyleChange={this.handleMappingStyleChange}
          />
        </Menu.SubMenu>
      )
      openSubMenuItems.push('parametricmaps')
    }

    let annotationGroupMenu
    if (annotationGroups.length > 0) {
      const defaultAnnotationGroupStyles: {
        [annotationGroupUID: string]: {
          opacity: number
        }
      } = {}
      const annotationGroupMetadata: {
        [annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations
      } = {}
      const annotationGroups = this.volumeViewer.getAllAnnotationGroups()
      annotationGroups.forEach(annotationGroup => {
        defaultAnnotationGroupStyles[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupStyle(
          annotationGroup.uid
        )
        annotationGroupMetadata[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupMetadata(
          annotationGroup.uid
        )
      })
      annotationGroupMenu = (
        <Menu.SubMenu key='annotationGroups' title='Annotation Groups'>
          <AnnotationGroupList
            annotationGroups={annotationGroups}
            metadata={annotationGroupMetadata}
            defaultAnnotationGroupStyles={defaultAnnotationGroupStyles}
            visibleAnnotationGroupUIDs={this.state.visibleAnnotationGroupUIDs}
            onAnnotationGroupVisibilityChange={this.handleAnnotationGroupVisibilityChange}
            onAnnotationGroupStyleChange={this.handleAnnotationGroupStyleChange}
          />
        </Menu.SubMenu>
      )
      openSubMenuItems.push('annotationGroups')
    }

    let toolbar
    let toolbarHeight = '0px'
    if (this.props.enableAnnotationTools) {
      toolbar = (
        <Row>
          <Button
            tooltip='Draw ROI [d]'
            icon={FaDrawPolygon}
            onClick={this.handleRoiDrawing}
            isSelected={this.state.isRoiDrawingActive}
          />
          <Button
            tooltip='Modify ROIs [m]'
            icon={FaHandPointer}
            onClick={this.handleRoiModification}
            isSelected={this.state.isRoiModificationActive}
          />
          <Button
            tooltip='Translate ROIs [t]'
            icon={FaHandPaper}
            onClick={this.handleRoiTranslation}
            isSelected={this.state.isRoiTranslationActive}
          />
          <Button
            tooltip='Remove selected ROI [r]'
            onClick={this.handleRoiRemoval}
            icon={FaTrash}
          />
          <Button
            tooltip='Show/Hide ROIs [v]'
            icon={this.state.areRoisHidden ? FaEye : FaEyeSlash}
            onClick={this.handleRoiVisibilityChange}
            isSelected={this.state.areRoisHidden}
          />
          <Button
            tooltip='Save ROIs [s]'
            icon={FaSave}
            onClick={this.handleReportGeneration}
          />
        </Row>
      )
      toolbarHeight = '50px'
    }

    /* It would be nicer to use the ant Spin component, but that causes issues
     * with the positioning of the viewport.
     */
    let loadingDisplay = 'none'
    if (this.state.isLoading) {
      loadingDisplay = 'block'
    }

    return (
      <Layout style={{ height: '100%' }} hasSider>
        <Layout.Content style={{ height: '100%' }}>
          {toolbar}

          <div className='dimmer' style={{ display: loadingDisplay }} />
          <div className='spinner' style={{ display: loadingDisplay }} />
          <div
            style={{
              height: `calc(100% - ${toolbarHeight})`,
              overflow: 'hidden'
            }}
            ref={this.volumeViewportRef}
          />

          <Modal
            visible={this.state.isAnnotationModalVisible}
            title='Configure annotations'
            onOk={this.handleAnnotationConfigurationCompletion}
            onCancel={this.handleAnnotationConfigurationCancellation}
            okText='Select'
          >
            <Space align='start' direction='vertical'>
              {selections}
            </Space>
          </Modal>

          <Modal
            visible={this.state.isReportModalVisible}
            title='Verify and save report'
            onOk={this.handleReportVerification}
            onCancel={this.handleReportCancellation}
            okText='Save'
          >
            {report}
          </Modal>
        </Layout.Content>

        <Layout.Sider
          width={300}
          reverseArrow
          style={{
            borderLeft: 'solid',
            borderLeftWidth: 0.25,
            overflow: 'hidden',
            background: 'none'
          }}
        >
          <Menu
            mode='inline'
            defaultOpenKeys={openSubMenuItems}
            style={{ height: '100%' }}
            inlineIndent={14}
            forceSubMenuRender
          >
            <Menu.SubMenu key='label' title='Slide label'>
              <Menu.Item style={{ height: '100%' }}>
                <div
                  style={{ height: '220px' }}
                  ref={this.labelViewportRef}
                />
              </Menu.Item>
            </Menu.SubMenu>
            {specimenMenu}
            {equipmentMenu}
            {opticalPathMenu}
            {presentationStateMenu}
            <Menu.SubMenu key='annotations' title='Annotations'>
              {annotationMenuItems}
            </Menu.SubMenu>
            {annotationGroupMenu}
            {segmentationMenu}
            {parametricMapMenu}
          </Menu>
        </Layout.Sider>
      </Layout>
    )
  }
Example #13
Source File: Cell.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
Cell = memo(function Cell(props: CellProps) {
  let bg = 'var(--cell-bg)';
  if (props.isEnteringRebus && props.active) {
    /* noop */
  } else if (props.isBlock && props.active) {
    bg =
      'repeating-linear-gradient(-45deg,var(--cell-wall),var(--cell-wall) 10px,var(--primary) 10px,var(--primary) 20px);';
  } else if (props.isBlock) {
    bg = 'var(--cell-wall)';
  } else if (props.cellColor !== undefined) {
    bg = 'rgba(241, 167, 45, ' + props.cellColor + ')';
  } else if (props.isEnteringRebus) {
    /* noop */
  } else if (props.active) {
    bg = 'var(--primary)';
  } else if (props.entryCell) {
    bg = 'var(--lighter)';
  } else if (props.refedCell) {
    bg = 'var(--secondary)';
  }

  const cellSize = props.squareWidth / props.gridWidth;
  const filledValue =
    props.active && props.isEnteringRebus
      ? props.rebusValue || ''
      : props.value.trim();
  const value =
    props.active && props.isEnteringRebus
      ? filledValue
      : filledValue || props.autofill;

  let boxShadow = '';
  if (props.isEnteringRebus && props.active) {
    boxShadow = 'inset 0 0 0 0.1em var(--primary)';
  } else if (props.highlightCell) {
    boxShadow = 'inset 0 0 0 0.02em var(--black)';
  } else if (props.cellColor !== undefined) {
    if (props.active) {
      boxShadow = 'inset 0 0 0 0.05em var(--black)';
    } else if (props.entryCell) {
      boxShadow = 'inset 0 0 0 0.02em var(--black)';
    }
  }

  return (
    <div
      css={{
        width: 100 / props.gridWidth + '%',
        paddingBottom: 100 / props.gridWidth + '%',
        float: 'left',
        position: 'relative',
        margin: 0,
        overflow: 'hidden',
      }}
    >
      {/* eslint-disable-next-line */}
      <div
        aria-label={'cell' + props.row + 'x' + props.column}
        onClick={() => props.onClick({ row: props.row, col: props.column })}
        css={{
          userSelect: 'none',
          position: 'absolute',
          width: '100%',
          height: '100%',
          fontSize: cellSize,
          ...(props.hidden &&
            props.active && {
              background:
                'repeating-linear-gradient(-45deg,var(--cell-bg),var(--cell-bg) 10px,var(--primary) 10px,var(--primary) 20px)',
            }),
          ...(!props.hidden && {
            borderLeft: '1px solid var(--cell-wall)',
            borderTop: '1px solid var(--cell-wall)',
            ...((props.row === props.gridHeight - 1 || props.hiddenBottom) && {
              borderBottom: '1px solid var(--cell-wall)',
            }),
            ...(props.barBottom &&
              props.row !== props.gridHeight - 1 && {
                borderBottom: '0.05em solid var(--cell-wall)',
              }),
            ...((props.column === props.gridWidth - 1 || props.hiddenRight) && {
              borderRight: '1px solid var(--cell-wall)',
            }),
            ...(props.barRight &&
              props.column !== props.gridWidth - 1 && {
                borderRight: '0.05em solid var(--cell-wall)',
              }),
            background: bg,
            ...(boxShadow && { boxShadow }),
          }),
        }}
      >
        {!props.isBlock || (props.isEnteringRebus && props.active) ? (
          <>
            <div
              css={{
                position: 'absolute',
                left: '0.1em',
                top: 0,
                fontWeight: 'bold',
                lineHeight: '1em',
                color: props.active ? 'var(--onprimary)' : 'var(--text)',
                fontSize: '0.25em',
              }}
            >
              {props.wasRevealed ? (
                <div
                  css={{
                    position: 'absolute',
                    left: '1.85em',
                    top: '-0.1em',
                    fontSize: '1.2em',
                    color: 'var(--verified)',
                  }}
                >
                  <FaEye />
                </div>
              ) : (
                ''
              )}
              {props.number}
            </div>
            <div
              css={{
                color: props.isVerified
                  ? 'var(--verified)'
                  : filledValue
                  ? props.active && !props.isEnteringRebus
                    ? 'var(--onprimary)'
                    : 'var(--text)'
                  : 'var(--autofill)',
                textAlign: 'center',
                lineHeight: '1.2em',
                fontSize: '0.9em',
              }}
            >
              {props.isWrong ? (
                <div
                  css={{
                    position: 'absolute',
                    zIndex: 2,
                    left: '0.03em',
                    top: '-0.1em',
                    color: 'var(--error)',
                    fontSize: '1em',
                  }}
                >
                  <FaSlash />
                </div>
              ) : (
                ''
              )}
              {props.highlight === 'circle' ? (
                <div
                  css={{
                    zIndex: 0,
                    position: 'absolute',
                    left: 0,
                    top: 0,
                    right: 0,
                    bottom: 0,
                    border: '1px solid var(--black)',
                    borderRadius: '50%',
                  }}
                ></div>
              ) : (
                ''
              )}
              {props.highlight === 'shade' ? (
                <div
                  css={{
                    position: 'absolute',
                    left: 0,
                    top: 0,
                    right: 0,
                    bottom: 0,
                    backgroundColor: 'var(--shade-highlight)',
                  }}
                ></div>
              ) : (
                ''
              )}
              <div
                css={{
                  fontSize: 1.0 / Math.max(value.length - 0.4, 1) + 'em',
                }}
              >
                {props.active && props.isEnteringRebus ? (
                  <>
                    {value}
                    <Cursor />
                  </>
                ) : (
                  value
                )}
              </div>
            </div>
          </>
        ) : (
          ''
        )}
      </div>
    </div>
  );
})
Example #14
Source File: Puzzle.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
Puzzle = ({
  loadingPlayState,
  puzzle,
  play,
  ...props
}: PuzzleProps & AuthPropsOptional) => {
  const [state, dispatch] = useReducer(
    puzzleReducer,
    {
      type: 'puzzle',
      wasEntryClick: false,
      active: { col: 0, row: 0, dir: Direction.Across },
      grid: addClues(
        fromCells({
          mapper: (e) => e,
          width: puzzle.size.cols,
          height: puzzle.size.rows,
          cells: play
            ? play.g
            : puzzle.grid.map((s) => (s === BLOCK ? BLOCK : ' ')),
          vBars: new Set(puzzle.vBars),
          hBars: new Set(puzzle.hBars),
          allowBlockEditing: false,
          highlighted: new Set(puzzle.highlighted),
          highlight: puzzle.highlight,
          hidden: new Set(puzzle.hidden),
        }),
        puzzle.clues
      ),
      showExtraKeyLayout: false,
      answers: puzzle.grid,
      alternateSolutions: puzzle.alternateSolutions,
      verifiedCells: new Set<number>(play ? play.vc : []),
      wrongCells: new Set<number>(play ? play.wc : []),
      revealedCells: new Set<number>(play ? play.rc : []),
      downsOnly: play?.do || false,
      isEnteringRebus: false,
      rebusValue: '',
      success: play ? play.f : false,
      ranSuccessEffects: play ? play.f : false,
      filled: false,
      autocheck: false,
      dismissedKeepTrying: false,
      dismissedSuccess: false,
      moderating: false,
      showingEmbedOverlay: false,
      displaySeconds: play ? play.t : 0,
      bankedSeconds: play ? play.t : 0,
      ranMetaSubmitEffects: false,
      ...(play &&
        play.ct_rv && {
          contestRevealed: true,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      ...(play &&
        play.ct_sub && {
          ranMetaSubmitEffects: true,
          contestPriorSubmissions: play.ct_pr_subs,
          contestDisplayName: play.ct_n,
          contestSubmission: play.ct_sub,
          contestEmail: play.ct_em,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      currentTimeWindowStart: 0,
      didCheat: play ? play.ch : false,
      clueView: false,
      cellsUpdatedAt: play ? play.ct : puzzle.grid.map(() => 0),
      cellsIterationCount: play ? play.uc : puzzle.grid.map(() => 0),
      cellsEverMarkedWrong: new Set<number>(play ? play.we : []),
      loadedPlayState: !loadingPlayState,
      waitToResize: true,
      isEditable(cellIndex) {
        return !this.verifiedCells.has(cellIndex) && !this.success;
      },
    },
    advanceActiveToNonBlock
  );

  const authContext = useContext(AuthContext);
  useEffect(() => {
    if (!authContext.notifications?.length) {
      return;
    }
    for (const notification of authContext.notifications) {
      if (notification.r) {
        // shouldn't be possible but be defensive
        continue;
      }
      if (!isNewPuzzleNotification(notification)) {
        continue;
      }
      if (notification.p === puzzle.id) {
        App.firestore()
          .collection('n')
          .doc(notification.id)
          .update({ r: true });
        return;
      }
    }
  }, [authContext.notifications, puzzle.id]);

  useEffect(() => {
    if (loadingPlayState === false) {
      const action: LoadPlayAction = {
        type: 'LOADPLAY',
        play: play,
        prefs: props.prefs,
        isAuthor: props.user ? props.user.uid === puzzle.authorId : false,
      };
      dispatch(action);
    }
  }, [loadingPlayState, play, props.user, props.prefs, puzzle.authorId]);

  // Every (unpaused) second dispatch a tick action which updates the display time
  useEffect(() => {
    function tick() {
      if (state.currentTimeWindowStart) {
        dispatch({ type: 'TICKACTION' });
      }
    }
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [state.currentTimeWindowStart, dispatch]);

  // Pause when page goes out of focus
  function prodPause() {
    if (process.env.NODE_ENV !== 'development') {
      dispatch({ type: 'PAUSEACTION' });
      writePlayToDBIfNeeded();
    }
  }
  useEventListener('blur', prodPause);

  const [muted, setMuted] = usePersistedBoolean('muted', false);
  const [toggleKeyboard, setToggleKeyboard] = usePersistedBoolean(
    'keyboard',
    false
  );

  // Set up music player for success song
  const [audioContext, initAudioContext] = useContext(CrosshareAudioContext);
  const playSuccess = useRef<(() => void) | null>(null);
  useEffect(() => {
    if (!audioContext) {
      return initAudioContext();
    }
    if (!playSuccess.current && !muted && audioContext) {
      fetch('/success.mp3')
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          audioContext.decodeAudioData(buffer, (audioBuffer) => {
            playSuccess.current = () => {
              const source = audioContext.createBufferSource();
              source.buffer = audioBuffer;
              source.connect(audioContext.destination);
              source.start();
            };
          });
        });
    }
  }, [muted, audioContext, initAudioContext]);

  const writePlayToDBIfNeeded = useCallback(
    async (user?: firebase.User) => {
      console.log('doing write play');
      if (!state.loadedPlayState) {
        return;
      }
      if (puzzle.contestAnswers?.length) {
        // For a meta we need to have run both to skip
        if (state.ranSuccessEffects && state.ranMetaSubmitEffects) {
          return;
        }
      } else {
        // For a reg puzzle skip if success effects have run
        if (state.ranSuccessEffects) {
          return;
        }
      }
      const u = user || props.user;
      if (!u) {
        return;
      }
      if (!isDirty(u, puzzle.id)) {
        return;
      }
      writePlayToDB(u, puzzle.id)
        .then(() => {
          console.log('Finished writing play state to db');
        })
        .catch((reason) => {
          console.error('Failed to write play: ', reason);
        });
    },
    [
      puzzle.id,
      puzzle.contestAnswers,
      props.user,
      state.ranMetaSubmitEffects,
      state.ranSuccessEffects,
      state.loadedPlayState,
    ]
  );

  const cachePlayForUser = useCallback(
    (user: firebase.User | undefined) => {
      if (!state.loadedPlayState) {
        return;
      }
      const updatedAt = TimestampClass.now();
      const playTime =
        state.currentTimeWindowStart === 0
          ? state.bankedSeconds
          : state.bankedSeconds +
            (new Date().getTime() - state.currentTimeWindowStart) / 1000;

      const playForUser: PlayWithoutUserT = {
        c: puzzle.id,
        n: puzzle.title,
        ua: updatedAt,
        g: Array.from(state.grid.cells),
        ct: Array.from(state.cellsUpdatedAt),
        uc: Array.from(state.cellsIterationCount),
        vc: Array.from(state.verifiedCells),
        wc: Array.from(state.wrongCells),
        we: Array.from(state.cellsEverMarkedWrong),
        rc: Array.from(state.revealedCells),
        t: playTime,
        ch: state.didCheat,
        do: state.downsOnly,
        f: state.success,
        ...(state.contestRevealed && {
          ct_rv: state.contestRevealed,
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
        }),
        ...(state.contestSubmission && {
          ct_sub: state.contestSubmission,
          ct_pr_subs: state.contestPriorSubmissions || [],
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
          ...(state.contestEmail && {
            ct_em: state.contestEmail,
          }),
        }),
      };
      cachePlay(user, puzzle.id, playForUser);
    },
    [
      state.downsOnly,
      state.loadedPlayState,
      puzzle.id,
      state.cellsEverMarkedWrong,
      state.cellsIterationCount,
      state.cellsUpdatedAt,
      state.didCheat,
      state.grid.cells,
      state.revealedCells,
      state.success,
      state.verifiedCells,
      state.wrongCells,
      puzzle.title,
      state.bankedSeconds,
      state.currentTimeWindowStart,
      state.contestSubmission,
      state.contestSubmitTime,
      state.contestEmail,
      state.contestDisplayName,
      state.contestRevealed,
      state.contestPriorSubmissions,
    ]
  );

  useEffect(() => {
    cachePlayForUser(props.user);
  }, [props.user, cachePlayForUser]);

  const router = useRouter();
  useEffect(() => {
    const listener = () => {
      writePlayToDBIfNeeded();
    };
    window.addEventListener('beforeunload', listener);
    router.events.on('routeChangeStart', listener);

    return () => {
      window.removeEventListener('beforeunload', listener);
      router.events.off('routeChangeStart', listener);
    };
  }, [writePlayToDBIfNeeded, router]);

  const { addToast } = useSnackbar();

  useEffect(() => {
    if (
      (state.contestSubmission || state.contestRevealed) &&
      !state.ranMetaSubmitEffects
    ) {
      const action: RanMetaSubmitEffectsAction = { type: 'RANMETASUBMIT' };
      dispatch(action);
      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }
    }
  }, [
    cachePlayForUser,
    state.contestSubmission,
    state.contestRevealed,
    state.ranMetaSubmitEffects,
    props.user,
    writePlayToDBIfNeeded,
  ]);

  useEffect(() => {
    if (state.success && !state.ranSuccessEffects) {
      const action: RanSuccessEffectsAction = { type: 'RANSUCCESS' };
      dispatch(action);

      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }

      let delay = 0;
      if (state.bankedSeconds <= 60) {
        addToast('? Solved in under a minute!');
        delay += 500;
      }
      if (!state.didCheat && state.downsOnly) {
        addToast('? Solved downs-only!', delay);
      } else if (!state.didCheat) {
        addToast('? Solved without check/reveal!', delay);
      }
      if (!muted && playSuccess.current) {
        playSuccess.current();
      }
    }
  }, [
    addToast,
    cachePlayForUser,
    muted,
    props.user,
    state.bankedSeconds,
    state.didCheat,
    state.downsOnly,
    state.ranSuccessEffects,
    state.success,
    writePlayToDBIfNeeded,
  ]);

  const physicalKeyboardHandler = useCallback(
    (e: KeyboardEvent) => {
      // Disable keyboard when paused / loading play
      if (!(state.success && state.dismissedSuccess)) {
        if (loadingPlayState || !state.currentTimeWindowStart) {
          return;
        }
      }

      const mkey = fromKeyboardEvent(e);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
        e.preventDefault();
      }
    },
    [
      dispatch,
      loadingPlayState,
      state.currentTimeWindowStart,
      state.success,
      state.dismissedSuccess,
    ]
  );
  useEventListener('keydown', physicalKeyboardHandler);

  const pasteHandler = useCallback(
    (e: ClipboardEvent) => {
      const tagName = (e.target as HTMLElement)?.tagName?.toLowerCase();
      if (tagName === 'textarea' || tagName === 'input') {
        return;
      }

      const pa: PasteAction = {
        type: 'PASTE',
        content: e.clipboardData?.getData('Text') || '',
      };
      dispatch(pa);
      e.preventDefault();
    },
    [dispatch]
  );
  useEventListener('paste', pasteHandler);

  let [entry, cross] = entryAndCrossAtPosition(state.grid, state.active);
  if (entry === null && cross !== null) {
    dispatch({ type: 'CHANGEDIRECTION' });
    [entry, cross] = [cross, entry];
  }

  const keyboardHandler = useCallback(
    (key: string) => {
      const mkey = fromKeyString(key);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
      }
    },
    [dispatch]
  );

  const { acrossEntries, downEntries } = useMemo(() => {
    return {
      acrossEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Across
      ),
      downEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Down
      ),
    };
  }, [state.grid.entries]);

  const isEmbed = useContext(EmbedContext);

  /* `clueMap` is a map from ENTRYWORD => '5D: This is the clue' - we use this
   *    for comment clue tooltips. */
  const clueMap = useMemo(() => {
    return getEntryToClueMap(state.grid, state.answers);
  }, [state.grid, state.answers]);

  /* `refs` is a set of referenced entry indexes for each entry in the grid - we use this
   * for grid highlights when an entry is selected.
   *
   * `refPositions` is an array for each entry of [reffedEntry, clueTextStart, clueTextEnd] tuples
   */
  const [refs, refPositions] = useMemo(() => {
    return getRefs(state.grid);
  }, [state.grid]);

  const scrollToCross = useMatchMedia(SMALL_AND_UP_RULES);

  const overlayBaseProps: PuzzleOverlayBaseProps = {
    publishTime: puzzle.isPrivateUntil || puzzle.publishTime,
    coverImage: props.coverImage,
    profilePicture: props.profilePicture,
    downsOnly: state.downsOnly,
    clueMap: clueMap,
    user: props.user,
    nextPuzzle: props.nextPuzzle,
    puzzle: puzzle,
    isMuted: muted,
    solveTime: state.displaySeconds,
    didCheat: state.didCheat,
    dispatch: dispatch,
  };

  let puzzleView: ReactNode;

  const entryIdx = entryIndexAtPosition(state.grid, state.active);
  let refed: Set<number> = new Set();
  if (entryIdx !== null) {
    refed = refs[entryIdx] || new Set();
  }

  const shouldConceal =
    state.currentTimeWindowStart === 0 &&
    !(state.success && state.dismissedSuccess);
  if (state.clueView) {
    puzzleView = (
      <TwoCol
        left={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  } else {
    puzzleView = (
      <SquareAndCols
        leftIsActive={state.active.dir === Direction.Across}
        waitToResize={state.waitToResize}
        dispatch={dispatch}
        aspectRatio={state.grid.width / state.grid.height}
        square={(width: number, _height: number) => {
          return (
            <GridView
              isEnteringRebus={state.isEnteringRebus}
              rebusValue={state.rebusValue}
              squareWidth={width}
              grid={state.grid}
              active={state.active}
              entryRefs={refs}
              dispatch={dispatch}
              revealedCells={state.revealedCells}
              verifiedCells={state.verifiedCells}
              wrongCells={state.wrongCells}
              showAlternates={state.success ? state.alternateSolutions : null}
              answers={state.answers}
            />
          );
        }}
        header={
          <div
            css={{
              height: SQUARE_HEADER_HEIGHT,
              fontSize: 18,
              lineHeight: '24px',
              backgroundColor: 'var(--lighter)',
              overflowY: 'scroll',
              scrollbarWidth: 'none',
              display: 'flex',
            }}
          >
            {entry ? (
              <div css={{ margin: 'auto 1em' }}>
                <span
                  css={{
                    fontWeight: 'bold',
                    paddingRight: '0.5em',
                  }}
                >
                  {entry.labelNumber}
                  {entry.direction === Direction.Across ? 'A' : 'D'}
                </span>
                <span
                  css={{
                    color: shouldConceal ? 'transparent' : 'var(--text)',
                    textShadow: shouldConceal
                      ? '0 0 1em var(--conceal-text)'
                      : '',
                  }}
                >
                  <ClueText
                    refPositions={refPositions}
                    entryIndex={entry.index}
                    allEntries={state.grid.entries}
                    grid={state.grid}
                    downsOnly={state.downsOnly && !state.success}
                  />
                </span>
              </div>
            ) : (
              ''
            )}
          </div>
        }
        left={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  }

  const checkRevealMenus = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEye />} text={t`Reveal`}>
          {() => (
            <>
              <TopBarDropDownLink
                icon={<RevealSquare />}
                text={t`Reveal Square`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Square,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealEntry />}
                text={t`Reveal Word`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Entry,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealPuzzle />}
                text={t`Reveal Puzzle`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Puzzle,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
            </>
          )}
        </TopBarDropDown>
        {!state.autocheck ? (
          <TopBarDropDown icon={<FaCheck />} text={t`Check`}>
            {() => (
              <>
                <TopBarDropDownLink
                  icon={<FaCheckSquare />}
                  text={t`Autocheck`}
                  onClick={() => {
                    const action: ToggleAutocheckAction = {
                      type: 'TOGGLEAUTOCHECK',
                    };
                    dispatch(action);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckSquare />}
                  text={t`Check Square`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Square,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckEntry />}
                  text={t`Check Word`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Entry,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckPuzzle />}
                  text={t`Check Puzzle`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Puzzle,
                    };
                    dispatch(ca);
                  }}
                />
              </>
            )}
          </TopBarDropDown>
        ) : (
          <TopBarLink
            icon={<FaCheckSquare />}
            text={t`Autochecking`}
            onClick={() => {
              const action: ToggleAutocheckAction = { type: 'TOGGLEAUTOCHECK' };
              dispatch(action);
            }}
          />
        )}
      </>
    ),
    [state.autocheck]
  );

  const moreMenu = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEllipsisH />} text={t`More`}>
          {() => (
            <>
              {!state.success ? (
                <TopBarDropDownLink
                  icon={<Rebus />}
                  text={t`Enter Rebus`}
                  shortcutHint={<EscapeKey />}
                  onClick={() => {
                    const kpa: KeypressAction = {
                      type: 'KEYPRESS',
                      key: { k: KeyK.Escape },
                    };
                    dispatch(kpa);
                  }}
                />
              ) : (
                ''
              )}
              {muted ? (
                <TopBarDropDownLink
                  icon={<FaVolumeUp />}
                  text={t`Unmute`}
                  onClick={() => setMuted(false)}
                />
              ) : (
                <TopBarDropDownLink
                  icon={<FaVolumeMute />}
                  text={t`Mute`}
                  onClick={() => setMuted(true)}
                />
              )}
              <TopBarDropDownLink
                icon={<FaKeyboard />}
                text={t`Toggle Keyboard`}
                onClick={() => setToggleKeyboard(!toggleKeyboard)}
              />
              {props.isAdmin ? (
                <>
                  <TopBarDropDownLink
                    icon={<FaGlasses />}
                    text="Moderate"
                    onClick={() => dispatch({ type: 'TOGGLEMODERATING' })}
                  />
                  <TopBarDropDownLinkA
                    href="/admin"
                    icon={<FaUserLock />}
                    text="Admin"
                  />
                </>
              ) : (
                ''
              )}
              {props.isAdmin || props.user?.uid === puzzle.authorId ? (
                <>
                  <TopBarDropDownLinkA
                    href={`/stats/${puzzle.id}`}
                    icon={<IoMdStats />}
                    text={t`Stats`}
                  />
                  <TopBarDropDownLinkA
                    href={`/edit/${puzzle.id}`}
                    icon={<FaEdit />}
                    text={t`Edit`}
                  />
                  {!isEmbed ? (
                    <TopBarDropDownLink
                      icon={<ImEmbed />}
                      text={t`Embed`}
                      onClick={() => dispatch({ type: 'TOGGLEEMBEDOVERLAY' })}
                    />
                  ) : (
                    ''
                  )}
                </>
              ) : (
                ''
              )}
              <TopBarDropDownLinkSimpleA
                href={'/api/pdf/' + puzzle.id}
                icon={<FaPrint />}
                text={t`Print Puzzle`}
              />
              {puzzle.hBars.length || puzzle.vBars.length ? (
                ''
              ) : (
                <TopBarDropDownLinkSimpleA
                  href={'/api/puz/' + puzzle.id}
                  icon={<FaRegFile />}
                  text={t`Download .puz File`}
                />
              )}
              <TopBarDropDownLinkA
                href="/account"
                icon={<FaUser />}
                text={t`Account / Settings`}
              />
              <TopBarDropDownLinkA
                href="/construct"
                icon={<FaHammer />}
                text={t`Construct a Puzzle`}
              />
            </>
          )}
        </TopBarDropDown>
      </>
    ),
    [
      muted,
      props.isAdmin,
      props.user?.uid,
      puzzle,
      setMuted,
      state.success,
      toggleKeyboard,
      setToggleKeyboard,
      isEmbed,
    ]
  );

  const description = puzzle.blogPost
    ? puzzle.blogPost.slice(0, 160) + '...'
    : puzzle.clues.map(getClueText).slice(0, 10).join('; ');

  const locale = router.locale || 'en';

  return (
    <>
      <Global
        styles={FULLSCREEN_CSS}
      />
      <Head>
        <title>{puzzle.title} | Crosshare crossword puzzle</title>
        <I18nTags
          locale={locale}
          canonicalPath={`/crosswords/${puzzle.id}/${slugify(puzzle.title)}`}
        />
        <meta key="og:title" property="og:title" content={puzzle.title} />
        <meta
          key="og:description"
          property="og:description"
          content={description}
        />
        <meta
          key="og:image"
          property="og:image"
          content={'https://crosshare.org/api/ogimage/' + puzzle.id}
        />
        <meta key="og:image:width" property="og:image:width" content="1200" />
        <meta key="og:image:height" property="og:image:height" content="630" />
        <meta
          key="og:image:alt"
          property="og:image:alt"
          content="An image of the puzzle grid"
        />
        <meta key="description" name="description" content={description} />
      </Head>
      <div
        css={{
          display: 'flex',
          flexDirection: 'column',
          height: '100%',
        }}
      >
        <div css={{ flex: 'none' }}>
          <TopBar title={puzzle.title}>
            {!loadingPlayState ? (
              !state.success ? (
                <>
                  <TopBarLink
                    icon={<FaPause />}
                    hoverText={t`Pause Game`}
                    text={timeString(state.displaySeconds, true)}
                    onClick={() => {
                      dispatch({ type: 'PAUSEACTION' });
                      writePlayToDBIfNeeded();
                    }}
                    keepText={true}
                  />
                  <TopBarLink
                    icon={state.clueView ? <SpinnerFinished /> : <FaListOl />}
                    text={state.clueView ? t`Grid` : t`Clues`}
                    onClick={() => {
                      const a: ToggleClueViewAction = {
                        type: 'TOGGLECLUEVIEW',
                      };
                      dispatch(a);
                    }}
                  />
                  {checkRevealMenus}
                  {moreMenu}
                </>
              ) : (
                <>
                  <TopBarLink
                    icon={<FaComment />}
                    text={
                      puzzle.contestAnswers?.length
                        ? !isMetaSolution(
                            state.contestSubmission,
                            puzzle.contestAnswers
                          ) && !state.contestRevealed
                          ? t`Contest Prompt / Submission`
                          : t`Comments / Leaderboard`
                        : t`Show Comments`
                    }
                    onClick={() => dispatch({ type: 'UNDISMISSSUCCESS' })}
                  />
                  {moreMenu}
                </>
              )
            ) : (
              moreMenu
            )}
          </TopBar>
        </div>
        {state.filled && !state.success && !state.dismissedKeepTrying ? (
          <KeepTryingOverlay dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.success && !state.dismissedSuccess ? (
          <PuzzleOverlay
            {...overlayBaseProps}
            overlayType={OverlayType.Success}
            contestSubmission={state.contestSubmission}
            contestHasPrize={puzzle.contestHasPrize}
            contestRevealed={state.contestRevealed}
            contestRevealDelay={puzzle.contestRevealDelay}
          />
        ) : (
          ''
        )}
        {state.moderating ? (
          <ModeratingOverlay puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.showingEmbedOverlay && props.user ? (
          <EmbedOverlay user={props.user} puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.currentTimeWindowStart === 0 &&
        !state.success &&
        !(state.filled && !state.dismissedKeepTrying) ? (
          state.bankedSeconds === 0 ? (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Begin Puzzle`}
              message={t`Ready to get started?`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          ) : (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Resume`}
              message={t`Your puzzle is paused`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          )
        ) : (
          ''
        )}
        <div
          css={{
            flex: '1 1 auto',
            overflow: 'scroll',
            scrollbarWidth: 'none',
            position: 'relative',
          }}
        >
          {puzzleView}
        </div>
        <div css={{ flex: 'none', width: '100%' }}>
          <Keyboard
            toggleKeyboard={toggleKeyboard}
            keyboardHandler={keyboardHandler}
            muted={muted}
            showExtraKeyLayout={state.showExtraKeyLayout}
            includeBlockKey={false}
          />
        </div>
      </div>
    </>
  );
}