@mui/material#Avatar TypeScript Examples

The following examples show how to use @mui/material#Avatar. 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: accountCard.tsx    From Search-Next with GNU General Public License v3.0 6 votes vote down vote up
AccountCard: React.FC<AccoundCardProps> = ({ account }) => {
  return (
    <div className="flex py-4 pl-0 items-center">
      <div className="mr-4">
        <Avatar sx={{ width: 64, height: 64 }}>
          {account.username?.split('')[0].toLocaleUpperCase()}
        </Avatar>
      </div>
      <div>
        <div className="text-lg font-bold leading-7 mb-0">
          {account.username}
          <Chip
            className="ml-2"
            size="small"
            color="info"
            label={account.type === 'local' ? '本地账户' : '注册账户'}
          ></Chip>
        </div>
        <div className="text-sm mt-1 mb-0">
          创建时间: {dayjs(account.createdTime).format('YYYY-MM-DD HH:mm')}
        </div>
      </div>
    </div>
  );
}
Example #2
Source File: MemberCard.tsx    From frontend with MIT License 6 votes vote down vote up
export default function MemberCard({ info }: MemberCardProps) {
  const { t } = useTranslation()

  return (
    <Card sx={{ backgroundColor: theme.palette.secondary.light }}>
      <Box
        m="auto"
        width={200}
        height={200}
        display="flex"
        alignItems="center"
        justifyContent="center">
        <Avatar src={info.img} sx={{ width: 150, height: 150 }} />
      </Box>
      <CardContent sx={{ paddingX: theme.spacing(1) }}>
        <Typography gutterBottom variant="h6" component="div" align="center">
          {info.title}
        </Typography>
        <Typography variant="subtitle1" align="center">
          {info.content}
        </Typography>
      </CardContent>
      <CardActions style={{ justifyContent: 'center' }}>
        <Button
          size="small"
          variant="text"
          style={{ color: 'black' }}
          href={staticUrls.blog + info.blogLink}>
          {t('common:cta.read-more')}
        </Button>
        <DoubleArrowIcon fontSize="inherit" />
      </CardActions>
    </Card>
  )
}
Example #3
Source File: FileList.tsx    From frontend with MIT License 6 votes vote down vote up
function FileList({ files, onDelete }: Props) {
  return (
    <List dense>
      {files.map((file, key) => (
        <ListItem
          key={key}
          secondaryAction={
            <IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
              <Delete />
            </IconButton>
          }>
          <ListItemAvatar>
            <Avatar>
              <UploadFile />
            </Avatar>
          </ListItemAvatar>
          <ListItemText primary={file.type} />
          <ListItemText primary={file.name} />
        </ListItem>
      ))}
    </List>
  )
}
Example #4
Source File: DetailCreate.tsx    From airmessage-web with Apache License 2.0 6 votes vote down vote up
function DirectSendButton(props: {address: string, onClick: () => void}) {
	const theme = useTheme();
	
	return (
		<ButtonBase
			onClick={props.onClick}
			sx={{
				width: "100%",
				padding: "8px 0",
				transition: theme.transitions.create(["background-color", "box-shadow", "border"], {
					duration: theme.transitions.duration.short,
				}),
				borderRadius: theme.shape.borderRadius,
				display: "flex",
				flexDirection: "row",
				justifyContent: "flex-start",
				"&:hover": {
					backgroundColor: alpha(theme.palette.text.primary, theme.palette.action.hoverOpacity),
				}
			}}>
			<Avatar sx={{
				backgroundColor: theme.palette.primary.main,
				marginRight: "16px !important"
			}} />
			<Typography>Send to <b>{props.address}</b></Typography>
		</ButtonBase>
	);
}
Example #5
Source File: Dashboard.tsx    From NekoMaid with MIT License 6 votes vote down vote up
TopCard: React.FC<{ title: string, content: React.ReactNode, icon: React.ReactNode, color: string }> = ({ title, content, icon, children, color }) =>
  <Card sx={{ height: '100%' }}>
    <CardContent>
      <Grid container spacing={3} sx={{ justifyContent: 'space-between', flexWrap: 'nowrap' }}>
        <Grid item sx={{ overflow: 'hidden' }}>
          <Typography color='textSecondary' gutterBottom variant='h6'>{title}</Typography>
          <Typography color='textPrimary' variant='h4' noWrap sx={{ textOverflow: 'ellipsis' }}>{content}</Typography>
        </Grid>
        <Grid item sx={{ paddingLeft: '0 !important' }}>
          <Avatar sx={{ backgroundColor: color, height: 50, width: 50 }}>{icon}</Avatar>
        </Grid>
      </Grid>
      {children}
    </CardContent>
  </Card>
Example #6
Source File: App.tsx    From wrap.scrt.network with MIT License 6 votes vote down vote up
ReactDOM.render(
  <BreakpointProvider>
    <React.StrictMode>
      <div style={{ minHeight: `calc(100vh - ${footerHeight})` }}>
        <App />
      </div>
      <a
        href="https://SCRT.network"
        target="_blank"
        style={{
          height: footerHeight,
          backgroundColor: "#e7e7e7",
          display: "flex",
          placeContent: "center",
          placeItems: "center",
          position: "relative",
          left: 0,
          bottom: 0,
          gap: "0.3em",
          textDecoration: "none",
        }}
      >
        <Avatar src="/scrt.svg" sx={{ width: "1em", height: "1em" }} />
        <span style={{ color: "black" }}>Powered by Secret Network</span>
      </a>
    </React.StrictMode>
  </BreakpointProvider>,
  document.getElementById("root")
);
Example #7
Source File: FileList.tsx    From frontend with MIT License 5 votes vote down vote up
function FileList({ files, onDelete, onSetFileRole, filesRole = [] }: Props) {
  const setFileRole = (file: File) => {
    return (event: SelectChangeEvent<CampaignFileRole>) => {
      if (Object.values(CampaignFileRole).includes(event.target.value as CampaignFileRole)) {
        onSetFileRole(file, event.target.value as CampaignFileRole)
      }
    }
  }

  return (
    <List dense>
      {files.map((file, key) => (
        <ListItem
          key={key}
          secondaryAction={
            <IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
              <Delete />
            </IconButton>
          }>
          <ListItemAvatar>
            <Avatar>
              <UploadFile />
            </Avatar>
          </ListItemAvatar>
          <ListItemText primary={file.type} />
          <ListItemText primary={file.name} />
          <FormControl>
            <InputLabel id="choose-type-label">{'Избери роля'}</InputLabel>
            <Select<CampaignFileRole>
              id="choose-type"
              label="Избери роля"
              labelId="choose-type-label"
              value={
                filesRole.find((f) => f.file === file.name)?.role ?? CampaignFileRole.background
              }
              onChange={setFileRole(file)}>
              {Object.values(CampaignFileRole).map((role) => (
                <MenuItem key={role} value={role}>
                  {role}
                </MenuItem>
              ))}
            </Select>
          </FormControl>
        </ListItem>
      ))}
    </List>
  )
}
Example #8
Source File: IrregularityFile.tsx    From frontend with MIT License 5 votes vote down vote up
export default function IrregularityFile({ file, irregularityId }: Props) {
  const { t } = useTranslation('irregularity')
  const queryClient = useQueryClient()
  const router = useRouter()

  const mutation = useMutation<AxiosResponse<IrregularityFileResponse>, AxiosError<ApiErrors>>({
    mutationFn: deleteIrregularityFile(file.id),
    onError: () => AlertStore.show(t('admin.alerts.error'), 'error'),
    onSuccess: () => {
      AlertStore.show(t('admin.alerts.delete-file'), 'success')
      queryClient.invalidateQueries(endpoints.irregularity.viewIrregularity(irregularityId).url)
      router.push(routes.admin.irregularity.index)
    },
  })

  const deleteFileHandler = () => {
    mutation.mutate()
  }

  return (
    <ListItem key={file.id}>
      <ListItemAvatar>
        <Avatar>
          <FilePresentIcon />
        </Avatar>
      </ListItemAvatar>
      <ListItemText primary={file.filename} />
      <></>
      <Tooltip
        title={
          'Note: This link is public on the API side! Need to correct before official release!'
        }>
        <Button>
          {/* TODO: to be discussed. Tracked in issue: https://github.com/podkrepi-bg/frontend/issues/811 */}
          <a style={{ color: 'red' }} href={API_URL + `/irregularity-file/${file.id}`}>
            {t('admin.cta.download') + '*'}
          </a>
        </Button>
      </Tooltip>
      <Button onClick={deleteFileHandler}>{t('admin.cta.delete')}</Button>
    </ListItem>
  )
}
Example #9
Source File: Dashboard.tsx    From NekoMaid with MIT License 5 votes vote down vote up
Players: React.FC<{ players?: CurrentStatus['players'] }> = React.memo(({ players }) => {
  const his = useHistory()
  const plugin = usePlugin()
  const globalData = useGlobalData()
  const [page, setPage] = useState(1)
  const [id, update] = useState(0)
  return <Card>
    <CardHeader title={lang.dashboard.onlinePlayers} />
    <Divider />
    <CardContent>
      {players?.length === 0
        ? <Empty />
        : <>
        <List sx={{ paddingTop: 0 }}>
          {players
            ? players.slice((page - 1) * 8, page * 8).map(p => {
              const name = typeof p === 'string' ? p : p.name
              return <Tooltip key={name} title={'IP: ' + ((p as any).ip || lang.unknown)}>
                <ListItem
                  secondaryAction={<>
                    <IconButton
                      edge='end'
                      size='small'
                      onClick={() => dialog(lang.dashboard.confirmKick(<span className='bold'>{name}</span>), lang.reason)
                        .then(it => it != null && plugin.emit('dashboard:kick', (res: boolean) => {
                          action(res)
                          if (!players) return
                          players.splice(players.indexOf(it!), 1)
                          update(id + 1)
                        }, name, it || null))
                      }
                    ><ExitToApp /></IconButton>
                    <IconButton edge='end' onClick={() => his.push('/NekoMaid/playerList/' + name)} size='small'><MoreHoriz /></IconButton>
                  </>
                  }
                >
                  <ListItemAvatar>
                    <Avatar
                      src={getSkin(globalData, name, true)}
                      imgProps={{ crossOrigin: 'anonymous', onClick () { his.push('/NekoMaid/playerList/' + name) }, style: { width: 40, height: 40 } }}
                      sx={{ cursor: 'pointer' }}
                      variant='rounded'
                    />
                  </ListItemAvatar>
                  <ListItemText primary={name} />
                </ListItem>
              </Tooltip>
            })
            : <LoadingList />
          }
        </List>
        {players && <Pagination
          page={page}
          onChange={(_, it) => setPage(it)}
          count={Math.max(Math.ceil(players.length / 8), 1)}
          sx={{ display: 'flex', justifyContent: 'flex-end' }}
        />}
      </>}
    </CardContent>
  </Card>
})
Example #10
Source File: PrivateMenu.tsx    From frontend with MIT License 5 votes vote down vote up
export default function PrivateMenu() {
  const { t } = useTranslation()
  const { data: session, status } = useSession()
  const [anchorEl, setAnchorEl] = useState<Element | null>(null)

  const handleMenu = (event: React.MouseEvent) => setAnchorEl(event.currentTarget)
  const handleClose = () => setAnchorEl(null)

  if (!session) {
    return null
  }

  const title = `${session.name}\n(${session.email})`
  return (
    <StyledGrid item>
      <IconButton onClick={handleMenu} size="large">
        {session?.user?.picture ? (
          <Avatar title={title} alt={title} src={session?.user?.picture} />
        ) : (
          <AccountCircle sx={{ fill: theme.palette.info.light }} />
        )}
      </IconButton>
      <Menu
        keepMounted
        id="menu-appbar"
        anchorEl={anchorEl}
        onClose={handleClose}
        open={Boolean(anchorEl)}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
        transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
        <LinkMenuItem href={routes.profile.index} className={classes.dropdownLinkText}>
          <Typography variant="button">{t('nav.profile')}</Typography>
        </LinkMenuItem>
        {status === 'authenticated' && isAdmin(session) && (
          <LinkMenuItem href={routes.admin.index} className={classes.dropdownLinkText}>
            <Typography variant="button">{t('nav.admin.index')}</Typography>
          </LinkMenuItem>
        )}
        <LinkMenuItem href={routes.logout} className={classes.dropdownLinkText}>
          <Typography variant="button">{t('nav.logout')}</Typography>
        </LinkMenuItem>
      </Menu>
    </StyledGrid>
  )
}
Example #11
Source File: GroupAvatar.tsx    From airmessage-web with Apache License 2.0 5 votes vote down vote up
function PersonAvatar(props: {person?: PersonData, className?: string, style?: React.CSSProperties}) {
	return <Avatar alt={props.person?.name} src={props.person?.avatar} className={props.className} style={props.style} />;
}
Example #12
Source File: DetailCreate.tsx    From airmessage-web with Apache License 2.0 5 votes vote down vote up
function SelectionChip(props: {selection: SelectionData, label: string, tooltip?: string, onDelete: () => void, className?: string}) {
	const chip = <Chip className={props.className} label={props.label} avatar={<Avatar src={props.selection.avatar} alt={props.selection.name} />} onDelete={props.onDelete} />;
	return !props.tooltip ? chip : <Tooltip title={props.tooltip}>{chip}</Tooltip>;
}
Example #13
Source File: Deposit.tsx    From wrap.scrt.network with MIT License 4 votes vote down vote up
export default function Deposit({
  token,
  secretAddress,
  onSuccess,
  onFailure,
}: {
  token: Token;
  secretAddress: string;
  onSuccess: (txhash: string) => any;
  onFailure: (error: any) => any;
}) {
  const [sourceAddress, setSourceAddress] = useState<string>("");
  const [availableBalance, setAvailableBalance] = useState<string>("");
  const [loadingTx, setLoading] = useState<boolean>(false);
  const [sourceCosmJs, setSourceCosmJs] =
    useState<SigningStargateClient | null>(null);
  const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
  const [fetchBalanceInterval, setFetchBalanceInterval] = useState<any>(null);
  const inputRef = useRef<any>();
  const maxButtonRef = useRef<any>();

  const sourceChain =
    chains[token.deposits[selectedChainIndex].source_chain_name];
  const targetChain = chains["Secret Network"];

  const fetchSourceBalance = async (sourceAddress: string) => {
    const url = `${
      chains[token.deposits[selectedChainIndex].source_chain_name].lcd
    }/bank/balances/${sourceAddress}`;
    try {
      const response = await fetch(url);
      const result: {
        height: string;
        result: Array<{ denom: string; amount: string }>;
      } = await response.json();

      const balance =
        result.result.find(
          (c) => c.denom === token.deposits[selectedChainIndex].from_denom
        )?.amount || "0";

      setAvailableBalance(balance);
    } catch (e) {
      console.error(`Error while trying to query ${url}:`, e);
      setAvailableBalance("Error");
    }
  };

  useEffect(() => {
    setAvailableBalance("");

    if (!sourceAddress) {
      return;
    }

    if (fetchBalanceInterval) {
      clearInterval(fetchBalanceInterval);
    }

    fetchSourceBalance(sourceAddress);
    const interval = setInterval(
      () => fetchSourceBalance(sourceAddress),
      10_000
    );
    setFetchBalanceInterval(interval);

    return () => clearInterval(interval);
  }, [sourceAddress]);

  useEffect(() => {
    (async () => {
      while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
        await sleep(100);
      }

      if (["LUNA", "UST"].includes(token.name.toUpperCase())) {
        await suggestTerraToKeplr(window.keplr);
      }
      // Initialize cosmjs on the target chain, because it has sendIbcTokens()
      const { chain_id, rpc, bech32_prefix } =
        chains[token.deposits[selectedChainIndex].source_chain_name];
      await window.keplr.enable(chain_id);
      const sourceOfflineSigner = window.getOfflineSignerOnlyAmino(chain_id);
      const depositFromAccounts = await sourceOfflineSigner.getAccounts();
      setSourceAddress(depositFromAccounts[0].address);
      const cosmjs = await SigningStargateClient.connectWithSigner(
        rpc,
        sourceOfflineSigner,
        { prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 }
      );
      setSourceCosmJs(cosmjs);
    })();
  }, [selectedChainIndex]);

  return (
    <>
      <div style={{ padding: "1.5em" }}>
        <div
          style={{
            display: "flex",
            placeItems: "center",
            gap: token.deposits.length === 1 ? "0.3em" : "0.5em",
          }}
        >
          <Typography>
            Deposit <strong>{token.name}</strong> from
          </Typography>
          <If condition={token.deposits.length === 1}>
            <Then>
              <Typography>
                <strong>
                  {token.deposits[selectedChainIndex].source_chain_name}
                </strong>
              </Typography>
            </Then>
            <Else>
              <FormControl>
                <Select
                  value={selectedChainIndex}
                  onChange={(e) =>
                    setSelectedChainIndex(Number(e.target.value))
                  }
                >
                  {token.deposits.map((chain, index) => (
                    <MenuItem value={index} key={index}>
                      <div
                        style={{
                          display: "flex",
                          gap: "0.5em",
                          placeItems: "center",
                        }}
                      >
                        <Avatar
                          src={chains[chain.source_chain_name].chain_image}
                          sx={{
                            marginLeft: "0.3em",
                            width: "1em",
                            height: "1em",
                            boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
                          }}
                        />
                        <strong>{chain.source_chain_name}</strong>
                      </div>
                    </MenuItem>
                  ))}
                </Select>
              </FormControl>
            </Else>
          </If>
          <Typography>
            to <strong>Secret Network</strong>
          </Typography>
        </div>
        <br />
        <div
          style={{
            display: "flex",
            placeContent: "space-between",
            placeItems: "center",
            gap: "1em",
          }}
        >
          <Typography sx={{ fontWeight: "bold" }}>From:</Typography>
          <CopyableAddress
            address={sourceAddress}
            explorerPrefix={sourceChain.explorer_account}
          />
        </div>
        <div
          style={{
            display: "flex",
            placeContent: "space-between",
            placeItems: "center",
            gap: "1em",
          }}
        >
          <Typography sx={{ fontWeight: "bold" }}>To:</Typography>
          <CopyableAddress
            address={secretAddress}
            explorerPrefix={targetChain.explorer_account}
          />
        </div>
        <br />
        <div
          style={{
            display: "flex",
            placeItems: "center",
            gap: "0.3em",
            marginBottom: "0.8em",
          }}
        >
          <Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
            Available to Deposit:
          </Typography>
          <Typography
            sx={{
              fontSize: "0.8em",
              opacity: 0.8,
              cursor: "pointer",
            }}
            onClick={() => {
              maxButtonRef.current.click();
            }}
          >
            {(() => {
              if (availableBalance === "") {
                return <CircularProgress size="0.6em" />;
              }

              const prettyBalance = new BigNumber(availableBalance)
                .dividedBy(`1e${token.decimals}`)
                .toFormat();

              if (prettyBalance === "NaN") {
                return "Error";
              }

              return `${prettyBalance} ${token.name}`;
            })()}
          </Typography>
        </div>
        <FormControl sx={{ width: "100%" }} variant="standard">
          <InputLabel htmlFor="Amount to Deposit">Amount to Deposit</InputLabel>
          <Input
            autoFocus
            id="Amount to Deposit"
            fullWidth
            type="text"
            inputRef={inputRef}
            startAdornment={
              <InputAdornment position="start">
                <Avatar
                  src={token.image}
                  sx={{
                    width: "1em",
                    height: "1em",
                    boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
                  }}
                />
              </InputAdornment>
            }
            endAdornment={
              <InputAdornment position="end">
                <Button
                  ref={maxButtonRef}
                  style={{
                    padding: "0.1em 0.5em",
                    minWidth: 0,
                  }}
                  onClick={() => {
                    if (availableBalance === "") {
                      return;
                    }

                    const prettyBalance = new BigNumber(availableBalance)
                      .dividedBy(`1e${token.decimals}`)
                      .toFormat();

                    if (prettyBalance === "NaN") {
                      return;
                    }

                    inputRef.current.value = prettyBalance;
                  }}
                >
                  MAX
                </Button>
              </InputAdornment>
            }
          />
        </FormControl>
      </div>
      <div
        style={{
          display: "flex",
          placeContent: "center",
          marginBottom: "0.4em",
        }}
      >
        <LoadingButton
          variant="contained"
          sx={{
            padding: "0.5em 0",
            width: "10em",
            fontWeight: "bold",
            fontSize: "1.2em",
          }}
          loading={loadingTx}
          onClick={async () => {
            if (!sourceCosmJs) {
              console.error("No cosmjs");
              return;
            }

            if (!inputRef?.current?.value) {
              console.error("Empty deposit");
              return;
            }

            const normalizedAmount = (inputRef.current.value as string).replace(
              /,/g,
              ""
            );

            if (!(Number(normalizedAmount) > 0)) {
              console.error(`${normalizedAmount} not bigger than 0`);
              return;
            }

            setLoading(true);

            const amount = new BigNumber(normalizedAmount)
              .multipliedBy(`1e${token.decimals}`)
              .toFixed(0, BigNumber.ROUND_DOWN);

            const { deposit_channel_id, deposit_gas } =
              chains[token.deposits[selectedChainIndex].source_chain_name];
            try {
              const { transactionHash } = await sourceCosmJs.sendIbcTokens(
                sourceAddress,
                secretAddress,
                {
                  amount,
                  denom: token.deposits[selectedChainIndex].from_denom,
                },
                "transfer",
                deposit_channel_id,
                undefined,
                Math.floor(Date.now() / 1000) + 15 * 60, // 15 minute timeout
                gasToFee(deposit_gas)
              );
              inputRef.current.value = "";
              onSuccess(transactionHash);
            } catch (e) {
              onFailure(e);
            } finally {
              setLoading(false);
            }
          }}
        >
          Deposit
        </LoadingButton>
      </div>
    </>
  );
}
Example #14
Source File: TokenRow.tsx    From wrap.scrt.network with MIT License 4 votes vote down vote up
export default function TokenRow({
  secretjs,
  secretAddress,
  token,
  balances,
  loadingCoinBalances,
  price,
}: {
  secretjs: SecretNetworkClient | null;
  secretAddress: string;
  loadingCoinBalances: boolean;
  token: Token;
  balances: Map<string, string>;
  price: number;
}) {
  const wrapInputRef = useRef<any>();

  const [loadingWrap, setLoadingWrap] = useState<boolean>(false);
  const [loadingUnwrap, setLoadingUnwrap] = useState<boolean>(false);
  const [tokenBalance, setTokenBalance] = useState<string>("");
  const [loadingTokenBalance, setLoadingTokenBalance] =
    useState<boolean>(false);
  const [isDepositWithdrawDialogOpen, setIsDepositWithdrawDialogOpen] =
    useState<boolean>(false);

  const updateTokenBalance = async () => {
    if (!token.address) {
      return;
    }

    if (!secretjs) {
      return;
    }

    const key = await getKeplrViewingKey(token.address);
    if (!key) {
      setTokenBalance(viewingKeyErrorString);
      return;
    }

    try {
      const result = await secretjs.query.compute.queryContract({
        address: token.address,
        codeHash: token.code_hash,
        query: {
          balance: { address: secretAddress, key },
        },
      });

      if (result.viewing_key_error) {
        setTokenBalance(viewingKeyErrorString);
        return;
      }
      setTokenBalance(result.balance.amount);
    } catch (e) {
      console.error(`Error getting balance for s${token.name}`, e);

      setTokenBalance(viewingKeyErrorString);
    }
  };

  useEffect(() => {
    (async () => {
      try {
        setLoadingTokenBalance(true);
        await updateTokenBalance();
      } finally {
        setLoadingTokenBalance(false);
      }
    })();
  }, [secretjs]);

  const denomOnSecret = token.withdrawals[0]?.from_denom;
  let balanceIbcCoin;
  let balanceToken;

  if (token.address) {
    if (loadingCoinBalances) {
      balanceIbcCoin = (
        <div>
          <div>
            Balance: <CircularProgress size="0.8em" />
          </div>
          <div style={{ opacity: 0 }}>placeholder</div>
        </div>
      );
    } else if (balances.get(denomOnSecret)) {
      balanceIbcCoin = (
        <div>
          <div
            style={{ cursor: "pointer" }}
            onClick={() => {
              wrapInputRef.current.value = new BigNumber(
                balances.get(denomOnSecret)!
              )
                .dividedBy(`1e${token.decimals}`)
                .toFixed();
            }}
          >
            {`Balance: ${new BigNumber(balances.get(denomOnSecret)!)
              .dividedBy(`1e${token.decimals}`)
              .toFormat()}`}
          </div>
          <div style={{ display: "flex", opacity: 0.7 }}>
            {usdString.format(
              new BigNumber(balances.get(denomOnSecret)!)
                .dividedBy(`1e${token.decimals}`)
                .multipliedBy(price)
                .toNumber()
            )}
          </div>
        </div>
      );
    } else {
      balanceIbcCoin = (
        <div>
          <div
            style={{ cursor: "pointer" }}
            onClick={() => {
              document.getElementById("keplr-button")?.click();
            }}
          >
            connect wallet
          </div>
          <div style={{ opacity: 0 }}>(please)</div>
        </div>
      );
    }
  } else {
    balanceIbcCoin = (
      <div>
        <div>coming soon</div>
        <div>(?)</div>
      </div>
    );
  }

  if (token.address) {
    if (!secretjs) {
      balanceToken = (
        <div>
          <div
            style={{ cursor: "pointer" }}
            onClick={() => {
              document.getElementById("keplr-button")?.click();
            }}
          >
            connect wallet
          </div>
          <div style={{ opacity: 0 }}>(please)</div>
        </div>
      );
    } else if (loadingTokenBalance) {
      balanceToken = (
        <div>
          <div>
            Balance: <CircularProgress size="0.8em" />
          </div>
          <div style={{ opacity: 0 }}>placeholder</div>
        </div>
      );
    } else if (tokenBalance == viewingKeyErrorString) {
      balanceToken = (
        <div>
          <Tooltip title="Set Viewing Key" placement="top">
            <div
              style={{ cursor: "pointer" }}
              onClick={async () => {
                await setKeplrViewingKey(token.address);
                try {
                  setLoadingTokenBalance(true);
                  await sleep(1000); // sometimes query nodes lag
                  await updateTokenBalance();
                } finally {
                  setLoadingTokenBalance(false);
                }
              }}
            >
              {`Balance: ${viewingKeyErrorString}`}
            </div>
          </Tooltip>
          <div style={{ opacity: 0 }}>placeholder</div>
        </div>
      );
    } else if (Number(tokenBalance) > -1) {
      balanceToken = (
        <div>
          <div
            style={{ cursor: "pointer" }}
            onClick={() => {
              wrapInputRef.current.value = new BigNumber(tokenBalance)
                .dividedBy(`1e${token.decimals}`)
                .toFixed();
            }}
          >
            {`Balance: ${new BigNumber(tokenBalance)
              .dividedBy(`1e${token.decimals}`)
              .toFormat()}`}
          </div>
          <div
            style={{ display: "flex", placeContent: "flex-end", opacity: 0.7 }}
          >
            {usdString.format(
              new BigNumber(tokenBalance)
                .dividedBy(`1e${token.decimals}`)
                .multipliedBy(price)
                .toNumber()
            )}
          </div>
        </div>
      );
    }
  } else {
    balanceToken = (
      <div>
        <div>coming soon</div>
        <div style={{ display: "flex", placeContent: "flex-end" }}>(?)</div>
      </div>
    );
  }

  return (
    <>
      <Breakpoint small down>
        <div
          style={{
            display: "flex",
            placeContent: "space-between",
            placeItems: "center",
            padding: "0.8rem",
            fontSize: "1.2rem",
          }}
        >
          <Avatar
            src={token.image}
            sx={{
              width: 44,
              height: 44,
              boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
            }}
          />
          <div>{token.name}</div>
          <div style={{ display: "flex" }}>
            <If condition={token.address}>
              <Then>
                <CompareArrowsIcon style={{ transform: "scale(0.9)" }} />
              </Then>
              <Else>
                <div style={{ fontSize: "0.65rem" }}>(soon™)</div>
              </Else>
            </If>
          </div>
          <div>s{token.name}</div>
          <Avatar
            src={token.image}
            sx={{
              width: 44,
              height: 44,
              boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
            }}
          />
        </div>
      </Breakpoint>
      <Breakpoint medium up>
        <div
          style={{
            display: "flex",
            placeItems: "center",
            gap: "0.8rem",
            padding: "0.4rem",
            borderRadius: 20,
          }}
        >
          <span style={{ flex: 1 }}></span>
          <Avatar
            src={token.image}
            sx={{
              width: 38,
              height: 38,
              boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
            }}
          />
          <div
            style={{
              display: "flex",
              width: 150,
            }}
          >
            <div
              style={{
                display: "flex",
                flexDirection: "column",
                placeItems: "flex-start",
              }}
            >
              <div
                style={{
                  display: "flex",
                  placeContent: "space-between",
                  placeItems: "center",
                  minWidth: "5.6em",
                }}
              >
                <span>{token.name}</span>
                <When condition={token.address && secretAddress}>
                  <>
                    <Tooltip title={`IBC Deposit & Withdraw`} placement="top">
                      <Button
                        style={{
                          minWidth: 0,
                          padding: 0,
                          marginBottom: "0.2em",
                          color: "black",
                          opacity: 0.8,
                        }}
                        onClick={async () =>
                          setIsDepositWithdrawDialogOpen(true)
                        }
                      >
                        <CompareArrowsIcon sx={{ height: "0.8em" }} />
                      </Button>
                    </Tooltip>
                    <DepositWithdrawDialog
                      token={token}
                      balances={balances}
                      secretAddress={secretAddress}
                      secretjs={secretjs}
                      isOpen={isDepositWithdrawDialogOpen}
                      setIsOpen={setIsDepositWithdrawDialogOpen}
                    />
                  </>
                </When>
              </div>
              <span style={{ fontSize: "0.75rem" }}>{balanceIbcCoin}</span>
            </div>
          </div>
          <div
            style={{
              display: "flex",
              placeItems: "center",
              gap: "0.3rem",
            }}
          >
            <Button
              disabled={token.address === ""}
              size="small"
              variant="text"
              startIcon={
                <If condition={loadingUnwrap}>
                  <Then>
                    <CircularProgress size="0.8em" />
                  </Then>
                  <Else>
                    <KeyboardArrowLeftIcon />
                  </Else>
                </If>
              }
              onClick={async () => {
                if (
                  !secretjs ||
                  !secretAddress ||
                  loadingWrap ||
                  loadingUnwrap
                ) {
                  return;
                }
                const baseAmount = wrapInputRef?.current?.value;
                const amount = new BigNumber(baseAmount)
                  .multipliedBy(`1e${token.decimals}`)
                  .toFixed(0, BigNumber.ROUND_DOWN);
                if (amount === "NaN") {
                  console.error("NaN amount", baseAmount);
                  return;
                }
                setLoadingUnwrap(true);
                try {
                  const tx = await secretjs.tx.broadcast(
                    [
                      new MsgExecuteContract({
                        sender: secretAddress,
                        contract: token.address,
                        codeHash: token.code_hash,
                        sentFunds: [],
                        msg: {
                          redeem: {
                            amount,
                            denom:
                              token.name === "SCRT"
                                ? undefined
                                : token.withdrawals[0].from_denom,
                          },
                        },
                      }),
                    ],
                    {
                      gasLimit: 40_000,
                      gasPriceInFeeDenom: 0.25,
                      feeDenom: "uscrt",
                    }
                  );

                  if (tx.code === 0) {
                    wrapInputRef.current.value = "";
                    console.log(`Unwrapped successfully`);
                  } else {
                    console.error(`Tx failed: ${tx.rawLog}`);
                  }
                } finally {
                  setLoadingUnwrap(false);
                  try {
                    setLoadingTokenBalance(true);
                    await sleep(1000); // sometimes query nodes lag
                    await updateTokenBalance();
                  } finally {
                    setLoadingTokenBalance(false);
                  }
                }
              }}
            >
              <Breakpoint medium up>
                Unwrap
              </Breakpoint>
            </Button>
            <Input
              disabled={token.address === ""}
              // TODO add input validation
              placeholder="Amount"
              inputProps={{
                style: {
                  textAlign: "center",
                  textOverflow: "ellipsis",
                },
              }}
              inputRef={wrapInputRef}
            />
            <Button
              disabled={token.address === ""}
              size="small"
              variant="text"
              endIcon={
                loadingWrap ? (
                  <CircularProgress size="0.8em" />
                ) : (
                  <KeyboardArrowRightIcon />
                )
              }
              onClick={async () => {
                if (
                  !secretjs ||
                  !secretAddress ||
                  loadingWrap ||
                  loadingUnwrap
                ) {
                  return;
                }
                const baseAmount = wrapInputRef?.current?.value;
                const amount = new BigNumber(baseAmount)
                  .multipliedBy(`1e${token.decimals}`)
                  .toFixed(0, BigNumber.ROUND_DOWN);
                if (amount === "NaN") {
                  console.error("NaN amount", baseAmount);
                  return;
                }
                setLoadingWrap(true);
                try {
                  const tx = await secretjs.tx.broadcast(
                    [
                      new MsgExecuteContract({
                        sender: secretAddress,
                        contract: token.address,
                        codeHash: token.code_hash,
                        sentFunds: [
                          { denom: token.withdrawals[0].from_denom, amount },
                        ],
                        msg: { deposit: {} },
                      }),
                    ],
                    {
                      gasLimit: 40_000,
                      gasPriceInFeeDenom: 0.25,
                      feeDenom: "uscrt",
                    }
                  );

                  if (tx.code === 0) {
                    wrapInputRef.current.value = "";
                    console.log(`Wrapped successfully`);
                  } else {
                    console.error(`Tx failed: ${tx.rawLog}`);
                  }
                } finally {
                  setLoadingWrap(false);
                  try {
                    setLoadingTokenBalance(true);
                    await sleep(1000); // sometimes query nodes lag
                    await updateTokenBalance();
                  } finally {
                    setLoadingTokenBalance(false);
                  }
                }
              }}
            >
              <Breakpoint medium up>
                Wrap
              </Breakpoint>
            </Button>
          </div>
          <div
            style={{
              display: "flex",
              width: 150,
              placeContent: "flex-end",
            }}
          >
            <div
              style={{
                display: "flex",
                flexDirection: "column",
                placeItems: "flex-end",
              }}
            >
              <span>s{token.name}</span>
              <div
                style={{
                  fontSize: "0.75rem",
                  display: "flex",
                  placeItems: "flex-start",
                  gap: "0.2em",
                }}
              >
                <span>{balanceToken}</span>
                <When condition={token.address && secretAddress}>
                  <Tooltip title="Refresh Balance" placement="top">
                    <Button
                      style={{
                        color: "black",
                        minWidth: 0,
                        padding: 0,
                        display: loadingTokenBalance ? "none" : undefined,
                      }}
                      onClick={async () => {
                        try {
                          setLoadingTokenBalance(true);
                          await updateTokenBalance();
                        } finally {
                          setLoadingTokenBalance(false);
                        }
                      }}
                    >
                      <RefreshIcon sx={{ height: "0.7em" }} />
                    </Button>
                  </Tooltip>
                </When>
              </div>
            </div>
          </div>
          <Avatar
            src={token.image}
            sx={{
              width: 38,
              height: 38,
              boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
            }}
          />
          <span style={{ flex: 1 }}></span>
        </div>
      </Breakpoint>
    </>
  );
}
Example #15
Source File: Withdraw.tsx    From wrap.scrt.network with MIT License 4 votes vote down vote up
export default function Withdraw({
  token,
  secretjs,
  secretAddress,
  balances,
  onSuccess,
  onFailure,
}: {
  token: Token;
  secretjs: SecretNetworkClient | null;
  secretAddress: string;
  balances: Map<string, string>;
  onSuccess: (txhash: string) => any;
  onFailure: (error: any) => any;
}) {
  const [targetAddress, setTargetAddress] = useState<string>("");
  const [loadingTx, setLoading] = useState<boolean>(false);
  const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
  const inputRef = useRef<any>();
  const maxButtonRef = useRef<any>();

  const sourceChain = chains["Secret Network"];
  const targetChain =
    chains[token.withdrawals[selectedChainIndex].target_chain_name];

  const availableBalance =
    balances.get(token.withdrawals[selectedChainIndex].from_denom) || "";

  useEffect(() => {
    (async () => {
      while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
        await sleep(100);
      }

      // Find address on target chain
      const { chain_id: targetChainId } =
        chains[token.withdrawals[selectedChainIndex].target_chain_name];
      if (token.withdrawals[selectedChainIndex].target_chain_name === "Terra") {
        await suggestTerraToKeplr(window.keplr);
      }
      await window.keplr.enable(targetChainId);
      const targetOfflineSigner =
        window.getOfflineSignerOnlyAmino(targetChainId);
      const targetFromAccounts = await targetOfflineSigner.getAccounts();
      setTargetAddress(targetFromAccounts[0].address);
    })();
  }, [selectedChainIndex]);

  return (
    <>
      <div style={{ padding: "1.5em" }}>
        <div
          style={{
            display: "flex",
            placeItems: "center",
            gap: "0.5em",
          }}
        >
          <Typography>
            Withdraw <strong>{token.name}</strong> from{" "}
            <strong>Secret Network</strong> to
          </Typography>
          <If condition={token.withdrawals.length === 1}>
            <Then>
              <Typography sx={{ marginLeft: "-0.2em" }}>
                <strong>
                  {token.withdrawals[selectedChainIndex].target_chain_name}
                </strong>
              </Typography>
            </Then>
            <Else>
              <FormControl>
                <Select
                  value={selectedChainIndex}
                  onChange={(e) =>
                    setSelectedChainIndex(Number(e.target.value))
                  }
                >
                  {token.withdrawals.map((chain, index) => (
                    <MenuItem value={index} key={index}>
                      <div
                        style={{
                          display: "flex",
                          gap: "0.5em",
                          placeItems: "center",
                        }}
                      >
                        <Avatar
                          src={chains[chain.target_chain_name].chain_image}
                          sx={{
                            marginLeft: "0.3em",
                            width: "1em",
                            height: "1em",
                            boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
                          }}
                        />
                        <strong>{chain.target_chain_name}</strong>
                      </div>
                    </MenuItem>
                  ))}
                </Select>
              </FormControl>
            </Else>
          </If>
        </div>
        <br />
        <div
          style={{
            display: "flex",
            placeContent: "space-between",
            placeItems: "center",
            gap: "1em",
          }}
        >
          <Typography sx={{ fontWeight: "bold" }}>From:</Typography>
          <CopyableAddress
            address={secretAddress}
            explorerPrefix={sourceChain.explorer_account}
          />
        </div>
        <div
          style={{
            display: "flex",
            placeContent: "space-between",
            placeItems: "center",
            gap: "1em",
          }}
        >
          <Typography sx={{ fontWeight: "bold" }}>To:</Typography>
          <CopyableAddress
            address={targetAddress}
            explorerPrefix={targetChain.explorer_account}
          />
        </div>
        <br />
        <div
          style={{
            display: "flex",
            placeItems: "center",
            gap: "0.3em",
            marginBottom: "0.8em",
          }}
        >
          <Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
            Available to Withdraw:
          </Typography>
          <Typography
            sx={{
              fontSize: "0.8em",
              opacity: 0.8,
              cursor: "pointer",
            }}
            onClick={() => {
              maxButtonRef.current.click();
            }}
          >
            {(() => {
              if (availableBalance === "") {
                return <CircularProgress size="0.6em" />;
              }

              const prettyBalance = new BigNumber(availableBalance)
                .dividedBy(`1e${token.decimals}`)
                .toFormat();

              if (prettyBalance === "NaN") {
                return "Error";
              }

              return `${prettyBalance} ${token.name}`;
            })()}
          </Typography>
        </div>
        <FormControl sx={{ width: "100%" }} variant="standard">
          <InputLabel htmlFor="Amount to Withdraw">
            Amount to Withdraw
          </InputLabel>
          <Input
            autoFocus
            id="Amount to Withdraw"
            fullWidth
            type="text"
            inputRef={inputRef}
            startAdornment={
              <InputAdornment position="start">
                <Avatar
                  src={token.image}
                  sx={{
                    width: "1em",
                    height: "1em",
                    boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
                  }}
                />
              </InputAdornment>
            }
            endAdornment={
              <InputAdornment position="end">
                <Button
                  ref={maxButtonRef}
                  style={{
                    padding: "0.1em 0.5em",
                    minWidth: 0,
                  }}
                  onClick={() => {
                    if (availableBalance === "") {
                      return;
                    }

                    const prettyBalance = new BigNumber(availableBalance)
                      .dividedBy(`1e${token.decimals}`)
                      .toFormat();

                    if (prettyBalance === "NaN") {
                      return;
                    }

                    inputRef.current.value = prettyBalance;
                  }}
                >
                  MAX
                </Button>
              </InputAdornment>
            }
          />
        </FormControl>
      </div>
      <div
        style={{
          display: "flex",
          placeContent: "center",
          marginBottom: "0.4em",
        }}
      >
        <LoadingButton
          variant="contained"
          sx={{
            padding: "0.5em 0",
            width: "10em",
            fontWeight: "bold",
            fontSize: "1.2em",
          }}
          loading={loadingTx}
          onClick={async () => {
            if (!secretjs) {
              console.error("No secretjs");
              return;
            }

            if (!inputRef?.current?.value) {
              console.error("Empty withdraw");
              return;
            }

            const normalizedAmount = (inputRef.current.value as string).replace(
              /,/g,
              ""
            );

            if (!(Number(normalizedAmount) > 0)) {
              console.error(`${normalizedAmount} not bigger than 0`);
              return;
            }

            setLoading(true);

            const amount = new BigNumber(normalizedAmount)
              .multipliedBy(`1e${token.decimals}`)
              .toFixed(0, BigNumber.ROUND_DOWN);

            const { withdraw_channel_id, withdraw_gas } =
              chains[token.withdrawals[selectedChainIndex].target_chain_name];
            try {
              const tx = await secretjs.tx.broadcast(
                [
                  new MsgTransfer({
                    sender: secretAddress,
                    receiver: targetAddress,
                    sourceChannel: withdraw_channel_id,
                    sourcePort: "transfer",
                    token: {
                      amount,
                      denom: token.withdrawals[selectedChainIndex].from_denom,
                    },
                    timeoutTimestampSec: String(
                      Math.floor(Date.now() / 1000) + 15 * 60
                    ), // 15 minute timeout
                  }),
                ],
                {
                  gasLimit: withdraw_gas,
                  gasPriceInFeeDenom: 0.25,
                  feeDenom: "uscrt",
                }
              );

              if (tx.code === 0) {
                inputRef.current.value = "";
                onSuccess(tx.transactionHash);
              } else {
                onFailure(tx.rawLog);
              }
            } catch (e) {
              onFailure(e);
            } finally {
              setLoading(false);
            }
          }}
        >
          Withdraw
        </LoadingButton>
      </div>
    </>
  );
}
Example #16
Source File: siteCard.tsx    From Search-Next with GNU General Public License v3.0 4 votes vote down vote up
SiteCard: React.FC<SiteCardPropTypes> = ({
  type = 'view',
  item,
  onClick,
  onAdd,
  onEdit,
  onRemove,
}) => {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);

  const onMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.stopPropagation();
    setAnchorEl(event.currentTarget);
  };

  const onMenuClose = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.stopPropagation();
    setAnchorEl(null);
  };

  const siteClick = () => {
    if (onClick) onClick();
  };

  const handleEdit = () => {
    setAnchorEl(null);
    if (onEdit && item) onEdit(item);
  };

  const handleRemove = () => {
    setAnchorEl(null);
    if (onRemove && item) onRemove(item._id);
  };

  return (
    <>
      <div
        className={classNames(
          'cursor-pointer bg-transparent rounded shadow-none relative w-28 h-20 transition-all hover:bg-rgba-gray-3',
          css`
            &:hover {
              .handle-container {
                opacity: 1 !important;
              }
            }
          `,
        )}
        onClick={onClick}
      >
        <div
          className={classNames([
            'content-container transition-all box-border flex flex-col items-center w-full h-full px-3 py-2 rounded',
            type === 'add' ? 'justify-start text-primary' : 'justify-end',
          ])}
        >
          <Avatar
            className={classNames(
              'bg-white rounded mx-auto',
              {
                'text-primary': type === 'add',
              },
              css`
                img {
                  max-width: 24px;
                  height: 24px;
                }
              `,
            )}
            variant="rounded"
            src={item?.iconUrl ? item?.iconUrl : getWebIconByUrl(item?.url)}
            onClick={onAdd ? onAdd : undefined}
          >
            {type === 'add' && <AddOutlined />}
          </Avatar>
          <p className="text-center flex justify-center mt-1 w-full text-var-main-10">
            <a className="overflow-hidden text-ellipsis">{item?.name}</a>
          </p>
        </div>
        {type === 'view' && (
          <span className="handle-container w-6 h-6 flex justify-center absolute top-1 right-1 z-10 opacity-0 transition-all">
            <IconButton
              className="rounded"
              size="small"
              aria-controls={`site-${item?.name}-menu`}
              aria-haspopup="true"
              onClick={onMenuOpen}
            >
              <MoreHorizOutlined className="text-var-main-10" />
            </IconButton>
          </span>
        )}
      </div>
      <Menu
        id={`site-${item?.name}-menu`}
        open={Boolean(anchorEl)}
        anchorEl={anchorEl}
        onClose={onMenuClose}
        config={[
          {
            label: '修改',
            onClick: handleEdit,
            icon: EditOutlined,
          },
          {
            label: '删除',
            onClick: handleRemove,
            icon: DeleteOutlineOutlined,
          },
        ]}
      />
    </>
  );
}
Example #17
Source File: websiteCardNew.tsx    From Search-Next with GNU General Public License v3.0 4 votes vote down vote up
WebsiteCardNew: React.FC<WebsiteCardNewProps> = (props) => {
  const { datasource } = props;
  const { name, intro, color, url } = datasource;

  const onAdd = () => {
    const res = addSite({
      name,
      url: url.substring(0, url.lastIndexOf('/')),
    });
    if (res) toast.success('添加成功');
  };

  const onCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(url);
      toast.success(`已复制 ${name} (${url})`);
    } else {
      const copy = new Clipboard(`.copy-button_${name}`);
      copy.on('success', (e) => {
        toast.success(`已复制 ${name} (${url})`);
      });
      copy.on('error', function (e) {
        toast.warning(
          `您的浏览器不支持复制功能,请点击跳转到该网站手动复制地址`,
        );
      });
    }
  };

  const onMore = () => {
    toast.warning('功能开发中...');
  };

  return (
    <div
      className={classNames(
        'cursor-pointer shadow-md rounded border-b-2',
        css`
          --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
            0 1px 3px 0 ${hexToRgba(color ?? '#000', 0.45).rgba} !important;
          border-bottom-color: ${color};
        `,
      )}
    >
      <CardActionArea>
        <Tooltip title={intro || '暂无介绍'}>
          <div className="p-3 flex gap-3" onClick={() => window.open(url)}>
            <Avatar
              // style={{ backgroundColor: color }}
              src={getWebIconByUrl(url)}
            >
              {name.split('')[0].toUpperCase()}
            </Avatar>
            <div className="flex-grow overflow-hidden">
              <p className="font-bold text-base whitespace-nowrap overflow-x-hidden">
                {name}
              </p>
              <Overflow>{(intro as any) || ('暂无介绍' as any)}</Overflow>
            </div>
          </div>
        </Tooltip>
      </CardActionArea>
      <div>
        <ButtonGroup
          disableElevation
          variant="text"
          size="small"
          className={classNames(
            'w-full h-full flex',
            css`
              justify-content: flex-end;
              button {
                height: 100%;
                border-right: 0px !important;
              }
            `,
          )}
        >
          <Tooltip title="添加到首页">
            <Button onClick={onAdd}>
              <Add />
            </Button>
          </Tooltip>
          <Tooltip title="复制网站链接">
            <Button
              className={`copy-button_${name}`}
              data-clipboard-text={url}
              onClick={onCopy}
            >
              <CopyAll />
            </Button>
          </Tooltip>
          {false && (
            <Tooltip title="更多">
              <Button onClick={onMore}>
                <MoreHoriz />
              </Button>
            </Tooltip>
          )}
        </ButtonGroup>
      </div>
    </div>
  );
}
Example #18
Source File: PlayerList.tsx    From NekoMaid with MIT License 4 votes vote down vote up
Players: React.FC = () => {
  const his = useHistory()
  const plugin = usePlugin()
  const [page, setPage] = useState(0)
  const [loading, setLoading] = useState(true)
  const [state, setState] = useState<number | null>(null)
  const [activedPlayer, setActivedPlayer] = useState<PlayerData | null>(null)
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
  const [data, setData] = useState<{ count: number, players: PlayerData[] }>(() => ({ count: 0, players: [] }))
  const globalData = useGlobalData()
  const { hasWhitelist } = globalData
  const refresh = () => {
    setLoading(true)
    plugin.emit('playerList:fetchPage', (it: any) => {
      if (it.players == null) it.players = []
      setData(it)
      setLoading(false)
    }, page, state === 1 || state === 2 ? state : 0, null)
  }
  useMemo(refresh, [page, state])
  const close = () => {
    setAnchorEl(null)
    setActivedPlayer(null)
  }

  return <Card>
    <CardHeader
      title={lang.playerList.title}
      action={
        <ToggleButtonGroup
          size='small'
          color={(state === 1 ? 'warning' : state === 2 ? 'error' : undefined) as any}
          value={state}
          exclusive
          onChange={(_, it) => {
            if (it === 3) return
            setState(it)
            if (state === 3) refresh()
          }}
        >
          <ToggleButton disabled={loading} value={1}><Star /></ToggleButton>
          <ToggleButton disabled={loading} value={2}><Block /></ToggleButton>
          <ToggleButton disabled={loading} value={3} onClick={() => state !== 3 && dialog(lang.playerList.nameToSearch, lang.username)
            .then(filter => {
              if (filter == null) return
              his.push('/NekoMaid/playerList/' + filter)
              setState(3)
              setLoading(true)
              plugin.emit('playerList:fetchPage', (it: any) => {
                if (it.players == null) it.players = []
                setPage(0)
                setData(it)
                setLoading(false)
              }, page, 0, filter.toLowerCase())
            })}><Search /></ToggleButton>
        </ToggleButtonGroup>
      }
    />
    <Divider />
    <Box sx={{ position: 'relative' }}>
      <CircularLoading loading={loading} />
      <TableContainer>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell padding='checkbox' />
              <TableCell>{lang.username}</TableCell>
              <TableCell align='right'>{minecraft['stat.minecraft.play_time']}</TableCell>
              <TableCell align='right'>{lang.playerList.lastPlay}</TableCell>
              <TableCell align='right'>{lang.operations}</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {data.players.map(it => <TableRow key={it.name}>
              <TableCell sx={{ cursor: 'pointer', padding: theme => theme.spacing(1, 1, 1, 2) }} onClick={() => his.push('/NekoMaid/playerList/' + it.name)}>
                <Avatar src={getSkin(globalData, it.name, true)} imgProps={{ crossOrigin: 'anonymous', style: { width: 40, height: 40 } }} variant='rounded' />
              </TableCell>
              <TableCell>{it.name}</TableCell>
              <TableCell align='right'>{dayjs.duration(it.playTime / 20, 'seconds').humanize()}</TableCell>
              <TableCell align='right'>{dayjs(it.lastOnline).fromNow()}</TableCell>
              <TableCell align='right'>
                {(state === 1 || hasWhitelist) && <Tooltip title={lang.playerList[it.whitelisted ? 'clickToRemoveWhitelist' : 'clickToAddWhitelist']}>
                  <IconButton onClick={() => whitelist(it.name, plugin, refresh, !it.whitelisted)}>
                    {it.whitelisted ? <Star color='warning' /> : <StarBorder />}
                  </IconButton>
                </Tooltip>}
                <Tooltip title={it.ban == null ? lang.playerList.clickToBan : lang.playerList.banned + ': ' + it.ban}>
                  <IconButton onClick={() => banPlayer(it.name, plugin, refresh, it.ban == null)}>
                    <Block color={it.ban == null ? undefined : 'error'} />
                  </IconButton>
                </Tooltip>
                {actions.length
                  ? <IconButton onClick={e => {
                    setActivedPlayer(anchorEl ? null : it)
                    setAnchorEl(anchorEl ? null : e.currentTarget)
                  }}><MoreHoriz /></IconButton>
                  : null}
              </TableCell>
            </TableRow>)}
          </TableBody>
        </Table>
      </TableContainer>
      <TablePagination
        rowsPerPageOptions={[]}
        component='div'
        count={data.count}
        rowsPerPage={10}
        page={page}
        onPageChange={(_, it) => !loading && setPage(it)}
      />
    </Box>
    <Menu
      anchorEl={anchorEl}
      open={Boolean(anchorEl)}
      onClose={() => setAnchorEl(null)}
    >{actions.map((It, i) => <It key={i} onClose={close} player={activedPlayer} />)}</Menu>
  </Card>
}
Example #19
Source File: DonationTable.tsx    From frontend with MIT License 4 votes vote down vote up
function DonationTable({ donations }: DonationTableProps) {
  const { t, i18n } = useTranslation()
  const [fromDate, setFromDate] = React.useState<Date | null>(null)
  const [toDate, setToDate] = React.useState<Date | null>(null)
  const [monthly, setMonthly] = React.useState(true)
  const [oneTime, setOneTime] = React.useState(true)
  const filteredByTypeDonations = useMemo(() => {
    if (monthly && oneTime) {
      return donations
    }
    if (!monthly && !oneTime) {
      return []
    }
    if (monthly) {
      return donations?.filter((d) => d.type !== 'donation')
    }
    if (oneTime) {
      return donations?.filter((d) => d.type === 'donation')
    }
    return donations
  }, [donations, monthly, oneTime])
  const filteredDonations = useMemo(() => {
    if (!fromDate && !toDate) {
      return filteredByTypeDonations
    }
    if (fromDate && toDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isAfter(createdAtDate, fromDate) && isBefore(createdAtDate, toDate)
      })
    }
    if (fromDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isAfter(createdAtDate, fromDate)
      })
    }
    if (toDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isBefore(createdAtDate, toDate)
      })
    }
  }, [filteredByTypeDonations, fromDate, toDate])
  return (
    <Card sx={{ padding: theme.spacing(2) }}>
      <Grid container alignItems={'flex-start'} spacing={theme.spacing(2)}>
        <Grid item xs={6} sm={3}>
          <CheckboxLabel>{t('profile:donations.oneTime')}</CheckboxLabel>
          <Checkbox
            onChange={(e, checked) => setOneTime(checked)}
            checked={oneTime}
            name="oneTime"
          />
        </Grid>
        <Grid item xs={6} sm={3}>
          <CheckboxLabel>{t('profile:donations.monthly')}</CheckboxLabel>
          <Checkbox
            onChange={(e, checked) => setMonthly(checked)}
            checked={monthly}
            name="monthly"
          />
        </Grid>
        <LocalizationProvider
          locale={i18n.language === 'bg' ? bg : enUS}
          dateAdapter={AdapterDateFns}>
          <Grid item xs={12} sm={3}>
            <DatePicker
              label={t('profile:donations.fromDate')}
              value={fromDate}
              onChange={setFromDate}
              renderInput={(params) => <TextField size="small" {...params} />}
            />
          </Grid>
          <Grid item xs={12} sm={3}>
            <DatePicker
              label={t('profile:donations.toDate')}
              value={toDate}
              onChange={setToDate}
              renderInput={(params) => <TextField size="small" {...params} />}
            />
          </Grid>
        </LocalizationProvider>
      </Grid>
      {filteredDonations?.length ? (
        <TableContainer>
          <Table sx={{ minWidth: 650, backgroundColor: 'white' }} aria-label="simple table">
            <TableHead>
              <TableRow>
                <TableCell>№</TableCell>
                <TableCell>{t('profile:donations.date')}</TableCell>
                <TableCell>{t('profile:donations.type')}</TableCell>
                <TableCell>{t('profile:donations.cause')}</TableCell>
                <TableCell>{t('profile:donations.amount')}</TableCell>
                <TableCell>{t('profile:donations.certificate')}</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {filteredDonations.map((donation, index) => (
                <TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
                  <TableCell component="th" scope="row">
                    {index + 1}
                  </TableCell>
                  <TableCell>
                    {format(parseISO(donation.createdAt), 'd.LL.yyyy', {
                      locale: i18n.language === 'bg' ? bg : enUS,
                    })}
                  </TableCell>
                  <TableCell>
                    <Avatar sx={{ background: darken(theme.palette.secondary.main, 0.175) }}>
                      <StarIcon />
                    </Avatar>
                  </TableCell>
                  <TableCell>{donation.targetVault.campaign.title}</TableCell>
                  <TableCell>{money(donation.amount)}</TableCell>
                  <TableCell>
                    <Button variant="outlined" disabled={donation.status != 'succeeded'}>
                      <Link target="_blank" href={routes.donation.viewCertificate(donation.id)}>
                        {t('profile:donations.download')} <ArrowForwardIcon />
                      </Link>
                    </Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      ) : (
        <Box sx={{ fontSize: 20, mt: 4 }}>Към момента няма направени дарения</Box>
      )}
    </Card>
  )
}
Example #20
Source File: Vault.tsx    From NekoMaid with MIT License 4 votes vote down vote up
Vault: React.FC = () => {
  const his = useHistory()
  const plugin = usePlugin()
  const globalData = useGlobalData()
  const { hasVaultPermission, hasVaultChat, vaultEconomy, hasVaultGroups } = globalData
  const [players, setPlayers] = useState<PlayerInfo[]>([])
  const [count, setCount] = useState(-1)
  const [page, setPage] = useState(0)
  const [sortModel, setSortModel] = useState<GridSortItem[]>([])
  const [groups, setGroups] = useState<GroupInfo[]>([])
  const [selectedId, setSelectedId] = useState<string | undefined>()
  const [selectedPlayer, setSelectedPlayer] = useState<string | undefined>()
  const [isGroup, setIsGroup] = useState(false)
  const balanceSort = sortModel[0]?.sort
  const refresh = (res?: boolean) => {
    if (res != null) action(res)
    setCount(-1)
    plugin.emit('vault:fetch', (a, b) => {
      setCount(a)
      setPlayers(b)
    }, page, balanceSort)
  }
  useEffect(refresh, [page, balanceSort])
  useEffect(() => { plugin.emit('vault:fetchGroups', setGroups) }, [])

  const columns: any[] = [
    {
      field: '',
      sortable: false,
      width: 60,
      renderCell: (it: GridCellParams) => <Avatar
        src={getSkin(globalData, it.id, true)}
        imgProps={{ crossOrigin: 'anonymous', onClick () { his.push('/NekoMaid/playerList/' + it.id) }, style: { width: 40, height: 40 } }}
        variant='rounded'
        sx={{ cursor: 'pointer' }}
      />
    },
    {
      field: 'id',
      headerName: lang.username,
      sortable: false,
      width: 200
    }
  ]
  const columns2: any[] = [
    {
      field: 'id',
      headerName: lang.vault.groupName,
      sortable: false,
      width: 160
    }
  ]

  if (hasVaultGroups) {
    columns.push({
      field: 'group',
      headerName: lang.vault.defaultGroup,
      width: 110,
      sortable: false
    })
  }
  if (hasVaultChat) {
    const a = {
      field: 'prefix',
      headerName: lang.vault.prefix,
      width: 110,
      editable: true,
      sortable: false
    }
    const b = {
      field: 'suffix',
      headerName: lang.vault.suffix,
      width: 110,
      editable: true,
      sortable: false
    }
    columns.push(a, b)
    columns2.push(a, b)
  }
  if (vaultEconomy) {
    columns.push({
      field: 'balance',
      headerName: lang.vault.balance,
      editable: true,
      width: 110,
      valueFormatter: ({ value }: any) => (value === 0 || value === 1 ? vaultEconomy.singular : vaultEconomy.plural) +
        (vaultEconomy.digits === -1 ? value : value.toFixed(vaultEconomy.digits))
    })
  }
  if (hasVaultPermission) {
    columns.push({
      field: '_',
      headerName: lang.operations,
      width: 88,
      sortable: false,
      renderCell: (it: GridCellParams) => <>
        {hasVaultGroups && <Tooltip title={lang.vault.managePermissionGroup}>
          <IconButton onClick={() => setSelectedPlayer(it.id as any)} size='small'><GroupsIcon /></IconButton>
        </Tooltip>}
        <Tooltip title={lang.vault.managePermission}><IconButton onClick={() => {
          setSelectedId(it.id as any)
          setIsGroup(false)
        }} size='small'><ListIcon /></IconButton></Tooltip>
      </>
    })
    if (hasVaultGroups) {
      columns2.push({
        field: '_',
        headerName: lang.operations,
        width: 66,
        sortable: false,
        renderCell: (it: GridCellParams) => <Tooltip title={lang.vault.managePermission}><IconButton onClick={() => {
          setSelectedId(it.id as any)
          setIsGroup(true)
        }} size='small'><ListIcon /></IconButton></Tooltip>
      })
    }
  }

  const playerList = <Card>
    <CardHeader
      title={lang.playerList.title}
      action={<IconButton onClick={() => dialog(lang.playerList.nameToSearch, lang.username).then(filter => {
        if (!filter) return refresh()
        setCount(-1)
        plugin.emit('vault:fetch', (a, b) => {
          setCount(a)
          setPlayers(b)
          success()
        }, page, sortModel.find(it => it.field === 'balance'), filter.toLowerCase())
      })}
    ><Search /></IconButton>} />
    <Divider />
    <div style={{ height: 594, width: '100%' }}>
      <DataGrid
        pagination
        disableColumnMenu
        hideFooterSelectedRowCount
        rows={players}
        columns={columns}
        pageSize={10}
        rowCount={count === -1 ? 0 : count}
        loading={count === -1}
        onPageChange={setPage}
        paginationMode='server'
        sortingMode='server'
        sortModel={sortModel}
        onSortModelChange={it => !isEqual(sortModel, it) && setSortModel(it)}
        onCellEditCommit={({ field, id, value }) => {
          let flag = false
          switch (field) {
            case 'balance':
              if (isNaN(+value!)) refresh()
              else plugin.emit('vault:setBalance', refresh, id, +value!)
              break
            case 'prefix': flag = true
            // eslint-disable-next-line no-fallthrough
            case 'suffix':
              plugin.emit('vault:setChat', refresh, id, false, flag, value || null)
          }
        }}
      />
    </div>
  </Card>

  return <Box sx={{ minHeight: '100%', py: 3, '& .MuiDataGrid-root': { border: 'none' } }}>
    <Toolbar />
    <Container maxWidth={false}>
      {hasVaultGroups
        ? <Grid container spacing={3}>
        <Grid item lg={8} md={12} xl={8} xs={12}>{playerList}</Grid>
        <Grid item lg={4} md={12} xl={4} xs={12}>
          <Card>
            <CardHeader title={lang.vault.permissionGroup} />
            <Divider />
            <div style={{ height: 594, width: '100%' }}>
              <DataGrid
                hideFooter
                disableColumnMenu
                rows={groups}
                columns={columns2}
                onCellEditCommit={({ field, id, value }) => {
                  let flag = false
                  switch (field) {
                    case 'prefix': flag = true
                    // eslint-disable-next-line no-fallthrough
                    case 'suffix':
                      plugin.emit('vault:setChat', (res: boolean) => {
                        action(res)
                        plugin.emit('vault:fetchGroups', setGroups)
                      }, id, true, flag, value || null)
                  }
                }}
              />
            </div>
          </Card>
        </Grid>
        </Grid>
        : playerList}
    </Container>
    <PermissionDialog plugin={plugin} id={selectedId} onClose={() => setSelectedId(undefined)} isGroup={isGroup} />
    {hasVaultGroups && <Groups plugin={plugin} id={selectedPlayer} onClose={() => setSelectedPlayer(undefined)} groups={groups} />}
  </Box>
}
Example #21
Source File: FireCMSAppBar.tsx    From firecms with MIT License 4 votes vote down vote up
export function FireCMSAppBar({
                                  title,
                                  handleDrawerToggle,
                                  toolbarExtraWidget
                              }: CMSAppBarProps) {

    const classes = useStyles();

    const breadcrumbsContext = useBreadcrumbsContext();
    const { breadcrumbs } = breadcrumbsContext;

    const authController = useAuthController();
    const { mode, toggleMode } = useModeState();

    const initial = authController.user?.displayName
        ? authController.user.displayName[0].toUpperCase()
        : (authController.user?.email ? authController.user.email[0].toUpperCase() : "A");

    return (
        <Slide
            direction="down" in={true} mountOnEnter unmountOnExit>
            <AppBar
                className={classes.appbar}
                position={"relative"}
                elevation={1}>
                <Toolbar>
                    <IconButton
                        color="inherit"
                        aria-label="Open drawer"
                        edge="start"
                        onClick={handleDrawerToggle}
                        className={classes.menuButton}
                        size="large">
                        <MenuIcon/>
                    </IconButton>

                    <Hidden lgDown>
                        <Box mr={3}>
                            <Link
                                underline={"none"}
                                key={"breadcrumb-home"}
                                color="inherit"
                                component={ReactLink}
                                to={"/"}>
                                <Typography variant="h6" noWrap>
                                    {title}
                                </Typography>
                            </Link>
                        </Box>
                    </Hidden>

                    <Box mr={2}>
                        <Breadcrumbs
                            separator={<NavigateNextIcon
                                htmlColor={"rgb(0,0,0,0.87)"}
                                fontSize="small"/>}
                            aria-label="breadcrumb">
                            {breadcrumbs.map((entry, index) => (
                                <Link
                                    underline={"none"}
                                    key={`breadcrumb-${index}`}
                                    color="inherit"
                                    component={ReactLink}
                                    to={entry.url}>
                                    <Chip
                                        classes={{ root: classes.breadcrumb }}
                                        label={entry.title}
                                    />
                                </Link>)
                            )
                            }
                        </Breadcrumbs>
                    </Box>

                    <Box flexGrow={1}/>

                    {toolbarExtraWidget &&
                    <ErrorBoundary>
                        {
                            toolbarExtraWidget
                        }
                    </ErrorBoundary>}


                    <Box p={1} mr={1}>
                        <IconButton
                            color="inherit"
                            aria-label="Open drawer"
                            edge="start"
                            onClick={() => toggleMode()}
                            size="large">
                            {mode === "dark"
                                ? <Brightness3Icon/>
                                : <Brightness5Icon/>}
                        </IconButton>
                    </Box>

                    <Box p={1} mr={1}>
                        {authController.user && authController.user.photoURL
                            ? <Avatar
                                src={authController.user.photoURL}/>
                            : <Avatar>{initial}</Avatar>
                        }
                    </Box>

                    <Button variant="text"
                            color="inherit"
                            onClick={authController.signOut}>
                        Log Out
                    </Button>

                </Toolbar>
            </AppBar>
        </Slide>
    );
}
Example #22
Source File: Config.tsx    From NekoMaid with MIT License 4 votes vote down vote up
configs.push({
  title: lang.config.serverConfig,
  component () {
    const plugin = usePlugin()
    const globalData = useGlobalData()
    const [flag, update] = useState(0)
    const [info, setInfo] = useState<Record<string, string>>({ })
    const [open, setOpen] = useState(false)
    const [canGetData, setCanGetData] = useState(true)
    const [loading, setLoading] = useState(false)
    const setValue = (field: string, value: any, isGlobal = true) => {
      plugin.emit('server:set', field, value)
      success()
      if (isGlobal) {
        (globalData as any)[field] = value
        update(flag + 1)
        location.reload()
      }
    }
    const createEditButtom = (field: string, isGlobal?: boolean, isInt = true) => <IconButton
      onClick={() => dialog(
        {
          content: lang.inputValue,
          input: isInt
            ? {
                error: true,
                type: 'number',
                helperText: lang.invalidValue,
                validator: (it: string) => /^\d+$/.test(it) && +it >= 0
              }
            : { }
        }).then(res => res != null && setValue(field, isInt ? parseInt(res as any) : (res || null), isGlobal))}
    ><Edit /></IconButton>

    const infoElm: JSX.Element[] = []
    for (const key in info) {
      const name = (lang.config as any)[key]
      infoElm.push(<ListItem key={key} sx={{ pl: 4 }}>
        <ListItemText
          primary={key === 'isAikarFlags' ? <Link href='https://mcflags.emc.gs' target='_blank' rel='noopener'>{name}</Link> : name}
          secondary={info[key].toString()}
        />
      </ListItem>)
    }

    return <List>
      <CircularLoading loading={loading} />
      <ListItem secondaryAction={globalData.canSetMaxPlayers
        ? createEditButtom('maxPlayers')
        : undefined}>
        <ListItemText primary={lang.config.maxPlayers + ': ' + globalData.maxPlayers} />
      </ListItem>
      <ListItem secondaryAction={createEditButtom('spawnRadius')}>
        <ListItemText primary={lang.config.spawnRadius + ': ' + globalData.spawnRadius} />
      </ListItem>
      <ListItem secondaryAction={createEditButtom('motd', false, false)}>
        <ListItemText primary={lang.config.motd} />
      </ListItem>
      <ListItem secondaryAction={<Switch checked={globalData.hasWhitelist} onChange={e => setValue('hasWhitelist', e.target.checked)} />}>
        <ListItemText primary={lang.config.whitelist} />
      </ListItem>
      {canGetData && <>
        <ListItemButton onClick={() => {
          if (infoElm.length) setOpen(!open)
          else {
            setLoading(true)
            plugin.emit('server:fetchInfo', (data: any) => {
              setLoading(false)
              if (!data) {
                failed(lang.unsupported)
                setCanGetData(false)
                return
              }
              setInfo(data)
              setOpen(true)
            })
          }
        }}>
        <ListItemIcon><Equalizer /></ListItemIcon>
          <ListItemText primary={lang.info} />
          {open ? <ExpandLess /> : <ExpandMore />}
        </ListItemButton>
        <Collapse in={open} timeout='auto' unmountOnExit>
          <List component='div' dense disablePadding>{infoElm}</List>
        </Collapse>
      </>}
    </List>
  }
},
{
  title: lang.history,
  component () {
    const [cur, update] = useState(0)
    const list: ServerRecord[] = JSON.parse(localStorage.getItem('NekoMaid:servers') || '[]')
    return <List>
      {list.sort((a, b) => b.time - a.time).map(it => {
        const i = it.address.indexOf('?')
        return <ListItem
          disablePadding
          key={it.address}
          secondaryAction={<IconButton edge='end' size='small' onClick={() => {
            localStorage.setItem('NekoMaid:servers', JSON.stringify(list.filter(s => s.address !== it.address)))
            success()
            update(cur + 1)
          }}><Delete /></IconButton>}
        >
          <ListItemButton onClick={() => {
            location.hash = ''
            location.search = it.address
          }} dense>
            <ListItemAvatar><Avatar src={it.icon} variant='rounded'><HelpOutline /></Avatar></ListItemAvatar>
            <ListItemText primary={<Tooltip title={it.address.slice(i + 1)}>
              <span>{it.address.slice(0, i)}</span></Tooltip>} secondary={dayjs(it.time).fromNow()} />
          </ListItemButton>
        </ListItem>
      })}
    </List>
  }
},
{
  title: lang.config.theme,
  component () {
    const color = localStorage.getItem('NekoMaid:color') || 'blue'
    return <CardContent sx={{ textAlign: 'center' }}>
      <Box>
        <ToggleButtonGroup exclusive value={localStorage.getItem('NekoMaid:colorMode') || ''} onChange={(_, it) => {
          localStorage.setItem('NekoMaid:colorMode', it)
          location.reload()
        }}>
          <ToggleButton value='light'><Brightness7 /> {lang.config.light}</ToggleButton>
          <ToggleButton value=''><SettingsBrightness /> {lang.config.system}</ToggleButton>
          <ToggleButton value='dark'><Brightness4 /> {lang.config.dark}</ToggleButton>
        </ToggleButtonGroup>
      </Box>
      <Paper sx={{ marginTop: 2, width: '176px', overflow: 'hidden', display: 'inline-block' }}>
        {Object.keys(colors).slice(1, 17).map((key, i) => {
          const checked = color === key
          const elm = <Box
            key={key}
            onClick={() => {
              localStorage.setItem('NekoMaid:color', key)
              location.reload()
            }}
            sx={{
              backgroundColor: (colors as any)[key][600],
              width: '44px',
              height: '44px',
              display: 'inline-block',
              cursor: 'pointer'
            }}
          ><Check htmlColor='white' sx={{ top: '10px', position: 'relative', opacity: checked ? 1 : 0 }} /></Box>
          return (i + 1) % 4 === 0 ? <React.Fragment key={key}>{elm}<br /></React.Fragment> : elm
        })}
      </Paper>
    </CardContent>
  }
})
Example #23
Source File: CommentItemModule.tsx    From bouncecode-cms with GNU General Public License v3.0 4 votes vote down vote up
function CommentItemModule({post, comment}: ICommentItemModule) {
  const [moreVert, setMoreVert] = React.useState(null);
  const openMoreVert = Boolean(moreVert);
  const toggleMoreVert = open => {
    return event => {
      if (open) {
        setMoreVert(event.currentTarget);
      } else {
        setMoreVert(null);
      }
    };
  };

  const [commentDrawer, setCommentDrawer] = React.useState(null);
  const toggleCommentDrawer = open => {
    return event => {
      if (
        event.type === 'keydown' &&
        (event.key === 'Tab' || event.key === 'Shift')
      ) {
        return;
      }

      setCommentDrawer(open);
    };
  };

  const [
    emotionMutation,
    {loading: emotionLoading},
  ] = useCommentEmotionMutation();
  const [
    undoEmotionMutation,
    {loading: undoEmotionLoading},
  ] = useCommentEmotionUndoMutation();
  const updateEmotion = emotion => {
    return () => {
      console.log(myEmotion);
      console.log(emotion);
      if (myEmotion === emotion.toLowerCase()) {
        undoEmotionMutation({
          variables: {
            where: {
              id: comment.id,
            },
          },
          refetchQueries: [
            {
              query: CommentMyEmotionDocument,
              variables: {where: {id: comment.id}},
            },
          ],
        });
      } else {
        emotionMutation({
          variables: {
            where: {
              id: comment.id,
            },
            data: {emotion},
          },
          refetchQueries: [
            {
              query: CommentMyEmotionDocument,
              variables: {where: {id: comment.id}},
            },
          ],
        });
      }
    };
  };

  const {
    data: myEmotionData,
    loading: myEmotionLoading,
  } = useCommentMyEmotionQuery({
    variables: {where: {id: comment.id}},
  });
  const myEmotion = myEmotionData?.commentMyEmotion?.emotion;

  return (
    <Grid container direction="column" spacing={1}>
      <Grid item>
        <Grid container spacing={2}>
          <Grid item>
            <Avatar></Avatar>
          </Grid>
          <Grid item xs>
            <Grid container direction="column" spacing={1}>
              <Grid item>
                <Grid container spacing={2} alignItems="center">
                  <Grid item>
                    <Typography variant="body2">
                      {comment.user.email}
                    </Typography>
                  </Grid>
                  <Grid item>
                    <Typography variant="caption">
                      {formatDistance(
                        new Date(comment.createdDate),
                        new Date(),
                        {addSuffix: true},
                      )}
                    </Typography>
                  </Grid>
                </Grid>
              </Grid>
              <Grid item>
                <Typography variant="body1">{comment.text}</Typography>
              </Grid>
              <Grid
                container
                justifyContent="space-between"
                alignItems="center">
                <Grid item>
                  <Grid container alignItems="center">
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<ThumbUpOutlined />}
                        onClick={updateEmotion('LIKE')}
                        disabled={
                          myEmotionLoading ||
                          emotionLoading ||
                          undoEmotionLoading
                        }>
                        {comment.like || ''}
                      </Button>
                    </Grid>
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<ThumbDownOutlined />}
                        onClick={updateEmotion('UNLIKE')}
                        disabled={
                          myEmotionLoading ||
                          emotionLoading ||
                          undoEmotionLoading
                        }>
                        {comment.unlike || ''}
                      </Button>
                    </Grid>
                    {post ? (
                      <Grid item>
                        <Button
                          variant="text"
                          size="small"
                          onClick={toggleCommentDrawer(true)}
                          disabled>
                          답글
                        </Button>
                        <Drawer
                          anchor="bottom"
                          open={commentDrawer}
                          onClose={toggleCommentDrawer(false)}>
                          <Container maxWidth="md">
                            <Box pt={2} pb={2}>
                              <CommentCreateFormModule />
                            </Box>
                          </Container>
                        </Drawer>
                      </Grid>
                    ) : (
                      undefined
                    )}
                  </Grid>
                </Grid>
                <Grid item>
                  <IconButton onClick={toggleMoreVert(true)} disabled>
                    <MoreVert fontSize="small" />
                  </IconButton>
                  <Menu
                    anchorEl={moreVert}
                    open={openMoreVert}
                    onClose={toggleMoreVert(false)}>
                    <MenuItem
                      // selected={option === 'Pyxis'}
                      onClick={toggleMoreVert(false)}>
                      <ListItemIcon>
                        <AssistantPhotoOutlined />
                      </ListItemIcon>
                      <Typography variant="inherit">신고</Typography>
                    </MenuItem>
                  </Menu>
                </Grid>
              </Grid>
              {/* {post ? (
                <Grid item>
                  <Grid container direction="column" spacing={2}>
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<KeyboardArrowDown />}>
                        답글 1개 보기
                      </Button>
                    </Grid>
                    <Grid item>
                      <CommentItemModule comment />
                    </Grid>
                  </Grid>
                </Grid>
              ) : (
                undefined
              )} */}
            </Grid>
          </Grid>
        </Grid>
      </Grid>
    </Grid>
  );
}
Example #24
Source File: CommentCreateFormModule.tsx    From bouncecode-cms with GNU General Public License v3.0 4 votes vote down vote up
function CommentCreateFormModule({post}: ICommentDefaultListModule) {
  const postId = post?.id;

  const {
    values,
    handleSubmit,
    handleChange,
    errors,
    touched,
    isSubmitting,
    handleReset,
  } = useCommentCreateFormik(postId);

  console.log('errors', errors);

  const {data} = useMeQuery();

  return (
    <form onSubmit={handleSubmit}>
      <Grid container spacing={2}>
        <Grid item>
          <Avatar></Avatar>
        </Grid>
        <Grid item xs>
          <Grid container spacing={2} direction="column">
            <Grid item>
              <TextField
                name="text"
                // label="댓글을 입력해주세요."
                placeholder="댓글을 입력해주세요."
                multiline
                // variant="outlined"
                value={values.text}
                onChange={handleChange}
                error={touched.text && Boolean(errors.text)}
                helperText={touched.text ? errors.text : ''}
                fullWidth
                disabled={isSubmitting}
              />
            </Grid>
            <Grid item>
              <Grid
                container
                justifyContent="space-between"
                alignItems="center">
                <Grid item>
                  {/* <Rating
                          name="rating"
                          onChange={handleChange}
                          value={values.rating}
                        /> */}
                </Grid>
                <Grid item>
                  <Grid container spacing={1} alignItems="center">
                    <Grid item>
                      <Button
                        fullWidth
                        variant="text"
                        color="primary"
                        onClick={handleReset}>
                        취소
                      </Button>
                    </Grid>
                    <Grid item>
                      <Button
                        fullWidth
                        type="submit"
                        variant="contained"
                        color="primary"
                        // size="large"
                        disabled={!values.text || isSubmitting}
                        endIcon={
                          isSubmitting ? (
                            <CircularProgress size={16} />
                          ) : (
                            undefined
                          )
                        }>
                        댓글
                      </Button>
                    </Grid>
                  </Grid>
                </Grid>
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      </Grid>
    </form>
  );
}
Example #25
Source File: Message.tsx    From airmessage-web with Apache License 2.0 4 votes vote down vote up
export default function Message(props: Props) {
	//State
	const [attachmentDataArray, setAttachmentDataArray] = useState<FileDownloadResult[]>([]);
	const [dialogOpen, setDialogOpen] = useState<"error" | "rawError" | undefined>(undefined);
	
	function closeDialog() {
		setDialogOpen(undefined);
	}
	function openDialogError() {
		setDialogOpen("error");
	}
	function openDialogRawError() {
		setDialogOpen("rawError");
	}
	function copyRawErrorAndClose() {
		navigator.clipboard.writeText(props.message.error!.detail!);
		closeDialog();
	}
	
	//Getting the message information
	const isOutgoing = props.message.sender === undefined;
	const displayAvatar = !isOutgoing && !props.flow.anchorTop;
	const displaySender = props.isGroupChat && displayAvatar;
	const messageConfirmed = props.message.status !== MessageStatusCode.Unconfirmed;
	
	//Building the message style
	const theme = useTheme();
	let colorPalette: PaletteColor;
	if(isOutgoing) {
		if(props.service === appleServiceAppleMessage) colorPalette = theme.palette.messageOutgoing;
		else colorPalette = theme.palette.messageOutgoingTextMessage;
	} else {
		colorPalette = theme.palette.messageIncoming;
	}
	const messagePartPropsBase: Partial<MessagePartProps> = {
		alignSelf: isOutgoing ? "flex-end" : "flex-start",
		color: colorPalette.contrastText,
		backgroundColor: colorPalette.main,
		opacity: messageConfirmed ? opacityConfirmed : opacityUnconfirmed
	};
	
	//Splitting the modifiers for each message part
	const stickerGroups = props.message.stickers.reduce((accumulator: {[index: number]: StickerItem[]}, item: StickerItem) => {
		if(accumulator[item.messageIndex]) accumulator[item.messageIndex].push(item);
		else accumulator[item.messageIndex] = [item];
		return accumulator;
	}, {});
	const tapbackGroups = props.message.tapbacks.reduce((accumulator: {[index: number]: TapbackItem[]}, item: TapbackItem) => {
		if(accumulator[item.messageIndex]) accumulator[item.messageIndex].push(item);
		else accumulator[item.messageIndex] = [item];
		return accumulator;
	}, {});
	
	//Adding the message text
	const components: React.ReactNode[] = [];
	if(props.message.text) {
		const partProps: MessagePartProps = {
			...messagePartPropsBase,
			borderRadius: getBorderRadius(props.flow.anchorTop, props.flow.anchorBottom || props.message.attachments.length > 0, isOutgoing),
			marginTop: 0
		} as MessagePartProps;
		
		const component = <MessageBubble key="messagetext" text={props.message.text!} index={0} partProps={partProps} stickers={stickerGroups[0]} tapbacks={tapbackGroups[0]} />;
		
		components.push(component);
	}
	
	function onAttachmentData(attachmentIndex: number, shouldDownload: boolean, result: FileDownloadResult) {
		if(shouldDownload) {
			//Downloading the file
			const attachment = props.message.attachments[attachmentIndex];
			downloadArrayBuffer(result.data, result.downloadType ?? attachment.type, result.downloadName ?? attachment.name);
		} else {
			//Updating the data
			const newArray = [...attachmentDataArray];
			newArray[attachmentIndex] = result;
			setAttachmentDataArray(newArray);
		}
	}
	
	function downloadData(attachmentIndex: number, data: ArrayBuffer | Blob) {
		const attachment = props.message.attachments[attachmentIndex];
		if(data instanceof ArrayBuffer) {
			downloadArrayBuffer(data, attachment.type, attachment.name);
		} else {
			downloadBlob(data, attachment.type, attachment.name);
		}
	}
	
	/**
	 * Computes the file data to display to the user
	 */
	const getComputedFileData = useCallback((attachmentIndex: number): FileDisplayResult => {
		const attachment = props.message.attachments[attachmentIndex];
		const downloadData = attachmentDataArray[attachmentIndex];
		
		if(downloadData === undefined) {
			return {
				data: attachment.data,
				name: attachment.name,
				type: attachment.type
			};
		} else return {
			data: downloadData.data,
			name: downloadData.downloadName ?? attachment.name,
			type: downloadData.downloadType ?? attachment.type
		};
	}, [props.message.attachments, attachmentDataArray]);
	
	//Adding the attachments
	for(let i = 0; i < props.message.attachments.length; i++) {
		const index = props.message.text ? i + 1 : i;
		const attachment = props.message.attachments[i];
		
		const partProps: MessagePartProps = {
			...messagePartPropsBase,
			borderRadius: getBorderRadius(props.flow.anchorTop || index > 0, props.flow.anchorBottom || i + 1 < props.message.attachments.length, isOutgoing),
			marginTop: index > 0 ? marginLinked : undefined
		} as MessagePartProps;
		
		//Checking if the attachment has data
		const attachmentData = getComputedFileData(i);
		if(attachmentData.data !== undefined && isAttachmentPreviewable(attachmentData.type)) {
			//Custom background color
			const imagePartProps = {
				...partProps,
				backgroundColor: theme.palette.background.sidebar,
			};
			
			if(attachmentData.type.startsWith("image/")) {
				components.push(<MessageAttachmentImage key={attachment.guid ?? attachment.localID} data={attachmentData.data} name={attachmentData.name} type={attachmentData.type} partProps={imagePartProps} stickers={stickerGroups[index]} tapbacks={tapbackGroups[index]} />);
			}
		} else {
			//Adding a generic download attachment
			components.push(<MessageAttachmentDownloadable
				key={attachment.guid ?? attachment.localID}
				data={attachmentData.data}
				name={attachmentData.name}
				type={attachmentData.type}
				size={attachment.size}
				guid={attachment.guid!}
				onDataAvailable={(data) => onAttachmentData(i, !isAttachmentPreviewable(data.downloadType ?? attachment.type), data)}
				onDataClicked={(data) => downloadData(i, data)}
				partProps={partProps}
				stickers={stickerGroups[index]}
				tapbacks={tapbackGroups[index]} />);
		}
	}
	
	const messageStyle: CSSProperties = {
		marginTop: props.flow.anchorTop ? marginLinked : marginUnlinked,
	};
	
	//Initializing state
	const [personData, setPersonData] = useState<PersonData | undefined>();
	useEffect(() => {
		if(!props.message.sender) return;
		
		//Requesting contact data
		findPerson(props.message.sender).then(setPersonData, console.warn);
	}, [props.message.sender]);
	
	//Building and returning the component
	return (
		<div className={styles.message} style={messageStyle}>
			{props.flow.showDivider && <Typography className={styles.separator} variant="body2" color="textSecondary">{getTimeDivider(props.message.date)}</Typography>}
			{displaySender && <Typography className={styles.labelSender} variant="caption" color="textSecondary">{personData?.name ?? props.message.sender}</Typography>}
			<div className={styles.messageSplit}>
				{<Avatar className={styles.avatar} src={personData?.avatar} style={displayAvatar ? {visibility: "visible", backgroundColor: colorFromContact(props.message.sender ?? "")} : {visibility: "hidden"}} />}
				<div className={styles.messageParts}>
					{components}
				</div>
				{props.message.progress && !props.message.error && <CircularProgress className={styles.messageProgress} size={24} variant={props.message.progress === -1 ? "indeterminate" : "determinate"} value={props.message.progress} />}
				{props.message.error && <IconButton className={styles.messageError} style={{color: theme.palette.error.light}} size="small" onClick={openDialogError}>
					<ErrorRounded />
				</IconButton>}
				<Dialog open={dialogOpen === "error"} onClose={closeDialog}>
					<DialogTitle>Your message could not be sent</DialogTitle>
					{props.message.error !== undefined && <React.Fragment>
						<DialogContent>
							<DialogContentText>{messageErrorToUserString(props.message.error!.code)}</DialogContentText>
						</DialogContent>
						<DialogActions>
							{props.message.error!.detail && <Button onClick={openDialogRawError} color="primary">
								Error details
							</Button>}
							<Button onClick={closeDialog} color="primary" autoFocus>
								Dismiss
							</Button>
						</DialogActions>
					</React.Fragment>}
				</Dialog>
				<Dialog open={dialogOpen === "rawError"} onClose={closeDialog}>
					<DialogTitle>Error details</DialogTitle>
					{props.message.error !== undefined && <React.Fragment>
						<DialogContent>
							<DialogContentText className={styles.rawErrorText}>{props.message.error.detail!}</DialogContentText>
						</DialogContent>
						<DialogActions>
							<Button onClick={copyRawErrorAndClose} color="primary">
								Copy to clipboard
							</Button>
							<Button onClick={closeDialog} color="primary" autoFocus>
								Dismiss
							</Button>
						</DialogActions>
					</React.Fragment>}
				</Dialog>
			</div>
			{props.showStatus && <Typography className={styles.labelStatus} variant="caption" color="textSecondary">{getStatusString(props.message)}</Typography>}
		</div>
	);
}
Example #26
Source File: DetailCreate.tsx    From airmessage-web with Apache License 2.0 4 votes vote down vote up
export default function DetailCreate(props: {onConversationCreated: (conversation: Conversation) => void}) {
	const [query, setQuery] = useState<string>("");
	const [peopleSelection, setPeopleSelection] = useState<SelectionData[]>([]);
	const [peoplePool, setPeoplePool] = useState<PersonData[]>();
	const [isLoading, setLoading] = useState(false);
	
	const displaySnackbar = useContext(SnackbarContext);
	
	const theme = useTheme();
	
	useEffect(() => {
		//Loading the people
		getPeople().then((people) => {
			setPeoplePool(people);
		});
	}, []);
	
	const groupedPeople = peoplePool ? filterAndGroup(peoplePool, query) : undefined;
	
	function handleTextChange(event: React.ChangeEvent<HTMLInputElement>) {
		const text = event.target.value;
		
		//Updating the query text
		setQuery(text);
	}
	
	function handleKeyDown(event: React.KeyboardEvent<any>) {
		if(event.key === "Backspace") {
			//Removing the last person
			if(query.length === 0 && peopleSelection.length > 0) {
				setPeopleSelection(peopleSelection.slice(0, peopleSelection.length - 1));
				event.preventDefault();
			}
		} else if(event.key === "Enter") {
			//Manually entering the query
			handleDirectEntry();
		}
	}
	
	function handleAddressClick(person: PersonData, address: AddressData) {
		//Clearing the query text
		setQuery("");
		
		//Toggling the item
		const index = peopleSelection.findIndex((selection) => selection.address === address.value);
		if(index === -1) {
			setPeopleSelection(peopleSelection.concat({
				name: person.name,
				avatar: person.avatar,
				address: address.value,
				displayAddress: address.displayValue,
				addressLabel: address.label
			}));
		} else {
			setPeopleSelection([...peopleSelection.slice(0, index), ...peopleSelection.slice(index + 1)]);
		}
	}
	
	function findPersonInfo(address: string): [PersonData, AddressData] | undefined {
		if(!peoplePool) return undefined;
		
		for(const person of peoplePool) {
			for(const personAddress of person.addresses) {
				if(personAddress.value === address) {
					return [person, personAddress];
				}
			}
		}
		
		return undefined;
	}
	
	function handleDirectEntry() {
		const address = query;
		
		//Checking if the item is an email address
		if(address.match(regexEmail)) {
			//Clearing the query text
			setQuery("");
			
			//Returning if the addition will conflict with any existing entries
			if(peopleSelection.find((selection) => selection.address === address)) return;
			
			//Searching for the user in the listings
			const [personData, addressData] = findPersonInfo(query) ?? [undefined, undefined];
			
			//Adding the item
			setPeopleSelection(peopleSelection.concat({
				name: personData?.name,
				avatar: personData?.avatar,
				address: address,
				displayAddress: address,
				addressLabel: addressData?.label
			}));
		} else {
			//Checking if the item is a phone number
			const phone = parsePhoneNumberFromString(query, "US");
			const formatted = phone?.number.toString();
			if(phone && phone.isValid()) {
				//Clearing the query text
				setQuery("");
				
				//Returning if the addition will conflict with any existing entries
				if(peopleSelection.find((selection) => selection.address === formatted!)) return;
				
				//Searching for the user in the listings
				const [personData, addressData] = findPersonInfo(formatted!) ?? [undefined, undefined];
				
				//Adding the item
				setPeopleSelection(peopleSelection.concat({
					name: personData?.name,
					avatar: personData?.avatar,
					address: formatted!,
					displayAddress: phone.formatNational(),
					addressLabel: addressData?.label
				}));
			}
		}
	}
	
	function handleRemoveSelection(selection: SelectionData) {
		setPeopleSelection(peopleSelection.filter((value) => value !== selection));
	}
	
	function confirmParticipants() {
		//Starting the loading view
		setLoading(true);
		
		//Mapping the people selection to their addresses
		const chatMembers = peopleSelection.map((selection) => selection.address);
		ConnectionManager.createChat(chatMembers, messagingService)
			.then((chatGUID) => {
				//Adding the chat
				props.onConversationCreated({
					localID: generateConversationLocalID(),
					guid: chatGUID,
					service: messagingService,
					members: chatMembers,
					preview: {
						type: ConversationPreviewType.ChatCreation,
						date: new Date()
					},
					localOnly: false,
					unreadMessages: false
				});
			}).catch(([errorCode, errorDesc]: [CreateChatErrorCode, string | undefined]) => {
				if(errorCode === CreateChatErrorCode.NotSupported) {
					//If the server doesn't support creating chats,
					//create this chat locally and defer its creation
					//until the user sends a message
					props.onConversationCreated({
						localID: generateConversationLocalID(),
						service: messagingService,
						members: chatMembers,
						preview: {
							type: ConversationPreviewType.ChatCreation,
							date: new Date()
						},
						localOnly: true,
						unreadMessages: false
					});
					
					return;
				}
				
				//Cancelling loading
				setLoading(false);
				
				//Displaying a snackbar
				displaySnackbar({message: "Failed to create conversation"});
				
				//Logging the error
				console.warn(`Failed to create chat: ${errorCode} / ${errorDesc}`);
			});
	}
	
	let queryDirectState: boolean; //Is the current input query a valid address that can be added directly?
	let queryDirectDisplay: string;
	{
		//Checking email address
		if(query.match(regexEmail)) {
			queryDirectState = true;
			queryDirectDisplay = query;
		} else {
			//Checking phone number
			const phone = parsePhoneNumberFromString(query, "US");
			if(phone && phone.isValid()) {
				queryDirectState = true;
				queryDirectDisplay = phone.formatNational();
			} else {
				queryDirectState = false;
				queryDirectDisplay = query;
			}
		}
	}
	
	return (
		<DetailFrame title="New conversation">
			<div className={styles.content}>
				<div className={`${isLoading ? styles.scrimShown : styles.scrimHidden} ${styles.progressContainer}`} style={{backgroundColor: theme.palette.mode === "light" ? "rgba(255, 255, 255, 0.7)" : "rgba(0, 0, 0, 0.7)"}}>
					<CircularProgress />
				</div>
				
				<div className={styles.participantInputWrapper}>
					<div className={styles.participantInputLayout} style={{backgroundColor: theme.palette.messageIncoming.main}}>
						<div className={styles.participantInputFlow}>
							<SelectionList className={styles.participantInputChip} selections={peopleSelection} onRemove={handleRemoveSelection} />
							<InputBase className={styles.participantInputField} placeholder={peopleSelection.length > 0 ? undefined : "Type a name, email address, or phone number"} value={query} onChange={handleTextChange} onKeyDown={handleKeyDown} autoFocus />
						</div>
						<Button className={styles.participantInputButton} variant="contained" color="primary" disabled={peopleSelection.length === 0} disableElevation onClick={confirmParticipants}>Done</Button>
					</div>
				</div>
				
				{
					groupedPeople ? <div className={styles.listWrapper}>
						<div className={styles.list}>
							{queryDirectState && <DirectSendButton address={queryDirectDisplay} onClick={handleDirectEntry} />}
							{Object.entries(groupedPeople).map(([firstLetter, people]) => (
								<React.Fragment key={`section-${firstLetter}`}>
									<ListSubheader>{firstLetter}</ListSubheader>
									{people.map((person) => (
										<div key={person.id} className={styles.contactRoot}>
											<Avatar src={person.avatar} alt={person.name} />
											<div className={styles.contactText}>
												<Typography variant="body1" color="textPrimary">{person.name}</Typography>
												<div className={styles.contactAddresses}>
													{person.addresses.map((address) => (
														<AddressButton key={address.value + "/" + address.label} address={address} selected={peopleSelection.find((selection) => selection.address === address.value) !== undefined} onClick={() => handleAddressClick(person, address)} />
													))}
												</div>
											</div>
										</div>
									))}
								</React.Fragment>
							))}
						</div>
					</div> : <div className={`${styles.listWrapper} ${styles.progressContainer}`}>
						<CircularProgress />
					</div>
				}
			</div>
		</DetailFrame>
	);
}
Example #27
Source File: MangaCard.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
    const {
        manga: {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary,
        },
        gridLayout,
        dimensions,
    } = props;
    const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext();

    const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
    const [useCache] = useLocalStorage<boolean>('useCache', true);
    const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);

    if (gridLayout !== 2) {
        const colomns = Math.round(dimensions / ItemWidth);
        return (
            // @ts-ignore gridsize type isnt allowed to be a decimal but it works fine
            <Grid item xs={12 / colomns} sm={12 / colomns} md={12 / colomns} lg={12 / colomns}>
                <Link to={`/manga/${id}/`} style={(gridLayout === 1) ? { textDecoration: 'none' } : {}}>
                    <Box
                        sx={{
                            display: 'flex',
                            flexDirection: 'column',
                        }}
                    >
                        <Card
                            sx={{
                            // force standard aspect ratio of manga covers
                                aspectRatio: '225/350',
                                display: 'flex',
                            }}
                            ref={ref}
                        >
                            <CardActionArea
                                sx={{
                                    position: 'relative',
                                    height: '100%',
                                }}
                            >

                                <BadgeContainer>
                                    {inLibrary && (
                                        <Typography
                                            sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
                                        >
                                            In library
                                        </Typography>
                                    )}
                                    { showUnreadBadge && unread! > 0 && (
                                        <Typography
                                            sx={{ backgroundColor: 'primary.dark' }}
                                        >
                                            {unread}
                                        </Typography>
                                    )}
                                    { showDownloadBadge && downloadCount! > 0 && (
                                        <Typography sx={{
                                            backgroundColor: 'success.dark',
                                        }}
                                        >
                                            {downloadCount}
                                        </Typography>
                                    )}
                                </BadgeContainer>
                                <SpinnerImage
                                    alt={title}
                                    src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
                                    imgStyle={inLibrary
                                        ? {
                                            height: '100%',
                                            width: '100%',
                                            objectFit: 'cover',
                                            filter: 'brightness(0.4)',
                                        }
                                        : {
                                            height: '100%',
                                            width: '100%',
                                            objectFit: 'cover',
                                        }}
                                    spinnerStyle={{
                                        display: 'grid',
                                        placeItems: 'center',
                                    }}
                                />
                                {(gridLayout === 1) ? (<></>) : (
                                    <>
                                        <BottomGradient />
                                        <BottomGradientDoubledDown />
                                    </>
                                )}
                                {(gridLayout === 1) ? (
                                    <></>
                                ) : (
                                    <MangaTitle>
                                        {truncateText(title, 61)}
                                    </MangaTitle>
                                )}
                            </CardActionArea>
                        </Card>
                        {(gridLayout === 1) ? (
                            <MangaTitle
                                sx={{
                                    position: 'relative',
                                }}
                            >
                                {truncateText(title, 61)}
                            </MangaTitle>
                        ) : (<></>)}
                    </Box>
                </Link>
            </Grid>
        );
    }
    return (
        <Grid item xs={12} sm={12} md={12} lg={12}>
            <Link to={`/manga/${id}/`} style={{ textDecoration: 'none', color: 'unset' }}>
                <CardContent sx={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                    padding: 2,
                    '&:hover': {
                        backgroundColor: 'action.hover',
                        transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                    },
                    '&:active': {
                        backgroundColor: 'action.selected',
                        transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                    },
                    position: 'relative',
                }}
                >
                    <Avatar
                        variant="rounded"
                        sx={inLibrary
                            ? {
                                width: 56,
                                height: 56,
                                flex: '0 0 auto',
                                marginRight: 2,
                                imageRendering: 'pixelated',
                                filter: 'brightness(0.4)',
                            }
                            : {
                                width: 56,
                                height: 56,
                                flex: '0 0 auto',
                                marginRight: 2,
                                imageRendering: 'pixelated',
                            }}
                        src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
                    />
                    <Box
                        sx={{
                            display: 'flex',
                            flexDirection: 'row',
                            flexGrow: 1,
                            width: 'min-content',
                        }}
                    >
                        <Typography variant="h5" component="h2">
                            {truncateText(title, 61)}
                        </Typography>
                    </Box>
                    <BadgeContainer sx={{ position: 'relative' }}>
                        {inLibrary && (
                            <Typography
                                sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
                            >
                                In library
                            </Typography>
                        )}
                        { showUnreadBadge && unread! > 0 && (
                            <Typography
                                sx={{ backgroundColor: 'primary.dark' }}
                            >
                                {unread}
                            </Typography>
                        )}
                        { showDownloadBadge && downloadCount! > 0 && (
                            <Typography sx={{
                                backgroundColor: 'success.dark',
                            }}
                            >
                                {downloadCount}
                            </Typography>
                        )}
                    </BadgeContainer>
                </CardContent>
            </Link>
        </Grid>
    );
})
Example #28
Source File: Register.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function Register() {
    const loggedIn = useRecoilValue(isAuthenticated);
    const [loading, setLoading] = useState(true);
    const query = useQuery();
    const history = useHistory();

    const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : "";

    useTitle("Abrechnung - Register");

    useEffect(() => {
        if (loggedIn) {
            setLoading(false);
            if (query.get("next") !== null && query.get("next") !== undefined) {
                history.push(query.get("next"));
            } else {
                history.push("/");
            }
        } else {
            setLoading(false);
        }
    }, [loggedIn, history, query]);

    const handleSubmit = (values, { setSubmitting }) => {
        // extract a potential invite token (which should be a uuid) from the query args
        let inviteToken = undefined;
        console.log(query.get("next"));
        if (query.get("next") !== null && query.get("next") !== undefined) {
            const re = /\/invite\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/;
            const m = query.get("next").match(re);
            if (m != null) {
                inviteToken = m[1];
            }
        }
        const payload = {
            ...values,
            inviteToken: inviteToken,
        };

        register(payload)
            .then((res) => {
                toast.success(`Registered successfully, please confirm your email before logging in...`, {
                    autoClose: 20000,
                });
                setSubmitting(false);
                history.push(`/login${queryArgsForward}`);
            })
            .catch((err) => {
                toast.error(err);
                setSubmitting(false);
            });
    };

    const validate = (values) => {
        let errors = {};
        if (values.password !== values.password2) {
            errors["password2"] = "Passwords do not match";
        }
        return errors;
    };

    return (
        <>
            {loading ? (
                <Loading />
            ) : (
                <>
                    <Container maxWidth="xs">
                        <CssBaseline />
                        <Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
                            <Avatar sx={{ margin: 1, backgroundColor: "primary.main" }}>
                                <LockOutlined />
                            </Avatar>
                            <Typography component="h1" variant="h5">
                                Register a new account
                            </Typography>
                            <Formik
                                validate={validate}
                                validationSchema={validationSchema}
                                initialValues={{
                                    username: "",
                                    email: "",
                                    password: "",
                                    password2: "",
                                }}
                                onSubmit={handleSubmit}
                            >
                                {({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => (
                                    <Form onSubmit={handleSubmit}>
                                        <TextField
                                            variant="outlined"
                                            margin="normal"
                                            required
                                            fullWidth
                                            autoFocus
                                            type="text"
                                            label="Username"
                                            name="username"
                                            onBlur={handleBlur}
                                            onChange={handleChange}
                                            value={values.username}
                                        />
                                        <TextField
                                            variant="outlined"
                                            margin="normal"
                                            required
                                            fullWidth
                                            type="email"
                                            name="email"
                                            label="E-Mail"
                                            onBlur={handleBlur}
                                            onChange={handleChange}
                                            value={values.email}
                                        />

                                        <TextField
                                            variant="outlined"
                                            margin="normal"
                                            required
                                            fullWidth
                                            type="password"
                                            name="password"
                                            label="Password"
                                            onBlur={handleBlur}
                                            onChange={handleChange}
                                            value={values.password}
                                        />

                                        <TextField
                                            variant="outlined"
                                            margin="normal"
                                            required
                                            fullWidth
                                            type="password"
                                            name="password2"
                                            label="Repeat Password"
                                            onBlur={handleBlur}
                                            onChange={handleChange}
                                            value={values.password2}
                                        />

                                        {isSubmitting && <LinearProgress />}
                                        <Button
                                            type="submit"
                                            fullWidth
                                            variant="contained"
                                            color="primary"
                                            disabled={isSubmitting}
                                            sx={{ mt: 1 }}
                                        >
                                            Register
                                        </Button>
                                        <Grid container={true} sx={{ justifyContent: "flex-end" }}>
                                            <Grid item>
                                                <Link
                                                    to={`/login${queryArgsForward}`}
                                                    component={RouterLink}
                                                    variant="body2"
                                                >
                                                    Already have an account? Sign in
                                                </Link>
                                            </Grid>
                                        </Grid>
                                    </Form>
                                )}
                            </Formik>
                        </Box>
                    </Container>
                </>
            )}
        </>
    );
}
Example #29
Source File: Login.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function Login() {
    const setUserData = useSetRecoilState(userData);
    const isLoggedIn = useRecoilValue(isAuthenticated);
    const query = useQuery();
    const history = useHistory();

    const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : "";

    useTitle("Abrechnung - Login");

    useEffect(() => {
        if (isLoggedIn) {
            if (query.get("next") !== null && query.get("next") !== undefined) {
                history.push(query.get("next"));
            } else {
                history.push("/");
            }
        }
    }, [isLoggedIn, history, query]);

    const handleSubmit = (values, { setSubmitting }) => {
        login(values)
            .then((res) => {
                toast.success(`Logged in...`);
                setSubmitting(false);
                fetchProfile()
                    .then((result) => {
                        setUserData(result);
                    })
                    .catch((err) => {
                        toast.error(err);
                        removeToken();
                        setUserData(null);
                    });
            })
            .catch((err) => {
                toast.error(err);
                setSubmitting(false);
                removeToken();
                setUserData(null);
            });
    };

    return (
        <Container component="main" maxWidth="xs">
            <CssBaseline />
            <Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
                <Avatar sx={{ margin: 1, backgroundColor: "primary.main" }}>
                    <LockOutlined />
                </Avatar>
                <Typography component="h1" variant="h5">
                    Sign in
                </Typography>
                <Formik
                    initialValues={{ password: "", username: "" }}
                    onSubmit={handleSubmit}
                    validationSchema={validationSchema}
                >
                    {({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => (
                        <Form onSubmit={handleSubmit}>
                            <input type="hidden" name="remember" value="true" />
                            <TextField
                                variant="outlined"
                                margin="normal"
                                required
                                fullWidth
                                autoFocus
                                type="text"
                                label="Username"
                                name="username"
                                onBlur={handleBlur}
                                onChange={handleChange}
                                value={values.username}
                            />

                            <TextField
                                variant="outlined"
                                margin="normal"
                                required
                                fullWidth
                                type="password"
                                name="password"
                                label="Password"
                                onBlur={handleBlur}
                                onChange={handleChange}
                                value={values.password}
                            />

                            {isSubmitting && <LinearProgress />}
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                color="primary"
                                disabled={isSubmitting}
                                sx={{ mt: 1 }}
                            >
                                Login
                            </Button>
                            <Grid container={true} sx={{ justifyContent: "flex-end" }}>
                                <Grid item>
                                    <Link to={`/register${queryArgsForward}`} component={RouterLink} variant="body2">
                                        No account? register
                                    </Link>
                                </Grid>
                            </Grid>
                            <Grid container={true} sx={{ justifyContent: "flex-end" }}>
                                <Grid item>
                                    <Link to="/recover-password" component={RouterLink} variant="body2">
                                        Forgot your password?
                                    </Link>
                                </Grid>
                            </Grid>
                        </Form>
                    )}
                </Formik>
            </Box>
        </Container>
    );
}