react-copy-to-clipboard#CopyToClipboard TypeScript Examples

The following examples show how to use react-copy-to-clipboard#CopyToClipboard. 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: [id].tsx    From livepeer-com with MIT License 6 votes vote down vote up
ClipBut = ({ text }) => {
  const [isCopied, setCopied] = useState(0);
  useEffect(() => {
    if (isCopied) {
      const timeout = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(timeout);
    }
  }, [isCopied]);
  return (
    <CopyToClipboard text={text} onCopy={() => setCopied(2000)}>
      <Flex
        sx={{
          alignItems: "center",
          cursor: "pointer",
          ml: 0,
          mr: 0,
        }}>
        <Box sx={{ mr: 2 }}>{text}</Box>
        <Copy
          sx={{
            mr: 1,
            width: 14,
            height: 14,
            color: "offBlack",
          }}
        />
        {!!isCopied && (
          <Box sx={{ fontSize: 12, color: "offBlack" }}>Copied</Box>
        )}
      </Flex>
    </CopyToClipboard>
  );
}
Example #2
Source File: index.tsx    From ever-wallet-browser-extension with GNU General Public License v3.0 6 votes vote down vote up
export function CopyButton({ children, id = 'copy-button', text }: Props): JSX.Element {
    const intl = useIntl()
    return (
        <div
            data-tip={intl.formatMessage({ id: 'COPIED_TOOLTIP' })}
            data-for={id}
            data-event="click focus"
        >
            <CopyToClipboard text={text}>{children}</CopyToClipboard>
            <ReactTooltip id={id} type="dark" effect="solid" place="top" />
        </div>
    )
}
Example #3
Source File: index.tsx    From crust-apps with Apache License 2.0 6 votes vote down vote up
CopyButton:React.FC<Props> = ({ text, children, onCopy, message }) => {
  const { t } = useTranslation("order");
  const { queueAction } = useContext(StatusContext);
  const _onCopy = useCallback(() => {
    onCopy && onCopy();
    queueAction &&
      queueAction({
        action: t("clipboard"),
        message,
        status: "queued"
      });
  }, [queueAction, t]);

  return (
    <CopyToClipboard text={text.toString()} onCopy={_onCopy}>
      {children}
    </CopyToClipboard>
  );
}
Example #4
Source File: shareDeal.component.tsx    From filecoin-CID-checker with Apache License 2.0 6 votes vote down vote up
ShareDeal = (props: Props) => {
  const { text, className, title = 'Click to copy Deal link to share' } = props

  return (
    <CopyToClipboard text={`${window.location.origin}/deal/${text}`}>
      <ShareWrapper className={className} title={title}>
        <ShareImageWrapper />
      </ShareWrapper>
    </CopyToClipboard>
  )
}
Example #5
Source File: copyText.component.tsx    From filecoin-CID-checker with Apache License 2.0 6 votes vote down vote up
CopyText = (props: Props) => {
  const { text, className, title = 'Click to copy' } = props

  return (
    <div onClick={(e: any) => e.stopPropagation()}>
      <CopyToClipboard onClick={(e: any) => e.stopPropagation()} text={text}>
        <CopyWrapper className={className} title={title}>
          <CopyImageWrapper />
        </CopyWrapper>
      </CopyToClipboard>
    </div>
  )
}
Example #6
Source File: utils.tsx    From NekoMaid with MIT License 6 votes vote down vote up
ParsedComponent: React.FC<{ component: TextComponent, runCommand?: (it: string) => void, suggest?: (it: string) => void }> =
  ({ component: it, runCommand, suggest }) => {
    let className: string | undefined
    const style: any = { }
    if (it.bold) style.fontWeight = 'bold'
    if (it.italic) style.fontStyle = 'italic'
    if (it.underlined) style.textDecoration = 'underline'
    if (it.strikethrough) style.textDecoration = (style.textDecoration ? style.textDecoration + ' ' : '') + 'line-through'
    let color = it.color
    if (typeof color === 'string') color = parseStringColor(color)
    if (color?.name && color.name in colorsMap) {
      style.color = colors[colorsMap[color.name]]
      if (color.name === 'white' || color.name === 'black') className = color.name
    } else if (color?.color) style.color = `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.alpha})`
    if (style.color && !(color?.name === 'white' || color?.name === 'black')) style.textShadow = 'none'
    if (it.clickEvent) style.cursor = 'pointer'
    let content: any
    if (it.translate) {
      if (it.translate in minecraft) {
        let i = 0
        const arr = it.with || []
        content = minecraft[it.translate].split('%').map((str, j) => {
          let comp: any
          if (j) {
            if (str[0] === 's') {
              comp = arr[i++]
              str = str.slice(1)
            } else {
              const id = parseInt(str)
              if (id > 0) {
                comp = arr[id - 1]
                str = str.slice(id.toString().length + 2)
              }
            }
          }
          return <>{comp && parseStringOrComponent(comp)}{str}</>
        })
      } else content = it.text ? parseMessage(it.text) : it.translate
    } else if (it.text) content = parseMessage(it.text)
    let elm = <span
      style={style}
      className={className}
      onClick={it.clickEvent
        ? () => {
            const value = it.clickEvent!.value
            switch (it.clickEvent!.action) {
              case 'OPEN_URL': return window.open(value, '_blank')
              case 'RUN_COMMAND': return runCommand && runCommand(value.slice(1))
              case 'SUGGEST_COMMAND': return suggest && suggest(value.slice(1))
            }
          }
        : undefined
      }
    >{content}{it.extra && parseComponents(it.extra, runCommand, suggest)}</span>
    if (it.hoverEvent?.action === 'SHOW_TEXT' && it.hoverEvent.contents.value) {
      elm = <Tooltip title={<>{parseComponents(it.hoverEvent.contents.value, runCommand, suggest)}</>}>{elm}</Tooltip>
    }
    return it.clickEvent?.action === 'COPY_TO_CLIPBOARD' ? <CopyToClipboard text={it.clickEvent!.value}>{elm}</CopyToClipboard> : elm
  }
Example #7
Source File: ClipboardCopy.tsx    From bee-dashboard with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
export default function ClipboardCopy({ value }: Props): ReactElement {
  const { enqueueSnackbar } = useSnackbar()
  const handleCopy = () => enqueueSnackbar(`Copied: ${value}`, { variant: 'success' })

  return (
    <div style={{ marginRight: '3px', marginLeft: '3px' }}>
      <IconButton color="primary" size="small" onClick={handleCopy}>
        <CopyToClipboard text={value}>
          <Clipboard style={{ height: '20px' }} />
        </CopyToClipboard>
      </IconButton>
    </div>
  )
}
Example #8
Source File: CodeCopy.tsx    From frontend with Apache License 2.0 6 votes vote down vote up
CodeCopy: FunctionComponent<Props> = ({ text, onCopy, className, nested }) => {
  const { t } = useTranslation()
  const [copied, setCopied] = useState(false)

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (copied) setCopied(false);
    }, 1200);

    return () => clearTimeout(timeout);
  }, [copied]);


  return (< div className={`${styles.pre} ${className} ${nested ? styles.nested : ''}`}>
    {text}
    <CopyToClipboard text={text} onCopy={() => {
      setCopied(true)
      onCopy()
    }}>
      <button className={styles.copy} title={t('copy-text')}>
        {!copied && <MdContentCopy></MdContentCopy>}
        {copied && <MdCheck style={{ 'color': "green" }}></MdCheck>}
      </button>
    </CopyToClipboard >
  </div >
  )
}
Example #9
Source File: streamDetail.tsx    From livepeer-com with MIT License 5 votes vote down vote up
ShowURL = ({ url, shortendUrl, anchor = false }: ShowURLProps) => {
  const [isCopied, setCopied] = useState(0);
  const [openSnackbar] = useSnackbar();

  useEffect(() => {
    if (isCopied) {
      const interval = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(interval);
    }
  }, [isCopied]);

  return (
    <HoverCardRoot openDelay={200}>
      <HoverCardTrigger>
        <Flex css={{ ai: "center" }}>
          <CopyToClipboard
            text={url}
            onCopy={() => {
              openSnackbar("Copied to clipboard");
              setCopied(2000);
            }}>
            <Flex
              css={{
                alignItems: "center",
                cursor: "pointer",
                ml: 0,
                mr: 0,
              }}>
              {anchor ? (
                <A
                  css={{ fontSize: "$2", mr: "$1" }}
                  href={url}
                  target="_blank">
                  {shortendUrl ? shortendUrl : url}
                </A>
              ) : (
                <Box css={{ fontSize: "$2", mr: "$1" }}>
                  {shortendUrl ? shortendUrl : url}
                </Box>
              )}
              <Copy
                css={{
                  mr: "$2",
                  width: 14,
                  height: 14,
                  color: "$hiContrast",
                }}
              />
            </Flex>
          </CopyToClipboard>
        </Flex>
      </HoverCardTrigger>
      <HoverCardContent>
        <Text
          variant="gray"
          css={{
            backgroundColor: "$panel",
            borderRadius: 6,
            px: "$3",
            py: "$1",
            fontSize: "$1",
            display: "flex",
            ai: "center",
          }}>
          <Box>{isCopied ? "Copied" : "Copy to Clipboard"}</Box>
        </Text>
      </HoverCardContent>
    </HoverCardRoot>
  );
}
Example #10
Source File: CreateTokenDialog.tsx    From livepeer-com with MIT License 5 votes vote down vote up
ClipBut = ({ text }) => {
  const [isCopied, setCopied] = useState(0);
  const [openSnackbar] = useSnackbar();

  useEffect(() => {
    if (isCopied) {
      const timeout = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(timeout);
    }
  }, [isCopied]);

  return (
    <HoverCardRoot openDelay={200}>
      <HoverCardTrigger>
        <Flex css={{ height: 25, ai: "center" }}>
          <CopyToClipboard
            text={text}
            onCopy={() => {
              openSnackbar("Copied to clipboard");
              setCopied(2000);
            }}>
            <Flex
              css={{
                alignItems: "center",
                cursor: "pointer",
                ml: 0,
                mr: 0,
              }}>
              <Box css={{ mr: "$1" }}>{text}</Box>
              <Copy
                css={{
                  mr: "$2",
                  width: 14,
                  height: 14,
                  color: "$hiContrast",
                }}
              />
            </Flex>
          </CopyToClipboard>
        </Flex>
      </HoverCardTrigger>
      <HoverCardContent>
        <Text
          variant="gray"
          css={{
            backgroundColor: "$panel",
            borderRadius: 6,
            px: "$3",
            py: "$1",
            fontSize: "$1",
            display: "flex",
            ai: "center",
          }}>
          <Box>{isCopied ? "Copied" : "Copy to Clipboard"}</Box>
        </Text>
      </HoverCardContent>
    </HoverCardRoot>
  );
}
Example #11
Source File: streamDetail.tsx    From livepeer-com with MIT License 5 votes vote down vote up
ClipBut = ({ text }) => {
  const [isCopied, setCopied] = useState(0);
  const [openSnackbar] = useSnackbar();

  useEffect(() => {
    if (isCopied) {
      const timeout = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(timeout);
    }
  }, [isCopied]);

  return (
    <HoverCardRoot openDelay={200}>
      <HoverCardTrigger>
        <Flex css={{ ai: "center" }}>
          <CopyToClipboard
            text={text}
            onCopy={() => {
              openSnackbar("Copied to clipboard");
              setCopied(2000);
            }}>
            <Flex
              css={{
                alignItems: "center",
                cursor: "pointer",
                ml: 0,
                mr: 0,
              }}>
              <Box css={{ mr: "$1" }}>{text}</Box>
              <Copy
                css={{
                  mr: "$2",
                  width: 14,
                  height: 14,
                  color: "$hiContrast",
                }}
              />
            </Flex>
          </CopyToClipboard>
        </Flex>
      </HoverCardTrigger>
      <HoverCardContent>
        <Text
          variant="gray"
          css={{
            backgroundColor: "$panel",
            borderRadius: 6,
            px: "$3",
            py: "$1",
            fontSize: "$1",
            display: "flex",
            ai: "center",
          }}>
          <Box>{isCopied ? "Copied" : "Copy to Clipboard"}</Box>
        </Text>
      </HoverCardContent>
    </HoverCardRoot>
  );
}
Example #12
Source File: [id].tsx    From livepeer-com with MIT License 5 votes vote down vote up
ShowURL = ({ text, url, urlToCopy, anchor = false }: ShowURLProps) => {
  const [isCopied, setCopied] = useState(0);
  useEffect(() => {
    if (isCopied) {
      const interval = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(interval);
    }
  }, [isCopied]);
  const ccurl = urlToCopy ? urlToCopy : url;
  return (
    <Flex sx={{ justifyContent: "flex-start", alignItems: "center" }}>
      {text ? (
        <Box sx={{ minWidth: 125, fontSize: 12, paddingRight: "1em" }}>
          {text}:
        </Box>
      ) : null}
      <CopyToClipboard text={ccurl} onCopy={() => setCopied(2000)}>
        <Flex sx={{ alignItems: "center" }}>
          {anchor ? (
            <Box
              as="a"
              sx={{ fontSize: 12, fontFamily: "monospace", mr: 1 }}
              href={url}
              target="_blank">
              {url}
            </Box>
          ) : (
            <Box
              as="span"
              sx={{ fontSize: 12, fontFamily: "monospace", mr: 1 }}>
              {url}
            </Box>
          )}
          <Copy
            sx={{
              mr: 1,
              cursor: "pointer",
              width: 14,
              height: 14,
              color: "offBlack",
            }}
          />
        </Flex>
      </CopyToClipboard>
      {!!isCopied && <Box sx={{ fontSize: 12, color: "offBlack" }}>Copied</Box>}
    </Flex>
  );
}
Example #13
Source File: ExpandableListItemKey.tsx    From bee-dashboard with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
export default function ExpandableListItemKey({ label, value, expanded }: Props): ReactElement | null {
  const classes = useStyles()
  const [open, setOpen] = useState(expanded || false)
  const [copied, setCopied] = useState(false)
  const toggleOpen = () => setOpen(!open)

  const tooltipClickHandler = () => setCopied(true)
  const tooltipCloseHandler = () => setCopied(false)

  const splitValues = split(value)
  const hasPrefix = isPrefixedHexString(value)
  const spanText = `${hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]}[…]${
    splitValues[splitValues.length - 1]
  }`

  return (
    <ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
      <Grid container direction="column" justifyContent="space-between" alignItems="stretch">
        <Grid container direction="row" justifyContent="space-between" alignItems="center">
          {label && <Typography variant="body1">{label}</Typography>}
          <Typography variant="body2">
            <div>
              {!open && (
                <span className={classes.copyValue}>
                  <Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
                    <CopyToClipboard text={value}>
                      <span onClick={tooltipClickHandler}>{value ? spanText : ''}</span>
                    </CopyToClipboard>
                  </Tooltip>
                </span>
              )}
              <IconButton size="small" className={classes.copyValue}>
                {open ? <Minus onClick={toggleOpen} strokeWidth={1} /> : <Eye onClick={toggleOpen} strokeWidth={1} />}
              </IconButton>
            </div>
          </Typography>
        </Grid>
        <Collapse in={open} timeout="auto" unmountOnExit>
          <div className={classes.content}>
            <Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
              <CopyToClipboard text={value}>
                {/* This has to be wrapped in two spans otherwise either the tooltip or the highlighting does not work*/}
                <span onClick={tooltipClickHandler}>
                  <span className={classes.copyValue}>
                    {splitValues.map((s, i) => (
                      <Typography variant="body2" key={i} className={classes.keyMargin} component="span">
                        {s}
                      </Typography>
                    ))}
                  </span>
                </span>
              </CopyToClipboard>
            </Tooltip>
          </div>
        </Collapse>
      </Grid>
    </ListItem>
  )
}
Example #14
Source File: index.tsx    From rabet-extension with GNU General Public License v3.0 5 votes vote down vote up
CopyText = ({ text, custom, fullIcon }: AppProps) => {
  const [visible, setVisible] = useState(false);
  const [tooltipText, setTooltipText] = useState('Copy to clipboard');

  const toggle = () => {
    setTooltipText('Copied!');
    setVisible(true);
  };

  const renderCopyTrigger = () => {
    if (custom) {
      return custom;
    }

    if (fullIcon) {
      return (
        <StyledButton type="button">
          <Copy />
          Copy
        </StyledButton>
      );
    }

    return <Copy />;
  };

  const onMouseEnter = () => setVisible(true);
  const onMouseLeave = () => {
    setVisible(false);
    setTooltipText('Copy to clipboard');
  };

  return (
    <span
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      onClick={toggle}
      className="cursor-pointer"
    >
      <Tooltips
        text={tooltipText}
        placement="top"
        isVisible={visible}
        controlled
      >
        <CopyToClipboard text={text}>
          {renderCopyTrigger()}
        </CopyToClipboard>
      </Tooltips>
    </span>
  );
}
Example #15
Source File: PublicToken.tsx    From your_spotify with GNU General Public License v3.0 5 votes vote down vote up
export default function PublicToken() {
  const dispatch = useAppDispatch();
  const user = useSelector(selectUser);
  const location = window.location.origin;

  const generate = useCallback(() => {
    dispatch(generateNewPublicToken());
  }, [dispatch]);

  const onCopy = useCallback(() => {
    dispatch(
      alertMessage({
        level: 'info',
        message: 'Public url copied to clipboard',
      }),
    );
  }, [dispatch]);

  if (!user) {
    return null;
  }

  const link = `${location}/?token=${user.publicToken}`;

  return (
    <div>
      <Text element="div" className={s.disclaimer}>
        The generated url will allow anyone with it to view your stats indefinitely. The user
        won&apos;t be able to execute any action that modifies your account. Regenerating it will
        cause the older link to be deprecated instantly. You can also share the page you&apos;re
        currently viewing using the <code>Share this page</code> button on the side.
      </Text>
      <SettingLine
        left="Your public token"
        right={
          user.publicToken ? (
            <CopyToClipboard text={link} onCopy={onCopy}>
              <div className={s.link}>
                <Text element="div">{link}</Text>
              </div>
            </CopyToClipboard>
          ) : (
            'No token generated'
          )
        }
      />
      <SettingLine left="Regenerate" right={<Button onClick={generate}>Generate</Button>} />
    </div>
  );
}
Example #16
Source File: snackbar.tsx    From rugenerous-frontend with MIT License 5 votes vote down vote up
SnackMessage = forwardRef<HTMLDivElement, { id: string | number; message: Message }>((props, ref) => {
  const classes = useStyles();
  const { closeSnackbar } = useSnackbar();
  const [expanded, setExpanded] = useState(false);
  const [isCopy, setIsCopy] = useState(false);

  const handleExpandClick = useCallback(() => {
    setExpanded(oldExpanded => !oldExpanded);
  }, []);

  const handleDismiss = useCallback(() => {
    closeSnackbar(props.id);
  }, [props.id, closeSnackbar]);

  const getIcon = (severity: Color) => {
    switch (severity) {
      case "error":
        return <ErrorIcon color="inherit" />;
      case "info":
        return <InfoIcon color="inherit" />;
      case "success":
        return <SuccessIcon color="inherit" />;
      case "warning":
        return <WarningIcon color="inherit" />;
      default:
        return <div />;
    }
  };

  return (
    <SnackbarContent ref={ref} className={classes.root}>
      <Card className={classnames(classes.card, classes[props.message.severity])}>
        <CardActions classes={{ root: classes.actionRoot }}>
          {getIcon(props.message.severity)}
          <Typography variant="subtitle2" className={classes.typography}>
            {props.message.text}
          </Typography>
          <div className={classes.icons}>
            {props.message.error && (
              <IconButton
                aria-label="Show more"
                className={classnames(classes.expand, { [classes.expandOpen]: expanded })}
                onClick={handleExpandClick}
              >
                <ExpandMoreIcon color="inherit" />
              </IconButton>
            )}
            <IconButton className={classes.expand} onClick={handleDismiss}>
              <CloseIcon color="inherit" />
            </IconButton>
          </div>
        </CardActions>
        <Collapse in={expanded} timeout="auto" unmountOnExit>
          <Paper className={classes.collapse}>
            <CopyToClipboard text={JSON.stringify(props.message.error)} onCopy={() => setIsCopy(true)}>
              <Button size="small" className={classes.button}>
                <CheckCircleIcon className={classnames(classes.checkIcon, { [classes.checkIconCopy]: isCopy })} />
                Copy to clipboard
              </Button>
            </CopyToClipboard>
            <div className={classes.errorWrap}>
              <Typography>Error message: </Typography>
              <Typography className={classes.errorText}>{JSON.stringify(props.message.error, null, 2)}</Typography>
            </div>
          </Paper>
        </Collapse>
      </Card>
    </SnackbarContent>
  );
})
Example #17
Source File: snackbar.tsx    From wonderland-frontend with MIT License 5 votes vote down vote up
SnackMessage = forwardRef<HTMLDivElement, { id: string | number; message: Message }>((props, ref) => {
    const classes = useStyles();
    const { closeSnackbar } = useSnackbar();
    const [expanded, setExpanded] = useState(false);
    const [isCopy, setIsCopy] = useState(false);

    const handleExpandClick = useCallback(() => {
        setExpanded(oldExpanded => !oldExpanded);
    }, []);

    const handleDismiss = useCallback(() => {
        closeSnackbar(props.id);
    }, [props.id, closeSnackbar]);

    const getIcon = (severity: Color) => {
        switch (severity) {
            case "error":
                return <ErrorIcon color="inherit" />;
            case "info":
                return <InfoIcon color="inherit" />;
            case "success":
                return <SuccessIcon color="inherit" />;
            case "warning":
                return <WarningIcon color="inherit" />;
            default:
                return <div />;
        }
    };

    return (
        <SnackbarContent ref={ref} className={classes.root}>
            <Card className={classnames(classes.card, classes[props.message.severity])}>
                <CardActions classes={{ root: classes.actionRoot }}>
                    {getIcon(props.message.severity)}
                    <Typography variant="subtitle2" className={classes.typography}>
                        {props.message.text}
                    </Typography>
                    <div className={classes.icons}>
                        {props.message.error && (
                            <IconButton aria-label="Show more" className={classnames(classes.expand, { [classes.expandOpen]: expanded })} onClick={handleExpandClick}>
                                <ExpandMoreIcon color="inherit" />
                            </IconButton>
                        )}
                        <IconButton className={classes.expand} onClick={handleDismiss}>
                            <CloseIcon color="inherit" />
                        </IconButton>
                    </div>
                </CardActions>
                <Collapse in={expanded} timeout="auto" unmountOnExit>
                    <Paper className={classes.collapse}>
                        <CopyToClipboard text={JSON.stringify(props.message.error)} onCopy={() => setIsCopy(true)}>
                            <Button size="small" className={classes.button}>
                                <CheckCircleIcon className={classnames(classes.checkIcon, { [classes.checkIconCopy]: isCopy })} />
                                Copy to clipboard
                            </Button>
                        </CopyToClipboard>
                        <div className={classes.errorWrap}>
                            <Typography>Error message: </Typography>
                            <Typography className={classes.errorText}>{JSON.stringify(props.message.error, null, 2)}</Typography>
                        </div>
                    </Paper>
                </Collapse>
            </Card>
        </SnackbarContent>
    );
})
Example #18
Source File: [id].tsx    From livepeer-com with MIT License 4 votes vote down vote up
ID = () => {
  useLoggedIn();
  const {
    user,
    logout,
    getStreamInfo,
    deleteStream,
    getIngest,
    patchStream,
    getAdminStreams,
    terminateStream,
  } = useApi();
  const userIsAdmin = user && user.admin;
  const router = useRouter();
  const { query } = router;
  const id = query.id?.toString();
  const [stream, setStream] = useState<Stream>(null);
  const [streamOwner, setStreamOwner] = useState<User>(null);
  const [ingest, setIngest] = useState([]);
  const [deleteModal, setDeleteModal] = useState(false);
  const [terminateModal, setTerminateModal] = useState(false);
  const [suspendUserModal, setSuspendUserModal] = useState(false);
  const [suspendModal, setSuspendModal] = useState(false);
  const [notFound, setNotFound] = useState(false);
  const [recordOffModal, setRecordOffModal] = useState(false);
  const [isCopied, setCopied] = useState(0);
  const [lastSession, setLastSession] = useState(null);
  const [lastSessionLoading, setLastSessionLoading] = useState(false);
  const [regionalUrlsVisible, setRegionalUrlsVisible] = useState(false);
  const [resultText, setResultText] = useState("");
  const [alertText, setAlertText] = useState("");

  useEffect(() => {
    if (user && user.admin && stream && !lastSessionLoading) {
      setLastSessionLoading(true);
      getAdminStreams({
        sessionsonly: true,
        limit: 1,
        order: "createdAt-true",
        filters: [{ id: "parentId", value: stream.id }],
        userId: stream.userId,
      })
        .then((res) => {
          const [streamsOrError] = res;
          if (Array.isArray(streamsOrError) && streamsOrError.length > 0) {
            setLastSession(streamsOrError[0]);
          }
        })
        .catch((e) => console.log(e))
        .finally(() => setLastSessionLoading(false));
    }
  }, [user, stream]);

  useEffect(() => {
    if (isCopied) {
      const timeout = setTimeout(() => {
        setCopied(0);
      }, isCopied);
      return () => clearTimeout(timeout);
    }
  }, [isCopied]);

  useEffect(() => {
    getIngest(true)
      .then((ingest) => {
        if (Array.isArray(ingest)) {
          ingest.sort((a, b) => a.base.localeCompare(b.base));
        }
        setIngest(ingest);
      })
      .catch((err) => console.error(err)); // todo: surface this
  }, [id]);
  const fetchStream = useCallback(async () => {
    if (!id) {
      return;
    }
    try {
      const [res, info] = await getStreamInfo(id);
      if (res.status === 404) {
        return setNotFound(true);
      } else if ("errors" in info) {
        throw new Error(info.errors.toString());
      }
      setStream(info.stream);
      setStreamOwner(info.user);
    } catch (err) {
      console.error(err); // todo: surface this
    }
  }, [id]);
  useEffect(() => {
    fetchStream();
  }, [fetchStream]);
  const isVisible = usePageVisibility();
  useEffect(() => {
    if (!isVisible || notFound) {
      return;
    }
    const interval = setInterval(fetchStream, 5000);
    return () => clearInterval(interval);
  }, [fetchStream, isVisible, notFound]);
  const userField = useMemo(() => {
    let value = streamOwner?.email;
    if (streamOwner?.admin) {
      value += " (admin)";
    }
    if (streamOwner?.suspended) {
      value += " (suspended)";
    }
    return value;
  }, [streamOwner?.email, streamOwner?.admin, streamOwner?.suspended]);
  const playerUrl = useMemo(() => {
    if (!stream?.playbackId) {
      return "https://lvpr.tv/";
    }
    const autoplay = query.autoplay?.toString() ?? "0";
    let url = `https://lvpr.tv/?theme=fantasy&live=${stream?.playbackId}&autoplay=${autoplay}`;
    if (isStaging() || isDevelopment()) {
      url += "&monster";
    }
    return url;
  }, [query.autoplay, stream?.playbackId]);
  const [keyRevealed, setKeyRevealed] = useState(false);
  const close = () => {
    setSuspendModal(false);
    setTerminateModal(false);
    setSuspendUserModal(false);
    setDeleteModal(false);
    setRecordOffModal(false);
  };

  if (!user) {
    return <Layout />;
  }

  const getIngestURL = (
    stream: Stream,
    showKey: boolean,
    i: number
  ): string => {
    const key = showKey ? stream.streamKey : "";
    return i < ingest.length ? pathJoin(ingest[i].ingest, key) : key || "";
  };
  const getPlaybackURL = (stream: Stream, i: number): string => {
    return i < ingest.length
      ? pathJoin(ingest[i].base, "hls", `${stream.playbackId}/index.m3u8`)
      : stream.playbackId || "";
  };
  const doSetRecord = async (stream: Stream, record: boolean) => {
    console.log(`do set record ${stream.id} record ${record}`);
    setStream(null); // shows 'loading wheel' immediately
    await patchStream(stream.id, { record });
    setStream(null); // make sure that we will load updated stream
  };

  const isAdmin = query.admin === "true";
  const tabs = getTabsAdmin(2);
  const backLink = isAdmin ? "/app/admin/streams" : "/app/user";
  let { broadcasterHost, region } = stream || {};
  if (!broadcasterHost && lastSession && lastSession.broadcasterHost) {
    broadcasterHost = lastSession.broadcasterHost;
  }
  if (!region && lastSession && lastSession.region) {
    region = lastSession.region;
  }
  let broadcasterPlaybackUrl;
  const playbackId = (stream || {}).playbackId || "";
  const domain = isStaging() ? "monster" : "com";
  const globalIngestUrl = `rtmp://rtmp.livepeer.${domain}/live`;
  const globalPlaybackUrl = `https://livepeercdn.${domain}/hls/${playbackId}/index.m3u8`;

  if (stream && stream.region && !lastSession) {
    broadcasterPlaybackUrl = `https://${stream.region}.livepeer.${domain}/stream/${stream.id}.m3u8`;
  } else if (lastSession && lastSession.region) {
    broadcasterPlaybackUrl = `https://${lastSession.region}.livepeer.${domain}/stream/${playbackId}.m3u8`;
  }

  return (
    <TabbedLayout tabs={tabs} logout={logout}>
      <Container>
        {suspendModal && stream && (
          <ConfirmationModal
            actionText="Confirm"
            onClose={close}
            onAction={() => {
              const suspended = !stream.suspended;
              patchStream(stream.id, { suspended })
                .then((res) => {
                  stream.suspended = suspended;
                  setStream({ ...stream, suspended });
                })
                .catch((e) => {
                  console.error(e);
                })
                .finally(close);
            }}>
            {!stream.suspended ? (
              <div>
                Are you sure you want to suspend and block this stream? Any
                active stream sessions will immediately end. New sessions will
                be prevented from starting until unchecked.
              </div>
            ) : (
              <div>
                Are you sure you want to allow new stream sessions again?
              </div>
            )}
          </ConfirmationModal>
        )}
        {terminateModal && stream && (
          <ConfirmationModal
            actionText="Terminate"
            onClose={close}
            onAction={() => {
              terminateStream(stream.id)
                .then((res) => {
                  setResultText(`sucess: ${res}`);
                })
                .catch((e) => {
                  console.error(e);
                  setAlertText(`${e}`);
                })
                .finally(close);
            }}>
            <div>
              Are you sure you want to terminate (stop running live) stream{" "}
              <b>{stream.name}</b>? Terminating a stream will break RTMP
              connection.
            </div>
          </ConfirmationModal>
        )}
        <SuspendUserModal
          user={streamOwner}
          isOpen={suspendUserModal}
          onClose={close}
          onSuspend={fetchStream}
        />
        {deleteModal && stream && (
          <DeleteStreamModal
            streamName={stream.name}
            onClose={close}
            onDelete={() => {
              deleteStream(stream.id).then(() => Router.replace("/app/user"));
            }}
          />
        )}
        {recordOffModal && stream && (
          <Modal onClose={close}>
            <h3>Are you sure you want to turn off recoding?</h3>
            <p>
              Future stream sessions will not be recorded. In progress stream
              sessions will be recorded. Past sessions recordings will still be
              available.
            </p>
            <Flex sx={{ justifyContent: "flex-end" }}>
              <Button
                type="button"
                variant="outlineSmall"
                onClick={close}
                sx={{ mr: 2 }}>
                Cancel
              </Button>
              <Button
                type="button"
                variant="secondarySmall"
                onClick={() => {
                  close();
                  doSetRecord(stream, false);
                }}>
                Turn off recording
              </Button>
            </Flex>
          </Modal>
        )}
        <Link href={backLink} passHref>
          <A
            sx={{
              mt: 4,
              fontWeight: 500,
              mb: 3,
              color: "text",
              display: "block",
            }}>
            {"← stream list"}
          </A>
        </Link>
        {stream ? (
          <>
            <Flex
              sx={{
                justifyContent: "flex-start",
                alignItems: "baseline",
                flexDirection: "column",
              }}>
              <Heading as="h3" sx={{ mb: "0.5em" }}>
                {stream.name}
              </Heading>
              <Flex
                sx={{
                  justifyContent: "flex-end",
                  mb: 3,
                }}>
                <Box
                  sx={{
                    display: "grid",
                    alignItems: "center",
                    gridTemplateColumns: "10em auto",
                    width: "60%",
                    fontSize: 0,
                    position: "relative",
                  }}>
                  <Cell>Stream name</Cell>
                  <Cell>{stream.name}</Cell>
                  <Cell>Stream ID</Cell>
                  <Cell>
                    <ClipBut text={stream.id}></ClipBut>
                  </Cell>
                  <Cell>Stream key</Cell>
                  <Cell>
                    {keyRevealed ? (
                      <Flex>
                        {stream.streamKey}
                        <CopyToClipboard
                          text={stream.streamKey}
                          onCopy={() => setCopied(2000)}>
                          <Flex
                            sx={{
                              alignItems: "center",
                              cursor: "pointer",
                              ml: 1,
                            }}>
                            <Copy
                              sx={{
                                mr: 1,
                                width: 14,
                                height: 14,
                                color: "offBlack",
                              }}
                            />
                            {!!isCopied && (
                              <Box sx={{ fontSize: 12, color: "offBlack" }}>
                                Copied
                              </Box>
                            )}
                          </Flex>
                        </CopyToClipboard>
                      </Flex>
                    ) : (
                      <Button
                        type="button"
                        variant="outlineSmall"
                        onClick={() => setKeyRevealed(true)}
                        sx={{ mr: 0, py: "4px", fontSize: 0 }}>
                        Show secret stream key
                      </Button>
                    )}
                  </Cell>
                  <Cell>RTMP ingest URL</Cell>
                  <Cell>
                    <ShowURL text="" url={globalIngestUrl} anchor={true} />
                  </Cell>
                  <Cell>Playback URL</Cell>
                  <Cell>
                    <ShowURL text="" url={globalPlaybackUrl} anchor={true} />
                  </Cell>
                  <Box
                    sx={{
                      mx: "0.4em",
                      mt: "0.4em",
                      mb: "0",
                      gridColumn: "1/-1",
                    }}>
                    <Box
                      onClick={() =>
                        setRegionalUrlsVisible(!regionalUrlsVisible)
                      }
                      sx={{
                        cursor: "pointer",
                        display: "inline-block",
                        transform: regionalUrlsVisible
                          ? "rotate(90deg)"
                          : "rotate(0deg)",
                        transition: "transform 0.4s ease",
                      }}>
                      ▶
                    </Box>{" "}
                    Regional ingest and playback URL pairs
                  </Box>
                  <Box
                    sx={{
                      gridColumn: "1/-1",
                      position: "relative",
                      overflow: "hidden",
                      mb: "0.8em",
                    }}>
                    <Box
                      sx={{
                        position: "relative",
                        overflow: "hidden",
                        transition: "margin-bottom .4s ease",
                        mb: regionalUrlsVisible ? "0" : "-100%",
                        display: "grid",
                        alignItems: "center",
                        gridTemplateColumns: "10em auto",
                      }}>
                      <Box
                        sx={{
                          mx: "0.4em",
                          mt: "0.4em",
                          gridColumn: "1/-1",
                          width: ["100%", "100%", "75%", "50%"],
                        }}>
                        The global RTMP ingest and playback URL pair above auto
                        detects livestreamer and viewer locations to provide the
                        optimal Livepeer.com experience.
                        <Link
                          href="/docs/guides/dashboard/ingest-playback-url-pair"
                          passHref>
                          <A target="_blank">
                            <i>
                              Learn more about forgoing the global ingest and
                              playback URLs before selecting a regional URL
                              pair.
                            </i>
                          </A>
                        </Link>
                      </Box>
                      {!ingest.length && (
                        <Spinner sx={{ mb: 3, width: 32, height: 32 }} />
                      )}
                      {ingest.map((_, i) => {
                        return (
                          <>
                            <Cell>RTMP ingest URL {i + 1}</Cell>
                            <Cell>
                              <ShowURL
                                text=""
                                url={getIngestURL(stream, false, i)}
                                urlToCopy={getIngestURL(stream, false, i)}
                                anchor={false}
                              />
                            </Cell>
                            <Box
                              sx={{
                                m: "0.4em",
                                mb: "1.4em",
                              }}>
                              Playback URL {i + 1}
                            </Box>
                            <Box
                              sx={{
                                m: "0.4em",
                                mb: "1.4em",
                              }}>
                              <ShowURL
                                text=""
                                url={getPlaybackURL(stream, i)}
                                anchor={true}
                              />
                            </Box>
                          </>
                        );
                      })}
                    </Box>
                  </Box>
                  <Box sx={{ m: "0.4em", gridColumn: "1/-1" }}>
                    <hr />
                  </Box>
                  <Cell>Record sessions</Cell>
                  <Box
                    sx={{
                      m: "0.4em",
                      justifySelf: "flex-start",
                      cursor: "pointer",
                    }}>
                    <Flex
                      sx={{
                        alignItems: "flex-start",
                        justifyItems: "center",
                      }}>
                      <Label
                        onClick={() => {
                          if (!stream.record) {
                            doSetRecord(stream, true);
                          }
                        }}>
                        <Radio
                          autocomplete="off"
                          name="record-mode"
                          value={`${!!stream.record}`}
                          checked={!!stream.record}
                        />
                        <Flex sx={{ alignItems: "center" }}>On</Flex>
                      </Label>
                      <Label sx={{ ml: "0.5em" }}>
                        <Radio
                          autocomplete="off"
                          name="record-mode"
                          value={`${!stream.record}`}
                          checked={!stream.record}
                          onClick={(e) => {
                            if (stream.record) {
                              setRecordOffModal(true);
                            }
                          }}
                        />
                        <Flex sx={{ alignItems: "center" }}>Off</Flex>
                      </Label>
                      <Flex
                        sx={{
                          ml: "0.5em",
                          minWidth: "24px",
                          height: "24px",
                          alignItems: "center",
                        }}>
                        <Help
                          data-tip
                          data-for={`tooltip-record-${stream.id}`}
                          sx={{
                            color: "muted",
                            cursor: "pointer",
                            ml: 1,
                            width: "18px",
                            height: "18px",
                          }}
                        />
                      </Flex>
                    </Flex>
                    <ReactTooltip
                      id={`tooltip-record-${stream.id}`}
                      className="tooltip"
                      place="top"
                      type="dark"
                      effect="solid">
                      <p>
                        When checked, transcoded streaming sessions will be
                        recorded and stored by Livepeer.com.
                        <br /> Each recorded session will have a recording .m3u8
                        URL for playback and an MP4 download link.
                        <br />
                        This feature is currently free.
                      </p>
                    </ReactTooltip>
                  </Box>
                  <Box sx={{ m: "0.4em", gridColumn: "1/-1" }}>
                    <hr />
                  </Box>
                  <Cell>Suspend and block</Cell>
                  <Box
                    sx={{
                      m: "0.4em",
                      justifySelf: "flex-start",
                      cursor: "pointer",
                    }}>
                    <Flex
                      sx={{
                        alignItems: "flex-start",
                        justifyItems: "center",
                      }}>
                      <Label
                        onClick={() => {
                          if (!stream.suspended) {
                            setSuspendModal(true);
                          }
                        }}>
                        <Radio
                          autocomplete="off"
                          name="suspend-mode"
                          value={`${!!stream.suspended}`}
                          checked={!!stream.suspended}
                        />
                        <Flex sx={{ alignItems: "center" }}>On</Flex>
                      </Label>
                      <Label sx={{ ml: "0.5em" }}>
                        <Radio
                          autocomplete="off"
                          name="suspend-mode"
                          value={`${!stream.suspended}`}
                          checked={!stream.suspended}
                          onClick={(e) => {
                            if (stream.suspended) {
                              setSuspendModal(true);
                            }
                          }}
                        />
                        <Flex sx={{ alignItems: "center" }}>Off</Flex>
                      </Label>
                      <Flex
                        sx={{
                          ml: "0.5em",
                          minWidth: "24px",
                          height: "24px",
                          alignItems: "center",
                        }}>
                        <Help
                          data-tip
                          data-for={`tooltip-suspend-${stream.id}`}
                          sx={{
                            color: "muted",
                            cursor: "pointer",
                            ml: 1,
                            width: "18px",
                            height: "18px",
                          }}
                        />
                      </Flex>
                    </Flex>
                    <ReactTooltip
                      id={`tooltip-suspend-${stream.id}`}
                      className="tooltip"
                      place="top"
                      type="dark"
                      effect="solid">
                      <p>
                        When turned on, any active stream sessions will
                        immediately end.
                        <br />
                        New sessions will be prevented from starting until
                        turned off.
                      </p>
                    </ReactTooltip>
                  </Box>
                  <Box sx={{ m: "0.4em", gridColumn: "1/-1" }}>
                    <hr />
                  </Box>
                  <Cell>User</Cell>
                  <Cell>{userField}</Cell>
                  <Cell>Renditions</Cell>
                  <Cell>
                    <RenditionsDetails stream={stream} />
                  </Cell>
                  <Cell>Created at</Cell>
                  <Cell>
                    <RelativeTime
                      id="cat"
                      prefix="createdat"
                      tm={stream.createdAt}
                      swap={true}
                    />
                  </Cell>
                  <Cell>Last seen</Cell>
                  <Cell>
                    <RelativeTime
                      id="last"
                      prefix="lastSeen"
                      tm={stream.lastSeen}
                      swap={true}
                    />
                  </Cell>
                  <Cell>Status</Cell>
                  <Cell>{stream.isActive ? "Active" : "Idle"}</Cell>
                  <Cell>Suspended</Cell>
                  <Cell>{stream.suspended ? "Yes" : " No"}</Cell>
                  {user.admin || isStaging() || isDevelopment() ? (
                    <>
                      <Cell> </Cell>
                      <Cell>
                        <strong>Admin or staging only fields:</strong>
                      </Cell>
                    </>
                  ) : null}
                  {user.admin ? (
                    <>
                      <Cell> </Cell>
                      <Cell>
                        <strong>Admin only fields:</strong>
                      </Cell>
                      <Cell>Deleted</Cell>
                      <Cell>
                        {stream.deleted ? <strong>Yes</strong> : "No"}
                      </Cell>
                      <Cell>Source segments</Cell>
                      <Cell>{stream.sourceSegments || 0}</Cell>
                      <Cell>Transcoded segments</Cell>
                      <Cell>{stream.transcodedSegments || 0}</Cell>
                      <Cell>Source duration</Cell>
                      <Cell>
                        {formatNumber(stream.sourceSegmentsDuration || 0, 0)}{" "}
                        sec (
                        {formatNumber(
                          (stream.sourceSegmentsDuration || 0) / 60,
                          2
                        )}{" "}
                        min)
                      </Cell>
                      <Cell>Transcoded duration</Cell>
                      <Cell>
                        {formatNumber(
                          stream.transcodedSegmentsDuration || 0,
                          0
                        )}{" "}
                        sec (
                        {formatNumber(
                          (stream.transcodedSegmentsDuration || 0) / 60,
                          2
                        )}{" "}
                        min)
                      </Cell>
                      <Cell>Source bytes</Cell>
                      <Cell>{formatNumber(stream.sourceBytes || 0, 0)}</Cell>
                      <Cell>Transcoded bytes</Cell>
                      <Cell>
                        {formatNumber(stream.transcodedBytes || 0, 0)}
                      </Cell>
                      <Cell>Ingest rate</Cell>
                      <Cell>
                        {formatNumber(stream.ingestRate || 0, 3)} bytes/sec (
                        {formatNumber((stream.ingestRate || 0) * 8, 0)})
                        bits/sec
                      </Cell>
                      <Cell>Outgoing rate</Cell>
                      <Cell>
                        {formatNumber(stream.outgoingRate || 0, 3)} bytes/sec (
                        {formatNumber((stream.outgoingRate || 0) * 8, 0)})
                        bits/sec
                      </Cell>
                      <Cell>Papertrail to stream key</Cell>
                      <Cell>
                        <Box
                          as="a"
                          target="_blank"
                          href={`https://papertrailapp.com/groups/16613582/events?q=${stream.streamKey}`}
                          sx={{ userSelect: "all" }}>
                          {stream.streamKey}
                        </Box>
                      </Cell>
                      <Cell>Papertrail to playback id</Cell>
                      <Cell>
                        <Box
                          as="a"
                          target="_blank"
                          href={`https://papertrailapp.com/groups/16613582/events?q=${stream.playbackId}`}
                          sx={{ userSelect: "all" }}>
                          {stream.playbackId}
                        </Box>
                      </Cell>
                      <Cell>Papertrail to stream id</Cell>
                      <Cell>
                        <Box
                          as="a"
                          target="_blank"
                          href={`https://papertrailapp.com/groups/16613582/events?q=${stream.id}`}
                          sx={{ userSelect: "all" }}>
                          {stream.id}
                        </Box>
                      </Cell>
                      <Cell>Region/Broadcaster</Cell>
                      <Cell>
                        {region}{" "}
                        {broadcasterHost ? " / " + broadcasterHost : ""}
                        {stream && stream.mistHost
                          ? " / " + stream.mistHost
                          : ""}
                      </Cell>
                      {broadcasterPlaybackUrl ? (
                        <>
                          <Cell>Broadcaster playback</Cell>
                          <Cell>
                            <Box
                              as="a"
                              target="_blank"
                              href={broadcasterPlaybackUrl}
                              sx={{ userSelect: "all" }}>
                              {broadcasterPlaybackUrl}
                            </Box>
                          </Cell>
                        </>
                      ) : null}
                    </>
                  ) : null}
                </Box>
                <Box
                  sx={{
                    display: "block",
                    alignItems: "center",
                    width: "40%",
                  }}>
                  <iframe
                    src={playerUrl}
                    style={{ width: "100%", aspectRatio: "4 / 3" }}
                    frameBorder="0"
                    allowFullScreen
                    allow="autoplay; encrypted-media; picture-in-picture"
                    sandbox="allow-scripts"></iframe>
                </Box>
              </Flex>
              <TimedAlert
                text={resultText}
                close={() => setResultText("")}
                variant="info"
              />
              <TimedAlert
                text={alertText}
                close={() => setAlertText("")}
                variant="attention"
              />
            </Flex>
            <Flex
              sx={{
                justifyContent: "flex-end",
                mb: 3,
              }}>
              {userIsAdmin ? (
                <Flex>
                  <Button
                    sx={{ mr: 3 }}
                    type="button"
                    variant="outlineSmall"
                    onClick={() => setTerminateModal(true)}>
                    Terminate
                  </Button>
                </Flex>
              ) : null}
              {userIsAdmin ? (
                <Flex>
                  <Button
                    sx={{ mr: 3 }}
                    type="button"
                    variant="outlineSmall"
                    disabled={
                      streamOwner?.id === user?.id ||
                      streamOwner?.suspended === true
                    }
                    onClick={() => setSuspendUserModal(true)}>
                    Suspend User
                  </Button>
                </Flex>
              ) : null}
              <Button
                type="button"
                variant="outlineSmall"
                onClick={() => setDeleteModal(true)}>
                Delete
              </Button>
            </Flex>
            <StreamSessionsTable
              streamId={stream.id}
              streamName={stream.name}
            />
          </>
        ) : notFound ? (
          <Box>Not found</Box>
        ) : (
          <Flex sx={{ justifyContent: "center", alignItems: "center" }}>
            <Spinner sx={{ mr: "1em" }} />
            <Box sx={{ color: "text" }}>Loading</Box>
          </Flex>
        )}
      </Container>
    </TabbedLayout>
  );
}
Example #19
Source File: PayloadManager.tsx    From yakit with GNU Affero General Public License v3.0 4 votes vote down vote up
PayloadManagerPage: React.FC<PayloadManagerPageProp> = (props) => {
    const [groups, setGroups] = useState<string[]>([])
    const [selected, setSelected] = useState("")
    const [response, setResponse] = useState<QueryGeneralResponse<Payload>>()
    const [params, setParams] = useState<QueryPayloadParams>({
        Keyword: "",
        Group: "",
        Pagination: {Page: 1, Limit: 20, Order: "desc", OrderBy: "updated_at"}
    })
    const [selectedRows, setSelectedRows] = useState<Payload[]>([])
    const [loading, setLoading] = useState(false)
    const rowSelection = {
        selectedRowKeys: selectedRows.map((item) => item.Id),
        onChange: (selectedRowKeys, selectedRows) => setSelectedRows(selectedRows),
        fixed: true
    }
    const pagination: PaginationSchema | undefined = response?.Pagination

    const updateGroup = () => {
        ipcRenderer
            .invoke("GetAllPayloadGroup")
            .then((data: { Groups: string[] }) => {
                setGroups(data.Groups || [])
            })
            .catch((e: any) => {
                failed(e?.details || "call GetAllPayloadGroup failed")
            })
            .finally()
    }
    const updateDict = (page?: number, limit?: number) => {
        ipcRenderer
            .invoke("QueryPayload", {
                ...params,
                Group: selected,
                Pagination: {
                    ...params.Pagination,
                    Page: page || params.Pagination.Page,
                    Limit: limit || params.Pagination.Limit
                }
            } as QueryPayloadParams)
            .then((data) => {
                setResponse(data)
            })
            .catch((e: any) => {
                failed(e?.details || "query payload failed")
            })
    }
    const delDictContent = (id?: number) => {
        let params: any = {}
        if (id !== undefined) params.Id = +id
        else params.Ids = selectedRows.map((item) => +item.Id)

        ipcRenderer
            .invoke("DeletePayloadByContent", params)
            .then(() => {
                setSelectedRows([])
                updateDict()
            })
            .catch((e: any) => {
                failed("batch delete failed")
            })
    }

    useEffect(() => {
        updateGroup()
    }, [])

    useEffect(() => {
        if (!selected) {
            return
        }

        updateDict()
    }, [selected])

    return (
        <div className='payload-manager-page'>
            <PageHeader
                title={"Payload / 字典管理"}
                subTitle={`增加 / 删除 / 管理字典,可以通过 fuzz 模块 {{x(字典名)}} 来渲染`}
            />
            <Row gutter={18} style={{flexGrow: 1}}>
                <Col span={8}>
                    <AutoCard
                        title={"选择 / 查看已有字典"}
                        size={"small"} loading={loading}
                        bordered={false}
                        bodyStyle={{overflow: "auto"}}
                        extra={
                            !props.readOnly && <Form size={"small"} onSubmitCapture={(e) => e.preventDefault()}>
                                <Form.Item style={{marginBottom: 0}} label={" "} colon={false}>
                                    <Button.Group>
                                        <Button
                                            size={"small"}
                                            onClick={() => {
                                                let m = showModal({
                                                    title: "创建新的 Payload 组/字典",
                                                    content: (
                                                        <>
                                                            <CreatePayloadGroup
                                                                onLoading={() => {
                                                                    setLoading(true)
                                                                }}
                                                                onLoadingFinished={() => {
                                                                    setTimeout(() => setLoading(false), 300)
                                                                }}
                                                                Group={""}
                                                                onFinished={(e) => {
                                                                    info("创建/修改 Payload 字典/组成功")
                                                                    updateGroup()
                                                                    m.destroy()
                                                                }}
                                                            />
                                                        </>
                                                    ),
                                                    width: "60%"
                                                })
                                            }}
                                        >
                                            新增 / 扩充字典
                                        </Button>
                                        <Button
                                            size={"small"}
                                            onClick={() => {
                                                let m = showModal({
                                                    title: "上传新的 Payload 组/字典",
                                                    content: (
                                                        <>
                                                            <UploadPayloadGroup
                                                                Group={""}
                                                                onFinished={(e) => {
                                                                    info("上传 Payload 字典/组成功")
                                                                    updateGroup()
                                                                    m.destroy()
                                                                }}
                                                            />
                                                        </>
                                                    ),
                                                    width: "60%",
                                                    maskClosable: false
                                                })
                                            }}
                                        >
                                            上传字典
                                        </Button>
                                    </Button.Group>
                                </Form.Item>
                            </Form>
                        }
                    >
                        <List<string>
                            style={{height: 200}}
                            dataSource={groups}
                            renderItem={(element, index) => {
                                return (
                                    <List.Item id={index.toString()}>
                                        <Button.Group style={{width: "100%", textAlign: "left"}}>
                                            <Button
                                                style={{width: "100%", textAlign: "left"}}
                                                type={selected === element ? "primary" : undefined}
                                                onClick={(e) => setSelected(element)}
                                            >
                                                字典分组名:{element}
                                            </Button>
                                            {props.selectorHandle && <Popconfirm title={"确定要使用该字典?"}
                                                                                 onConfirm={() => {
                                                                                     props.selectorHandle && props.selectorHandle(fuzzTag(element))
                                                                                 }}
                                            >
                                                <Button type={"primary"} icon={<ThunderboltFilled/>}/>
                                            </Popconfirm>}
                                            {!props.readOnly && <Popconfirm
                                                title={"确定删除该字典吗?"}
                                                onConfirm={(e) => {
                                                    ipcRenderer
                                                        .invoke("DeletePayloadByGroup", {
                                                            Group: element
                                                        })
                                                        .then(() => {
                                                            updateGroup()
                                                            if (selected === element) {
                                                                setSelected("")
                                                                setResponse(undefined)
                                                            }
                                                        })
                                                        .catch((e: any) => {
                                                            failed("Delete Payload By Group failed")
                                                        })
                                                }}
                                            >
                                                <Button
                                                    danger={true}
                                                    icon={<DeleteOutlined/>}
                                                    type={selected === element ? "primary" : undefined}
                                                />
                                            </Popconfirm>}
                                        </Button.Group>
                                    </List.Item>
                                )
                            }}
                        />
                    </AutoCard>
                </Col>
                <Col span={16}>
                    <AutoCard
                        title={
                            <>
                                <span>字典内容</span>
                                {selectedRows.length > 0 && !props.readOnly && (
                                    <Button size='small' type='link' danger onClick={() => delDictContent()}>
                                        批量删除
                                    </Button>
                                )}
                            </>
                        }
                        size={"small"}
                        bordered={false}
                        bodyStyle={{overflow: "auto", padding: 0}}
                        extra={
                            props.readOnly ?
                                (
                                    !!props.selectorHandle ? <Button size={"small"} type={"primary"} onClick={() => {
                                        props.selectorHandle && props.selectorHandle(`{{x(${selected})}}`)
                                    }}>
                                        选择该Fuzz标签
                                    </Button> : <CopyToClipboard
                                        text={`{{x(${selected})}}`}
                                        onCopy={(text, ok) => {
                                            if (ok) success("已复制到粘贴板")
                                        }}
                                    >
                                        <Button size={"small"}>复制Fuzz标签</Button>
                                    </CopyToClipboard>
                                ) : <Form
                                    size={"small"}
                                    onSubmitCapture={(e) => {
                                        e.preventDefault()

                                        updateDict(1, 20)
                                    }}
                                    layout={"inline"}
                                    style={{marginBottom: 0}}
                                >
                                    <Form.Item style={{marginBottom: 0}}>
                                        {selected && <Tag color={"geekblue"}>{selected}</Tag>}
                                    </Form.Item>
                                    <InputItem
                                        label={"搜索"}
                                        style={{marginBottom: 0}}
                                        setValue={(Keyword) => setParams({...params, Keyword})}
                                        value={params.Keyword}
                                    />
                                    <Form.Item colon={false} label={" "} style={{marginBottom: 0}}>
                                        <Button.Group>
                                            <Button type='primary' htmlType='submit'>
                                                {" "}
                                                Search{" "}
                                            </Button>
                                            {!!props.selectorHandle ? <Button type={"primary"} onClick={() => {
                                                props.selectorHandle && props.selectorHandle(`{{x(${selected})}}`)
                                            }}>
                                                选择该Fuzz标签
                                            </Button> : <CopyToClipboard
                                                text={`{{x(${selected})}}`}
                                                onCopy={(text, ok) => {
                                                    if (ok) success("已复制到粘贴板")
                                                }}
                                            >
                                                <Button>复制Fuzz标签</Button>
                                            </CopyToClipboard>}
                                        </Button.Group>
                                    </Form.Item>
                                </Form>
                        }
                    >
                        <Table<Payload>
                            style={{height: 200}}
                            bordered={true}
                            size={"small"}
                            rowKey={(row) => row.Id}
                            rowSelection={rowSelection}
                            columns={[
                                {title: "所属字典", render: (e: Payload) => <Tag>{e.Group}</Tag>},
                                {
                                    title: "字典内容",
                                    render: (e: Payload) => (
                                        <Text style={{width: 500}} ellipsis={{tooltip: true}}>
                                            {e.Content}
                                        </Text>
                                    )
                                },
                                {
                                    title: "操作",
                                    fixed: "right",
                                    render: (e: Payload) => (
                                        <Button danger onClick={() => delDictContent(e.Id)}>
                                            删除
                                        </Button>
                                    )
                                }
                            ]}
                            onChange={(p) => {
                                updateDict(p.current, p.pageSize)
                            }}
                            pagination={{
                                size: "small",
                                pageSize: pagination?.Limit || 10,
                                total: response?.Total || 0,
                                showTotal: (i) => <Tag>共{i}条历史记录</Tag>
                            }}
                            dataSource={response?.Data}
                        />
                    </AutoCard>
                </Col>
            </Row>
        </div>
    )
}
Example #20
Source File: index.tsx    From livepeer-com with MIT License 4 votes vote down vote up
TokenTable = ({
  title = "API Keys",
  userId,
}: {
  title?: string;
  userId: string;
}) => {
  const { getApiTokens, deleteApiToken } = useApi();
  const tableProps = useTableState({ tableId: "tokenTable" });
  const deleteDialogState = useToggleState();
  const createDialogState = useToggleState();
  const [savingDeleteDialog, setSavingDeleteDialog] = useState(false);
  const [openSnackbar] = useSnackbar();

  const columns = useMemo(
    () => [
      {
        Header: "Name",
        accessor: "name",
        Cell: TextCell,
        sortType: (...params: SortTypeArgs) =>
          stringSort("original.name.children", ...params),
      },
      {
        Header: "Token",
        accessor: "token",
        width: 400,
        disableSortBy: true,
        Cell: TextCell,
      },
      {
        Header: "Last Used",
        accessor: "lastUsed",
        Cell: DateCell,
        sortType: (...params: SortTypeArgs) =>
          dateSort("original.lastUsed.date", ...params),
      },
      {
        Header: "Created",
        accessor: "createdAt",
        Cell: DateCell,
        sortType: (...params: SortTypeArgs) =>
          dateSort("original.createdAt.date", ...params),
      },
      {
        Header: "CORS Access",
        accessor: "cors",
        Cell: TextCell,
        disableSortBy: true,
      },
    ],
    []
  );

  const Key = ({ token }) => {
    const [keyRevealed, setKeyRevealed] = useState(false);

    return (
      <Box>
        {keyRevealed ? (
          <HoverCardRoot openDelay={200}>
            <HoverCardTrigger>
              <Flex css={{ height: 25, ai: "center" }}>
                <CopyToClipboard
                  text={token.id}
                  onCopy={() => openSnackbar("Copied to clipboard")}>
                  <Box
                    css={{
                      fontFamily: "monospace",
                      cursor: "pointer",
                      fontSize: "$1",
                    }}>
                    {token.id}
                  </Box>
                </CopyToClipboard>
              </Flex>
            </HoverCardTrigger>
            <HoverCardContent>
              <Text
                variant="gray"
                css={{
                  backgroundColor: "$panel",
                  borderRadius: 6,
                  px: "$3",
                  py: "$1",
                  fontSize: "$1",
                  display: "flex",
                  ai: "center",
                }}>
                <CopyIcon /> <Box css={{ ml: "$2" }}>Click to copy</Box>
              </Text>
            </HoverCardContent>
          </HoverCardRoot>
        ) : (
          <Button size="1" type="button" onClick={() => setKeyRevealed(true)}>
            Reveal key
          </Button>
        )}
      </Box>
    );
  };

  const CorsCell = (params: { cors: ApiToken["access"]["cors"] }) => {
    const { cors } = params;
    if (!cors?.allowedOrigins?.length) {
      return (
        <Tooltip
          content="This is the most secure mode for API keys, blocking access from any webpage."
          multiline>
          <Label>None</Label>
        </Tooltip>
      );
    }
    const accessLevel = cors.fullAccess ? "Full" : "Restricted";
    return (
      <Tooltip
        content={
          cors.allowedOrigins.includes("*")
            ? `${accessLevel} access allowed from any origin`
            : `${accessLevel} access allowed from: ${cors.allowedOrigins.join(
                ", "
              )}`
        }
        multiline>
        <Label>
          <i>{accessLevel}</i>
        </Label>
      </Tooltip>
    );
  };

  const fetcher: Fetcher<TokenTableData> = useCallback(async () => {
    const [tokens, nextCursor, resp, count] = await getApiTokens(userId, {
      count: true,
    });

    if (!resp.ok || !Array.isArray(tokens)) {
      throw resp;
    }

    return {
      nextCursor,
      count,
      rows: tokens.map((token) => {
        return {
          id: token.id,
          token: {
            children: <Key token={token} />,
          },
          name: {
            children: token.name,
          },
          createdAt: {
            date: new Date(token.createdAt),
            fallback: <Box css={{ color: "$primary8" }}>—</Box>,
          },
          lastUsed: {
            date: new Date(token.lastSeen),
            fallback: <i>unused</i>,
          },
          cors: {
            children: <CorsCell cors={token.access?.cors} />,
          },
        };
      }),
    };
  }, [userId]);

  const emptyState = (
    <Flex
      direction="column"
      justify="center"
      css={{
        margin: "0 auto",
        height: "calc(100vh - 400px)",
        maxWidth: 450,
      }}>
      <Heading css={{ fontWeight: 500, mb: "$3" }}>Create an API key</Heading>
      <Text variant="gray" css={{ lineHeight: 1.5, mb: "$3" }}>
        API keys allow you to authenticate API requests in your app
      </Text>
      <Link href="/docs/guides/api" passHref>
        <A variant="primary" css={{ display: "flex", ai: "center", mb: "$5" }}>
          <Box>Learn more</Box>
          <ArrowRightIcon />
        </A>
      </Link>
      <Button
        onClick={() => createDialogState.onOn()}
        css={{ alignSelf: "flex-start" }}
        size="2"
        variant="primary">
        <PlusIcon />{" "}
        <Box as="span" css={{ ml: "$2" }}>
          Create API key
        </Box>
      </Button>
    </Flex>
  );

  return (
    <>
      <Table
        {...tableProps}
        header={
          <>
            <Heading size="2" css={{ fontWeight: 600 }}>
              {title}
            </Heading>
          </>
        }
        columns={columns}
        fetcher={fetcher}
        rowSelection="all"
        emptyState={emptyState}
        selectAction={{
          onClick: deleteDialogState.onOn,
          children: (
            <>
              <Cross1Icon /> <Box css={{ ml: "$2" }}>Delete</Box>
            </>
          ),
          css: { display: "flex", alignItems: "center" },
          // @ts-ignore
          size: "2",
        }}
        createAction={{
          onClick: createDialogState.onOn,
          css: { display: "flex", alignItems: "center" },
          children: (
            <>
              <PlusIcon />{" "}
              <Box as="span" css={{ ml: "$2" }}>
                Create key
              </Box>
            </>
          ),
        }}
      />

      {/* Delete dialog */}
      <AlertDialog
        open={deleteDialogState.on}
        onOpenChange={deleteDialogState.onOff}>
        <AlertDialogContent
          css={{ maxWidth: 450, px: "$5", pt: "$4", pb: "$4" }}>
          <AlertDialogTitle asChild>
            <Heading size="1">
              Delete {tableProps.state.selectedRows.length} API token
              {tableProps.state.selectedRows.length > 1 && "s"}?
            </Heading>
          </AlertDialogTitle>
          <AlertDialogDescription asChild>
            <Text
              size="3"
              variant="gray"
              css={{ mt: "$2", lineHeight: "22px" }}>
              This will permanently remove the API token
              {tableProps.state.selectedRows.length > 1 && "s"}. This action
              cannot be undone.
            </Text>
          </AlertDialogDescription>

          <Flex css={{ jc: "flex-end", gap: "$3", mt: "$5" }}>
            <AlertDialogCancel asChild>
              <Button size="2" onClick={deleteDialogState.onOff} ghost>
                Cancel
              </Button>
            </AlertDialogCancel>
            <AlertDialogAction asChild>
              <Button
                size="2"
                disabled={savingDeleteDialog}
                onClick={async (e) => {
                  try {
                    e.preventDefault();
                    setSavingDeleteDialog(true);
                    const promises = tableProps.state.selectedRows.map(
                      async (row) => {
                        return deleteApiToken(row.original.id as string);
                      }
                    );
                    await Promise.all(promises);
                    await tableProps.state.invalidate();
                    openSnackbar(
                      `${tableProps.state.selectedRows.length} stream${
                        tableProps.state.selectedRows.length > 1 ? "s" : ""
                      } deleted.`
                    );
                    deleteDialogState.onOff();
                  } finally {
                    setSavingDeleteDialog(false);
                  }
                }}
                variant="red">
                {savingDeleteDialog && (
                  <Spinner
                    css={{
                      color: "$hiContrast",
                      width: 16,
                      height: 16,
                      mr: "$2",
                    }}
                  />
                )}
                Delete
              </Button>
            </AlertDialogAction>
          </Flex>
        </AlertDialogContent>
      </AlertDialog>

      {/* Create dialog */}
      <CreateTokenDialog
        isOpen={createDialogState.on}
        onClose={createDialogState.onOff}
        onOpenChange={createDialogState.onToggle}
        onCreateSuccess={tableProps.state.invalidate}
      />
    </>
  );
}
Example #21
Source File: index.tsx    From livepeer-com with MIT License 4 votes vote down vote up
StreamSessionsTable = ({
  title = "Sessions",
  streamId,
  emptyState = defaultEmptyState,
  border = false,
  tableLayout = "fixed",
}: {
  title?: string;
  streamId: string;
  emptyState?: React.ReactNode;
  border?: boolean;
  tableLayout?: string;
}) => {
  const { user, getStreamSessions } = useApi();
  const tableProps = useTableState({
    tableId: "streamSessionsTable",
  });
  const [openSnackbar] = useSnackbar();

  const columns = useMemo(
    () => [
      {
        Header: "Created at",
        accessor: "createdAt",
        Cell: DateCell,
        sortType: (...params: SortTypeArgs) =>
          dateSort("original.createdAt.date", ...params),
      },
      {
        Header: "Duration",
        accessor: "sourceSegmentsDuration",
        Cell: DurationCell,
        sortType: (...params: SortTypeArgs) =>
          numberSort(
            "original.sourceSegmentsDuration.sourceSegmentsDuration",
            ...params
          ),
      },
      {
        Header: "Recording URL",
        accessor: "recordingUrl",
        Cell: RecordingUrlCell,
        disableSortBy: true,
      },
    ],
    []
  );

  const fetcher: Fetcher<SessionsTableData> = useCallback(
    async (state) => {
      const [streams, nextCursor, count] = await getStreamSessions(
        streamId,
        state.cursor,
        state.pageSize,
        formatFiltersForApiRequest(state.filters, {
          parseNumber: (n) => n * 60,
        }),
        true
      );
      return {
        nextCursor,
        count,
        rows: streams.map((stream: any) => {
          return {
            id: stream.id,
            recordingUrl: {
              id: stream.id,
              showMP4: true,
              profiles:
                stream.recordingUrl &&
                stream.recordingStatus === "ready" &&
                stream.profiles?.length
                  ? [{ name: "source" }, ...stream.profiles]
                  : undefined,
              children:
                stream.recordingUrl && stream.recordingStatus === "ready" ? (
                  <HoverCardRoot openDelay={200}>
                    <HoverCardTrigger>
                      <Flex css={{ height: 25, ai: "center" }}>
                        <CopyToClipboard
                          text={stream.recordingUrl}
                          onCopy={() => openSnackbar("Copied to clipboard")}>
                          <Flex
                            css={{
                              cursor: "pointer",
                              fontSize: "$1",
                              ai: "center",
                            }}>
                            <Box css={{ mr: "$1" }}>
                              {truncate(stream.recordingUrl, 24)}
                            </Box>
                            <CopyIcon />
                          </Flex>
                        </CopyToClipboard>
                      </Flex>
                    </HoverCardTrigger>
                    <HoverCardContent>
                      <Text
                        variant="gray"
                        css={{
                          backgroundColor: "$panel",
                          borderRadius: 6,
                          px: "$3",
                          py: "$1",
                          fontSize: "$1",
                          display: "flex",
                          ai: "center",
                        }}>
                        <Box css={{ ml: "$2" }}>{stream.recordingUrl}</Box>
                      </Text>
                    </HoverCardContent>
                  </HoverCardRoot>
                ) : (
                  <Box css={{ color: "$primary8" }}>—</Box>
                ),
              mp4Url: stream.recordingUrl ? stream.recordingUrl : undefined,
            },
            sourceSegmentsDuration: {
              sourceSegmentsDuration: stream.sourceSegmentsDuration || 0,
              status: stream.recordingStatus,
            },
            createdAt: {
              date: new Date(stream.createdAt),
              fallback: <Box css={{ color: "$primary8" }}>—</Box>,
            },
          };
        }),
      };
    },
    [getStreamSessions, user.id]
  );

  return (
    <Box>
      <Table
        {...tableProps}
        header={
          <>
            <Heading>{title}</Heading>
          </>
        }
        border={border}
        filterItems={filterItems}
        columns={columns}
        fetcher={fetcher}
        rowSelection={null}
        initialSortBy={[{ id: "createdAt", desc: true }]}
        showOverflow={true}
        emptyState={emptyState}
        tableLayout={tableLayout}
      />
    </Box>
  );
}
Example #22
Source File: index.tsx    From livepeer-com with MIT License 4 votes vote down vote up
AllSessionsTable = ({ title = "Sessions" }: { title?: string }) => {
  const { user, getStreamSessionsByUserId } = useApi();
  const tableProps = useTableState({
    tableId: "allSessionsTable",
  });
  const [openSnackbar] = useSnackbar();

  const columns = useMemo(
    () => [
      {
        Header: "Stream",
        accessor: "parentStream",
        Cell: TextCell,
        sortType: (...params: SortTypeArgs) =>
          stringSort("original.parentStream.name", ...params),
      },
      {
        Header: "Created at",
        accessor: "createdAt",
        Cell: DateCell,
        sortType: (...params: SortTypeArgs) =>
          dateSort("original.createdAt.date", ...params),
      },
      {
        Header: "Duration",
        accessor: "sourceSegmentsDuration",
        Cell: DurationCell,
        sortType: (...params: SortTypeArgs) =>
          numberSort(
            "original.sourceSegmentsDuration.sourceSegmentsDuration",
            ...params
          ),
      },
      {
        Header: "Recording URL",
        accessor: "recordingUrl",
        Cell: RecordingUrlCell,
        disableSortBy: true,
      },
    ],
    []
  );

  const fetcher: Fetcher<SessionsTableData> = useCallback(
    async (state) => {
      const [streams, nextCursor, count] = await getStreamSessionsByUserId(
        user.id,
        state.cursor,
        state.pageSize,
        state.order,
        formatFiltersForApiRequest(state.filters, {
          parseNumber: (n) => n * 60,
        }),
        true
      );

      return {
        nextCursor,
        count,
        rows: streams.map((stream: any) => {
          return {
            id: stream.id,
            parentStream: {
              id: stream.parentId,
              name: stream.parentStream.name,
              children: (
                <A variant="primary" as={Box}>
                  {stream.parentStream.name}
                </A>
              ),
              tooltipChildren: stream.createdByTokenName ? (
                <>
                  Created by stream <b>{stream.parentStream.name}</b>
                </>
              ) : null,
              href: `/dashboard/streams/${stream.parentId}`,
            },
            recordingUrl: {
              id: stream.id,
              showMP4: true,
              profiles:
                stream.recordingUrl &&
                stream.recordingStatus === "ready" &&
                stream.profiles?.length
                  ? [{ name: "source" }, ...stream.profiles]
                  : undefined,
              children:
                stream.recordingUrl && stream.recordingStatus === "ready" ? (
                  <HoverCardRoot openDelay={200}>
                    <HoverCardTrigger>
                      <Flex css={{ ai: "center" }}>
                        <CopyToClipboard
                          text={stream.recordingUrl}
                          onCopy={() => openSnackbar("Copied to clipboard")}>
                          <Flex
                            css={{
                              cursor: "pointer",
                              fontSize: "$1",
                              ai: "center",
                            }}>
                            <Box css={{ mr: "$1" }}>
                              {truncate(stream.recordingUrl, 24)}
                            </Box>
                            <CopyIcon />
                          </Flex>
                        </CopyToClipboard>
                      </Flex>
                    </HoverCardTrigger>
                    <HoverCardContent>
                      <Text
                        variant="gray"
                        css={{
                          backgroundColor: "$panel",
                          borderRadius: 6,
                          px: "$3",
                          py: "$1",
                          fontSize: "$1",
                          display: "flex",
                          ai: "center",
                        }}>
                        <Box css={{ ml: "$2" }}>{stream.recordingUrl}</Box>
                      </Text>
                    </HoverCardContent>
                  </HoverCardRoot>
                ) : (
                  <Box css={{ color: "$primary8" }}>—</Box>
                ),
              mp4Url: stream.recordingUrl ? stream.recordingUrl : undefined,
            },
            sourceSegmentsDuration: {
              sourceSegmentsDuration: stream.sourceSegmentsDuration || 0,
              status: stream.recordingStatus,
            },
            createdAt: {
              date: new Date(stream.createdAt),
              fallback: <i>unseen</i>,
            },
          };
        }),
      };
    },
    [getStreamSessionsByUserId, user.id]
  );

  const emptyState = (
    <Flex
      direction="column"
      justify="center"
      css={{
        margin: "0 auto",
        height: "calc(100vh - 400px)",
        maxWidth: 450,
      }}>
      <Heading css={{ fontWeight: 500, mb: "$3" }}>No sessions</Heading>
      <Text variant="gray" css={{ lineHeight: 1.5, mb: "$3" }}>
        Stream sessions belong to parent streams.
      </Text>
      <Link href="/docs/api-reference/session/overview" passHref>
        <A variant="primary" css={{ display: "flex", ai: "center", mb: "$5" }}>
          <Box>Learn more</Box>
          <ArrowRightIcon />
        </A>
      </Link>
    </Flex>
  );

  return (
    <>
      <Table
        {...tableProps}
        columns={columns}
        fetcher={fetcher}
        initialSortBy={[{ id: "createdAt", desc: true }]}
        showOverflow={true}
        filterItems={filterItems}
        emptyState={emptyState}
        header={
          <>
            <Heading size="2" css={{ fontWeight: 600 }}>
              {title}
            </Heading>
          </>
        }
      />
    </>
  );
}
Example #23
Source File: index.tsx    From react-in-out-textarea with MIT License 4 votes vote down vote up
InOutTextarea: FC<Props> = props => {
  const [menuInOptions, setMenuInOptions] = useState<InOptions>([]);
  const [menuOutOptions, setMenuOutOptions] = useState<OutOptions>([]);
  const [inOptionsMenuRef, inOptionsMenuRefSizes] = useDimensions({
    liveMeasure,
  });
  const [outOptionsMenuRef, outOptionsMenuRefSizes] = useDimensions({
    liveMeasure,
  });
  const [convertCardRef, convertCardSizes] = useDimensions({ liveMeasure });

  const [showAdditionalInOptions, setShowAdditionalInOptions] = useState<
    boolean
  >(false);

  const [showAdditionalOutOptions, setShowAdditionalOutOptions] = useState<
    boolean
  >(false);

  const onInMoreOptionsClick = useCallback(() => {
    setShowAdditionalOutOptions(false);
    setShowAdditionalInOptions(!showAdditionalInOptions);
  }, [showAdditionalInOptions]);

  const onOutMoreOptionsClick = useCallback(() => {
    setShowAdditionalInOptions(false);
    setShowAdditionalOutOptions(!showAdditionalOutOptions);
  }, [showAdditionalOutOptions]);

  const {
    inOptions,
    inValue,
    onInInput,
    onInOptionsUpdate,
    outOptions,
    onOutOptionsUpdate,
    outValue,
    maxContentLength,
    onCopy,
    maxContentLengthIndicator,
    autoCloseMenuOnOptionSelection = true,
  } = props;

  const onInOverlayOptionClick = useCallback(() => {
    if (autoCloseMenuOnOptionSelection) setShowAdditionalInOptions(false);
  }, [autoCloseMenuOnOptionSelection]);

  const onOutOverlayOptionClick = useCallback(() => {
    if (autoCloseMenuOnOptionSelection) setShowAdditionalOutOptions(false);
  }, [autoCloseMenuOnOptionSelection]);

  return (
    <ConvertCard>
      <CaseBar>
        <SideBar>
          <OptionsContainer>
            {inOptions
              .sort(a => {
                if (a.active) return -1;
                return 0;
              })
              .map(option => {
                return (
                  <InMenuOptionStuff
                    key={option.name}
                    inOptionsMenuRefSizes={inOptionsMenuRefSizes}
                    liveMeasure={liveMeasure}
                    menuOptions={menuInOptions}
                    option={option}
                    inOptions={inOptions}
                    onInOptionsUpdate={onInOptionsUpdate}
                    setMenuOptions={setMenuInOptions}
                  />
                );
              })}
          </OptionsContainer>
          <MoreOptionsIconContainer
            ref={inOptionsMenuRef}
            onClick={onInMoreOptionsClick}
            active={showAdditionalInOptions}
          />
        </SideBar>
        <Spacer />
        <SideBar>
          <OptionsContainer>
            {outOptions
              .sort(a => {
                if (a.activeClicked) return -1;
                if (a.active) return -1;
                return 0;
              })
              .map(option => {
                return (
                  <OutMenuOptionStuff
                    key={option.name}
                    outOptionsMenuRefSizes={outOptionsMenuRefSizes}
                    liveMeasure={liveMeasure}
                    menuOptions={menuOutOptions}
                    option={option}
                    outOptions={outOptions}
                    onOutOptionsUpdate={onOutOptionsUpdate}
                    setMenuOptions={setMenuOutOptions}
                  />
                );
              })}
          </OptionsContainer>
          <MoreOptionsIconContainer
            right
            ref={outOptionsMenuRef}
            onClick={onOutMoreOptionsClick}
            active={showAdditionalOutOptions}
          />
        </SideBar>
      </CaseBar>
      <ConvertCardContent ref={convertCardRef}>
        {showAdditionalOutOptions && (
          <OptionsOverlay
            convertCardSizes={convertCardSizes}
            shownMenuOptions={menuOutOptions}
            allMenuOptions={outOptions}
            onAllMenuOptionsUpdate={onOutOptionsUpdate}
            onOptionClick={onOutOverlayOptionClick}
          />
        )}
        {showAdditionalInOptions && (
          <OptionsOverlay
            convertCardSizes={convertCardSizes}
            shownMenuOptions={menuInOptions}
            allMenuOptions={inOptions}
            onAllMenuOptionsUpdate={onInOptionsUpdate}
            onOptionClick={onInOverlayOptionClick}
          />
        )}
        <TextAreaContentTop>
          <Flex>
            <TextAreaWrapper>
              <Textarea
                data-test="from-textarea"
                placeholder="..."
                rows={2}
                smallerFont={false}
                value={inValue}
                maxLength={maxContentLength}
                onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
                  if (
                    event.target.value === null ||
                    event.target.value === undefined
                  )
                    return;
                  onInInput(event.target.value);
                }}
              />
            </TextAreaWrapper>
            <IconContainer onClick={() => onInInput('')}>
              <IconX size={32} />
            </IconContainer>
          </Flex>
          {maxContentLengthIndicator &&
            maxContentLengthIndicator.show &&
            maxContentLength && (
              <MaxContentLengthIndicator
                currentLength={inValue ? inValue.length : 0}
                maxContentLength={maxContentLength}
                maxContentLengthIndicator={maxContentLengthIndicator}
              />
            )}
        </TextAreaContentTop>
        <TextAreaContentBottom>
          <TextAreaWrapper>
            <Textarea
              disabled
              smallerFont={false}
              showCopyCursor
              value={outValue}
            />
          </TextAreaWrapper>
          <CopyToClipboard
            text={outValue}
            onCopy={() => {
              if (onCopy) {
                onCopy();
              }
            }}
          >
            <IconContainer>
              <IconCopy size={24} />
            </IconContainer>
          </CopyToClipboard>
        </TextAreaContentBottom>
      </ConvertCardContent>
    </ConvertCard>
  );
}
Example #24
Source File: Share.tsx    From gateway-ui with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
SharePage = ({ uploadReference, metadata }: Props): ReactElement => {
  const classes = useStyles()
  const navigate = useNavigate()
  const isWebsite = metadata?.isWebsite

  const bzzLink = `https://${encodeManifestReference(uploadReference)}.${BZZ_LINK_DOMAIN}/`
  const linkHeader = isWebsite ? 'Bzz Link' : 'Web link'
  const linkUrl = isWebsite ? bzzLink : `${GATEWAY_URL}${ROUTES.ACCESS_HASH(uploadReference)}`

  const [copiedToClipboard, setCopiedToClipboard] = useState<boolean>(false)
  const [activeValue, setActiveValue] = useState<string>(uploadReference)

  return (
    <Layout
      top={[
        <Header
          key="top1"
          leftAction={
            <IconButton
              onClick={() => {
                navigate(ROUTES.LANDING_PAGE)
              }}
            >
              <ArrowLeft strokeWidth={1} />
            </IconButton>
          }
        >
          {text.shareHashPage.header}
        </Header>,
        <Typography key="top2" variant="subtitle1">
          {text.shareHashPage.tagline}
        </Typography>,
      ]}
      center={[
        <Tabs
          key="center1"
          onChange={reference => {
            if (reference !== activeValue) {
              setActiveValue(reference)
              setCopiedToClipboard(false)
            }
          }}
          values={[
            {
              label: linkHeader,
              component: (
                <div>
                  <Paper
                    square
                    elevation={0}
                    style={{ overflowWrap: 'break-word', textAlign: 'left', padding: 16, margin: 4 }}
                  >
                    <Typography variant="caption">{linkUrl}</Typography>
                  </Paper>
                  {isWebsite && (
                    <Button
                      variant="contained"
                      style={{ margin: 4, width: 'auto' }}
                      className={classes.button}
                      href={bzzLink}
                      target="blank"
                    >
                      <ExternalLink strokeWidth={1} />
                      {text.accessHashPage.openWebsite}
                      <ExternalLink style={{ opacity: 0 }} />
                    </Button>
                  )}
                </div>
              ),
              value: linkUrl,
            },
            {
              label: 'Swarm hash',
              component: (
                <Paper
                  square
                  elevation={0}
                  style={{ overflowWrap: 'break-word', textAlign: 'left', padding: 16, margin: 4 }}
                >
                  <Typography variant="caption">{uploadReference}</Typography>
                </Paper>
              ),
              value: uploadReference,
            },
          ]}
        />,
      ]}
      bottom={[
        <Typography key="bottom1" variant="body2">
          {text.shareHashPage.disclaimer}
        </Typography>,
        <Footer key="bottom2">
          <CopyToClipboard text={activeValue}>
            <Button
              variant="contained"
              className={classes.button}
              size="large"
              onClick={e => {
                e.stopPropagation()
                setCopiedToClipboard(true)
              }}
            >
              {copiedToClipboard ? <Check strokeWidth={1} /> : <Clipboard strokeWidth={1} />}
              {copiedToClipboard ? text.shareHashPage.copyLinkActionSuccess : text.shareHashPage.copyLinkAction}
              {/* Needed to properly align icon to the right and label to center */}
              <Clipboard style={{ opacity: 0 }} />
            </Button>
          </CopyToClipboard>
        </Footer>,
      ]}
    />
  )
}
Example #25
Source File: RunningTimesModal.tsx    From yugong with MIT License 4 votes vote down vote up
RunningTimesModal: React.FC<Props> = ({
  visible = false,
  data,
  onCancel,
}) => {
  const [state, setstate] = useState<AnyObjectType>({});
  const [showHelp, setShowHelp] = useState<boolean>(false);
  const [runningPath, setRunningPath] = useState<string>()
  useEffect(() => {
    setstate(data);
  }, [data]);
  const onChange = useCallback(
    (e) => {
      const filterdata = {};
      Object.keys(state).forEach((key) => {
        if (key.indexOf(e.target.value) !== -1) {
          filterdata[key] = state[key];
        }
      });
      if (!e.target.value) {
        setstate(data);
      } else {
        setstate(filterdata);
      }
    },
    [data, state]
  );
  const handleClipboard = useCallback(
    (data) => {
      console.log(data);

      const arr = data.namespace;
      const path = arr.slice(1, arr.length);
      const runningPath = path.join('.');
      setRunningPath(runningPath);
      return true
    },
    [],
  )

  const onClip = useCallback(
    () => {

      const jspath = `js{{runningTimes.${runningPath}}}`;
      const path = `{{${runningPath}}}`;
      console.log(jspath, path);
    },
    [runningPath],
  )


  return (
    <>
      <Modal visible={visible} footer={null} onCancel={onCancel}>
        <div className={s.blank}>
          <Row>
            <Col>
              <Search onChange={onChange} placeholder="查找全局发布变量" />
            </Col>
            <Col>
              <div className={s.help} onClick={() => setShowHelp(true)}><ExceptionOutlined />&nbsp;运行时与EventEmitter</div>
            </Col>
          </Row>
          <ReactJson
            src={state}
            collapsed={1}
            style={{ padding: "20px" }}
            name="runningTimes"
            enableClipboard={handleClipboard}
          />
          {runningPath ? <Row gutter={10}>
            <Col>规则路径:</Col>
            <Col>{`{{${runningPath}}}`}</Col>
            <Col className={s.icon}>
              <CopyToClipboard
                text={`{{${runningPath}}}`}
                onCopy={() => message.info({ content: '已拷贝规则路径' })}
              >
                <span onClick={onClip}>{svgIcon()}</span>
              </CopyToClipboard>
            </Col></Row> : null}
          {runningPath ? <Row gutter={10}>
            <Col>脚本路径:</Col>
            <Col>{`js{{runningTimes.${runningPath}}}`}</Col>
            <Col className={s.icon}>
              <CopyToClipboard
                text={`js{{runningTimes.${runningPath}}}`}
                onCopy={() => message.info({ content: '已拷贝脚本路径' })}
              >
                <span onClick={onClip}>{svgIcon()}</span>
              </CopyToClipboard>
            </Col>
          </Row> : null}
        </div>
      </Modal>
      <Modal
        width={1000}
        footer={null}
        visible={showHelp}
        onCancel={() => setShowHelp(false)}
      >
        <img src={core} alt="运行时与事件调度" />
      </Modal>
    </>
  );
}
Example #26
Source File: CodeEditor.tsx    From yugong with MIT License 4 votes vote down vote up
Codeeditor: React.FC<Props> = () => {
  const activationItem = useSelector(
    (state: RootState) => state.activationItem
  );
  const appData = useSelector((state: RootState) => state.appData);
  const dispatch = useDispatch<Dispatch>();
  const [jsonData, setJsonData] = useState<AppDataLayoutItemTypes>();
  const [jsonMode, setJsonMode] = useState<"view" | "code">("view");

  useEffect(() => {
    setJsonData({ ...activationItem });
  }, [activationItem]);

  const [, setLocalStorage] = useLocalStorage("appData", null);
  const jsoneditor = useRef<JSONEditor>();
  const container = useRef<any>();

  const onsubmit = useCallback(() => {
    try {
      var json = jsoneditor.current?.get();
      if (
        json &&
        activationItem.moduleId === json.moduleId &&
        activationItem.moduleId === json.layout?.i
      ) {
        setJsonData(json);
        dispatch.activationItem.updateActivationItem(json);
        const operateData = produce([...appData].map((item) => {
          if (item.moduleId === json.moduleId) {
            return json;
          }
          return item;
        }), undefined, {
          name: `修改组件${activationItem.moduleName || activationItem.moduleId}`,
          desc: 'code'
        });
        dispatch.appData.updateAppData(operateData);
        setLocalStorage(operateData);
        dispatch.controller.forceUpdateByStateTag();
      }
    } catch (e) {
      return;
    }
  }, [activationItem.moduleId, activationItem.moduleName, appData, dispatch.activationItem, dispatch.appData, dispatch.controller, setLocalStorage]);

  useEffect(() => {
    if (container.current && jsonData) {
      jsoneditor.current = new JSONEditor(container.current, {
        mode: jsonMode,
        mainMenuBar: false,
      });
      jsoneditor.current.set(jsonData);
    }

    return () => {
      if (jsoneditor.current) {
        jsoneditor.current.destroy();
      }
    };
  }, [jsonData, jsonMode]);

  const onChangeJsonMode = useCallback((e) => {
    jsoneditor.current?.setMode(e.target.value);
    setJsonMode(e.target.value);
  }, []);
  return (
    <>
      <div className={s.toolbar}>
        <div className={s.modeid}>{activationItem.moduleId}</div>
        <div>
          &nbsp;
          <CopyToClipboard
            text={JSON.stringify(activationItem)}
            onCopy={() => message.info("已复制到剪切板")}
          >
            <Button size="small" icon={<CopyOutlined alt="复制到剪切板" />}>
              复制
            </Button>
          </CopyToClipboard>
          &nbsp;
          <Radio.Group
            value={jsonMode}
            size="small"
            onChange={onChangeJsonMode}
          >
            <Radio.Button value="view">预览</Radio.Button>
            <Radio.Button value="code">编辑</Radio.Button>
          </Radio.Group>
          &nbsp;
          {jsonMode === "code" ? (
            <Button
              size="small"
              type="primary"
              onClick={onsubmit}
              icon={<CopyOutlined alt="复制到剪切板" />}
            >
              保存
            </Button>
          ) : null}
          &nbsp;
        </div>
      </div>
      <div className={s.wrap} ref={container} />
    </>
  );
}
Example #27
Source File: MixedArguments.tsx    From yugong with MIT License 4 votes vote down vote up
Mixedarguments: React.FC<Props> = ({ typeArguments, onChange, className }) => {
  const [jsonData, setJsonData] = useState<AppDataLayoutItemTypes>();
  const [jsonMode, setJsonMode] = useState<'view' | 'code'>('view');

  useEffect(() => {
    const result = cloneDeep(typeArguments);
    setJsonData(result.data || {});
  }, [typeArguments]);

  const jsoneditor = useRef<JSONEditor>();
  const container = useRef<any>();

  const onsubmit = useCallback(() => {
    try {
      var json = jsoneditor.current?.get();
      if (json && onChange instanceof Function) {
        const result = cloneDeep(typeArguments);
        result.data = json;
        jsoneditor.current?.setMode('view');
        setJsonMode('view');
        onChange(result);
        message.success(`${typeArguments.name}已更新!`);
      }
    } catch (e) {
      message.error('保存失败!JSON数据格式不正确');
      return;
    }
  }, [onChange, typeArguments]);

  useEffect(() => {
    if (container.current && jsonData) {
      jsoneditor.current = new JSONEditor(container.current, {
        mode: jsonMode,
        mainMenuBar: false,
      });
      jsoneditor.current.set(jsonData);
    }

    return () => {
      if (jsoneditor.current) {
        jsoneditor.current.destroy();
      }
    };
  }, [jsonData, jsonMode]);

  const onChangeJsonMode = useCallback((e) => {
    try {
      var json = jsoneditor.current?.get();
      if (json) {
        jsoneditor.current?.setMode('code');
        setJsonMode('code');
      }
    } catch (error) {
      console.error(error);
    }
  }, []);

  return (
    <div className={classNames(className)}>
      <div className={s.toolbar} >
        <div>
          &nbsp;
          <CopyToClipboard
            text={JSON.stringify(jsonData)}
            onCopy={() => message.info('已复制到剪切板')}
          >
            <Button size="small" icon={<CopyOutlined alt="复制到剪切板" />}>
              复制
            </Button>
          </CopyToClipboard>
          &nbsp;
          {jsonMode === 'view' ? (
            <Button
              size="small"
              type="primary"
              onClick={onChangeJsonMode}
              icon={<FormOutlined alt="编辑JSON" />}
            >
              编辑
            </Button>
          ) : null}
          {jsonMode === 'code' ? (
            <Button
              size="small"
              type="primary"
              onClick={onsubmit}
              icon={<SaveOutlined alt="保存JSON" />}
            >
              保存
            </Button>
          ) : null}
          &nbsp;
        </div>
      </div>
      <div className={s.wrap} ref={container} />
    </div>
  );
}
Example #28
Source File: CodeBlock.tsx    From brilliant with MIT License 4 votes vote down vote up
CodeBlock: FC<CodeProps> = props => {
  const [isOpen, setIsOpen] = useState(false);
  const [isCopied, setIsCopied] = useState(false);
  const [isDown, setIsDown] = useState(false);
  const { blockProps, block } = props;
  const {
    getEditorState,
    setEditorState,
    readOnly,
    languages,
    renderLanguageSelect,
  } = blockProps;

  let { language } = blockProps;
  language = alias[language] || language;
  const selectedLabel = languages[language];
  const selectedValue = language;

  const copyValue = useMemo(() => (block as any).getText(), [block]);

  const options = Object.keys(languages).reduce(
    (acc, val) => [
      ...acc,
      {
        label: languages[val],
        value: val,
      },
    ],
    []
  );

  const onChange = (ev): void => {
    ev.preventDefault();
    ev.stopPropagation();
    setIsOpen(false);

    const blockKey = block.getKey();
    const editorState = getEditorState;
    const selection = editorState.getSelection();
    const language = ev.currentTarget.value;
    const blockSelection = selection.merge({
      anchorKey: blockKey,
      focusKey: blockKey,
    });

    let currentContent = editorState.getCurrentContent();
    currentContent = Modifier.mergeBlockData(currentContent, blockSelection, {
      language,
    } as any);

    const newEditorState = EditorState.push(
      editorState,
      currentContent,
      'change-block-data'
    );

    setEditorState(newEditorState);
  };

  const onSelectClick = (ev): void => {
    setIsOpen(true);
    ev.stopPropagation();
  };

  const onClickOutside = (): void => {
    if (isOpen === false) return;
    setIsOpen(false);
    const { getEditorState, setEditorState } = blockProps;

    const editorState = getEditorState;
    const selection = editorState.getSelection();

    setEditorState(EditorState.forceSelection(editorState, selection));
  };

  const handleCopyCode = (e: Event) => {
    e.stopPropagation();
    e.preventDefault();
    const plainText = (block as any).getText();
    const oDiv = document.createElement('div');
    oDiv.innerText = plainText;
    document.body.appendChild(oDiv);
    const range = document.createRange();
    window.getSelection().removeAllRanges();
    range.selectNode(oDiv);
    oDiv.style.position = 'absolute';
    oDiv.style.top = '9999';
    window.getSelection().addRange(range);
    document.execCommand('Copy');
    oDiv.remove();
  };

  const handleMouseDown = (): void => {
    toast('✅复制成功!', {
      position: 'top-center',
      autoClose: 700,
      hideProgressBar: true,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
      progress: undefined,
    });
    setIsDown(true);
  };

  return (
    <pre className="language-xxx">
      <EditorBlock {...props} />

      <CopyToClipboard
        contentEditable={false}
        text={copyValue}
        onCopy={() => setIsCopied(true)}
      >
        <div
          contentEditable={false}
          className={Styles.copy}
          onMouseDown={handleMouseDown}
          onMouseUp={() => {
            setIsDown(false);
          }}
        >
          <RichIcon
            type="icon-fuzhi"
            contentEditable={false}
            className={`${Styles.copyBtn} ${isDown ? Styles.btnDown : ''}`}
          ></RichIcon>
        </div>
      </CopyToClipboard>

      <ToastContainer className={Styles.toast} />

      {!readOnly && (
        <SwitchContainer
          onClickOutside={onClickOutside}
          onClick={onSelectClick}
        >
          {renderLanguageSelect({
            selectedLabel,
            selectedValue,
            onChange,
            options,
          })}
        </SwitchContainer>
      )}
    </pre>
  );
}
Example #29
Source File: Sider.tsx    From your_spotify with GNU General Public License v3.0 4 votes vote down vote up
export default function Sider({ className }: SiderProps) {
  const dispatch = useAppDispatch();
  const user = useSelector(selectUser);
  const location = useLocation();
  const inputRef = useRef<HTMLInputElement | null>(null);
  const [search, setSearch] = useState('');
  const results = useConditionalAPI(search.length >= 3, api.searchArtists, search);

  const reset = useCallback(() => {
    setSearch('');
  }, []);

  const copyCurrentPage = useCallback(() => {
    if (!user?.publicToken) {
      dispatch(
        alertMessage({
          level: 'error',
          message: 'No public token generated, go to the settings page to generate one',
        }),
      );
      return;
    }
    dispatch(
      alertMessage({
        level: 'info',
        message: 'Copied current page to clipboard with public token',
      }),
    );
  }, [dispatch, user?.publicToken]);

  const toCopy = useShareLink();

  return (
    <div className={clsx(s.root, className)}>
      <Link to="/" className={s.title}>
        <Text onDark element="h1">
          Your Spotify
        </Text>
      </Link>
      <input
        className={s.input}
        placeholder="Search..."
        value={search}
        onChange={(ev) => setSearch(ev.target.value)}
        ref={inputRef}
      />
      <Popper
        open={search.length > 0}
        anchorEl={inputRef.current}
        placement="bottom"
        className={s.popper}>
        <Paper className={s.results} style={{ width: inputRef.current?.clientWidth }}>
          {search.length < 3 && <Text element="strong">At least 3 characters</Text>}
          {results?.length === 0 && <Text element="strong">No results found</Text>}
          {results?.map((res) => (
            <Link to={`/artist/${res.id}`} className={s.result} key={res.id} onClick={reset}>
              <img className={s.resultimage} src={getImage(res)} alt="Artist" />
              <Text element="strong">{res.name}</Text>
            </Link>
          ))}
        </Paper>
      </Popper>
      <nav>
        {links.map((category) => (
          <div className={s.category} key={category.label}>
            <Text element="div" onDark className={s.categoryname}>
              {category.label}
            </Text>
            {toCopy &&
              category.items.map((link) => {
                if (link.link === '/share') {
                  return (
                    <CopyToClipboard key={link.label} onCopy={copyCurrentPage} text={toCopy}>
                      <div className={s.link} key={link.label}>
                        <Text onDark>
                          {location.pathname === link.link ? link.iconOn : link.icon}
                        </Text>
                        <Text onDark>{link.label}</Text>
                      </div>
                    </CopyToClipboard>
                  );
                }
                return (
                  <Link to={link.link} className={s.link} key={link.label}>
                    <Text onDark>{location.pathname === link.link ? link.iconOn : link.icon}</Text>
                    <Text onDark>{link.label}</Text>
                  </Link>
                );
              })}
          </div>
        ))}
      </nav>
    </div>
  );
}