react-spinners#ClipLoader JavaScript Examples

The following examples show how to use react-spinners#ClipLoader. 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: Loader.jsx    From ui with MIT License 6 votes vote down vote up
fastLoad = (message) => (
  <>
    <div style={{ padding: 25 }}>
      <ClipLoader
        size={50}
        color='#8f0b10'
      />
    </div>
    <p>
      <Text>
        {message || "We're getting your data ..."}
      </Text>
    </p>
  </>
)
Example #2
Source File: index.js    From js-docs with MIT License 6 votes vote down vote up
export default function Iframes({ url, description }) {
  const [isLoader, setIsLoader] = useState(true)

  return (
    <Container>
      {isLoader && (
        <Loader>
          <ClipLoader size={45} color={"#ffa7c4"} loading={isLoader} />
        </Loader>
      )}
      <IframesStyle
        onLoad={() => setIsLoader(false)}
        title={description}
        src={url}
      />
    </Container>
  )
}
Example #3
Source File: Action.jsx    From pooltogether-governance-ui with MIT License 5 votes vote down vote up
SimpleInput = (props) => {
  const {
    className,
    name,
    dataType,
    register,
    required,
    pattern,
    validate,
    autoCorrect,
    label,
    loading,
    errorMessage,
    autoComplete,
    autoFocus,
    ...inputProps
  } = props

  return (
    <>
      <span className={classnames('flex flex-col xs:flex-row w-full relative', className)}>
        <label className='xs:w-1/4 xs:text-right my-auto xs:mr-4' htmlFor={name}>
          {label} {dataType && <span className='ml-1 text-xxs opacity-70'>{`${dataType}`}</span>}
        </label>
        <input
          {...inputProps}
          type={inputProps.type || 'text'}
          className='bg-card xs:w-3/4 p-2 rounded-sm outline-none focus:outline-none active:outline-none hover:bg-primary focus:bg-primary trans trans-fast border border-transparent focus:border-card'
          id={name}
          autoFocus={autoFocus && isBrowser}
          name={name}
          ref={register?.({ required, pattern, validate })}
          autoCorrect={autoCorrect || 'off'}
          autoComplete={autoComplete || 'hidden'}
        />
        {loading && (
          <div className='absolute right-0 mr-2 mt-2'>
            <ClipLoader size={14} color='rgba(255,255,255,0.3)' />
          </div>
        )}
      </span>
      {errorMessage && <span className='ml-auto text-xxs text-red font-bold'>{errorMessage}</span>}
    </>
  )
}
Example #4
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 5 votes vote down vote up
BoostCollectionModal = ({ visible, onClose }) => {
  const { boostCollection } = useApi();
  const { authToken } = useSelector(state => state.ConnectWallet);

  const [boosting, setBoosting] = useState(false);
  const [address, setAddress] = useState('');

  useEffect(() => {
    if (visible) {
      setBoosting(false);
    }
  }, [visible]);

  const handleBoostCollection = async () => {
    if (boosting) return;

    try {
      setBoosting(true);
      await boostCollection(address, authToken);
      toast('success', 'Collection boosted successfully!');
    } catch (e) {
      console.log(e);
    }
    setBoosting(false);
  };

  return (
    <Modal
      visible={visible}
      title="Boost Collection"
      onClose={onClose}
      submitDisabled={boosting}
      submitLabel={boosting ? <ClipLoader color="#FFF" size={16} /> : 'Boost'}
      onSubmit={!boosting ? () => handleBoostCollection() : null}
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Contract Address</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={boosting}
          />
        </div>
      </div>
    </Modal>
  );
}
Example #5
Source File: Loader.js    From amazon-connect-chat-interface with MIT No Attribution 5 votes vote down vote up
render() {
    return (
      <span className="loader">
        <ClipLoader size={15} color={this.props.color || "#fff"} {...this.props}/>
      </span>
    )
  }
Example #6
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 5 votes vote down vote up
TransferModal = ({
  visible,
  totalSupply,
  transferring,
  onTransfer,
  onClose,
}) => {
  const [address, setAddress] = useState('');
  const [quantity, setQuantity] = useState('1');

  useEffect(() => {
    if (visible) {
      setAddress('');
      setQuantity('1');
    }
  }, [visible]);

  const handleQuantityChange = e => {
    const val = e.target.value;
    if (!val) {
      setQuantity('');
      return;
    }

    if (isNaN(val)) return;

    const _quantity = parseInt(val);
    setQuantity(Math.min(_quantity, totalSupply));
  };

  const handleTransfer = () => {
    let quant = 1;
    if (totalSupply > 1) {
      quant = parseInt(quantity);
    }
    onTransfer(address, quant);
  };

  return (
    <Modal
      visible={visible}
      title="Transfer Item"
      onClose={onClose}
      submitDisabled={transferring}
      submitLabel={
        transferring ? <ClipLoader color="#FFF" size={16} /> : 'Transfer'
      }
      onSubmit={!transferring ? () => handleTransfer() : null}
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Transfer to</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={transferring}
          />
        </div>
      </div>
      {totalSupply !== null && (
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Quantity</div>
          <div className={styles.formInputCont}>
            <input
              className={styles.formInput}
              placeholder={totalSupply}
              value={quantity}
              onChange={handleQuantityChange}
              disabled={transferring || totalSupply === 1}
            />
          </div>
        </div>
      )}
    </Modal>
  );
}
Example #7
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
WFTMModal = ({ visible, onClose }) => {
  const { account, chainId } = useWeb3React();
  const { explorerUrl } = useApi();
  const { getWFTMBalance, wrapFTM, unwrapFTM } = useWFTMContract();

  const [loading, setLoading] = useState(false);
  const [balance, setBalance] = useState(0);
  const [wrappedBalance, setWrappedBalance] = useState(0);
  const [confirming, setConfirming] = useState(false);
  const [wrap, setWrap] = useState(true);
  const [amount, setAmount] = useState('');
  const [inputError, setInputError] = useState(null);

  const { price } = useSelector(state => state.Price);

  const getBalances = async (overrideLoading = false) => {
    if (!overrideLoading) {
      setLoading(true);
    }

    await window.ethereum.enable();
    const provider = new ethers.providers.Web3Provider(window.ethereum);

    let [ftmBal, wftmBal] = await Promise.all([
      await provider.getBalance(account),
      await getWFTMBalance(account),
    ]);

    setBalance(parseFloat(ftmBal.toString()) / 10 ** 18);
    setWrappedBalance(parseFloat(wftmBal.toString()) / 10 ** 18);

    if (!overrideLoading) {
      setLoading(false);
    }

    return [
      parseFloat(ftmBal.toString()) / 10 ** 18,
      parseFloat(wftmBal.toString()) / 10 ** 18,
    ];
  };

  const pollBalanceChange = async (initialFtmBal, initialWftmBal) => {
    setLoading(true);
    let timeout;
    let updated = false;

    await new Promise(
      resolve =>
        (timeout = setTimeout(
          () =>
            getBalances(true).then(([ftmBal, wftmBal]) => {
              if (ftmBal !== initialFtmBal || wftmBal !== initialWftmBal) {
                updated = true;
              }
              resolve();
            }),
          200
        ))
    );

    if (!updated) {
      await pollBalanceChange(initialFtmBal, initialWftmBal);
    }

    clearTimeout(timeout);
    return setLoading(false);
  };

  useEffect(() => {
    if (visible) {
      setLoading(false);
      setConfirming(false);
      setWrap(true);
      setAmount('');
      getBalances();
    }
  }, [visible, chainId]);

  const parseBalance = bal => {
    return bal.toFixed(4);
  };

  const isMax = () => {
    if (wrap) {
      return amount === (balance - 0.01).toString();
    }
    return amount === wrappedBalance.toString();
  };

  const onMax = () => {
    if (wrap) {
      setAmount((balance - 0.01).toString());
    } else {
      setAmount(wrappedBalance.toString());
    }
  };

  const handleWrapFTM = async () => {
    if (confirming || loading) return;

    setConfirming(true);
    try {
      const price = ethers.utils.parseEther(amount);
      if (wrap) {
        const tx = await wrapFTM(price, account);
        await tx.wait();
        await pollBalanceChange(balance, wrappedBalance);
        const toastId = showToast(
          'success',
          'Wrapped FTM successfully!',
          '',
          () => {
            toast.dismiss(toastId);
            window.open(`${explorerUrl}/tx/${tx.hash}`, '_blank');
          }
        );
      } else {
        const tx = await unwrapFTM(price);
        await tx.wait();
        await pollBalanceChange(balance, wrappedBalance);
        const toastId = showToast(
          'success',
          'Unwrap W-FTM successfully!',
          '',
          () => {
            toast.dismiss(toastId);
            window.open(`${explorerUrl}/tx/${tx.hash}`, '_blank');
          }
        );
      }
      setAmount('');
    } catch (err) {
      showToast('error', formatError(err));
      console.log(err);
    } finally {
      setConfirming(false);
    }
    getBalances();
  };

  return (
    <Modal
      visible={visible}
      title="FTM / WFTM Station"
      onClose={onClose}
      submitDisabled={
        confirming ||
        loading ||
        inputError ||
        amount.length === 0 ||
        parseFloat(amount) === 0 ||
        parseFloat(amount) > (wrap ? balance - 0.01 : wrappedBalance)
      }
      submitLabel={
        confirming || loading ? (
          <ClipLoader color="#FFF" size={16} />
        ) : wrap ? (
          'Wrap'
        ) : (
          'Unwrap'
        )
      }
      onSubmit={() =>
        amount.length &&
        parseFloat(amount) > 0 &&
        parseFloat(amount) <= (wrap ? balance - 0.01 : wrappedBalance) &&
        handleWrapFTM()
      }
    >
      <div className={cx(styles.swapContainer, !wrap && styles.reverse)}>
        <div className={styles.swapBox}>
          <div className={styles.symbol}>FTM</div>
          <div className={styles.swapBoxInner}>
            <div className={styles.balance}>
              Balance:{' '}
              {loading ? (
                <Skeleton width={60} height={20} />
              ) : (
                parseBalance(balance)
              )}
              {wrap && !isMax() && !loading && balance > 0 && (
                <div className={styles.max} onClick={onMax}>
                  (Max)
                </div>
              )}
            </div>
            <div className={styles.rightBox}>
              <PriceInput
                className={styles.input}
                placeholder="0.0"
                decimals={18}
                value={'' + amount}
                onChange={setAmount}
                onInputError={setInputError}
              />
              <div className={styles.usdVal}>
                ${formatNumber(((parseFloat(amount) || 0) * price).toFixed(2))}
              </div>
            </div>
          </div>
        </div>
        <div className={styles.swapbtn} onClick={() => setWrap(!wrap)}>
          <SwapVertIcon className={styles.icon} />
        </div>
        <div className={styles.swapBox}>
          <div className={styles.symbol}>WFTM</div>
          <div className={styles.swapBoxInner}>
            <div className={styles.balance}>
              Balance:{' '}
              {loading ? (
                <Skeleton width={60} height={20} />
              ) : (
                parseBalance(wrappedBalance)
              )}
              {!wrap && !isMax() && !loading && balance > 0 && (
                <div className={styles.max} onClick={onMax}>
                  (Max)
                </div>
              )}
            </div>
            <div className={styles.rightBox}>
              <PriceInput
                className={styles.input}
                placeholder="0.0"
                decimals={18}
                value={'' + amount}
                onChange={setAmount}
                onInputError={setInputError}
              />
              <div className={styles.usdVal}>
                ${formatNumber(((parseFloat(amount) || 0) * price).toFixed(2))}
              </div>
            </div>
          </div>
        </div>
      </div>
      <InputError text={inputError} />
    </Modal>
  );
}
Example #8
Source File: index.jsx    From ui with MIT License 4 votes vote down vote up
DataManagementPage = () => {
  const dispatch = useDispatch();

  const samples = useSelector((state) => state.samples);

  const { activeExperimentId } = useSelector((state) => state.experiments.meta);
  const experiments = useSelector(((state) => state.experiments));

  const activeExperiment = experiments[activeExperimentId];
  const { saving: experimentsSaving } = experiments.meta;
  const { saving: samplesSaving } = samples.meta;

  const [newProjectModalVisible, setNewProjectModalVisible] = useState(false);

  useEffect(() => {
    if (experiments.ids.length === 0) dispatch(loadExperiments());
  }, []);

  const samplesAreLoaded = () => {
    const loadedSampleIds = Object.keys(samples);
    return activeExperiment.sampleIds.every((sampleId) => loadedSampleIds.includes(sampleId));
  };

  useEffect(() => {
    if (!activeExperimentId) return;

    dispatch(loadProcessingSettings(activeExperimentId));

    if (!samplesAreLoaded()) dispatch(loadSamples(activeExperimentId));

    dispatch(loadBackendStatus(activeExperimentId));
  }, [activeExperimentId]);

  const PROJECTS_LIST = 'Projects';
  const PROJECT_DETAILS = 'Project Details';

  const TILE_MAP = {
    [PROJECTS_LIST]: {
      toolbarControls: [],
      component: (width, height) => (
        <ProjectsListContainer
          height={height}
          onCreateNewProject={() => setNewProjectModalVisible(true)}
        />
      ),
    },
    [PROJECT_DETAILS]: {
      toolbarControls: [],
      component: (width, height) => {
        if (!activeExperimentId) {
          return <ExampleExperimentsSpace introductionText='You have no projects yet.' />;
        }

        return (
          <ProjectDetails
            width={width}
            height={height}
          />
        );
      },
    },
  };

  const windows = {
    direction: 'row',
    first: PROJECTS_LIST,
    second: PROJECT_DETAILS,
    splitPercentage: 23,
  };

  return (
    <>
      <Header title='Data Management' />
      {experimentsSaving || samplesSaving ? (
        <center>
          <Space direction='vertical'>
            <ClipLoader
              size={50}
              color='#8f0b10'
            />
            Loading...
          </Space>
        </center>
      ) : (<></>)}
      {newProjectModalVisible ? (
        <NewProjectModal
          onCancel={() => { setNewProjectModalVisible(false); }}
          onCreate={() => { setNewProjectModalVisible(false); }}
        />
      ) : (<></>)}
      <MultiTileContainer
        tileMap={TILE_MAP}
        initialArrangement={windows}
      />
    </>
  );
}
Example #9
Source File: SamplesTable.jsx    From ui with MIT License 4 votes vote down vote up
SamplesTable = forwardRef((props, ref) => {
  const dispatch = useDispatch();
  const [tableData, setTableData] = useState([]);

  const experiments = useSelector((state) => state.experiments);
  const samples = useSelector((state) => state.samples);
  const areSamplesLoading = useSelector((state) => state.samples.meta.loading);

  const activeExperimentId = useSelector((state) => state.experiments.meta.activeExperimentId);
  const activeExperiment = useSelector((state) => state.experiments[activeExperimentId]);

  const [sampleNames, setSampleNames] = useState(new Set());
  const DragHandle = sortableHandle(() => <MenuOutlined style={{ cursor: 'grab', color: '#999' }} />);

  const initialTableColumns = [
    {
      fixed: 'left',
      index: 0,
      key: 'sort',
      dataIndex: 'sort',
      width: 30,
      render: () => <DragHandle />,
    },
    {
      className: `${integrationTestConstants.classes.SAMPLE_CELL}`,
      index: 1,
      key: 'sample',
      title: 'Sample',
      dataIndex: 'name',
      fixed: true,
      render: (text, record, indx) => <SampleNameCell cellInfo={{ text, record, indx }} />,
    },
    {
      index: 2,
      key: 'barcodes',
      title: 'barcodes.tsv',
      dataIndex: 'barcodes',
      render: (tableCellData) => <UploadCell columnId='barcodes' tableCellData={tableCellData} />,
    },
    {
      index: 3,
      key: 'genes',
      title: 'genes.tsv',
      dataIndex: 'genes',
      render: (tableCellData) => <UploadCell columnId='genes' tableCellData={tableCellData} />,
    },
    {
      index: 4,
      key: 'matrix',
      title: 'matrix.mtx',
      dataIndex: 'matrix',
      render: (tableCellData) => <UploadCell columnId='matrix' tableCellData={tableCellData} />,
    },
  ];

  const [tableColumns, setTableColumns] = useState(initialTableColumns);

  useEffect(() => {
    if (activeExperiment.sampleIds.length > 0) {
      // if there are samples - build the table columns
      setSampleNames(new Set(activeExperiment.sampleIds.map((id) => samples[id]?.name.trim())));
      const metadataColumns = activeExperiment.metadataKeys.map(
        (metadataKey) => createInitializedMetadataColumn(metadataKeyToName(metadataKey)),
      ) || [];
      setTableColumns([...initialTableColumns, ...metadataColumns]);
    } else {
      setTableColumns([]);
      setSampleNames(new Set());
    }
  }, [samples, activeExperiment]);

  useConditionalEffect(() => {
    dispatch(loadSamples(activeExperimentId));
  }, [activeExperiment?.sampleIds], { lazy: true });

  const deleteMetadataColumn = (name) => {
    dispatch(deleteMetadataTrack(name, activeExperimentId));
  };

  const createInitializedMetadataColumn = (name) => {
    const key = metadataNameToKey(name);

    return {
      key,
      title: () => (
        <MetadataColumnTitle
          name={name}
          sampleNames={sampleNames}
          setCells={setCells}
          deleteMetadataColumn={deleteMetadataColumn}
          activeExperimentId={activeExperimentId}
        />
      ),
      width: 200,
      dataIndex: key,
      render: (cellValue, record, rowIdx) => (
        <EditableFieldCell
          cellText={cellValue}
          dataIndex={key}
          rowIdx={rowIdx}
          onAfterSubmit={(newValue) => {
            dispatch(updateValueInMetadataTrack(activeExperimentId, record.uuid, key, newValue));
          }}
        />
      ),
    };
  };

  const onMetadataCreate = (name) => {
    dispatch(createMetadataTrack(name, activeExperimentId));
  };

  useImperativeHandle(ref, () => ({

    createMetadataColumn() {
      const key = temporaryMetadataKey(tableColumns);
      const metadataCreateColumn = {
        key,
        fixed: 'right',
        title: () => (
          <MetadataPopover
            existingMetadata={activeExperiment.metadataKeys}
            onCreate={(name) => {
              onMetadataCreate(name);
            }}
            onCancel={() => {
              deleteMetadataColumn(key);
            }}
            message='Provide new metadata track name'
            visible
          >
            <Space>
              New Metadata Track
            </Space>
          </MetadataPopover>
        ),
        width: 200,
      };
      setTableColumns([...tableColumns, metadataCreateColumn]);
    },
  }));

  const MASS_EDIT_ACTIONS = [
    'REPLACE_EMPTY',
    'REPLACE_ALL',
    'CLEAR_ALL',
  ];

  const setCells = (value, metadataKey, actionType) => {
    if (!MASS_EDIT_ACTIONS.includes(actionType)) return;

    const canUpdateCell = (sampleUuid, action) => {
      if (action !== 'REPLACE_EMPTY') return true;

      const isMetadataEmpty = (uuid) => (
        !samples[uuid].metadata[metadataKey]
        || samples[uuid].metadata[metadataKey] === METADATA_DEFAULT_VALUE
      );

      return isMetadataEmpty(sampleUuid);
    };

    activeExperiment.sampleIds.forEach(
      (sampleUuid) => {
        if (canUpdateCell(sampleUuid, actionType)) {
          dispatch(updateValueInMetadataTrack(activeExperimentId, sampleUuid, metadataKey, value));
        }
      },
    );
  };

  useEffect(() => {
    if (activeExperiment.sampleIds.length === 0) {
      setTableData([]);
      return;
    }

    const newData = activeExperiment.sampleIds.map((sampleUuid, idx) => {
      // upload problems sometimes lead to partial updates and incosistent states
      // in this situation it's possible that the sampleUuid does not exist
      // this a temporary fix so that the whole UI doesn't crash preventing the
      // user from removing the dataset or uploading another one.
      const sampleFiles = samples[sampleUuid]?.files || {};

      const barcodesFile = sampleFiles['barcodes.tsv.gz'] ?? { upload: { status: UploadStatus.FILE_NOT_FOUND } };
      const genesFile = sampleFiles['features.tsv.gz'] ?? { upload: { status: UploadStatus.FILE_NOT_FOUND } };
      const matrixFile = sampleFiles['matrix.mtx.gz'] ?? { upload: { status: UploadStatus.FILE_NOT_FOUND } };

      const barcodesData = { sampleUuid, file: barcodesFile };
      const genesData = { sampleUuid, file: genesFile };
      const matrixData = { sampleUuid, file: matrixFile };

      return {
        key: idx,
        name: samples[sampleUuid]?.name || 'UPLOAD ERROR: Please reupload sample',
        uuid: sampleUuid,
        barcodes: barcodesData,
        genes: genesData,
        matrix: matrixData,
        ...samples[sampleUuid]?.metadata,
      };
    });
    setTableData(newData);
  }, [experiments, samples, activeExperimentId]);

  const noDataComponent = (
    <ExampleExperimentsSpace
      introductionText='Start uploading your samples by clicking on Add samples.'
      imageStyle={{ height: 60 }}
    />
  );

  const onSortEnd = async ({ oldIndex, newIndex }) => {
    if (oldIndex !== newIndex) {
      const newData = arrayMoveImmutable(tableData, oldIndex, newIndex).filter((el) => !!el);
      const newSampleOrder = newData.map((sample) => sample.uuid);

      try {
        await dispatch(reorderSamples(activeExperimentId, oldIndex, newIndex, newSampleOrder));
      } catch (e) {
        // If the fetch fails, avoid doing setTableData(newData)
        return;
      }

      setTableData(newData);
    }
  };

  const SortableRow = sortableElement((otherProps) => <tr {...otherProps} className={`${otherProps.className} drag-visible`} />);
  const SortableTable = sortableContainer((otherProps) => <tbody {...otherProps} />);

  const DragContainer = (otherProps) => (
    <SortableTable
      useDragHandle
      disableAutoscroll
      helperClass='row-dragging'
      onSortEnd={onSortEnd}
      {...otherProps}
    />
  );

  const DraggableRow = (otherProps) => {
    const index = tableData.findIndex((x) => x.key === otherProps['data-row-key']);
    return <SortableRow index={index} {...otherProps} />;
  };

  const renderLoader = () => (
    <>
      <Row justify='center'>
        <ClipLoader
          size={50}
          color='#8f0b10'
        />
      </Row>

      <Row justify='center'>
        <Text>
          We&apos;re getting your samples ...
        </Text>
      </Row>
    </>
  );

  const renderSamplesTable = () => (
    <Row>
      <Col>
        <Table
          id='samples-table'
          size='small'
          scroll={{
            x: 'max-content',
          }}
          bordered
          columns={tableColumns}
          dataSource={tableData}
          sticky
          pagination={false}
          locale={{ emptyText: noDataComponent }}
          components={{
            body: {
              wrapper: DragContainer,
              row: DraggableRow,
            },
          }}
        />
      </Col>
    </Row>
  );

  return (
    <>
      {areSamplesLoading ? renderLoader() : renderSamplesTable()}
    </>
  );
})
Example #10
Source File: NewProjectModal.jsx    From ui with MIT License 4 votes vote down vote up
NewProjectModal = (props) => {
  const {
    onCreate,
    onCancel,
  } = props;

  const experiments = useSelector(((state) => state.experiments));
  const { saving, error } = experiments.meta;

  const dispatch = useDispatch();
  const [projectNames, setProjectNames] = useState(new Set());
  const [projectName, setProjectName] = useState('');
  const [projectDescription, setProjectDescription] = useState('');
  const [isValidName, setIsValidName] = useState(false);

  const firstTimeFlow = experiments.ids.length === 0;
  const validationChecks = [
    rules.MIN_8_CHARS,
    rules.MIN_2_SEQUENTIAL_CHARS,
    rules.ALPHANUM_DASH_SPACE,
    rules.UNIQUE_NAME_CASE_INSENSITIVE,
  ];

  const validationParams = {
    existingNames: projectNames,
  };

  useEffect(() => {
    setProjectNames(new Set(experiments.ids.map((id) => experiments[id].name.trim())));
  }, [experiments.ids]);

  useEffect(() => {
    setIsValidName(validateInputs(projectName, validationChecks, validationParams).isValid);
  }, [projectName, projectNames]);

  const submit = () => {
    setProjectName('');

    dispatch(createExperiment(projectName, projectDescription));
    onCreate(projectName, projectDescription);
  };

  return (
    <Modal
      className={integrationTestConstants.classes.NEW_PROJECT_MODAL}
      title='Create a new project'
      visible
      footer={(
        <Button
          data-test-id={integrationTestConstants.ids.CONFIRM_CREATE_NEW_PROJECT}
          type='primary'
          key='create'
          block
          disabled={!isValidName}
          onClick={() => {
            submit();
          }}
        >
          Create Project
        </Button>
      )}
      onCancel={onCancel}
    >
      <Space>
        <Space direction='vertical' style={firstTimeFlow ? { marginTop: '2rem' } : {}}>
          {firstTimeFlow && (
            <Title level={3} style={{ textAlign: 'center' }}>
              Create a project to start analyzing
              your data in Cellenics
            </Title>
          )}
          <Paragraph>
            Projects are where you can organize your data into
            samples, assign metadata, and start your analysis
            in Cellenics. Name it after the experiment
            you&apos;re working on.
          </Paragraph>

          <Form layout='vertical'>
            <Form.Item
              validateStatus={isValidName ? 'success' : 'error'}
              help={(
                <ul>
                  {validateInputs(
                    projectName,
                    validationChecks,
                    validationParams,
                  ).results
                    .filter((msg) => msg !== true)
                    .map((msg) => <li>{msg}</li>)}
                </ul>
              )}
              label={(
                <span>
                  Project name
                  {' '}
                  <Text type='secondary'>(You can change this later)</Text>
                </span>
              )}
              required
              name='requiredMark'
            >
              <Input
                data-test-id={integrationTestConstants.ids.PROJECT_NAME}
                aria-label='new project name'
                onChange={(e) => {
                  setProjectName(e.target.value.trim());
                }}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && isValidName) {
                    onCreate(projectName, projectDescription);
                    setProjectName('');
                    setIsValidName(false);
                  }
                }}
                placeholder='Ex.: Lung gamma delta T cells'
                value={projectName}
                disabled={saving}
              />
            </Form.Item>
            <Form.Item
              label='Project description'
            >
              <TextArea
                data-test-id={integrationTestConstants.ids.PROJECT_DESCRIPTION}
                onChange={(e) => { setProjectDescription(e.target.value); }}
                placeholder='Type description'
                autoSize={{ minRows: 3, maxRows: 5 }}
                disabled={saving}
                aria-label='new project description'
              />
            </Form.Item>
          </Form>

          {
            saving && (
              <center>
                <Space direction='vertical'>
                  <ClipLoader
                    size={50}
                    color='#8f0b10'
                  />
                  <Text>Creating project...</Text>
                </Space>
              </center>
            )
          }

          {
            error && (
              <Text type='danger' style={{ fontSize: 14 }}>
                {error}
              </Text>
            )
          }

        </Space>
      </Space>
    </Modal>

  );
}
Example #11
Source File: NewProjectModal.test.jsx    From ui with MIT License 4 votes vote down vote up
describe('NewProjectModal', () => {
  it('renders without options', () => {
    const component = mount(
      <Provider store={mockStore(initialState)}>
        <NewProjectModal onCancel={onCancel} onCreate={onCreate} />
      </Provider>,
    );
    expect(component.exists()).toEqual(true);
  });

  it('contains required components for first time flow', () => {
    const component = mount(
      <Provider store={mockStore(initialState)}>
        <NewProjectModal onCancel={onCancel} onCreate={onCreate} />
      </Provider>,
    );

    // It has a header
    expect(component.find('h3').length).toBeGreaterThan(0);

    // It has an input
    expect(component.find(Input).length).toEqual(1);

    // It has a experiment description input
    expect(component.find(TextArea).length).toEqual(1);

    // It has a button
    expect(component.find(Button).length).toEqual(1);
  });

  it('contains required components for later flows', () => {
    const component = mount(
      <Provider store={mockStore(storeWithExperiments)}>
        <NewProjectModal onCancel={onCancel} onCreate={onCreate} />
      </Provider>,
    );

    // It has no header
    expect(component.find('h3').length).toEqual(0);

    // It has an input
    expect(component.find(Input).length).toEqual(1);

    // It has a experiment description input
    expect(component.find(TextArea).length).toEqual(1);

    // It has a button
    expect(component.find(Button).length).toEqual(1);
  });

  it('disables input and shows loading when experiment is being saved', () => {
    const savingState = {
      ...initialState,
      experiments: {
        meta: {
          ...initialState.experiments.meta,
          saving: true,
        },
        ids: ['123'],
        123: {
          name: 'my awesome experiment',
        },
      },
    };

    const component = mount(
      <Provider store={mockStore(savingState)}>
        <NewProjectModal onCancel={onCancel} onCreate={onCreate} />
      </Provider>,
    );

    // Named input is disabled
    expect(component.find(Input).props().disabled).toEqual(true);

    // Textarea is disabled
    expect(component.find(TextArea).props().disabled).toEqual(true);

    // It has a spinner
    expect(component.find(ClipLoader).length).toEqual(1);
  });

  it('disables input and shows error if experiment has errors', () => {
    const errMsg = 'Error message';

    const errorState = {
      ...initialState,
      experiments: {
        meta: {
          ...initialState.experiments.meta,
          error: errMsg,
        },
        ids: ['123'],
        123: {
          name: 'my awesome experiment',
        },
      },
    };

    const component = mount(
      <Provider store={mockStore(errorState)}>
        <NewProjectModal />
      </Provider>,
    );

    // Named input is not disabled
    expect(component.find(Input).props().disabled).toEqual(false);

    // Textarea is not disabled
    expect(component.find(TextArea).props().disabled).toEqual(false);

    // It has an error text
    expect(component.find(Text).last().text()).toEqual(errMsg);
  });
});
Example #12
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
NotificationSetting = () => {
  const dispatch = useDispatch();

  const { account } = useWeb3React();
  const {
    getNonce,
    getNotificationSettings,
    updateNotificationSettings,
  } = useApi();
  const { authToken } = useSelector(state => state.ConnectWallet);

  const [saving, setSaving] = useState(false);
  const [settings, setSettings] = useState({});

  const getSettings = async () => {
    const { data } = await getNotificationSettings(authToken);
    setSettings(data);
  };

  useEffect(() => {
    dispatch(HeaderActions.toggleSearchbar(true));
  }, []);

  useEffect(() => {
    if (authToken) {
      getSettings();
    }
  }, [authToken]);

  const handleChange = e => {
    const key = e.target.name;
    const newSettings = { ...settings };
    newSettings[key] = !settings[key];
    setSettings(newSettings);
  };

  const handleSave = async () => {
    setSaving(true);
    try {
      let signature;
      let addr;
      try {
        const { data: nonce } = await getNonce(account, authToken);
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          'You need to sign the message to be able to update your settings.'
        );
        setSaving(false);
        return;
      }
      const { status } = await updateNotificationSettings(
        settings,
        authToken,
        signature,
        addr
      );
      if (status === 'success') {
        toast('success', 'Notification settings updated!');
      }
    } catch (err) {
      console.log(err);
    }
    setSaving(false);
  };

  const getSetting = key =>
    settings[key] === undefined ? true : settings[key];

  return (
    <div className={styles.container}>
      <Header border />
      <div className={styles.inner}>
        <div className={styles.title}>Notification Settings</div>

        <div className={styles.body}>
          <div className={styles.group}>
            <FormControlLabel
              className={cx(styles.formControl, styles.selected)}
              classes={{ label: styles.groupTitle }}
              control={
                <CustomCheckbox
                  checked={getSetting('sNotification')}
                  onChange={handleChange}
                  name="sNotification"
                />
              }
              label="Your Activity Notifications"
            />
            <div className={styles.groupOptions}>
              {selfSettings.map((option, idx) => (
                <SettingOption
                  key={idx}
                  name={option.value}
                  title={option.title}
                  description={option.description}
                  disabled={!getSetting('sNotification')}
                  checked={
                    getSetting('sNotification') && getSetting(option.value)
                  }
                  onChange={handleChange}
                />
              ))}
            </div>
          </div>

          <div className={styles.group}>
            <FormControlLabel
              className={cx(styles.formControl, styles.selected)}
              classes={{ label: styles.groupTitle }}
              control={
                <CustomCheckbox
                  checked={getSetting('fNotification')}
                  onChange={handleChange}
                  name="fNotification"
                />
              }
              label="Follower Activity Notifications"
            />
            <div className={styles.groupOptions}>
              {followerSettings.map((option, idx) => (
                <SettingOption
                  key={idx}
                  name={option.value}
                  title={option.title}
                  description={option.description}
                  disabled={!getSetting('fNotification')}
                  checked={
                    getSetting('fNotification') && getSetting(option.value)
                  }
                  onChange={handleChange}
                />
              ))}
            </div>
          </div>
        </div>

        <div className={styles.buttonsWrapper}>
          <div
            className={cx(styles.createButton, saving && styles.disabled)}
            onClick={!saving ? handleSave : null}
          >
            {saving ? <ClipLoader color="#FFF" size={16} /> : 'Save Settings'}
          </div>
        </div>
      </div>
    </div>
  );
}
Example #13
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
CollectionCreate = () => {
  const dispatch = useDispatch();
  const history = useHistory();
  const { account } = useWeb3React();

  const {
    explorerUrl,
    fetchPendingCollections,
    approveCollection,
    rejectCollection,
  } = useApi();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [loading, setLoading] = useState(true);
  const [index, setIndex] = useState(null);
  const [approving, setApproving] = useState(false);
  const [rejecting, setRejecting] = useState(false);
  const [collections, setCollections] = useState([]);
  const [reason, setReason] = useState('');

  useEffect(() => {
    dispatch(HeaderActions.toggleSearchbar(true));
  }, []);

  useEffect(() => {
    if (account && authToken) {
      if (account === ADMIN_ADDRESS) {
        fetchCollections();
      } else {
        history.replace('/');
      }
    }
  }, [account, authToken]);

  const init = async () => {
    setLoading(true);
    setIndex(null);
    fetchCollections();
  };

  const fetchCollections = async () => {
    try {
      const { status, data } = await fetchPendingCollections(authToken);
      if (status === 'success') {
        setLoading(false);
        setCollections(data);
      } else {
        history.replace('/');
      }
    } catch (err) {
      console.log(err);
    }
  };

  const selectedCategories = Categories.filter(
    cat =>
      index !== null &&
      collections[index].categories.indexOf(cat.id.toString()) > -1
  );

  const handleApprove = async () => {
    setApproving(true);
    try {
      await approveCollection(collections[index].erc721Address, authToken);
      toast('success', 'Collection Approved!');
      init();
    } catch (err) {
      console.log(err);
    }
    setApproving(false);
  };

  const handleReject = async () => {
    setRejecting(true);
    try {
      await rejectCollection(
        collections[index].erc721Address,
        reason,
        authToken
      );
      toast('success', 'Collection Rejected!');
      init();
    } catch (err) {
      console.log(err);
    }
    setRejecting(false);
  };

  return (
    <div className={styles.container}>
      <Header border />
      {loading ? (
        <div className={styles.loadingPanel}>
          <ClipLoader color="#3D3D3D" size={40} />
        </div>
      ) : index === null ? (
        <div className={styles.inner}>
          {collections.map((collection, idx) => (
            <div
              className={styles.collection}
              key={idx}
              onClick={() => setIndex(idx)}
            >
              <img
                src={`https://cloudflare-ipfs.com/ipfs/${collection.logoImageHash}`}
                className={styles.collectionLogo}
              />
              <div className={styles.collectionName}>
                {collection.collectionName}
              </div>
            </div>
          ))}
        </div>
      ) : (
        <div className={styles.inner}>
          <div className={styles.title}>Review Collection</div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>Logo image</div>
            <div className={styles.inputWrapper}>
              <div className={styles.logoUploadBox}>
                <img
                  src={`https://cloudflare-ipfs.com/ipfs/${collections[index].logoImageHash}`}
                />
              </div>
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle1}>Name</div>
            <div className={styles.inputWrapper}>
              <input
                className={styles.input}
                value={collections[index].collectionName}
                disabled
              />
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle1}>Description</div>
            <div className={styles.inputWrapper}>
              <textarea
                className={cx(styles.input, styles.longInput)}
                value={collections[index].description}
                disabled
              />
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle1}>Royalty</div>
            <div className={styles.inputWrapper}>
              <input
                className={styles.input}
                value={collections[index].royalty}
                disabled
              />
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle1}>Fee Recipient</div>
            <div className={styles.inputWrapper}>
              <input
                className={styles.input}
                value={collections[index].feeRecipient}
                disabled
              />
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>Category</div>
            <div className={cx(styles.inputWrapper, styles.categoryList)}>
              {selectedCategories.map((cat, idx) => (
                <div className={styles.selectedCategory} key={idx}>
                  <img src={cat.icon} className={styles.categoryIcon} />
                  <span className={styles.categoryLabel}>{cat.label}</span>
                </div>
              ))}
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>Links</div>
            <div className={styles.inputWrapper}>
              <div className={styles.linksWrapper}>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={nftIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    {explorerUrl}/address/{collections[index].erc721Address}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={webIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    https://{collections[index].siteUrl}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={discordIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    https://discord.gg/{collections[index].discord}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={twitterIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    @{collections[index].twitterHandle}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={instagramIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    @{collections[index].instagramHandle}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={mediumIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    @{collections[index].mediumHandle}
                  </div>
                </div>
                <div className={styles.linkItem}>
                  <div className={styles.linkIconWrapper}>
                    <img src={telegramIcon} className={styles.linkIcon} />
                  </div>
                  <div className={styles.inputPrefix}>
                    https://t.me/{collections[index].telegram}
                  </div>
                </div>
              </div>
            </div>
          </div>

          <div className={styles.inputGroup}>
            <div className={styles.inputTitle1}>Reason</div>
            <div className={styles.inputWrapper}>
              <textarea
                className={cx(styles.input, styles.longInput)}
                maxLength={500}
                placeholder="Tell them why you rejected their collection"
                value={reason}
                onChange={e => setReason(e.target.value)}
              />
              <div className={styles.lengthIndicator}>{reason.length}/500</div>
            </div>
          </div>

          <div className={styles.buttonsWrapper}>
            <div
              className={cx(
                styles.createButton,
                (approving || rejecting) && styles.disabled
              )}
              onClick={handleApprove}
            >
              {approving ? <ClipLoader color="#FFF" size={16} /> : 'Approve'}
            </div>
            <div
              className={cx(
                styles.rejectButton,
                (approving || rejecting) && styles.disabled
              )}
              onClick={handleReject}
            >
              {rejecting ? <ClipLoader color="#FFF" size={16} /> : 'Reject'}
            </div>
            <div
              className={cx(
                styles.cancelButton,
                (approving || rejecting) && styles.disabled
              )}
              onClick={() => setIndex(null)}
            >
              Back
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
Example #14
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
CollectionCreate = ({ isRegister }) => {
  const dispatch = useDispatch();
  const history = useHistory();

  const { account } = useWeb3React();
  const { apiUrl, getNonce } = useApi();
  const {
    getFactoryContract,
    getPrivateFactoryContract,
    getArtFactoryContract,
    getPrivateArtFactoryContract,
    createNFTContract,
  } = useFactoryContract();

  const inputRef = useRef(null);

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [deploying, setDeploying] = useState(false);
  const [creating, setCreating] = useState(false);
  const [logo, setLogo] = useState(null);
  const [anchorEl, setAnchorEl] = useState(null);
  const [selected, setSelected] = useState([]);
  const [name, setName] = useState('');
  const [nameError, setNameError] = useState(null);
  const [symbol, setSymbol] = useState('');
  const [symbolError, setSymbolError] = useState(null);
  const [description, setDescription] = useState('');
  const [descriptionError, setDescriptionError] = useState(null);
  const [royalty, setRoyalty] = useState('');
  const [feeRecipient, setFeeRecipient] = useState('');
  const [recipientError, setRecipientError] = useState(null);
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState(null);
  const [address, setAddress] = useState('');
  const [addressError, setAddressError] = useState('');
  const [siteUrl, setSiteUrl] = useState('');
  const [discord, setDiscord] = useState('');
  const [twitterHandle, setTwitterHandle] = useState('');
  const [instagramHandle, setInstagramHandle] = useState('');
  const [mediumHandle, setMediumHandle] = useState('');
  const [telegram, setTelegram] = useState('');
  const [isPrivate, setIsPrivate] = useState(false);
  const [isSingle, setIsSingle] = useState(true);

  const isMenuOpen = Boolean(anchorEl);

  useEffect(() => {
    dispatch(HeaderActions.toggleSearchbar(true));
  }, []);

  useEffect(() => {
    setLogo(null);
    setAnchorEl(null);
    setSelected([]);
    setName('');
    setNameError(null);
    setSymbol('');
    setSymbolError(null);
    setDescription('');
    setDescriptionError(null);
    setEmail('');
    setEmailError(null);
    setAddress('');
    setAddressError(null);
    setSiteUrl('');
    setDiscord('');
    setTwitterHandle('');
    setInstagramHandle('');
    setMediumHandle('');
    setTelegram('');
  }, [isRegister]);

  const options = Categories.filter(cat => selected.indexOf(cat.id) === -1);
  const selectedCategories = Categories.filter(
    cat => selected.indexOf(cat.id) > -1
  );

  const removeImage = () => {
    setLogo(null);
  };

  const handleFileSelect = e => {
    if (e.target.files.length > 0) {
      const file = e.target.files[0];

      const reader = new FileReader();

      reader.onload = function(e) {
        setLogo(e.target.result);
      };

      reader.readAsDataURL(file);
    }
  };

  const validateName = () => {
    if (name.length === 0) {
      setNameError("This field can't be blank");
    } else {
      setNameError(null);
    }
  };

  const validateSymbol = () => {
    if (symbol.length === 0) {
      setSymbolError("This field can't be blank");
    } else if (symbol.includes(' ')) {
      setSymbolError("Symbol can't include spaces");
    } else {
      setSymbolError(null);
    }
  };

  const validateDescription = () => {
    if (description.length === 0) {
      setDescriptionError("This field can't be blank");
    } else {
      setDescriptionError(null);
    }
  };

  const validateFeeRecipient = () => {
    if (feeRecipient.length === 0) {
      setRecipientError("This field can't be blank");
    } else if (!isAddress(feeRecipient)) {
      setRecipientError('Invalid address');
    } else {
      setRecipientError(null);
    }
  };

  const validEmail = email => /(.+)@(.+){2,}\.(.+){2,}/.test(email);

  const validateEmail = () => {
    if (email.length === 0) {
      setEmailError("This field can't be blank");
    } else if (validEmail(email)) {
      setEmailError(null);
    } else {
      setEmailError('Invalid email address.');
    }
  };

  const validateAddress = () => {
    if (address.length === 0) {
      setAddressError("This field can't be blank");
    } else {
      setAddressError(null);
    }
  };

  const handleMenuOpen = e => {
    if (selected.length < 3) {
      setAnchorEl(e.currentTarget);
    }
  };

  const handleMenuClose = () => {
    setAnchorEl(null);
  };

  const selectCategory = catId => {
    setSelected([...selected, catId]);
    if (selected.length === 2) {
      setAnchorEl(null);
    }
  };

  const deselectCategory = catId => {
    setSelected(selected.filter(id => id !== catId));
  };

  const isValid = (() => {
    if (!logo) return false;
    if (nameError) return false;
    if (descriptionError) return false;
    if (addressError) return false;
    if (!isRegister && (symbol.length === 0 || symbol.includes(' ')))
      return false;
    if (email.length === 0) return false;
    if (!validEmail(email)) return false;
    if (isRegister && !isAddress(feeRecipient)) return false;
    return true;
  })();

  const clipImage = (image, clipX, clipY, clipWidth, clipHeight, cb) => {
    const CANVAS_SIZE = 128;
    const canvas = document.createElement('canvas');
    canvas.width = CANVAS_SIZE;
    canvas.height = CANVAS_SIZE;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(
      image,
      clipX,
      clipY,
      clipWidth,
      clipHeight,
      0,
      0,
      CANVAS_SIZE,
      CANVAS_SIZE
    );
    cb(canvas.toDataURL());
  };

  const handleRegister = async () => {
    if (creating) return;

    setCreating(true);

    const img = new Image();
    img.onload = function() {
      const w = this.width;
      const h = this.height;
      const size = Math.min(w, h);
      const x = (w - size) / 2;
      const y = (h - size) / 2;
      clipImage(img, x, y, size, size, async logodata => {
        try {
          const { data: nonce } = await getNonce(account, authToken);

          let signature;
          let signatureAddress;

          try {
            const signer = await getSigner();
            const msg = `Approve Signature on Artion.io with nonce ${nonce}`;

            signature = await signer.signMessage(msg);
            signatureAddress = ethers.utils.verifyMessage(msg, signature);
          } catch (err) {
            toast(
              'error',
              'You need to sign the message to be able to register a collection.'
            );
            setCreating(false);
            return;
          }

          const formData = new FormData();
          formData.append('collectionName', name);
          formData.append('erc721Address', address);
          formData.append('imgData', logodata);
          const result = await axios({
            method: 'post',
            url: `${apiUrl}/ipfs/uploadCollectionImage2Server`,
            data: formData,
            headers: {
              'Content-Type': 'multipart/form-data',
              Authorization: `Bearer ${authToken}`,
            },
          });

          const logoImageHash = result.data.data;
          const data = {
            email,
            erc721Address: address,
            collectionName: name,
            description,
            categories: selected.join(','),
            logoImageHash,
            siteUrl,
            discord,
            twitterHandle,
            instagramHandle,
            mediumHandle,
            telegram,
            signature,
            signatureAddress,
            royalty,
            feeRecipient,
          };

          await axios({
            method: 'post',
            url: `${apiUrl}/collection/collectiondetails`,
            data: JSON.stringify(data),
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${authToken}`,
            },
          });

          toast(
            'success',
            'Application submitted!',
            'Your collection registration application is successfully submitted for review.\nOnce approved, you will get an email notification.'
          );

          setCreating(false);

          history.push('/explore');
        } catch (e) {
          console.log('Error: ', e);
          setCreating(false);
        }
      });
    };
    img.src = logo;
  };

  const handleCreate = async () => {
    setDeploying(true);
    try {
      const tx = await createNFTContract(
        isSingle
          ? isPrivate
            ? await getPrivateFactoryContract()
            : await getFactoryContract()
          : isPrivate
          ? await getPrivateArtFactoryContract()
          : await getArtFactoryContract(),
        name,
        symbol,
        ethers.utils.parseEther('100'),
        account
      );
      const res = await tx.wait();
      res.events.map(evt => {
        if (
          evt.topics[0] ===
          '0x2d49c67975aadd2d389580b368cfff5b49965b0bd5da33c144922ce01e7a4d7b'
        ) {
          setDeploying(false);
          setCreating(true);

          const address = ethers.utils.hexDataSlice(evt.data, 44);

          const img = new Image();
          img.onload = function() {
            const w = this.width;
            const h = this.height;
            const size = Math.min(w, h);
            const x = (w - size) / 2;
            const y = (h - size) / 2;
            clipImage(img, x, y, size, size, async logodata => {
              try {
                const { data: nonce } = await getNonce(account, authToken);

                let signature;
                try {
                  const signer = await getSigner();
                  signature = await signer.signMessage(
                    `Approve Signature on Artion.io with nonce ${nonce}`
                  );
                } catch (err) {
                  toast(
                    'error',
                    'You need to sign the message to be able to create a collection.'
                  );
                  setCreating(false);
                  return;
                }

                const formData = new FormData();
                formData.append('collectionName', name);
                formData.append('erc721Address', address);
                formData.append('imgData', logodata);
                const result = await axios({
                  method: 'post',
                  url: `${apiUrl}/ipfs/uploadCollectionImage2Server`,
                  data: formData,
                  headers: {
                    'Content-Type': 'multipart/form-data',
                    Authorization: `Bearer ${authToken}`,
                  },
                });
                const logoImageHash = result.data.data;
                const data = {
                  email,
                  erc721Address: address,
                  collectionName: name,
                  description,
                  categories: selected.join(','),
                  logoImageHash,
                  siteUrl,
                  discord,
                  twitterHandle,
                  instagramHandle,
                  mediumHandle,
                  telegram,
                  signature,
                };
                await axios({
                  method: 'post',
                  url: `${apiUrl}/collection/collectiondetails`,
                  data: JSON.stringify(data),
                  headers: {
                    'Content-Type': 'application/json',
                    Authorization: `Bearer ${authToken}`,
                  },
                });

                toast('success', 'Collection created successfully!');

                setCreating(false);

                history.push('/explore');
              } catch (e) {
                setCreating(false);
              }
            });
          };
          img.src = logo;
        }
      });
    } catch (err) {
      showToast('error', formatError(err));
      console.log(err);
      setDeploying(false);
    }
  };

  const menuId = 'select-category-menu';
  const renderMenu = (
    <Menu
      anchorEl={anchorEl}
      anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
      id={menuId}
      keepMounted
      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
      open={isMenuOpen}
      onClose={handleMenuClose}
      classes={{
        paper: styles.menu,
      }}
    >
      {options.map((cat, idx) => (
        <MenuItem
          key={idx}
          className={styles.category}
          onClick={() => selectCategory(cat.id)}
        >
          <img src={cat.icon} />
          <span className={styles.categoryLabel}>{cat.label}</span>
        </MenuItem>
      ))}
    </Menu>
  );

  return (
    <div className={styles.container}>
      <Header border />
      <div className={styles.inner}>
        <div className={styles.title}>
          {isRegister ? 'Register' : 'Create New'} Collection
        </div>
        <br />
        <div style={{ fontSize: '13px' }}>
          Please submit using the owner address of the collection. If you cannot
          use the owner address, please email us on [email protected]
          with the information below (and proof of collection ownership, such as
          from the collection's official email address).
        </div>

        {!isRegister && (
          <div className={styles.inputGroup}>
            <RadioGroup
              className={styles.inputWrapper}
              value={JSON.stringify(isPrivate)}
              onChange={e => setIsPrivate(e.currentTarget.value === 'true')}
            >
              <FormControlLabel
                classes={{
                  root: cx(styles.option, !isPrivate && styles.active),
                  label: styles.optionLabel,
                }}
                value="false"
                control={<CustomRadio color="primary" />}
                label="Allow others mint NFTs under my collection"
              />
              <FormControlLabel
                classes={{
                  root: cx(styles.option, isPrivate && styles.active),
                  label: styles.optionLabel,
                }}
                value="true"
                control={<CustomRadio color="primary" />}
                label="Only I can mint NFTs under my collection"
              />
            </RadioGroup>
          </div>
        )}

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>Logo Image *</div>
          <div className={styles.inputSubTitle}>
            This image will also be used for navigation. 300x300 recommended.
          </div>
          <div className={styles.inputWrapper}>
            <div className={styles.logoUploadBox}>
              {logo ? (
                <>
                  <img src={logo} />
                  <div className={styles.removeOverlay}>
                    <div className={styles.removeIcon} onClick={removeImage}>
                      <img src={closeIcon} />
                    </div>
                  </div>
                </>
              ) : (
                <div
                  className={styles.uploadOverlay}
                  onClick={() => inputRef.current?.click()}
                >
                  <input
                    ref={inputRef}
                    type="file"
                    accept="image/*"
                    hidden
                    onChange={handleFileSelect}
                  />
                  <div className={styles.upload}>
                    <div className={styles.uploadInner}>
                      <img src={uploadIcon} />
                    </div>
                    <div className={styles.plusIcon}>
                      <img src={plusIcon} />
                    </div>
                  </div>
                </div>
              )}
            </div>
          </div>
        </div>

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>Name *</div>
          <div className={styles.inputWrapper}>
            <input
              className={cx(styles.input, nameError && styles.hasError)}
              maxLength={20}
              placeholder="Collection Name"
              value={name}
              onChange={e => setName(e.target.value)}
              onBlur={validateName}
            />
            <div className={styles.lengthIndicator}>{name.length}/20</div>
            {nameError && <div className={styles.error}>{nameError}</div>}
          </div>
        </div>

        {!isRegister && (
          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>
              Symbol *&nbsp;
              <BootstrapTooltip
                title="A symbol is used when we deploy your NFT contract. If you are not sure about symbol, be aware that name and symbol share the same value."
                placement="top"
              >
                <HelpOutlineIcon />
              </BootstrapTooltip>
            </div>
            <div className={styles.inputWrapper}>
              <input
                className={cx(styles.input, symbolError && styles.hasError)}
                maxLength={20}
                placeholder="Collection Symbol"
                value={symbol}
                onChange={e => setSymbol(e.target.value)}
                onBlur={validateSymbol}
              />
              <div className={styles.lengthIndicator}>{symbol.length}/20</div>
              {symbolError && <div className={styles.error}>{symbolError}</div>}
            </div>
          </div>
        )}

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>Description *</div>
          <div className={styles.inputWrapper}>
            <textarea
              className={cx(
                styles.input,
                styles.longInput,
                descriptionError && styles.hasError
              )}
              maxLength={200}
              placeholder="Provide your description for your collection"
              value={description}
              onChange={e => setDescription(e.target.value)}
              onBlur={validateDescription}
            />
            <div className={styles.lengthIndicator}>
              {description.length}/200
            </div>
            {descriptionError && (
              <div className={styles.error}>{descriptionError}</div>
            )}
          </div>
        </div>

        {isRegister && (
          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>
              Royalty *&nbsp;
              <BootstrapTooltip
                title="Each NFT under this collection exchanged through Artion will have a percentage of sale given to nominated wallet address."
                placement="top"
              >
                <HelpOutlineIcon />
              </BootstrapTooltip>
            </div>
            <div className={styles.inputWrapper}>
              <PriceInput
                className={styles.input}
                placeholder="Collection Royalty"
                decimals={2}
                value={'' + royalty}
                onChange={val =>
                  val[val.length - 1] === '.'
                    ? setRoyalty(val)
                    : setRoyalty(Math.min(100, +val))
                }
              />
            </div>
          </div>
        )}

        {isRegister && (
          <div className={styles.inputGroup}>
            <div className={styles.inputTitle}>
              Fee Recipient *&nbsp;
              <BootstrapTooltip
                title="The nominated Fantom Opera Network wallet address to receive royalties from each sale in this collection."
                placement="top"
              >
                <HelpOutlineIcon />
              </BootstrapTooltip>
            </div>
            <div className={styles.inputWrapper}>
              <input
                className={cx(styles.input, recipientError && styles.hasError)}
                placeholder="Fee Recipient"
                value={feeRecipient}
                onChange={e => setFeeRecipient(e.target.value)}
                onBlur={validateFeeRecipient}
              />
              {recipientError && (
                <div className={styles.error}>{recipientError}</div>
              )}
            </div>
          </div>
        )}

        {!isRegister && (
          <div className={styles.inputGroup}>
            <RadioGroup
              className={styles.inputWrapper}
              value={JSON.stringify(isSingle)}
              onChange={e => setIsSingle(e.currentTarget.value === 'true')}
            >
              <FormControlLabel
                classes={{
                  root: cx(styles.option, isSingle && styles.active),
                  label: styles.optionLabel,
                }}
                value="true"
                control={<CustomRadio color="primary" />}
                label="Single Token Standard"
              />
              <FormControlLabel
                classes={{
                  root: cx(styles.option, !isSingle && styles.active),
                  label: styles.optionLabel,
                }}
                value="false"
                control={<CustomRadio color="primary" />}
                label="Multi Token Standard"
              />
            </RadioGroup>
          </div>
        )}

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>Category</div>
          <div className={styles.inputSubTitle}>
            Adding a category will help make your item discoverable on Fantom.
          </div>
          <div className={styles.inputSubTitle}>
            For more information, read{' '}
            <a
              href="https://docs.fantom.foundation/tutorials/collection-and-bundle-guide-on-artion"
              target="_blank"
              rel="noopener noreferrer"
            >
              this
            </a>
          </div>
          <div className={cx(styles.inputWrapper, styles.categoryList)}>
            <div
              className={cx(
                styles.categoryButton,
                selected.length === 3 && styles.disabled
              )}
              onClick={handleMenuOpen}
            >
              Add Category
            </div>
            {selectedCategories.map((cat, idx) => (
              <div
                className={styles.selectedCategory}
                key={idx}
                onClick={() => deselectCategory(cat.id)}
              >
                <img src={cat.icon} className={styles.categoryIcon} />
                <span className={styles.categoryLabel}>{cat.label}</span>
                <CloseIcon className={styles.closeIcon} />
              </div>
            ))}
          </div>
        </div>

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>Links *</div>
          <div className={styles.inputWrapper}>
            <div className={styles.linksWrapper}>
              {isRegister && (
                <>
                  <div
                    className={cx(
                      styles.linkItem,
                      addressError && styles.hasError
                    )}
                  >
                    <div className={styles.linkIconWrapper}>
                      <img src={nftIcon} className={styles.linkIcon} />
                    </div>
                    <input
                      className={styles.linkInput}
                      placeholder="Enter your collection's address"
                      value={address}
                      onChange={e => setAddress(e.target.value)}
                      onBlur={validateAddress}
                    />
                  </div>
                  {addressError && (
                    <div className={styles.error}>{addressError}</div>
                  )}
                </>
              )}
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={webIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your website url"
                  value={siteUrl}
                  onChange={e => setSiteUrl(e.target.value)}
                />
              </div>
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={discordIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your Discord url"
                  value={discord}
                  onChange={e => setDiscord(e.target.value)}
                />
              </div>
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={twitterIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your Twitter profile link"
                  value={twitterHandle}
                  onChange={e => setTwitterHandle(e.target.value)}
                />
              </div>
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={instagramIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your Instagram profile link"
                  value={instagramHandle}
                  onChange={e => setInstagramHandle(e.target.value)}
                />
              </div>
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={mediumIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your Medium profile link"
                  value={mediumHandle}
                  onChange={e => setMediumHandle(e.target.value)}
                />
              </div>
              <div className={styles.linkItem}>
                <div className={styles.linkIconWrapper}>
                  <img src={telegramIcon} className={styles.linkIcon} />
                </div>
                <input
                  className={styles.linkInput}
                  placeholder="Enter your Telegram profile link"
                  value={telegram}
                  onChange={e => setTelegram(e.target.value)}
                />
              </div>
            </div>
          </div>
        </div>

        <div className={styles.inputGroup}>
          <div className={styles.inputTitle}>
            Contact Email *&nbsp;
            <BootstrapTooltip
              title="We will use this email to notify you about your collection application. This will not be shared with others."
              placement="top"
            >
              <HelpOutlineIcon />
            </BootstrapTooltip>
          </div>
          <div className={styles.inputWrapper}>
            <input
              className={cx(styles.input, emailError && styles.hasError)}
              placeholder="Email Address"
              value={email}
              onChange={e => setEmail(e.target.value)}
              onBlur={validateEmail}
            />
            {emailError && <div className={styles.error}>{emailError}</div>}
          </div>
        </div>

        <div className={styles.buttonsWrapper}>
          {isRegister ? (
            <div
              className={cx(
                styles.createButton,
                (creating || !isValid) && styles.disabled
              )}
              onClick={isValid ? handleRegister : null}
            >
              {creating ? <ClipLoader color="#FFF" size={16} /> : 'Submit'}
            </div>
          ) : (
            <div
              className={cx(
                styles.createButton,
                (creating || deploying || !isValid) && styles.disabled
              )}
              onClick={isValid && !creating && !deploying ? handleCreate : null}
            >
              {creating ? (
                <ClipLoader color="#FFF" size={16} />
              ) : deploying ? (
                'Deploying'
              ) : (
                'Create'
              )}
            </div>
          )}
        </div>
        {!isRegister && (
          <div className={styles.fee}>
            <InfoIcon />
            &nbsp;100 FTMs are charged to create a new collection.
          </div>
        )}
      </div>
      {renderMenu}
    </div>
  );
}
Example #15
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
AccountDetails = () => {
  const dispatch = useDispatch();

  const {
    storageUrl,
    getUserAccountDetails,
    getUserFigures,
    fetchCollections,
    fetchTokens,
    updateBanner,
    getAccountActivity,
    getActivityFromOthers,
    getMyOffers,
    getFollowing,
    followUser: _followUser,
    getFollowers,
    getFollowings,
    getMyLikes,
    getItemsLiked,
  } = useApi();
  const { getTokenByAddress } = useTokens();
  const { account, chainId } = useWeb3React();
  const { width, ref } = useResizeDetector();

  const { uid } = useParams();

  const { authToken } = useSelector(state => state.ConnectWallet);
  const { user: me } = useSelector(state => state.Auth);

  const fileInput = useRef();

  const [anchorEl, setAnchorEl] = useState(null);
  const [prevUID, setPrevUID] = useState(null);
  const [bundleModalVisible, setBundleModalVisible] = useState(false);
  const [followingsModalVisible, setFollowingsModalVisible] = useState(false);
  const [followersModalVisible, setFollowersModalVisible] = useState(false);
  const [fetching, setFetching] = useState(false);
  const [bundleFetching, setBundleFetching] = useState(false);
  const [favFetching, setFavFetching] = useState(false);
  const [fguresFetching, setFiguresFetching] = useState(false);
  const tokens = useRef([]);
  const bundles = useRef([]);
  const likes = useRef([]);
  const [followersLoading, setFollowersLoading] = useState(false);
  const followers = useRef([]);
  const followings = useRef([]);
  const [following, setFollowing] = useState(false);
  const [followingInProgress, setFollowingInProgress] = useState(false);
  const [count, setCount] = useState(0);
  const [bundleCount, setBundleCount] = useState(0);
  const [favCount, setFavCount] = useState(0);
  const [now, setNow] = useState(new Date());
  const [page, setPage] = useState(0);
  const [bannerHash, setBannerHash] = useState();
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState({});
  const [copied, setCopied] = useState(false);
  const [tooltipOpen, setTooltipOpen] = useState(false);
  const [tab, setTab] = useState(0);
  const [activityLoading, setActivityLoading] = useState(false);
  const [activities, setActivities] = useState([]);
  const [bidsLoading, setBidsLoading] = useState(false);
  const [bids, setBids] = useState([]);
  const [offersLoading, setOffersLoading] = useState(false);
  const [offers, setOffers] = useState([]);
  const [fetchInterval, setFetchInterval] = useState(null);
  const [likeCancelSource, setLikeCancelSource] = useState(null);
  const [prevNumPerRow, setPrevNumPerRow] = useState(null);
  const prevAuthToken = usePrevious(authToken);

  const numPerRow = Math.floor(width / 240);
  const fetchCount = numPerRow <= 3 ? 18 : numPerRow === 4 ? 16 : numPerRow * 3;

  const getUserDetails = async _account => {
    setLoading(true);
    try {
      const { data } = await getUserAccountDetails(_account);
      setUser(data);
    } catch {
      setUser({});
    }
    try {
      const { data: isFollowing } = await getFollowing(account, _account);

      if (account === undefined) {
        setFollowing(false);
      } else {
        setFollowing(isFollowing);
      }
    } catch {
      setFollowing(false);
    }
    setLoading(false);
  };

  const getFigures = async _account => {
    setFiguresFetching(true);

    try {
      const {
        data: { single, bundle, fav },
      } = await getUserFigures(_account);
      setCount(single);
      setBundleCount(bundle);
      setFavCount(fav);
    } catch {
      setCount(0);
      setBundleCount(0);
      setFavCount(0);
    }

    setFiguresFetching(false);
  };

  const fetchNFTs = async () => {
    if (tab === 0) {
      if (fetching) return;
      setFetching(true);
    } else {
      if (bundleFetching) return;
      setBundleFetching(true);
    }

    try {
      const start = tab === 0 ? tokens.current.length : bundles.current.length;
      const _count =
        fetchCount -
        ((tab === 0 ? tokens.current : bundles.current).length % numPerRow);
      const { data } = await fetchTokens(
        start,
        _count,
        tab === 0 ? 'single' : 'bundle',
        [],
        null,
        'createdAt',
        [],
        uid
      );

      if (tab === 0) {
        // eslint-disable-next-line require-atomic-updates
        tokens.current = [...tokens.current, ...data.tokens];
        setCount(data.total);
        if (authToken) {
          updateItems(tokens.current)
            .then(_tokens => (tokens.current = _tokens))
            .catch();
        }
      } else {
        // eslint-disable-next-line require-atomic-updates
        bundles.current = [...bundles.current, ...data.tokens];
        setBundleCount(data.total);
        if (authToken) {
          updateItems(bundles.current)
            .then(_bundles => (bundles.current = _bundles))
            .catch();
        }
      }

      setFetching(false);
      setBundleFetching(false);
    } catch {
      setFetching(false);
      setBundleFetching(false);
    }
  };

  const fetchLikes = async step => {
    if (fetching) return;

    setFavFetching(true);

    try {
      const { data } = await getMyLikes(step, uid);
      setFavFetching(false);
      likes.current = [...likes.current, ...data.tokens];
      setFavCount(data.total);
      if (authToken) {
        updateItems(likes.current)
          .then(_likes => (likes.current = _likes))
          .catch();
      }
      setPage(step);
    } catch {
      setFavFetching(false);
    }
  };

  useEffect(() => {
    setPrevNumPerRow(numPerRow);
    if (isNaN(numPerRow) || (prevNumPerRow && prevNumPerRow !== numPerRow))
      return;

    if (prevUID !== uid) {
      setPrevUID(uid);
      getUserDetails(uid);
      getFigures(uid);
      setTab(0);
      if (tab === 0) {
        init();
      }
    } else {
      init();
    }
  }, [uid, tab, chainId, numPerRow]);

  useEffect(() => {
    if (me && user && me.address?.toLowerCase() === uid.toLowerCase()) {
      setUser({ ...user, ...me });
    }

    if (account === undefined) {
      setFollowing(false);
    }
  }, [me, uid, account]);

  const updateCollections = async () => {
    try {
      dispatch(CollectionsActions.fetchStart());
      const res = await fetchCollections();
      if (res.status === 'success') {
        const verified = [];
        const unverified = [];
        res.data.map(item => {
          if (item.isVerified) verified.push(item);
          else unverified.push(item);
        });
        dispatch(CollectionsActions.fetchSuccess([...verified, ...unverified]));
      }
    } catch {
      dispatch(CollectionsActions.fetchFailed());
    }
  };

  useEffect(() => {
    if (fetchInterval) {
      clearInterval(fetchInterval);
    }

    updateCollections();
    setFetchInterval(setInterval(updateCollections, 1000 * 60 * 10));
  }, [chainId]);

  const isMe = account?.toLowerCase() === uid.toLowerCase();

  useEffect(() => {
    dispatch(HeaderActions.toggleSearchbar(true));
    setInterval(() => setNow(new Date()), 1000);
  }, []);

  const updateItems = async _tokens => {
    return new Promise((resolve, reject) => {
      if (!authToken) {
        return resolve(
          _tokens.map(tk => ({
            ...tk,
            isLiked: false,
          }))
        );
      }
      let missingTokens = _tokens.map((tk, index) =>
        tk.items
          ? {
              index,
              isLiked: tk.isLiked,
              bundleID: tk._id,
            }
          : {
              index,
              isLiked: tk.isLiked,
              contractAddress: tk.contractAddress,
              tokenID: tk.tokenID,
            }
      );
      if (prevAuthToken) {
        missingTokens = missingTokens.filter(tk => tk.isLiked === undefined);
      }

      if (missingTokens.length === 0) {
        reject();
        return;
      }

      const cancelTokenSource = axios.CancelToken.source();
      setLikeCancelSource(cancelTokenSource);
      getItemsLiked(missingTokens, authToken, cancelTokenSource.token)
        .then(({ data, status }) => {
          setLikeCancelSource(null);
          if (status === 'success') {
            const newTokens = [...tokens.current];
            missingTokens.map((tk, idx) => {
              newTokens[tk.index].isLiked = data[idx].isLiked;
            });
            resolve(newTokens);
          }
        })
        .catch(() => {
          reject();
        });
    });
  };

  useEffect(() => {
    if (likeCancelSource) {
      likeCancelSource.cancel();
    }

    if (tokens.current.length) {
      updateItems(tokens.current)
        .then(_tokens => (tokens.current = _tokens))
        .catch();
    }
    if (bundles.current.length) {
      updateItems(bundles.current)
        .then(_bundles => (bundles.current = _bundles))
        .catch();
    }
    if (likes.current.length) {
      updateItems(likes.current)
        .then(_likes => (likes.current = _likes))
        .catch();
    }
  }, [authToken]);

  const loadNextPage = () => {
    if (fetching || bundleFetching) return;

    if (tab === 0 && tokens.current.length === count) return;
    if (tab === 1 && bundles.current.length === bundleCount) return;
    if (tab === 2 && likes.current.length === favCount) return;

    if (tab === 0 || tab === 1) {
      fetchNFTs();
    } else {
      fetchLikes(page + 1);
    }
  };

  const handleScroll = e => {
    if (tab > 2) return;

    const obj = e.currentTarget;
    if (obj.scrollHeight - obj.clientHeight - obj.scrollTop < 100) {
      loadNextPage();
    }
  };

  const init = () => {
    if (tab === 0) {
      tokens.current = [];
      setCount(0);
      fetchNFTs();
    } else if (tab === 1) {
      bundles.current = [];
      setBundleCount(0);
      fetchNFTs();
    } else if (tab === 2) {
      likes.current = [];
      setFavCount(0);
      fetchLikes(0);
    } else if (tab === 3) {
      getActivity();
    } else if (tab === 4) {
      getOffersFromOthers();
    } else if (tab === 5) {
      getOffers();
    }
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  const handleCopyLink = () => {
    handleClose();
    toast('success', 'Link copied to clipboard!');
  };

  const handleShareOnFacebook = () => {
    handleClose();
    window.open(
      `https://www.facebook.com/sharer/sharer.php?u=${window.location.href}`,
      '_blank'
    );
  };

  const handleShareToTwitter = () => {
    handleClose();
    window.open(
      `https://twitter.com/intent/tweet?text=Check%20out%20this%20account%20on%20Artion&url=${window.location.href}`,
      '_blank'
    );
  };

  const goToTab = _tab => {
    tokens.current = [];
    bundles.current = [];
    likes.current = [];

    setTab(_tab);
  };

  const getActivity = async () => {
    try {
      setActivityLoading(true);
      const { data } = await getAccountActivity(uid);
      const _activities = [];

      data.bids.map(bActivity => _activities.push(bActivity));
      data.listings.map(lActivity => _activities.push(lActivity));
      data.offers.map(oActivity => _activities.push(oActivity));
      data.sold.map(sActivity => _activities.push(sActivity));

      _activities.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
      _activities.map(item => {
        item.token = getTokenByAddress(item.paymentToken);
      });
      setActivities(_activities);
      setActivityLoading(false);
    } catch {
      setActivityLoading(false);
    }
  };

  const getOffersFromOthers = async () => {
    try {
      setOffersLoading(true);
      const { data } = await getActivityFromOthers(uid);
      data.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
      data.map(item => {
        item.token = getTokenByAddress(item.paymentToken);
      });
      setOffers(data);
      setOffersLoading(false);
    } catch {
      setOffersLoading(false);
    }
  };

  const getOffers = async () => {
    try {
      setBidsLoading(true);
      const { data } = await getMyOffers(uid);
      data.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
      data.map(item => {
        item.token = getTokenByAddress(item.paymentToken);
      });
      setBids(data);
      setBidsLoading(false);
    } catch {
      setBidsLoading(false);
    }
  };

  const handleCopyAddress = () => {
    setCopied(true);
    setTimeout(() => {
      setCopied(false);
    }, 3000);
  };

  const handleMouseOver = () => {
    setTooltipOpen(true);
  };

  const handleMouseLeave = () => {
    setTooltipOpen(false);
    setCopied(false);
  };

  const selectBanner = () => {
    fileInput.current?.click();
  };

  const handleSelectFile = e => {
    if (e.target.files.length > 0) {
      const file = e.target.files[0];

      const reader = new FileReader();

      reader.onload = async function(e) {
        const { data } = await updateBanner(e.target.result, authToken);
        setBannerHash(data);
      };

      reader.readAsDataURL(file);
    }
    e.target.value = null;
  };

  const openAccountSettings = () => {
    dispatch(ModalActions.showAccountModal());
  };

  // const handleCreateBundle = () => {
  //   setBundleModalVisible(true);
  // };

  const fetchFollowers = async () => {
    setFollowersLoading(true);
    try {
      const { data } = await getFollowers(uid);
      followers.current = data;
    } catch {
      followers.current = [];
    }
    setFollowersLoading(false);
  };

  const fetchFollowings = async () => {
    setFollowersLoading(true);
    try {
      const { data } = await getFollowings(uid);
      followings.current = data;
    } catch {
      followings.current = [];
    }
    setFollowersLoading(false);
  };

  const showFollowers = () => {
    if (loading || !user.followers || user.followers === 0) return;

    setFollowersModalVisible(true);
    fetchFollowers();
  };

  const showFollowings = () => {
    if (loading || !user.followings || user.followings === 0) return;

    setFollowingsModalVisible(true);
    fetchFollowings();
  };

  const followUser = async () => {
    if (followingInProgress) return;

    if (account === undefined) {
      toast('error', 'Please connect your wallet!');
      return;
    }

    setFollowingInProgress(true);
    try {
      const { status, data } = await _followUser(uid, !following, authToken);
      if (status === 'success') {
        const { data } = await getUserAccountDetails(uid);
        setUser(data);
        setFollowing(!following);
      } else {
        toast('error', data);
      }
    } catch (e) {
      console.log(e);
    }
    setFollowingInProgress(false);
  };

  const formatDate = _date => {
    const date = new Date(_date);
    const diff = Math.floor((now - date.getTime()) / 1000);
    if (diff >= ONE_MONTH) {
      const m = Math.ceil(diff / ONE_MONTH);
      return `${m} Month${m > 1 ? 's' : ''} Ago`;
    }
    if (diff >= ONE_DAY) {
      const d = Math.ceil(diff / ONE_DAY);
      return `${d} Day${d > 1 ? 's' : ''} Ago`;
    }
    if (diff >= ONE_HOUR) {
      const h = Math.ceil(diff / ONE_HOUR);
      return `${h} Hour${h > 1 ? 's' : ''} Ago`;
    }
    if (diff >= ONE_MIN) {
      const h = Math.ceil(diff / ONE_MIN);
      return `${h} Min${h > 1 ? 's' : ''} Ago`;
    }
    return `${diff} Second${diff > 1 ? 's' : ''} Ago`;
  };

  if (!isAddress(uid)) {
    return <Redirect to="/404" />;
  }

  const renderMedia = image => {
    if (image?.includes('youtube')) {
      return (
        <ReactPlayer
          className={styles.mediaInner}
          url={image}
          controls={true}
          width="100%"
          height="100%"
        />
      );
    } else {
      return (
        <Suspense
          fallback={
            <Loader type="Oval" color="#007BFF" height={32} width={32} />
          }
        >
          <SuspenseImg className={styles.mediaInner} src={image} />
        </Suspense>
      );
    }
  };

  const renderTab = (label, Icon, idx, count, countLoading) => (
    <div
      className={cx(styles.tab, idx === tab && styles.selected)}
      onClick={() => goToTab(idx)}
    >
      <Icon className={styles.tabIcon} />
      <div className={styles.tabLabel}>{label}</div>
      <div className={styles.tabCount}>
        {countLoading ? (
          <Skeleton className={styles.tabCountLoading} width={40} height={22} />
        ) : (
          count
        )}
      </div>
    </div>
  );

  return (
    <div className={styles.container}>
      <Header border />
      <div className={styles.profile}>
        <div className={styles.banner}>
          {loading ? (
            <Skeleton width="100%" height={200} />
          ) : bannerHash || user.bannerHash ? (
            <img
              src={`https://cloudflare-ipfs.com/ipfs/${bannerHash ||
                user.bannerHash}`}
              className={styles.bannerImg}
            />
          ) : (
            <div className={styles.bannerPlaceholder} />
          )}
          {isMe && (
            <div className={styles.editBanner} onClick={selectBanner}>
              <input
                ref={fileInput}
                hidden
                type="file"
                onChange={handleSelectFile}
                accept="image/*"
              />
              <EditIcon className={styles.editIcon} />
            </div>
          )}
        </div>
        <div className={styles.buttonsWrapper}>
          {isMe && (
            <div className={styles.settings} onClick={openAccountSettings}>
              <img src={iconSettings} className={styles.settingsIcon} />
            </div>
          )}
          <div
            className={styles.settings}
            onClick={e => setAnchorEl(e.currentTarget)}
          >
            <img src={iconShare} className={styles.settingsIcon} />
          </div>
        </div>
        <div className={styles.wrapper}>
          <div className={styles.avatarWrapper}>
            {loading ? (
              <Skeleton width={160} height={160} className={styles.avatar} />
            ) : user.imageHash ? (
              <img
                src={`https://cloudflare-ipfs.com/ipfs/${user.imageHash}`}
                className={styles.avatar}
              />
            ) : (
              <Identicon className={styles.avatar} account={uid} size={160} />
            )}
          </div>
          <div className={styles.usernameWrapper}>
            {loading ? (
              <Skeleton width={120} height={24} />
            ) : (
              <div className={styles.username}>{user.alias || 'Unnamed'}</div>
            )}
            {isMe ? null : loading ? (
              <Skeleton width={80} height={26} style={{ marginLeft: 16 }} />
            ) : (
              <div
                className={cx(
                  styles.followBtn,
                  followingInProgress && styles.disabled
                )}
                onClick={followUser}
              >
                {followingInProgress ? (
                  <ClipLoader color="#FFF" size={14} />
                ) : following ? (
                  'Unfollow'
                ) : (
                  'Follow'
                )}
              </div>
            )}
          </div>
          <div className={styles.bio}>{user.bio || ''}</div>
          <div className={styles.bottomWrapper}>
            <div className={styles.addressWrapper}>
              {loading ? (
                <Skeleton width={120} height={20} />
              ) : (
                <Tooltip
                  title={copied ? 'Copied!' : 'Copy'}
                  open={tooltipOpen}
                  arrow
                  classes={{ tooltip: styles.tooltip }}
                >
                  <div className={styles.address}>{shortenAddress(uid)}</div>
                </Tooltip>
              )}
              <CopyToClipboard text={uid} onCopy={handleCopyAddress}>
                <div className={styles.copyIcon}>
                  <img
                    src={iconCopy}
                    onMouseOver={handleMouseOver}
                    onMouseLeave={handleMouseLeave}
                  />
                </div>
              </CopyToClipboard>
            </div>
            <div className={styles.followers} onClick={showFollowers}>
              {loading ? (
                <Skeleton width={100} height={24} />
              ) : (
                <>
                  <b>{formatFollowers(user.followers || 0)}</b> Followers
                </>
              )}
            </div>
            <div className={styles.followers} onClick={showFollowings}>
              {loading ? (
                <Skeleton width={100} height={24} />
              ) : (
                <>
                  <b>{formatFollowers(user.followings || 0)}</b> Following
                </>
              )}
            </div>
          </div>
        </div>
      </div>
      <div className={styles.content}>
        <div className={styles.contentSidebar}>
          <div className={styles.tabsGroup}>
            <div className={styles.groupTitle}>My Items</div>
            {renderTab(
              'Single Items',
              IconList,
              0,
              count,
              fetching || fguresFetching
            )}
            {/* {renderTab(
              'Bundles',
              IconBundle,
              1,
              bundleCount,
              bundleFetching || fguresFetching
            )} */}
            {renderTab(
              'Favorited',
              IconHeart,
              2,
              favCount,
              favFetching || fguresFetching
            )}
          </div>
          <div className={styles.tabsGroup}>
            <div className={styles.groupTitle}>Account</div>
            {renderTab('Activity', IconClock, 3)}
            {renderTab('Offers', IconList, 4)}
            {renderTab('My Offers', IconList, 5)}
          </div>
        </div>
        <div ref={ref} className={styles.contentBody} onScroll={handleScroll}>
          {tab === 0 ? (
            <NFTsGrid
              items={tokens.current}
              numPerRow={numPerRow}
              loading={fetching}
            />
          ) : // tab === 1 ? (
          //   <NFTsGrid
          //     items={bundles.current}
          //     numPerRow={numPerRow}
          //     loading={fetching}
          //     showCreate={isMe}
          //     onCreate={handleCreateBundle}
          //   />
          // ) :
          tab === 2 ? (
            <NFTsGrid
              items={likes.current}
              numPerRow={numPerRow}
              loading={fetching}
              onLike={() => {
                likes.current = [];
                fetchLikes(0);
              }}
            />
          ) : tab === 3 ? (
            <div className={styles.tableWapper}>
              <div className={styles.activityHeader}>
                <div className={styles.event}>Event</div>
                <div className={styles.name}>Item</div>
                <div className={styles.price}>Price</div>
                <div className={styles.quantity}>Quantity</div>
                <div className={styles.owner}>Owner</div>
                <div className={styles.date}>Date</div>
              </div>
              <div className={styles.activityList}>
                {(activityLoading ? new Array(5).fill(null) : activities).map(
                  (activity, idx) => (
                    <div key={idx} className={styles.activity}>
                      <div className={styles.event}>
                        {activity ? (
                          activity.event
                        ) : (
                          <Skeleton width={100} height={20} />
                        )}
                      </div>
                      {activity ? (
                        <Link
                          to={`/explore/${activity.contractAddress}/${activity.tokenID}`}
                          className={styles.name}
                        >
                          <div className={styles.media}>
                            {renderMedia(
                              activity.thumbnailPath.length > 10
                                ? `${storageUrl}/image/${activity.thumbnailPath}`
                                : activity.imageURL
                            )}
                          </div>
                          {activity.name}
                        </Link>
                      ) : (
                        <div className={styles.name}>
                          <Skeleton width={120} height={20} />
                        </div>
                      )}
                      <div className={styles.price}>
                        {activity ? (
                          <>
                            <div className={styles.tokenLogo}>
                              <img src={activity.token?.icon} />
                            </div>
                            {activity.price}
                          </>
                        ) : (
                          <Skeleton width={100} height={20} />
                        )}
                      </div>
                      <div className={styles.quantity}>
                        {activity ? (
                          activity.quantity
                        ) : (
                          <Skeleton width={80} height={20} />
                        )}
                      </div>
                      {activity ? (
                        activity.to ? (
                          <Link
                            to={`/account/${activity.to}`}
                            className={styles.owner}
                          >
                            <div className={styles.ownerAvatarWrapper}>
                              {activity.image ? (
                                <img
                                  src={`https://cloudflare-ipfs.com/ipfs/${activity.image}`}
                                  className={styles.ownerAvatar}
                                />
                              ) : (
                                <Identicon
                                  account={activity.to}
                                  size={24}
                                  className={styles.ownerAvatar}
                                />
                              )}
                            </div>
                            {activity.alias || shortenAddress(activity.to)}
                          </Link>
                        ) : (
                          <div className={styles.owner} />
                        )
                      ) : (
                        <div className={styles.owner}>
                          <Skeleton width={130} height={20} />
                        </div>
                      )}
                      <div className={styles.date}>
                        {activity ? (
                          formatDate(activity.createdAt)
                        ) : (
                          <Skeleton width={120} height={20} />
                        )}
                      </div>
                    </div>
                  )
                )}
              </div>
            </div>
          ) : tab === 4 ? (
            <>
              <div className={styles.activityHeader}>
                <div className={styles.name}>Item</div>
                <div className={styles.owner}>From</div>
                <div className={styles.price}>Price</div>
                <div className={styles.quantity}>Quantity</div>
                <div className={styles.date}>Date</div>
              </div>
              <div className={styles.activityList}>
                {(offersLoading
                  ? new Array(5).fill(null)
                  : offers.filter(
                      offer => offer.deadline * 1000 > now.getTime()
                    )
                ).map((offer, idx) => (
                  <div key={idx} className={styles.activity}>
                    {offer ? (
                      <Link
                        to={`/explore/${offer.contractAddress}/${offer.tokenID}`}
                        className={styles.name}
                      >
                        <div className={styles.media}>
                          {renderMedia(
                            offer.thumbnailPath.length > 10
                              ? `${storageUrl}/image/${offer.thumbnailPath}`
                              : offer.imageURL
                          )}
                        </div>
                        {offer.name}
                      </Link>
                    ) : (
                      <div className={styles.name}>
                        <Skeleton width={120} height={20} />
                      </div>
                    )}
                    {offer ? (
                      <Link
                        to={`/account/${offer.creator}`}
                        className={styles.owner}
                      >
                        <div className={styles.ownerAvatarWrapper}>
                          {offer.image ? (
                            <img
                              src={`https://cloudflare-ipfs.com/ipfs/${offer.image}`}
                              className={styles.ownerAvatar}
                            />
                          ) : (
                            <Identicon
                              account={offer.creator}
                              size={24}
                              className={styles.ownerAvatar}
                            />
                          )}
                        </div>
                        {offer.alias || shortenAddress(offer.creator)}
                      </Link>
                    ) : (
                      <div className={styles.owner}>
                        <Skeleton width={130} height={20} />
                      </div>
                    )}
                    <div className={styles.price}>
                      {offer ? (
                        <>
                          <div className={styles.tokenLogo}>
                            <img src={offer.token?.icon} />
                          </div>
                          {offer.pricePerItem}
                        </>
                      ) : (
                        <Skeleton width={100} height={20} />
                      )}
                    </div>
                    <div className={styles.quantity}>
                      {offer ? (
                        offer.quantity
                      ) : (
                        <Skeleton width={80} height={20} />
                      )}
                    </div>
                    <div className={styles.date}>
                      {offer ? (
                        formatDate(offer.createdAt)
                      ) : (
                        <Skeleton width={120} height={20} />
                      )}
                    </div>
                  </div>
                ))}
              </div>
            </>
          ) : (
            <>
              <div className={styles.activityHeader}>
                <div className={styles.name}>Item</div>
                <div className={styles.price}>Price</div>
                <div className={styles.quantity}>Quantity</div>
                <div className={styles.date}>Date</div>
              </div>
              <div className={styles.activityList}>
                {(bidsLoading
                  ? new Array(5).fill(null)
                  : bids.filter(bid => bid.deadline * 1000 > now.getTime())
                ).map((bid, idx) => (
                  <div key={idx} className={styles.activity}>
                    {bid ? (
                      <Link
                        to={`/explore/${bid.contractAddress}/${bid.tokenID}`}
                        className={styles.name}
                      >
                        <div className={styles.media}>
                          {renderMedia(
                            bid.thumbnailPath.length > 10
                              ? `${storageUrl}/image/${bid.thumbnailPath}`
                              : bid.imageURL
                          )}
                        </div>
                        {bid.name}
                      </Link>
                    ) : (
                      <div className={styles.name}>
                        <Skeleton width={120} height={20} />
                      </div>
                    )}
                    <div className={styles.price}>
                      {bid ? (
                        <>
                          <div className={styles.tokenLogo}>
                            <img src={bid.token?.icon} />
                          </div>
                          {bid.pricePerItem}
                        </>
                      ) : (
                        <Skeleton width={100} height={20} />
                      )}
                    </div>
                    <div className={styles.quantity}>
                      {bid ? bid.quantity : <Skeleton width={80} height={20} />}
                    </div>
                    <div className={styles.date}>
                      {bid ? (
                        formatDate(bid.createdAt)
                      ) : (
                        <Skeleton width={120} height={20} />
                      )}
                    </div>
                  </div>
                ))}
              </div>
            </>
          )}
        </div>
      </div>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        keepMounted
        open={Boolean(anchorEl)}
        onClose={handleClose}
        classes={{ paper: styles.shareMenu, list: styles.shareMenuList }}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'right',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'right',
        }}
      >
        <CopyToClipboard text={window.location.href} onCopy={handleCopyLink}>
          <MenuItem classes={{ root: styles.menuItem }}>
            <img src={iconArtion} />
            Copy Link
          </MenuItem>
        </CopyToClipboard>
        <MenuItem
          classes={{ root: styles.menuItem }}
          onClick={handleShareOnFacebook}
        >
          <img src={iconFacebook} />
          Share on Facebook
        </MenuItem>
        <MenuItem
          classes={{ root: styles.menuItem }}
          onClick={handleShareToTwitter}
        >
          <img src={iconTwitter} />
          Share to Twitter
        </MenuItem>
      </Menu>
      <NewBundleModal
        visible={bundleModalVisible}
        onClose={() => setBundleModalVisible(false)}
        onCreateSuccess={() => {
          bundles.current = [];
          fetchNFTs();
        }}
      />
      <FollowersModal
        visible={followersModalVisible || followingsModalVisible}
        onClose={() => {
          setFollowersModalVisible(false);
          setFollowingsModalVisible(false);
        }}
        title={followersModalVisible ? 'Followers' : 'Followings'}
        users={
          followersLoading
            ? new Array(5).fill(null)
            : followersModalVisible
            ? followers.current
            : followings.current
        }
      />
    </div>
  );
}
Example #16
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
AccountModal = () => {
  const { getNonce, updateAccountDetails } = useApi();
  const dispatch = useDispatch();
  const { account } = useWeb3React();

  const { fetching, user } = useSelector(state => state.Auth);

  const rootRef = useRef(null);
  const inputRef = useRef(null);

  const [alias, setAlias] = useState('');
  const [email, setEmail] = useState('');
  const [bio, setBio] = useState('');
  const [avatar, setAvatar] = useState(null);
  const [emailError, setEmailError] = useState(null);
  const [saving, setSaving] = useState(false);

  const { accountModalVisible } = useSelector(state => state.Modal);
  const { authToken } = useSelector(state => state.ConnectWallet);

  useEffect(() => {
    if (accountModalVisible) {
      if (user.imageHash) {
        setAvatar(`https://cloudflare-ipfs.com/ipfs/${user.imageHash}`);
      } else {
        setAvatar(null);
      }
      setAlias(user.alias || '');
      setEmail(user.email || '');
      setBio(user.bio || '');
      setEmailError(null);
    }
  }, [accountModalVisible]);

  const validEmail = email => /(.+)@(.+){2,}\.(.+){2,}/.test(email);

  const validateEmail = () => {
    if (email.length === 0 || validEmail(email)) {
      setEmailError(null);
    } else {
      setEmailError('Invalid email address.');
    }
  };

  const handleFileSelect = e => {
    if (e.target.files.length > 0) {
      const file = e.target.files[0];

      const reader = new FileReader();

      reader.onload = function(e) {
        setAvatar(e.target.result);
      };

      reader.readAsDataURL(file);
    }
  };

  const isValid = emailError === null;

  const closeModal = () => {
    dispatch(ModalActions.hideAccountModal());
  };

  const clipImage = (image, clipX, clipY, clipWidth, clipHeight, cb) => {
    const CANVAS_SIZE = 128;
    const canvas = document.createElement('canvas');
    canvas.width = CANVAS_SIZE;
    canvas.height = CANVAS_SIZE;
    const ctx = canvas.getContext('2d');
    ctx.imageSmoothingQuality = 'high';
    ctx.drawImage(
      image,
      clipX,
      clipY,
      clipWidth,
      clipHeight,
      0,
      0,
      CANVAS_SIZE,
      CANVAS_SIZE
    );
    cb(canvas.toDataURL());
  };

  const onSave = async () => {
    if (saving) return;

    try {
      setSaving(true);

      const { data: nonce } = await getNonce(account, authToken);

      let signature;
      let addr;
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          'You need to sign the message to be able to update account settings.'
        );
        setSaving(false);
        return;
      }

      if (!avatar || avatar.startsWith('https')) {
        const res = await updateAccountDetails(
          alias,
          email,
          bio,
          avatar,
          authToken,
          signature,
          addr
        );
        dispatch(AuthActions.fetchSuccess(res.data));
        toast('success', 'Account details saved!');
        setSaving(false);

        closeModal();
      } else {
        const img = new Image();
        img.onload = function() {
          const w = this.width;
          const h = this.height;
          const size = Math.min(w, h);
          const x = (w - size) / 2;
          const y = (h - size) / 2;
          clipImage(img, x, y, size, size, async data => {
            const res = await updateAccountDetails(
              alias,
              email,
              bio,
              data,
              authToken,
              signature,
              addr
            );
            dispatch(AuthActions.fetchSuccess(res.data));
            toast('success', 'Account details saved!');
            setSaving(false);

            closeModal();
          });
        };
        img.src = avatar;
      }
    } catch {
      setSaving(false);
    }
  };

  const onCancel = () => {
    closeModal();
  };

  if (!accountModalVisible) return null;

  return (
    <div className={styles.root} ref={rootRef}>
      <Modal open className={styles.modal} container={() => rootRef.current}>
        <div className={styles.paper}>
          <h2 className={styles.title}>Account Settings</h2>
          <div className={styles.formGroup}>
            <p className={styles.formLabel}>User Avatar</p>
            <input
              ref={inputRef}
              type="file"
              accept="image/*"
              hidden
              onChange={handleFileSelect}
            />
            <div className={styles.avatarBox}>
              {avatar && <img src={avatar} className={styles.avatar} />}
              <div
                className={styles.upload}
                onClick={() => !fetching && inputRef.current?.click()}
              >
                <CreateIcon className={styles.uploadIcon} />
              </div>
            </div>
          </div>
          <div className={styles.formGroup}>
            <p className={styles.formLabel}>Name</p>
            <input
              type="text"
              className={styles.formInput}
              maxLength={20}
              placeholder="Enter Username"
              value={alias}
              onChange={e => setAlias(e.target.value)}
              disabled={fetching}
            />
            <div className={styles.lengthIndicator}>{alias.length}/20</div>
          </div>
          <div className={styles.formGroup}>
            <p className={styles.formLabel}>Email Address</p>
            <input
              type="text"
              className={cx(
                styles.formInput,
                emailError !== null ? styles.hasError : null
              )}
              placeholder="Enter Email Address"
              value={email}
              onChange={e => setEmail(e.target.value)}
              onBlur={validateEmail}
              disabled={fetching}
            />
            {emailError !== null && (
              <p className={styles.error}>{emailError}</p>
            )}
          </div>
          <div className={styles.formGroup}>
            <p className={styles.formLabel}>Bio</p>
            <textarea
              className={cx(styles.formInput, styles.longInput)}
              maxLength={120}
              placeholder="Bio"
              value={bio}
              onChange={e => setBio(e.target.value)}
              disabled={fetching}
            />
            <div className={styles.lengthIndicator}>{bio.length}/120</div>
          </div>

          <div className={styles.footer}>
            <div
              className={cx(
                styles.button,
                styles.save,
                (saving || !isValid) && styles.disabled
              )}
              onClick={isValid ? onSave : null}
            >
              {saving ? <ClipLoader color="#FFF" size={16} /> : 'Save'}
            </div>

            <div
              className={cx(
                styles.button,
                styles.cancel,
                saving && styles.disabled
              )}
              onClick={!saving ? onCancel : null}
            >
              Cancel
            </div>
          </div>
        </div>
      </Modal>
    </div>
  );
}
Example #17
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
SellModal = ({
  visible,
  onClose,
  onSell,
  startPrice,
  confirming,
  approveContract,
  contractApproving,
  contractApproved,
  totalSupply,
}) => {
  const { tokens } = useTokens();
  const { getSalesContract } = useSalesContract();

  const [price, setPrice] = useState('');
  const [quantity, setQuantity] = useState('1');
  const [focused, setFocused] = useState(false);
  const [options, setOptions] = useState([]);
  const [selected, setSelected] = useState([]);
  const [tokenPrice, setTokenPrice] = useState();
  const [tokenPriceInterval, setTokenPriceInterval] = useState();
  const [inputError, setInputError] = useState(null);

  useEffect(() => {
    setPrice(startPrice > 0 ? startPrice.toString() : '');
    setQuantity('1');
    if (visible && tokens?.length) {
      setSelected([tokens[0]]);
    }
  }, [visible]);

  useEffect(() => {
    if (tokens?.length) {
      setOptions(tokens);
    }
  }, [tokens]);

  const getTokenPrice = () => {
    if (tokenPriceInterval) clearInterval(tokenPriceInterval);
    const func = async () => {
      const tk = selected[0].address || ethers.constants.AddressZero;
      try {
        const salesContract = await getSalesContract();
        const price = await salesContract.getPrice(tk);
        setTokenPrice(parseFloat(ethers.utils.formatUnits(price, 18)));
      } catch {
        setTokenPrice(null);
      }
    };
    func();
    setTokenPriceInterval(setInterval(func, 60 * 1000));
  };

  useEffect(() => {
    if (selected.length === 0) return;

    getTokenPrice();
  }, [selected]);

  const handleQuantityChange = e => {
    const val = e.target.value;
    if (!val) {
      setQuantity('');
      return;
    }

    if (isNaN(val)) return;

    const _quantity = parseInt(val);
    setQuantity(Math.min(_quantity, totalSupply));
  };

  const handleSellItem = () => {
    let quant = 1;
    if (totalSupply > 1) {
      quant = parseInt(quantity);
    }
    onSell(selected[0], price, quant);
  };

  const validateInput = () => {
    if (price.length === 0 || parseFloat(price) == 0) return false;
    if (totalSupply > 1 && quantity.length === 0) return false;
    if (selected.length === 0) return false;
    return true;
  };

  return (
    <Modal
      visible={visible}
      title={startPrice > 0 ? 'Update Your Listing' : 'Sell Your Item'}
      onClose={onClose}
      submitDisabled={
        contractApproving ||
        confirming ||
        (contractApproved && !validateInput()) ||
        inputError
      }
      submitLabel={
        contractApproved ? (
          confirming ? (
            <ClipLoader color="#FFF" size={16} />
          ) : startPrice > 0 ? (
            'Update Price'
          ) : (
            'List Item'
          )
        ) : contractApproving ? (
          'Approving Item'
        ) : (
          'Approve Item'
        )
      }
      onSubmit={() =>
        contractApproved
          ? !confirming && validateInput()
            ? handleSellItem()
            : null
          : approveContract()
      }
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Price</div>
        <div className={cx(styles.formInputCont, focused && styles.focused)}>
          <Select
            options={options}
            disabled={confirming}
            values={selected}
            onChange={tk => {
              setSelected(tk);
            }}
            className={styles.select}
            placeholder=""
            itemRenderer={({ item, itemIndex, methods }) => (
              <div
                key={itemIndex}
                className={styles.token}
                onClick={() => {
                  methods.clearAll();
                  methods.addItem(item);
                }}
              >
                <img src={item?.icon} className={styles.tokenIcon} />
                <div className={styles.tokenSymbol}>{item.symbol}</div>
              </div>
            )}
            contentRenderer={({ props: { values } }) =>
              values.length > 0 ? (
                <div className={styles.selectedToken}>
                  <img src={values[0]?.icon} className={styles.tokenIcon} />
                  <div className={styles.tokenSymbol}>{values[0].symbol}</div>
                </div>
              ) : (
                <div className={styles.selectedToken} />
              )
            }
          />
          <PriceInput
            className={styles.formInput}
            placeholder="0.00"
            decimals={selected[0]?.decimals || 0}
            value={'' + price}
            onChange={setPrice}
            onFocus={() => setFocused(true)}
            onBlur={() => setFocused(false)}
            disabled={contractApproving || confirming}
            onInputError={setInputError}
          />
          <div className={styles.usdPrice}>
            {!isNaN(tokenPrice) && tokenPrice !== null ? (
              `$${formatNumber(
                ((parseFloat(price) || 0) * tokenPrice).toFixed(2)
              )}`
            ) : (
              <Skeleton width={100} height={24} />
            )}
          </div>
        </div>
        <InputError text={inputError} />
      </div>
      {totalSupply !== null && (
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Quantity</div>
          <div className={styles.formInputCont}>
            <input
              className={styles.formInput}
              placeholder={totalSupply}
              value={quantity}
              onChange={handleQuantityChange}
              disabled={contractApproving || confirming || totalSupply === 1}
            />
          </div>
        </div>
      )}
    </Modal>
  );
}
Example #18
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
PaintBoard = () => {
  const dispatch = useDispatch();
  const history = useHistory();

  const {
    explorerUrl,
    apiUrl,
    fetchMintableCollections,
    getNonce,
    addUnlockableContent,
    checkBan,
  } = useApi();

  const { registerRoyalty } = useSalesContract();
  const { loadContract } = useContract();

  const { account, chainId } = useWeb3React();

  const imageRef = useRef();

  const [selected, setSelected] = useState([]);
  const [collections, setCollections] = useState([]);
  const [nft, setNft] = useState();
  const [type, setType] = useState();
  const [image, setImage] = useState(null);
  const [fee, setFee] = useState(null);

  const [name, setName] = useState('');
  const [symbol, setSymbol] = useState('');
  const [description, setDescription] = useState('');
  const [royalty, setRoyalty] = useState('');
  const [xtra, setXtra] = useState('');
  const [supply, setSupply] = useState(0);
  const [hasUnlockableContent, setHasUnlockableContent] = useState(false);
  const [unlockableContent, setUnlockableContent] = useState('');

  const [currentMintingStep, setCurrentMintingStep] = useState(0);
  const [isMinting, setIsMinting] = useState(false);

  const [lastMintedTnxId, setLastMintedTnxId] = useState('');

  const authToken = useSelector(state => state.ConnectWallet.authToken);

  const getFee = async () => {
    setFee(null);

    try {
      const contract = await loadContract(nft, FEE_ABI);
      const _fee = await contract.platformFee();
      setFee(parseFloat(_fee.toString()) / 10 ** 18);
    } catch {
      setFee(0);
    }
  };

  const getCollections = async () => {
    try {
      const { data } = await fetchMintableCollections(authToken);
      setCollections(data);
      if (data.length) {
        setSelected([data[0]]);
      }
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    if (authToken) {
      getCollections();
    }
  }, [authToken]);

  useEffect(() => {
    if (!nft) return;

    getFee();
  }, [nft]);

  useEffect(() => {
    dispatch(HeaderActions.toggleSearchbar(true));
  }, []);

  const onDrop = useCallback(acceptedFiles => {
    setImage(acceptedFiles[0]);
  }, []);

  const { getRootProps, getInputProps } = useDropzone({
    accept: accept.join(', '),
    multiple: false,
    onDrop,
    maxSize: 15728640,
  });

  const removeImage = () => {
    setImage(null);
    if (imageRef.current) {
      imageRef.current.value = '';
    }
  };

  const imageToBase64 = () => {
    return new Promise((resolve, reject) => {
      let reader = new FileReader();
      reader.readAsDataURL(image);
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.onerror = err => {
        reject(err);
      };
    });
  };

  const validateMetadata = () => {
    return name !== '' && account !== '' && image;
  };

  const resetMintingStatus = () => {
    setTimeout(() => {
      setIsMinting(false);
      setCurrentMintingStep(0);
    }, 1000);
  };

  const mintNFT = async () => {
    if (!account) {
      showToast('info', 'Connect your wallet first');
      return;
    }
    if (chainId !== ChainId.FANTOM && chainId !== ChainId.FANTOM_TESTNET) {
      showToast('info', 'You are not connected to Fantom Opera Network');
      return;
    }
    const balance = await WalletUtils.checkBalance(account);

    if (balance < fee) {
      showToast(
        'custom',
        `Your balance should be at least ${fee} FTM to mint an NFT`
      );
      return;
    }

    let isBanned = await checkBan(account, authToken);

    if (isBanned) {
      showToast('error', 'You are banned from minting');
      return;
    }

    setLastMintedTnxId('');
    // show stepper
    setIsMinting(true);
    console.log('created from ', account);
    if (!validateMetadata()) {
      resetMintingStatus();
      return;
    }

    let signature;
    let addr;

    if (hasUnlockableContent && unlockableContent.length > 0) {
      const { data: nonce } = await getNonce(account, authToken);
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        showToast(
          'error',
          'You need to sign the message to be able to update account settings.'
        );
        resetMintingStatus();
        return;
      }
    }

    let formData = new FormData();
    const base64 = await imageToBase64();
    formData.append('image', base64);
    formData.append('name', name);
    formData.append('account', account);
    formData.append('description', description);
    formData.append('symbol', symbol);
    formData.append('xtra', xtra);
    const _royalty = parseInt(royalty) * 100;
    formData.append('royalty', isNaN(_royalty) ? 0 : _royalty);
    try {
      let result = await axios({
        method: 'post',
        url: `${apiUrl}/ipfs/uploadImage2Server`,
        data: formData,
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: 'Bearer ' + authToken,
        },
      });

      console.log('upload image result is ');

      const jsonHash = result.data.jsonHash;

      const contract = await loadContract(
        nft,
        type === 721 ? SINGLE_NFT_ABI : MULTI_NFT_ABI
      );
      try {
        const args =
          type === 721 ? [account, jsonHash] : [account, supply, jsonHash];

        let tx;

        if (!fee) {
          tx = await contract.mint(...args);
        } else {
          const options = {
            value: ethers.utils.parseEther(fee.toString()),
            gasPrice: getHigherGWEI(),
          };
          const gasEstimate = await contract.estimateGas.mint(...args, options);
          options.gasLimit = calculateGasMargin(gasEstimate);
          tx = await contract.mint(...args, options);
        }
        setCurrentMintingStep(1);
        setLastMintedTnxId(tx.hash);

        setCurrentMintingStep(2);
        const confirmedTnx = await tx.wait();
        setCurrentMintingStep(3);
        let mintedTkId;
        if (type === 721) {
          const evtCaught = confirmedTnx.logs[0].topics;
          mintedTkId = BigNumber.from(evtCaught[3]);
        } else {
          mintedTkId = BigNumber.from(
            ethers.utils.hexDataSlice(confirmedTnx.logs[1].data, 0, 32)
          );
        }

        const royaltyTx = await registerRoyalty(
          nft,
          mintedTkId.toNumber(),
          isNaN(_royalty) ? 0 : _royalty
        );
        await royaltyTx.wait();

        // save unlockable content
        if (hasUnlockableContent && unlockableContent.length > 0) {
          await addUnlockableContent(
            nft,
            mintedTkId.toNumber(),
            unlockableContent,
            signature,
            addr,
            authToken
          );
        }

        showToast('success', 'New NFT item minted!');
        removeImage();
        setName('');
        setSymbol('');
        setDescription('');

        setTimeout(() => {
          history.push(`/explore/${nft}/${mintedTkId.toNumber()}`);
        }, 1000 + Math.random() * 2000);
      } catch (error) {
        showToast('error', formatError(error));
      }
    } catch (error) {
      showToast('error', error.message);
    }
    resetMintingStatus();
  };

  return (
    <div className={styles.container}>
      <Header border />
      <div className={styles.body}>
        <div className={styles.board}>
          <div {...getRootProps({ className: styles.uploadCont })}>
            <input {...getInputProps()} ref={imageRef} />
            {image ? (
              <>
                <img
                  className={styles.image}
                  src={URL.createObjectURL(image)}
                />
                <div className={styles.overlay}>
                  <CloseIcon className={styles.remove} onClick={removeImage} />
                </div>
              </>
            ) : (
              <>
                <div className={styles.uploadtitle}>
                  Drop files here or&nbsp;
                  <span
                    className={styles.browse}
                    onClick={() => imageRef.current?.click()}
                  >
                    browse
                  </span>
                </div>
                <div className={styles.uploadsubtitle}>
                  JPG, PNG, BMP, GIF Max 15mb.
                </div>
              </>
            )}
          </div>
        </div>
        <div className={styles.panel}>
          <div className={styles.panelInputs}>
            <div className={styles.panelLeft}>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>Collection</p>
                <Select
                  options={collections}
                  disabled={isMinting}
                  values={selected}
                  onChange={([col]) => {
                    setSelected([col]);
                    setNft(col.erc721Address);
                    setType(col.type);
                  }}
                  className={styles.select}
                  placeholder="Choose Collection"
                  itemRenderer={({ item, methods }) => (
                    <div
                      key={item.erc721Address}
                      className={styles.collection}
                      onClick={() => {
                        methods.clearAll();
                        methods.addItem(item);
                      }}
                    >
                      <img
                        src={`https://cloudflare-ipfs.com/ipfs/${item.logoImageHash}`}
                        className={styles.collectionLogo}
                      />
                      <div className={styles.collectionName}>
                        {item.collectionName}
                      </div>
                    </div>
                  )}
                  contentRenderer={({ props: { values } }) =>
                    values.length > 0 ? (
                      <div className={styles.collection}>
                        <img
                          src={`https://cloudflare-ipfs.com/ipfs/${values[0].logoImageHash}`}
                          className={styles.collectionLogo}
                        />
                        <div className={styles.collectionName}>
                          {values[0].collectionName}
                        </div>
                      </div>
                    ) : (
                      <div className={styles.collection} />
                    )
                  }
                />
              </div>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>Name</p>
                <input
                  className={styles.formInput}
                  maxLength={40}
                  placeholder="Name"
                  value={name}
                  onChange={e => setName(e.target.value)}
                  disabled={isMinting}
                />
                <div className={styles.lengthIndicator}>{name.length}/40</div>
              </div>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>Symbol</p>
                <input
                  className={styles.formInput}
                  maxLength={20}
                  placeholder="Symbol"
                  value={symbol}
                  onChange={e => setSymbol(e.target.value)}
                  disabled={isMinting}
                />
                <div className={styles.lengthIndicator}>{symbol.length}/20</div>
              </div>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>Description</p>
                <textarea
                  className={cx(styles.formInput, styles.longInput)}
                  maxLength={120}
                  placeholder="Description"
                  value={description}
                  onChange={e => setDescription(e.target.value)}
                  disabled={isMinting}
                />
                <div className={styles.lengthIndicator}>
                  {description.length}/120
                </div>
              </div>
            </div>
            <div className={styles.panelRight}>
              {type === 1155 && (
                <div className={styles.formGroup}>
                  <p className={styles.formLabel}>Supply</p>
                  <PriceInput
                    className={styles.formInput}
                    placeholder="Supply"
                    decimals={0}
                    value={'' + supply}
                    onChange={setSupply}
                    disabled={isMinting}
                  />
                </div>
              )}
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>
                  Royalty (%)&nbsp;
                  <BootstrapTooltip
                    title="If you set a royalty here, you will get X percent of sales price each time an NFT is sold on our platform."
                    placement="top"
                  >
                    <HelpOutlineIcon />
                  </BootstrapTooltip>
                </p>
                <PriceInput
                  className={styles.formInput}
                  placeholder="Royalty"
                  decimals={2}
                  value={'' + royalty}
                  onChange={val =>
                    val[val.length - 1] === '.'
                      ? setRoyalty(val)
                      : setRoyalty(Math.min(100, +val))
                  }
                  disabled={isMinting}
                />
              </div>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>
                  Link to IP Rights Document (Optional)&nbsp;
                  <BootstrapTooltip
                    title="Link to the document which proves your ownership of this image."
                    placement="top"
                  >
                    <HelpOutlineIcon />
                  </BootstrapTooltip>
                </p>
                <input
                  className={styles.formInput}
                  placeholder="Enter Link"
                  value={xtra}
                  onChange={e => setXtra(e.target.value)}
                  disabled={isMinting}
                />
              </div>
              <div className={styles.formGroup}>
                <p className={styles.formLabel}>
                  Unlockable Content&nbsp;
                  <PurpleSwitch
                    checked={hasUnlockableContent}
                    onChange={e => {
                      setHasUnlockableContent(e.target.checked);
                      setUnlockableContent('');
                    }}
                    name="unlockableContent"
                  />
                </p>
                {hasUnlockableContent && (
                  <textarea
                    className={cx(styles.formInput, styles.longInput)}
                    maxLength={500}
                    placeholder="Unlockable Content"
                    value={unlockableContent}
                    onChange={e => setUnlockableContent(e.target.value)}
                    disabled={isMinting}
                  />
                )}
              </div>
            </div>
          </div>

          {isMinting && (
            <div>
              <Stepper activeStep={currentMintingStep} alternativeLabel>
                {mintSteps.map(label => (
                  <Step key={label}>
                    <StepLabel>{label}</StepLabel>
                  </Step>
                ))}
              </Stepper>
            </div>
          )}
          <div
            className={cx(
              styles.button,
              (isMinting || !account || !validateMetadata()) && styles.disabled
            )}
            onClick={
              isMinting || !account || !validateMetadata() ? null : mintNFT
            }
          >
            {isMinting ? (
              <ClipLoader size="16" color="white"></ClipLoader>
            ) : (
              'Mint'
            )}
          </div>
          <div className={styles.fee}>
            {fee !== null ? (
              <>
                <InfoIcon />
                &nbsp;{fee} FTM are charged to create a new NFT.
              </>
            ) : (
              <Skeleton width={330} height={22} />
            )}
          </div>
          <div className={styles.mintStatusContainer}>
            {lastMintedTnxId !== '' && (
              <a
                className={styles.tnxAnchor}
                target="_blank"
                rel="noopener noreferrer"
                href={`${explorerUrl}/tx/${lastMintedTnxId}`}
              >
                You can track the last transaction here ...
              </a>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
Example #19
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
OfferModal = ({
  visible,
  onClose,
  onMakeOffer,
  confirming,
  totalSupply,
}) => {
  const { tokens } = useTokens();
  const { getSalesContract } = useSalesContract();

  const [price, setPrice] = useState('');
  const [quantity, setQuantity] = useState('1');
  const [endTime, setEndTime] = useState(
    new Date(new Date().getTime() + 24 * 60 * 60 * 1000)
  );
  const [options, setOptions] = useState([]);
  const [selected, setSelected] = useState([]);
  const [tokenPrice, setTokenPrice] = useState();
  const [tokenPriceInterval, setTokenPriceInterval] = useState();
  const [inputError, setInputError] = useState(null);

  useEffect(() => {
    if (tokens?.length > 1) {
      setOptions(tokens);
    }
  }, [tokens]);

  useEffect(() => {
    if (visible) {
      setPrice('');
      setQuantity('1');
      setEndTime(new Date(new Date().getTime() + 24 * 60 * 60 * 1000));
      if (tokens?.length > 1) {
        setSelected([tokens[0]]);
      }
    }
  }, [visible]);

  const getTokenPrice = () => {
    if (tokenPriceInterval) clearInterval(tokenPriceInterval);
    const func = async () => {
      const tk = selected[0].address || ethers.constants.AddressZero;
      try {
        const salesContract = await getSalesContract();
        const price = await salesContract.getPrice(tk);
        setTokenPrice(parseFloat(ethers.utils.formatUnits(price, 18)));
      } catch {
        setTokenPrice(null);
      }
    };
    func();
    setTokenPriceInterval(setInterval(func, 60 * 1000));
  };

  useEffect(() => {
    if (selected.length === 0) return;

    getTokenPrice();
  }, [selected]);

  const handleQuantityChange = e => {
    const val = e.target.value;
    if (!val) {
      setQuantity('');
      return;
    }

    if (isNaN(val)) return;

    const _quantity = parseInt(val);
    setQuantity(Math.min(_quantity, totalSupply));
  };

  const handleMakeOffer = () => {
    let quant = 1;
    if (totalSupply > 1) {
      quant = parseInt(quantity);
    }
    onMakeOffer(selected[0], price, quant, endTime);
  };

  const validateInput = () => {
    if (price.length === 0 || parseFloat(price) == 0) return false;
    if (totalSupply > 1 && quantity.length === 0) return false;
    if (endTime.getTime() < new Date().getTime()) return false;
    return true;
  };

  return (
    <Modal
      visible={visible}
      title="Place your offer"
      onClose={onClose}
      submitDisabled={confirming || !validateInput() || inputError}
      submitLabel={
        confirming ? <ClipLoader color="#FFF" size={16} /> : 'Place Offer'
      }
      onSubmit={() =>
        !confirming && validateInput() ? handleMakeOffer() : null
      }
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Price</div>
        <div className={styles.formInputCont}>
          <Select
            options={options}
            disabled={confirming}
            values={selected}
            onChange={tk => {
              setSelected(tk);
            }}
            className={styles.select}
            placeholder=""
            itemRenderer={({ item, itemIndex, methods }) => (
              <div
                key={itemIndex}
                className={styles.token}
                onClick={() => {
                  methods.clearAll();
                  methods.addItem(item);
                }}
              >
                <img src={item?.icon} className={styles.tokenIcon} />
                <div className={styles.tokenSymbol}>{item.symbol}</div>
              </div>
            )}
            contentRenderer={({ props: { values } }) =>
              values.length > 0 ? (
                <div className={styles.selectedToken}>
                  <img src={values[0]?.icon} className={styles.tokenIcon} />
                  <div className={styles.tokenSymbol}>{values[0].symbol}</div>
                </div>
              ) : (
                <div className={styles.selectedToken} />
              )
            }
          />
          <PriceInput
            className={styles.formInput}
            placeholder="0.00"
            decimals={selected[0]?.decimals || 0}
            value={'' + price}
            onChange={setPrice}
            disabled={confirming}
            onInputError={err => setInputError(err)}
          />
          <div className={styles.usdPrice}>
            {!isNaN(tokenPrice) && tokenPrice !== null ? (
              `$${formatNumber(
                ((parseFloat(price) || 0) * tokenPrice).toFixed(2)
              )}`
            ) : (
              <Skeleton width={100} height={24} />
            )}
          </div>
        </div>
        <InputError text={inputError} />
      </div>
      {totalSupply !== null && (
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Quantity</div>
          <div className={styles.formInputCont}>
            <input
              className={styles.formInput}
              placeholder={totalSupply}
              value={quantity}
              onChange={handleQuantityChange}
              disabled={confirming || totalSupply === 1}
            />
          </div>
        </div>
      )}
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Offer Expiration</div>
        <div className={styles.formInputCont}>
          <Datetime
            value={endTime}
            onChange={val => setEndTime(val.toDate())}
            inputProps={{
              className: styles.formInput,
              onKeyDown: e => e.preventDefault(),
              disabled: confirming,
            }}
            closeOnSelect
            isValidDate={cur =>
              cur.valueOf() > new Date().getTime() - 1000 * 60 * 60 * 24
            }
          />
        </div>
      </div>
    </Modal>
  );
}
Example #20
Source File: index.js    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
NewBundleModal = ({ visible, onClose, onCreateSuccess = () => {} }) => {
  const { tokens: payTokens } = useTokens();
  const { account, chainId } = useWeb3React();
  const { getSalesContract } = useSalesContract();

  const { uid } = useParams();

  const { fetchTokens, createBundle, deleteBundle } = useApi();
  const { getERC721Contract } = useNFTContract();
  const { listBundle } = useBundleSalesContract();

  const rootRef = useRef(null);

  const selected = useRef([]);
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [creating, setCreating] = useState(false);
  const [loadingStatus, setLoadingStatus] = useState(false);
  const [approved, setApproved] = useState(true);
  const [approving, setApproving] = useState(false);

  const [fetching, setFetching] = useState(false);
  const [page, setPage] = useState(0);
  const tokens = useRef([]);
  const [count, setCount] = useState(0);
  const [options, setOptions] = useState([]);
  const [paySelected, setPaySelected] = useState([]);
  const [tokenPrice, setTokenPrice] = useState();
  const [tokenPriceInterval, setTokenPriceInterval] = useState();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const fetchNFTs = async step => {
    if (fetching) return;

    setFetching(true);
    setCount(0);

    try {
      const { data } = await fetchTokens(
        step * 18,
        18,
        'single',
        [],
        null,
        'createdAt',
        [],
        uid
      );
      setFetching(false);
      tokens.current.push(...data.tokens);
      setCount(data.total);
      setPage(step);
    } catch {
      setFetching(false);
    }
  };

  const loadNextPage = () => {
    if (fetching) return;
    if (tokens.current.length === count) return;

    fetchNFTs(page + 1);
  };

  useEffect(() => {
    if (visible) {
      selected.current = [];
      setName('');
      setPrice('');
      tokens.current = [];
      setCount(0);
      fetchNFTs(0);

      if (payTokens?.length) {
        setPaySelected([payTokens[0]]);
      }
    }
  }, [visible]);

  useEffect(() => {
    if (payTokens?.length) {
      setOptions(payTokens);
    }
  }, [payTokens]);

  const getTokenPrice = () => {
    if (tokenPriceInterval) clearInterval(tokenPriceInterval);
    const func = async () => {
      const tk = selected[0].address || ethers.constants.AddressZero;
      try {
        const salesContract = await getSalesContract();
        const price = await salesContract.getPrice(tk);
        setTokenPrice(parseFloat(ethers.utils.formatUnits(price, 18)));
      } catch {
        setTokenPrice(null);
      }
    };
    func();
    setTokenPriceInterval(setInterval(func, 60 * 1000));
  };

  useEffect(() => {
    if (paySelected.length === 0) return;

    getTokenPrice();
  }, [paySelected]);

  const getContractApprovedStatus = async () => {
    setLoadingStatus(true);
    let contractAddresses = selected.current.map(
      idx => tokens.current[idx].contractAddress
    );
    contractAddresses = contractAddresses.filter(
      (addr, idx) => contractAddresses.indexOf(addr) === idx
    );
    let approved = true;
    try {
      await Promise.all(
        contractAddresses.map(async address => {
          const contract = await getERC721Contract(address);
          try {
            const _approved = await contract.isApprovedForAll(
              account,
              Contracts[chainId].bundleSales
            );
            approved = approved && _approved;
          } catch (e) {
            console.log(e);
          }
        })
      );
    } catch (e) {
      console.log(e);
    }
    setApproved(approved);
    setLoadingStatus(false);
  };

  const isValid = () => {
    return name && price && parseFloat(price) > 0 && selected.current.length;
  };

  const closeModal = () => {
    onClose();
  };

  const handleScroll = e => {
    const obj = e.currentTarget;
    if (obj.scrollHeight - obj.clientHeight - obj.scrollTop < 100) {
      loadNextPage();
    }
  };

  const handleItemClick = idx => {
    const index = selected.current.indexOf(idx);
    if (index > -1) {
      selected.current.splice(index, 1);
    } else {
      selected.current.push(idx);
    }
    getContractApprovedStatus();
  };

  const onApprove = async () => {
    setApproving(true);
    let contractAddresses = selected.current.map(
      idx => tokens.current[idx].contractAddress
    );
    contractAddresses = contractAddresses.filter(
      (addr, idx) => contractAddresses.indexOf(addr) === idx
    );
    try {
      await Promise.all(
        contractAddresses.map(async address => {
          const contract = await getERC721Contract(address);
          const _approved = await contract.isApprovedForAll(
            account,
            Contracts[chainId].bundleSales
          );
          if (!_approved) {
            const tx = await contract.setApprovalForAll(
              Contracts[chainId].bundleSales,
              true
            );
            await tx.wait();
          }
        })
      );
    } catch (e) {
      console.log(e);
    }
    setApproved(true);
    setApproving(false);
  };

  const onCreate = async () => {
    if (creating) return;

    let bundleID;
    const selectedItems = [];
    const token = paySelected[0];
    try {
      setCreating(true);

      for (let i = 0; i < selected.current.length; i++) {
        const item = tokens.current[selected.current[i]];
        selectedItems.push({
          address: item.contractAddress,
          tokenID: item.tokenID,
          supply: item?.holderSupply || item?.supply || 1,
        });
      }
      const { data } = await createBundle(
        name,
        token.address,
        parseFloat(price),
        selectedItems,
        authToken
      );
      bundleID = data;
    } catch {
      setCreating(false);
    }

    try {
      const _price = ethers.utils.parseUnits(price, token.decimals);
      const tx = await listBundle(
        bundleID,
        selectedItems.map(item => item.address),
        selectedItems.map(item => item.tokenID),
        selectedItems.map(item => item.supply),
        token.address === '' ? ethers.constants.AddressZero : token.address,
        _price,
        ethers.BigNumber.from(Math.floor(new Date().getTime() / 1000))
      );
      await tx.wait();

      toast('success', 'Bundle created successfully!');
      setCreating(false);

      closeModal();
      onCreateSuccess();
    } catch {
      setCreating(false);
      try {
        await deleteBundle(bundleID, authToken);
      } catch (e) {
        console.log(e);
      }
    }
  };

  if (!visible) return null;

  return (
    <div className={styles.root} ref={rootRef}>
      <Modal open className={styles.modal} container={() => rootRef.current}>
        <div className={styles.paper}>
          <div className={styles.header}>
            <div className={styles.title}>Create Bundle</div>
            <div className={styles.closeButton} onClick={onClose}>
              <img src={closeIcon} />
            </div>
          </div>
          <div className={styles.body}>
            <div className={styles.formGroup}>
              <p className={styles.formLabel}>Name</p>
              <div className={styles.formInputCont}>
                <input
                  type="text"
                  className={styles.formInput}
                  maxLength={20}
                  placeholder="Bundle Name"
                  value={name}
                  onChange={e => setName(e.target.value)}
                  disabled={creating}
                />
              </div>
              <div className={styles.lengthIndicator}>{name.length}/20</div>
            </div>
            <div className={styles.formGroup}>
              <div className={styles.formLabel}>Price</div>
              <div className={styles.formInputCont}>
                <Select
                  options={options}
                  disabled={creating}
                  values={paySelected}
                  onChange={tk => {
                    setPaySelected(tk);
                  }}
                  className={commonStyles.select}
                  placeholder=""
                  itemRenderer={({ item, itemIndex, methods }) => (
                    <div
                      key={itemIndex}
                      className={commonStyles.token}
                      onClick={() => {
                        methods.clearAll();
                        methods.addItem(item);
                      }}
                    >
                      <img
                        src={item?.icon}
                        className={commonStyles.tokenIcon}
                      />
                      <div className={commonStyles.tokenSymbol}>
                        {item.symbol}
                      </div>
                    </div>
                  )}
                  contentRenderer={({ props: { values } }) =>
                    values.length > 0 ? (
                      <div className={commonStyles.selectedToken}>
                        <img
                          src={values[0]?.icon}
                          className={commonStyles.tokenIcon}
                        />
                        <div className={commonStyles.tokenSymbol}>
                          {values[0].symbol}
                        </div>
                      </div>
                    ) : (
                      <div className={commonStyles.selectedToken} />
                    )
                  }
                />
                <PriceInput
                  className={styles.formInput}
                  placeholder="0.00"
                  value={'' + price}
                  onChange={setPrice}
                  disabled={creating}
                />
                <div className={styles.usdPrice}>
                  {!isNaN(tokenPrice) && tokenPrice !== null ? (
                    `$${((parseFloat(price) || 0) * tokenPrice).toFixed(2)}`
                  ) : (
                    <Skeleton width={100} height={24} />
                  )}
                </div>
              </div>
            </div>
            <div className={styles.formGroup}>
              <p className={styles.formLabel}>Items</p>
              <div className={styles.itemList} onScroll={handleScroll}>
                {tokens.current.map((item, idx) => (
                  <NFTItem
                    key={idx}
                    item={item}
                    onClick={() => handleItemClick(idx)}
                    selected={selected.current.indexOf(idx) > -1}
                  />
                ))}
                {fetching &&
                  new Array(5)
                    .fill(null)
                    .map((item, idx) => <NFTItem key={idx} item={item} />)}
              </div>
            </div>
          </div>

          <div className={styles.footer}>
            <div
              className={cx(
                styles.button,
                styles.save,
                (creating || loadingStatus || approving || !isValid()) &&
                  styles.disabled
              )}
              onClick={
                isValid() && !creating && !loadingStatus && !approving
                  ? approved
                    ? onCreate
                    : onApprove
                  : null
              }
            >
              {creating || loadingStatus || approving ? (
                <ClipLoader color="#FFF" size={16} />
              ) : approved ? (
                'Create'
              ) : (
                'Approve Items'
              )}
            </div>
          </div>
        </div>
      </Modal>
    </div>
  );
}
Example #21
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
ModModal = ({ visible, onClose, isAdding }) => {
  const { getNonce, addMod, removeMod } = useApi();
  const { account } = useWeb3React();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [adding, setAdding] = useState(false);
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');

  useEffect(() => {
    if (visible) {
      setAdding(false);
    }
  }, [visible]);

  const handleAddMod = async () => {
    if (adding) return;

    try {
      setAdding(true);

      const { data: nonce } = await getNonce(account, authToken);

      let signature;
      let addr;
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          `You need to sign the message to be able to ${
            isAdding ? 'add' : 'remove'
          } a new moderator.`
        );
        setAdding(false);
        return;
      }

      if (isAdding) {
        await addMod(name, address, authToken, signature, addr);
        toast('success', 'Added new moderator!');
      } else {
        await removeMod(address, authToken, signature, addr);
        toast('success', 'Removed moderator!');
      }
    } catch (e) {
      console.log(e);
    }
    setAdding(false);
  };

  return (
    <Modal
      visible={visible}
      title={isAdding ? 'Add Moderator' : 'Remove Moderator'}
      onClose={onClose}
      submitDisabled={adding}
      submitLabel={
        adding ? (
          <ClipLoader color="#FFF" size={16} />
        ) : isAdding ? (
          'Add'
        ) : (
          'Remove'
        )
      }
      onSubmit={!adding ? () => handleAddMod() : null}
    >
      {isAdding && (
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Name</div>
          <div className={styles.formInputCont}>
            <input
              className={styles.formInput}
              placeholder="Name"
              value={name}
              onChange={e => setName(e.target.value)}
              disabled={adding}
            />
          </div>
        </div>
      )}
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Address</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={adding}
          />
        </div>
      </div>
    </Modal>
  );
}
Example #22
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
BidModal = ({
  visible,
  onClose,
  onPlaceBid,
  minBidAmount,
  confirming,
  token,
  firstBid,
}) => {
  const { tokens } = useTokens();
  const { getSalesContract } = useSalesContract();
  const [currentBid, setCurrentBid] = useState(0);
  const [price, setPrice] = useState('');
  const [focused, setFocused] = useState(false);
  const [options, setOptions] = useState([]);
  const [tokenPrice, setTokenPrice] = useState();
  const [tokenPriceInterval, setTokenPriceInterval] = useState();
  const [inputError, setInputError] = useState(null);

  useEffect(() => {
    setPrice(minBidAmount);
    setCurrentBid(parseFloat(minBidAmount));
  }, [visible]);

  useEffect(() => {
    if (tokens?.length) {
      setOptions(tokens);
    }
  }, [tokens]);

  const getTokenPrice = () => {
    if (tokenPriceInterval) clearInterval(tokenPriceInterval);
    const func = async () => {
      const tk = token.address || ethers.constants.AddressZero;
      try {
        const salesContract = await getSalesContract();
        const price = await salesContract.getPrice(tk);
        setTokenPrice(parseFloat(ethers.utils.formatUnits(price, 18)));
      } catch {
        setTokenPrice(null);
      }
    };
    func();
    setTokenPriceInterval(setInterval(func, 60 * 1000));
  };

  useEffect(() => {
    if (token) {
      getTokenPrice();
    }
  }, [token]);

  const validateInput = () => {
    return (
      price.length > 0 &&
      parseFloat(price) > 0 &&
      (firstBid
        ? parseFloat(price) >= currentBid
        : parseFloat(price) > currentBid)
    );
  };

  return (
    <Modal
      visible={visible}
      title="Place Bid"
      onClose={onClose}
      submitDisabled={confirming || !validateInput() || inputError}
      submitLabel={confirming ? <ClipLoader color="#FFF" size={16} /> : 'Place'}
      onSubmit={() =>
        !confirming && validateInput() ? onPlaceBid(price) : null
      }
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Price</div>
        <div className={cx(styles.formInputCont, focused && styles.focused)}>
          <Select
            options={options}
            disabled
            values={token ? [token] : []}
            className={styles.select}
            placeholder=""
            itemRenderer={({ item, itemIndex, methods }) => (
              <div
                key={itemIndex}
                className={styles.token}
                onClick={() => {
                  methods.clearAll();
                  methods.addItem(item);
                }}
              >
                <img src={item?.icon} className={styles.tokenIcon} />
                <div className={styles.tokenSymbol}>{item.symbol}</div>
              </div>
            )}
            contentRenderer={({ props: { values } }) =>
              values.length > 0 ? (
                <div className={styles.selectedToken}>
                  <img src={values[0]?.icon} className={styles.tokenIcon} />
                  <div className={styles.tokenSymbol}>{values[0].symbol}</div>
                </div>
              ) : (
                <div className={styles.selectedToken} />
              )
            }
          />
          <PriceInput
            className={styles.formInput}
            placeholder="0.00"
            decimals={token?.decimals || 0}
            value={'' + price}
            onChange={setPrice}
            onFocus={() => setFocused(true)}
            onBlur={() => setFocused(false)}
            disabled={confirming}
            onInputError={err => setInputError(err)}
          />
          <div className={styles.usdPrice}>
            {!isNaN(tokenPrice) && tokenPrice !== null ? (
              `$${formatNumber(
                ((parseFloat(price) || 0) * tokenPrice).toFixed(2)
              )}`
            ) : (
              <Skeleton width={100} height={24} />
            )}
          </div>
        </div>
        <InputError text={inputError} />
      </div>
    </Modal>
  );
}
Example #23
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
BanUserModal = ({ visible, onClose, isForBanning }) => {
  const { getNonce, banUser, unbanUser } = useApi(); //unban user
  const { account } = useWeb3React();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [banning, setBanning] = useState(false);
  const [address, setAddress] = useState('');

  useEffect(() => {
    if (visible) {
      setBanning(false);
    }
  }, [visible]);

  const handleBanUser = async () => {
    if (banning) return;

    try {
      setBanning(true);

      const { data: nonce } = await getNonce(account, authToken);

      let signature;
      let addr;
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          'You need to sign the message to be able to ban/unban user.'
        );
        setBanning(false);
        return;
      }

      let response = isForBanning
        ? await banUser(address, authToken, signature, addr)
        : await unbanUser(address, authToken, signature, addr);

      response.status == 'success'
        ? isForBanning
          ? toast('success', 'User banned successfully!')
          : toast('success', 'User unbanned successfully!')
        : isForBanning
        ? toast('error', response.data)
        : toast('error', response.data);

      setAddress('');
      setBanning(false);
    } catch (e) {
      console.log(e);
      toast('error', 'Error occured while banning/unbanning a user!');
    }
    setBanning(false);
  };

  return (
    <Modal
      visible={visible}
      title={isForBanning ? 'Ban User' : 'Unban User'}
      onClose={onClose}
      submitDisabled={banning}
      submitLabel={
        banning ? (
          <ClipLoader color="#FFF" size={16} />
        ) : isForBanning ? (
          'Ban User'
        ) : (
          'Unban User'
        )
      }
      onSubmit={!banning ? () => handleBanUser() : null}
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>User Address</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={banning}
          />
        </div>
      </div>
    </Modal>
  );
}
Example #24
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
BanItemModal = ({ visible, onClose }) => {
  const { getNonce, banItems } = useApi();
  const { account } = useWeb3React();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [banning, setBanning] = useState(false);
  const [address, setAddress] = useState('');
  const [tokenIDs, setTokenIDs] = useState('');

  useEffect(() => {
    if (visible) {
      setBanning(false);
    }
  }, [visible]);

  const handleBanItem = async () => {
    if (banning) return;

    try {
      setBanning(true);

      const { data: nonce } = await getNonce(account, authToken);

      let signature;
      let addr;
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          'You need to sign the message to be able to ban NFT items.'
        );
        setBanning(false);
        return;
      }

      await banItems(address, tokenIDs, authToken, signature, addr);
      toast('success', 'Item banned successfully!');
    } catch (e) {
      console.log(e);
    }
    setBanning(false);
  };

  return (
    <Modal
      visible={visible}
      title="Ban NFT Items"
      onClose={onClose}
      submitDisabled={banning}
      submitLabel={banning ? <ClipLoader color="#FFF" size={16} /> : 'Ban'}
      onSubmit={!banning ? () => handleBanItem() : null}
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Contract Address</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={banning}
          />
        </div>
      </div>
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Token ID</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0"
            value={tokenIDs}
            onChange={e => setTokenIDs(e.target.value)}
            disabled={banning}
          />
        </div>
      </div>
    </Modal>
  );
}
Example #25
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
BanCollectionModal = ({ visible, isBan, onClose }) => {
  const { getNonce, banCollection, unbanCollection } = useApi();
  const { account } = useWeb3React();

  const { authToken } = useSelector(state => state.ConnectWallet);

  const [banning, setBanning] = useState(false);
  const [address, setAddress] = useState('');

  useEffect(() => {
    if (visible) {
      setBanning(false);
    }
  }, [visible]);

  const handleBanItem = async () => {
    if (banning) return;

    try {
      setBanning(true);

      const { data: nonce } = await getNonce(account, authToken);

      let signature;
      let addr;
      try {
        const signer = await getSigner();
        const msg = `Approve Signature on Artion.io with nonce ${nonce}`;
        signature = await signer.signMessage(msg);
        addr = ethers.utils.verifyMessage(msg, signature);
      } catch (err) {
        toast(
          'error',
          'You need to sign the message to be able to ban/unban collection.'
        );
        setBanning(false);
        return;
      }

      await (isBan ? banCollection : unbanCollection)(
        address,
        authToken,
        signature,
        addr
      );
      toast('success', 'Success!');
    } catch (e) {
      console.log(e);
    }
    setBanning(false);
  };

  return (
    <Modal
      visible={visible}
      title={isBan ? 'Ban Collection' : 'Unban Collection'}
      onClose={onClose}
      submitDisabled={banning}
      submitLabel={
        banning ? (
          <ClipLoader color="#FFF" size={16} />
        ) : isBan ? (
          'Ban'
        ) : (
          'Unban'
        )
      }
      onSubmit={!banning ? () => handleBanItem() : null}
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>Collection Address</div>
        <div className={styles.formInputCont}>
          <input
            className={styles.formInput}
            placeholder="0x0000"
            value={address}
            onChange={e => setAddress(e.target.value)}
            disabled={banning}
          />
        </div>
      </div>
    </Modal>
  );
}
Example #26
Source File: index.jsx    From Artion-Client with GNU General Public License v3.0 4 votes vote down vote up
AuctionModal = ({
  visible,
  onClose,
  onStartAuction,
  auction,
  auctionStarted,
  confirming,
  approveContract,
  contractApproving,
  contractApproved,
}) => {
  const { tokens } = useTokens();
  const { getSalesContract } = useSalesContract();

  const [now, setNow] = useState(new Date());
  const [reservePrice, setReservePrice] = useState('');
  const [startTime, setStartTime] = useState(
    new Date(new Date().getTime() + 2 * 60 * 1000)
  );
  const [endTime, setEndTime] = useState(
    new Date(new Date().getTime() + 24 * 60 * 60 * 1000)
  );
  const [focused, setFocused] = useState(false);
  const [minBidReserve, setMinBidReserve] = useState(false);
  const [options, setOptions] = useState([]);
  const [selected, setSelected] = useState([]);
  const [tokenPrice, setTokenPrice] = useState();
  const [tokenPriceInterval, setTokenPriceInterval] = useState();
  const [inputError, setInputError] = useState(null);

  useEffect(() => {
    setInterval(() => setNow(new Date()), 1000);
  }, []);

  useEffect(() => {
    if (tokens?.length) {
      setOptions(tokens);
    }
  }, [tokens]);

  useEffect(() => {
    setReservePrice(auction?.reservePrice || '');
    setStartTime(
      auction?.startTime
        ? new Date(auction.startTime * 1000)
        : new Date(new Date().getTime() + 2 * 60 * 1000)
    );
    setEndTime(
      auction?.endTime
        ? new Date(auction.endTime * 1000)
        : new Date(new Date().getTime() + 24 * 60 * 60 * 1000)
    );
  }, [visible, auction]);

  useEffect(() => {
    if (visible && tokens?.length) {
      setSelected([auction ? auction.token : tokens[0]]);
    }
  }, [visible, auction]);

  const CustomCheckbox = withStyles({
    root: {
      '&$checked': {
        color: '#1969FF',
      },
    },
    checked: {},
  })(props => <Checkbox color="default" {...props} />);

  const getTokenPrice = () => {
    if (tokenPriceInterval) clearInterval(tokenPriceInterval);
    const func = async () => {
      const tk = selected[0].address || ethers.constants.AddressZero;
      try {
        const salesContract = await getSalesContract();
        const price = await salesContract.getPrice(tk);
        setTokenPrice(parseFloat(ethers.utils.formatUnits(price, 18)));
      } catch {
        setTokenPrice(null);
      }
    };
    func();
    setTokenPriceInterval(setInterval(func, 60 * 1000));
  };

  useEffect(() => {
    if (selected.length === 0) return;

    getTokenPrice();
  }, [selected]);

  const validateInput = (() => {
    if (reservePrice.length === 0 || parseFloat(reservePrice) == 0)
      return false;
    if (!auctionStarted && startTime.getTime() < now.getTime()) return false;
    return (
      endTime.getTime() >= now.getTime() + 1000 * 60 * 5 &&
      endTime.getTime() >= startTime.getTime() + 1000 * 60 * 5
    );
  })();

  return (
    <Modal
      visible={visible}
      title={auction ? 'Update Auction' : 'Start Auction'}
      onClose={onClose}
      submitDisabled={
        contractApproving ||
        confirming ||
        (contractApproved && !validateInput) ||
        inputError
      }
      submitLabel={
        contractApproved ? (
          confirming ? (
            <ClipLoader color="#FFF" size={16} />
          ) : auction ? (
            'Update Auction'
          ) : (
            'Start Auction'
          )
        ) : contractApproving ? (
          'Approving Item'
        ) : (
          'Approve Item'
        )
      }
      onSubmit={() =>
        contractApproved
          ? !confirming && validateInput
            ? onStartAuction(
                selected[0],
                reservePrice,
                startTime,
                endTime,
                minBidReserve
              )
            : null
          : approveContract()
      }
    >
      <div className={styles.formGroup}>
        <div className={styles.formLabel}>
          Reserve Price&nbsp;
          <BootstrapTooltip
            title="Reserve price is your desired one you want to get from this auction."
            placement="top"
          >
            <HelpOutlineIcon />
          </BootstrapTooltip>
        </div>
        <div className={cx(styles.formInputCont, focused && styles.focused)}>
          <Select
            options={options}
            disabled={auction || confirming}
            values={selected}
            onChange={tk => {
              setSelected(tk);
            }}
            className={styles.select}
            placeholder=""
            itemRenderer={({ item, itemIndex, methods }) => (
              <div
                key={itemIndex}
                className={styles.token}
                onClick={() => {
                  methods.clearAll();
                  methods.addItem(item);
                }}
              >
                <img src={item?.icon} className={styles.tokenIcon} />
                <div className={styles.tokenSymbol}>{item.symbol}</div>
              </div>
            )}
            contentRenderer={({ props: { values } }) =>
              values.length > 0 ? (
                <div className={styles.selectedToken}>
                  <img src={values[0]?.icon} className={styles.tokenIcon} />
                  <div className={styles.tokenSymbol}>{values[0].symbol}</div>
                </div>
              ) : (
                <div className={styles.selectedToken} />
              )
            }
          />
          <PriceInput
            className={styles.formInput}
            placeholder="0.00"
            value={'' + reservePrice}
            decimals={selected[0]?.decimals || 0}
            onChange={setReservePrice}
            onFocus={() => setFocused(true)}
            onBlur={() => setFocused(false)}
            disabled={contractApproving || confirming}
            onInputError={err => setInputError(err)}
          />
          <div className={styles.usdPrice}>
            {!isNaN(tokenPrice) && tokenPrice !== null ? (
              `$${formatNumber(
                ((parseFloat(reservePrice) || 0) * tokenPrice).toFixed(2)
              )}`
            ) : (
              <Skeleton width={100} height={24} />
            )}
          </div>
        </div>
        <InputError text={inputError} />
      </div>
      <div className={styles.formGroupDates}>
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Start Time</div>
          <div className={styles.formInputCont}>
            <Datetime
              value={startTime}
              className={'calendarAboveInput'}
              onChange={val => setStartTime(val.toDate())}
              inputProps={{
                className: styles.formInput,
                onKeyDown: e => e.preventDefault(),
                disabled: auctionStarted || contractApproving || confirming,
              }}
              closeOnSelect
              isValidDate={cur =>
                cur.valueOf() > now.getTime() - 1000 * 60 * 60 * 24
              }
            />
          </div>
        </div>
        <div className={styles.formGroup}>
          <div className={styles.formLabel}>Auction Expiration</div>
          <div className={styles.formInputCont}>
            <Datetime
              value={endTime}
              className={'calendarAboveInput'}
              onChange={val => setEndTime(val.toDate())}
              inputProps={{
                className: styles.formInput,
                onKeyDown: e => e.preventDefault(),
                disabled: contractApproving || confirming,
              }}
              closeOnSelect
              isValidDate={cur =>
                cur.valueOf() > startTime.getTime() - 1000 * 60 * 60 * 23
              }
            />
          </div>
        </div>
      </div>
      <FormControlLabel
        className={cx(styles.formControl, styles.selected)}
        classes={{ label: styles.groupTitle }}
        control={
          <CustomCheckbox
            checked={minBidReserve}
            onChange={() => setMinBidReserve(prevState => !prevState)}
          />
        }
        label="Minimum bid should be equal or greater than reserve price"
      />
    </Modal>
  );
}