lodash#zipObject JavaScript Examples

The following examples show how to use lodash#zipObject. 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: createDataLoaders.js    From rate-repository-api with MIT License 6 votes vote down vote up
createModelLoader = Model =>
  new DataLoader(
    async ids => {
      const idColumns = isArray(Model.idColumn)
        ? Model.idColumn
        : [Model.idColumn];

      const camelCasedIdColumns = idColumns.map(id => camelCase(id));

      const results = await Model.query().findByIds(ids);

      return ids.map(
        id =>
          find(
            results,
            zipObject(camelCasedIdColumns, isArray(id) ? id : [id]),
          ) || null,
      );
    },
    {
      cacheKeyFn: jsonCacheKeyFn,
    },
  )
Example #2
Source File: data-generator-utils.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
function handleUpdated(updatedNodes, prevNodes) {
  const modifiedNodesIndex = zipObject((updatedNodes || []).map(n => [n.id, n]));
  return prevNodes.toIndexedSeq().toJS().map(n => (
    Object.assign({}, mergeMetrics(n), modifiedNodesIndex[n.id])
  ));
}
Example #3
Source File: LedgerDerivationContent.js    From origin-dollar with MIT License 4 votes vote down vote up
LedgerDerivationContent = ({}) => {
  const [displayError, setDisplayError] = useState(null)
  const [ledgerPath, setLedgerPath] = useState({})
  const [addresses, setAddresses] = useState({})
  const [addressBalances, setAddressBalances] = useState({})
  const [addressStableBalances, setAddressStableBalances] = useState({})
  const [pathTotals, setPathTotals] = useState({})
  const [ready, setReady] = useState(false)
  const [preloaded, setPreloaded] = useState(false)
  const [activePath, setActivePath] = useState()
  const [next, setNext] = useState({})
  const [nextLoading, setNextLoading] = useState({})

  const contractData = useStoreState(ContractStore, (s) => {
    if (s.coinInfoList) {
      return [
        s.coinInfoList.usdt,
        s.coinInfoList.dai,
        s.coinInfoList.usdc,
        s.coinInfoList.ousd,
      ]
    }
    return []
  })

  const errorMessageMap = (error) => {
    if (!error || !error.message) {
      return 'Unknown error'
    }
    if (
      error.message.includes('Ledger device: UNKNOWN_ERROR') ||
      error.message.includes(
        'Failed to sign with Ledger device: U2F DEVICE_INELIGIBLE'
      )
    ) {
      return fbt(
        'Unlock your Ledger wallet and open the Ethereum application',
        'Unlock ledger'
      )
    } else if (error.message.includes('MULTIPLE_OPEN_CONNECTIONS_DISALLOWED')) {
      return fbt(
        'Unexpected error occurred. Please refresh page and try again.',
        'Unexpected login error'
      )
    }
    return error.message
  }

  const options = [
    {
      display: `Ledger Live`,
      path: LEDGER_LIVE_BASE_PATH,
    },
    {
      display: `Legacy`,
      path: LEDGER_LEGACY_BASE_PATH,
    },
    {
      display: `Ethereum`,
      path: LEDGER_OTHER,
    },
  ]

  const loadBalances = async (path) => {
    if (!(ledgerConnector.provider && addresses[path])) {
      return
    }

    const [balances, stableBalances] = await Promise.all([
      Promise.all(
        addresses[path].map((a) =>
          ledgerConnector
            .getBalance(a)
            .then((r) => (Number(r) / 10 ** 18).toFixed(2))
        )
      ),
      Promise.all(
        addresses[path].map((a) => {
          return Promise.all(
            contractData.map((c) =>
              c.contract.balanceOf(a).then((r) => Number(r) / 10 ** c.decimals)
            )
          )
        })
      ),
    ])

    const ethTotal = balances
      .map((balance) => Number(balance))
      .reduce((a, b) => a + b, 0)

    const stableTotals = stableBalances.map((balance) => {
      return balance.reduce((a, b) => a + b, 0).toFixed(2)
    })

    setAddressBalances({
      ...addressBalances,
      [path]: zipObject(addresses[path], balances),
    })
    setAddressStableBalances({
      ...addressStableBalances,
      [path]: zipObject(addresses[path], stableTotals),
    })
    setPathTotals({ ...pathTotals, [path]: ethTotal })

    // preload addresses for each path
    if (!ledgerPath[LEDGER_LIVE_BASE_PATH]) {
      setLedgerPath({ ...ledgerPath, [LEDGER_LIVE_BASE_PATH]: true })
      onSelectDerivationPath(LEDGER_LIVE_BASE_PATH)
    } else if (!ledgerPath[LEDGER_LEGACY_BASE_PATH]) {
      setLedgerPath({ ...ledgerPath, [LEDGER_LEGACY_BASE_PATH]: true })
      onSelectDerivationPath(LEDGER_LEGACY_BASE_PATH)
    } else if (!ledgerPath[LEDGER_OTHER]) {
      setLedgerPath({ ...ledgerPath, [LEDGER_OTHER]: true })
      onSelectDerivationPath(LEDGER_OTHER)
    } else if (!activePath) {
      // autoselect first path with non-zero ETH balance
      if (pathTotals[LEDGER_LIVE_BASE_PATH] > 0) {
        setActivePath(LEDGER_LIVE_BASE_PATH)
      } else if (pathTotals[LEDGER_LEGACY_BASE_PATH] > 0) {
        setActivePath(LEDGER_LEGACY_BASE_PATH)
      } else if (ethTotal > 0) {
        setActivePath(LEDGER_OTHER)
      } else setActivePath(LEDGER_LIVE_BASE_PATH)
    }
    setPreloaded(
      ledgerPath[LEDGER_LIVE_BASE_PATH] &&
        ledgerPath[LEDGER_LEGACY_BASE_PATH] &&
        ledgerPath[LEDGER_OTHER]
    )

    // indicators for scrolling to next address page within path
    setNext({
      [activePath]: nextLoading[activePath] ? !next[activePath] : false,
    })
    setNextLoading({ [activePath]: false })
  }

  useEffect(() => {
    if (ready) loadBalances(LEDGER_LIVE_BASE_PATH)
  }, [addresses[LEDGER_LIVE_BASE_PATH]])

  useEffect(() => {
    if (ready) loadBalances(LEDGER_LEGACY_BASE_PATH)
  }, [addresses[LEDGER_LEGACY_BASE_PATH]])

  useEffect(() => {
    if (ready) loadBalances(LEDGER_OTHER)
  }, [addresses[LEDGER_OTHER]])

  const loadAddresses = async (path, next) => {
    if (!ledgerConnector.provider) {
      return
    }

    setLedgerPath({ ...ledgerPath, [path]: true })
    if (next) {
      setAddresses({
        ...addresses,
        [path]: (await ledgerConnector.getAccounts(10)).slice(5),
      })
    } else {
      setAddresses({
        ...addresses,
        [path]: await ledgerConnector.getAccounts(5),
      })
    }
  }

  useEffect(() => {
    onSelectDerivationPath(LEDGER_LIVE_BASE_PATH)
    setReady(true)
  }, [])

  const onSelectDerivationPath = async (path, next) => {
    try {
      await ledgerConnector.activate()
      await ledgerConnector.setPath(path)
      setDisplayError(null)
    } catch (error) {
      setDisplayError(errorMessageMap(error))
      return
    }
    loadAddresses(path, next)
  }

  return (
    <>
      <div
        onClick={(e) => {
          e.stopPropagation()
        }}
        className={`ledger-derivation-content d-flex flex-column`}
      >
        <h2>
          {fbt(
            'Select a Ledger derivation path',
            'Select a Ledger derivation path'
          )}
        </h2>
        <div className={`paths d-flex flex-row`}>
          {options.map((option) => {
            return (
              <button
                key={option.path}
                className={
                  'text-center ' + (activePath === option.path && 'active')
                }
                onClick={() => {
                  if (!nextLoading[option.path]) {
                    setActivePath(option.path)
                    if (next[activePath]) {
                      // reset path to first address page in the background after clicking different path
                      setNextLoading({ [activePath]: true })
                      onSelectDerivationPath(activePath)
                    }
                  }
                }}
              >
                {option.display}
                <br />
                <span className="button-path">{`m/${option.path}`}</span>
              </button>
            )
          })}
        </div>
        {displayError && (
          <div className="error d-flex align-items-center justify-content-center">
            {displayError}
          </div>
        )}
        <div className="d-flex flex-column align-items-center justify-content-center">
          {activePath && preloaded ? (
            <>
              {nextLoading[activePath] && (
                <img
                  className="waiting-icon rotating mx-auto"
                  src={assetRootPath('/images/spinner-green-small.png')}
                />
              )}
              {!nextLoading[activePath] && (
                <LedgerAccountContent
                  addresses={addresses[activePath]}
                  addressBalances={addressBalances[activePath]}
                  addressStableBalances={addressStableBalances[activePath]}
                  activePath={activePath}
                />
              )}
              {!next[activePath] && !nextLoading[activePath] && (
                <button
                  className="button-arrow"
                  onClick={() => {
                    setNextLoading({ [activePath]: true })
                    onSelectDerivationPath(activePath, true)
                  }}
                >
                  <img
                    className="arrow-icon"
                    src={assetRootPath('/images/arrow-down.png')}
                  />
                </button>
              )}
              {next[activePath] && !nextLoading[activePath] && (
                <button
                  className="button-arrow"
                  onClick={() => {
                    setNextLoading({ [activePath]: true })
                    onSelectDerivationPath(activePath)
                  }}
                >
                  <img
                    className="arrow-icon"
                    src={assetRootPath('/images/arrow-up.png')}
                  />
                </button>
              )}
            </>
          ) : (
            !displayError && (
              <img
                className="waiting-icon rotating mx-auto"
                src={assetRootPath('/images/spinner-green-small.png')}
              />
            )
          )}
        </div>
      </div>
      <style jsx>{`
        .ledger-derivation-content {
          padding: 26px 22px 20px 22px;
          max-width: 500px;
          min-width: 500px;
          box-shadow: 0 0 14px 0 rgba(24, 49, 64, 0.1);
          background-color: white;
          border-radius: 10px;
          align-items: center;
          justify-content: center;
        }

        .ledger-derivation-content h2 {
          padding-left: 12px;
          padding-right: 12px;
          font-size: 18px;
          font-weight: bold;
          text-align: center;
          line-height: normal;
          margin-bottom: 14px;
        }

        .ledger-derivation-content .paths {
          width: 100%;
          justify-content: center;
        }

        .ledger-derivation-content button {
          width: 100%;
          height: 55px;
          border-radius: 50px;
          border: solid 1px #1a82ff;
          background-color: white;
          font-size: 18px;
          font-weight: bold;
          text-align: center;
          color: #1a82ff;
          padding: 5px 10px;
          margin: 10px 5px 20px 5px;
          line-height: 22px;
        }

        .ledger-derivation-content .button-path {
          font-size: 14px;
          color: #a0a0a0;
        }

        .active {
          background-color: #c0e0ff !important;
        }

        .error {
          margin-top: 20px;
          padding: 5px 8px;
          font-size: 14px;
          line-height: 1.36;
          text-align: center;
          color: #ed2a28;
          border-radius: 5px;
          border: solid 1px #ed2a28;
          min-height: 50px;
          width: 100%;
        }

        .button-arrow {
          width: 70px !important;
          height: 35px !important;
          padding: 0 !important;
          margin: 10px 0 0 0 !important;
        }

        .arrow-icon {
          width: 25px;
          height: 25px;
        }

        .waiting-icon {
          width: 25px;
          height: 25px;
        }

        .rotating {
          -webkit-animation: spin 2s linear infinite;
          -moz-animation: spin 2s linear infinite;
          animation: spin 2s linear infinite;
        }

        @-moz-keyframes spin {
          100% {
            -moz-transform: rotate(360deg);
          }
        }
        @-webkit-keyframes spin {
          100% {
            -webkit-transform: rotate(360deg);
          }
        }
        @keyframes spin {
          100% {
            -webkit-transform: rotate(360deg);
            transform: rotate(360deg);
          }
        }
      `}</style>
    </>
  )
}
Example #4
Source File: BalanceHeader.js    From origin-dollar with MIT License 4 votes vote down vote up
BalanceHeader = ({
  storeTransaction,
  storeTransactionError,
  rpcProvider,
  isMobile,
}) => {
  const { connector, account } = useWeb3React()
  const DEFAULT_SELECTED_APY = 365
  const apyOptions = useStoreState(ContractStore, (s) =>
    apyDayOptions.map((d) => {
      return s.apy[`apy${d}`] || 0
    })
  )
  const daysToApy = zipObject(apyDayOptions, apyOptions)
  const [apyDays, setApyDays] = useState(
    process.browser && localStorage.getItem('last_user_selected_apy') !== null
      ? localStorage.getItem('last_user_selected_apy')
      : DEFAULT_SELECTED_APY
  )

  const vault = useStoreState(ContractStore, (s) => _get(s, 'contracts.vault'))
  const ousdContract = useStoreState(ContractStore, (s) =>
    _get(s, 'contracts.ousd')
  )
  const ousdBalance = useStoreState(AccountStore, (s) => s.balances['ousd'])
  const lifetimeYield = useStoreState(AccountStore, (s) => s.lifetimeYield)
  const ousdBalanceLoaded = typeof ousdBalance === 'string'
  const animatedOusdBalance = useStoreState(
    AnimatedOusdStore,
    (s) => s.animatedOusdBalance
  )
  const mintAnimationLimit = 0.5
  const walletConnected = useStoreState(ContractStore, (s) => s.walletConnected)

  const [balanceEmphasised, setBalanceEmphasised] = useState(false)
  const prevOusdBalance = usePrevious(ousdBalance)
  const { animatedExpectedIncrease } = useExpectedYield()

  const normalOusdAnimation = (from, to) => {
    setBalanceEmphasised(true)
    return animateValue({
      from: parseFloat(from) || 0,
      to: parseFloat(to),
      callbackValue: (val) => {
        AnimatedOusdStore.update((s) => {
          s.animatedOusdBalance = val
        })
      },
      onCompleteCallback: () => {
        setBalanceEmphasised(false)
      },
      // non even duration number so more of the decimals in ousdBalance animate
      duration: 1985,
      id: 'header-balance-ousd-animation',
      stepTime: 30,
    })
  }

  useEffect(() => {
    if (ousdBalanceLoaded) {
      const ousdBalanceNum = parseFloat(ousdBalance)
      const prevOusdBalanceNum = parseFloat(prevOusdBalance)
      // user must have minted the OUSD
      if (
        !isNaN(parseFloat(ousdBalanceNum)) &&
        !isNaN(parseFloat(prevOusdBalanceNum)) &&
        Math.abs(ousdBalanceNum - prevOusdBalanceNum) > mintAnimationLimit
      ) {
        normalOusdAnimation(prevOusdBalance, ousdBalance)
      } else if (
        !isNaN(parseFloat(ousdBalanceNum)) &&
        ousdBalanceNum > mintAnimationLimit
      ) {
        normalOusdAnimation(0, ousdBalance)
      } else {
        normalOusdAnimation(prevOusdBalance, 0)
      }
    }
  }, [ousdBalance])

  /*
   * Type: number or percentage
   */
  const Statistic = ({
    dropdown,
    title,
    value,
    type,
    titleLink,
    marginBottom = false,
  }) => {
    return (
      <>
        <div
          className={`d-flex holder flex-row flex-md-column align-items-end align-items-md-start justify-content-start ${
            marginBottom ? 'margin-bottom' : ''
          }`}
        >
          <div className={`value ${type}`}>{value}</div>
          <div className="flex-row">
            <span className="dropdown">{dropdown}</span>
            {titleLink && (
              <a
                className={`title link ${type}`}
                href={adjustLinkHref(titleLink)}
                rel="noopener noreferrer"
                target="blank"
              >
                {title}
              </a>
            )}
            {!titleLink && <div className="title">{title}</div>}
          </div>
        </div>
        <style jsx>{`
          .dropdown {
            display: inline-block;
          }

          .title {
            color: #8293a4;
            font-size: 14px;
            display: inline;
          }

          .title.link {
            cursor: pointer;
            text-decoration: underline;
          }

          .value {
            color: white;
            font-size: 28px;
          }

          .value.percentage::after {
            content: '%';
            padding-left: 2px;
          }

          @media (max-width: 767px) {
            .dropdown {
              padding-bottom: 10px;
            }

            .title {
              width: 55%;
              text-align: left;
              margin-bottom: 3px;
            }

            .title.percentage {
              margin-bottom: 10px;
            }

            .holder {
              width: 100%;
            }

            .value.percentage {
              font-size: 32px;
            }

            .value {
              color: white;
              font-size: 20px;
              width: 45%;
              text-align: left;
            }

            .margin-bottom {
              margin-bottom: 20px;
            }
          }
        `}</style>
      </>
    )
  }

  const displayedBalance = formatCurrency(animatedOusdBalance || 0, 2)

  useEffect(() => {
    localStorage.setItem('last_user_selected_apy', apyDays)
  }, [apyDays])

  const ApySelect = () => {
    const [open, setOpen] = useState(false)
    return (
      <>
        <Dropdown
          content={
            <div className="dropdown-menu d-flex flex-column">
              {apyDayOptions.map((days) => {
                return (
                  <div
                    key={days}
                    className="dropdown-item justify-content-start align-items-center"
                    onClick={() => {
                      setApyDays(days)
                      setOpen(false)
                    }}
                  >
                    {`${days}d`}
                  </div>
                )
              })}
            </div>
          }
          open={open}
          onClose={() => setOpen(false)}
        >
          <div
            className="apy-select d-flex flex-row align-items-center"
            onClick={() => setOpen(!open)}
          >
            {`${apyDays}d`}
            <span className="downcaret">
              <DownCaret color="black" size="26" />
            </span>
          </div>
        </Dropdown>
        <style jsx>{`
          .apy-select {
            background-color: white;
            font-size: 16px;
            font-weight: 500;
            color: black;
            width: 68px;
            height: 25px;
            padding: 0 22px 2px 8px;
            margin-right: 8px;
            border-radius: 20px;
            cursor: pointer;
          }

          .apy-select:hover {
            background-color: #f2f3f5;
          }

          .dropdown-menu {
            margin-right: 200px;
            background-color: white;
            font-size: 16px;
            color: black;
            min-width: 90px;
            top: 100%;
            left: 0;
            padding: 5px;
          }

          .dropdown-item {
            background-color: white;
            color: black;
            padding: 3px 5px 3px 10px;
            line-height: 20px;
            cursor: pointer;
          }

          .dropdown-item:hover {
            background-color: #f2f3f5;
          }

          .downcaret {
            position: absolute;
            left: 42px;
          }
        `}</style>
      </>
    )
  }
  return (
    <>
      <div className="balance-header d-flex flex-column justify-content-start">
        <div className="d-flex flex-column flex-md-row balance-holder justify-content-start w-100">
          <div className="apy-container d-flex justify-content-center">
            <div
              className={`contents d-flex align-items-center justify-content-center box box-black ${
                isMobile ? 'w-50' : ''
              }`}
            >
              <Statistic
                dropdown={<ApySelect />}
                title={fbt('Trailing APY', 'Trailing APY')}
                titleLink="https://analytics.ousd.com/apy"
                value={
                  typeof daysToApy[apyDays] === 'number'
                    ? formatCurrency(daysToApy[apyDays] * 100, 2)
                    : '--.--'
                }
                type={
                  typeof daysToApy[apyDays] === 'number' ? 'percentage' : ''
                }
              />
            </div>
          </div>
          <div className="d-flex flex-column flex-md-row align-items-center justify-content-between box box-narrow w-100">
            <Statistic
              title={fbt('Balance', 'OUSD Balance')}
              value={
                !isNaN(parseFloat(displayedBalance)) && ousdBalanceLoaded
                  ? displayedBalance
                  : '--.--'
              }
              type={'number'}
              marginBottom={true}
            />
            <Statistic
              title={fbt('Pending yield', 'Pending yield')}
              value={
                walletConnected
                  ? formatCurrency(animatedExpectedIncrease, 2)
                  : '--.--'
              }
              type={'number'}
              marginBottom={true}
            />
            <Statistic
              title={fbt(
                'Lifetime earnings',
                'Lifetime OUSD balance header earnings'
              )}
              titleLink={
                account
                  ? `${
                      process.env.ANALYTICS_ENDPOINT
                    }/address/${account.toLowerCase()}`
                  : false
              }
              value={
                walletConnected && lifetimeYield
                  ? formatCurrency(lifetimeYield, 2)
                  : '--.--'
              }
              type={'number'}
            />
          </div>
        </div>
      </div>
      <style jsx>{`
        .balance-header {
          margin-bottom: 19px;
        }

        .balance-header .inaccurate-balance {
          border: 2px solid #ed2a28;
          border-radius: 5px;
          color: #ed2a28;
          margin-bottom: 40px;
          padding: 15px;
        }

        .balance-header .inaccurate-balance a {
          text-decoration: underline;
        }

        .balance-header .light-grey-label {
          font-size: 14px;
          font-weight: bold;
          color: #8293a4;
        }

        .balance-header .detail {
          font-size: 12px;
          color: #8293a4;
        }

        .balance-header a:hover {
          color: white;
        }

        .balance-header .ousd-value {
          font-size: 14px;
          color: white;
          text-align: left;
          text-overflow: ellipsis;
          transition: font-size 0.2s cubic-bezier(0.5, -0.5, 0.5, 1.5),
            color 0.2s cubic-bezier(0.5, -0.5, 0.5, 1.5);
          position: relative;
          margin-left: 11px;
        }

        .balance-header .ousd-value.big {
          color: #00d592;
        }

        .balance-header .apy-container {
          height: 100%;
        }

        .balance-header .apy-container .contents {
          z-index: 2;
        }

        .balance-header .apy-container .apy-percentage {
          font-size: 14px;
          color: #ffffff;
          font-weight: bold;
          margin-left: 8px;
        }

        .balance-header .apy-container .apy-percentage::after {
          content: '%';
          font-weight: bold;
          padding-left: 2px;
        }

        .balance-header .expected-increase {
          font-size: 12px;
          color: #8293a4;
        }

        .balance-header .expected-increase p {
          margin: auto;
        }

        .balance-header .expected-increase .dropdown {
          justify-content: center !important;
        }

        .balance-header .expected-increase .dropdown .disclaimer-tooltip {
          display: flex !important;
        }

        .balance-header .expected-increase .collect {
          color: #1a82ff;
          cursor: pointer;
        }

        .box {
          padding: 30px;
          min-width: 210px;
          min-height: 118px;
          border-radius: 10px;
          box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
          border: solid 1px black;
          color: white;
        }

        .box-narrow {
          padding: 30px 50px;
        }

        .box.box-black {
          background-color: black;
          margin-right: 10px;
          min-width: 230px;
        }

        @media (max-width: 767px) {
          .balance-header {
            align-items: center;
            text-align: center;
            padding: 0px 20px;
            min-height: 80px;
          }

          .apy-container {
            margin-bottom: 10px;
          }

          .balance-header .gradient-border {
            width: 100px;
            height: 100px;
            margin-right: 20px;
            padding-right: 20px;
          }

          .box {
            padding: 20px;
            min-width: auto;
            min-height: 90px;
          }

          .box.box-black {
            min-width: 100%;
            margin-right: 0px;
          }

          .balance-header .ousd-value.mio-club {
            font-size: 20px;
          }

          .balance-header .ousd-value .grey {
            color: #8293a4;
          }

          .balance-header .ousd-value-holder {
            white-space: nowrap;
          }

          .balance-header .apy-container .apy-label {
            font-family: Lato;
            font-size: 11px;
            font-weight: bold;
            text-align: center;
            color: #8293a4;
          }

          .balance-header .apy-container .apy-percentage {
            font-family: Lato;
            font-weight: normal;
          }

          .balance-header .ousd-value::after {
            content: '';
          }

          .balance-header .light-grey-label {
            font-family: Lato;
            font-size: 11px;
            font-weight: bold;
            color: #8293a4;
            margin-bottom: -2px;
          }

          .balance-holder {
            width: 100%;
          }
        }
      `}</style>
    </>
  )
}
Example #5
Source File: BalanceHeaderWrapped.js    From origin-dollar with MIT License 4 votes vote down vote up
BalanceHeaderWrapped = ({
  storeTransaction,
  storeTransactionError,
  rpcProvider,
  isMobile,
}) => {
  const DEFAULT_SELECTED_APY = 365
  const apyOptions = useStoreState(ContractStore, (s) =>
    apyDayOptions.map((d) => {
      return s.apy[`apy${d}`] || 0
    })
  )
  const daysToApy = zipObject(apyDayOptions, apyOptions)
  const [apyDays, setApyDays] = useState(
    process.browser && localStorage.getItem('last_user_selected_apy') !== null
      ? localStorage.getItem('last_user_selected_apy')
      : DEFAULT_SELECTED_APY
  )

  const walletConnected = useStoreState(ContractStore, (s) => s.walletConnected)
  const { animatedExpectedIncrease } = useExpectedYield(true)

  const wousdBalance = useStoreState(AccountStore, (s) => s.balances['wousd'])
  const wousdBalanceLoaded = typeof wousdBalance === 'string'
  const wousdValue = useStoreState(AccountStore, (s) => s.wousdValue)

  /*
   * Type: number or percentage
   */
  const Statistic = ({
    dropdown,
    title,
    value,
    type,
    titleLink,
    marginBottom = false,
  }) => {
    return (
      <>
        <div
          className={`d-flex holder flex-row flex-md-column align-items-end align-items-md-start justify-content-start ${
            marginBottom ? 'margin-bottom' : ''
          }`}
        >
          <div className={`value ${type}`}>{value}</div>
          <div className="flex-row">
            <span className="dropdown">{dropdown}</span>
            {titleLink && (
              <a
                className={`title link ${type}`}
                href={adjustLinkHref(titleLink)}
                rel="noopener noreferrer"
                target="blank"
              >
                {title}
              </a>
            )}
            {!titleLink && <div className="title">{title}</div>}
          </div>
        </div>
        <style jsx>{`
          .dropdown {
            display: inline-block;
          }

          .title {
            color: #8293a4;
            font-size: 14px;
            display: inline;
          }

          .title.link {
            cursor: pointer;
            text-decoration: underline;
          }

          .value {
            color: white;
            font-size: 28px;
          }

          .value.percentage::after {
            content: '%';
            padding-left: 2px;
          }

          @media (max-width: 767px) {
            .title {
              width: 55%;
              text-align: left;
              margin-bottom: 3px;
            }

            .title.percentage {
              margin-bottom: 10px;
            }

            .holder {
              width: 100%;
            }

            .value.percentage {
              font-size: 32px;
            }

            .value {
              color: white;
              font-size: 20px;
              width: 45%;
              text-align: left;
            }

            .margin-bottom {
              margin-bottom: 20px;
            }
          }
        `}</style>
      </>
    )
  }

  const displayedWousdBalance = formatCurrency(wousdBalance || 0, 2)

  useEffect(() => {
    localStorage.setItem('last_user_selected_apy', apyDays)
  }, [apyDays])

  const ApySelect = () => {
    const [open, setOpen] = useState(false)
    return (
      <>
        <Dropdown
          content={
            <div className="dropdown-menu d-flex flex-column">
              {apyDayOptions.map((days) => {
                return (
                  <div
                    key={days}
                    className="dropdown-item justify-content-start align-items-center"
                    onClick={() => {
                      setApyDays(days)
                      setOpen(false)
                    }}
                  >
                    {`${days}d`}
                  </div>
                )
              })}
            </div>
          }
          open={open}
          onClose={() => setOpen(false)}
        >
          <div
            className="apy-select d-flex flex-row align-items-center"
            onClick={() => setOpen(!open)}
          >
            {`${apyDays}d`}
            <span className="downcaret">
              <DownCaret color="black" size="26" />
            </span>
          </div>
        </Dropdown>
        <style jsx>{`
          .apy-select {
            background-color: white;
            font-size: 16px;
            font-weight: 500;
            color: black;
            width: 68px;
            height: 25px;
            padding: 0 22px 2px 8px;
            margin-right: 8px;
            border-radius: 20px;
            cursor: pointer;
          }

          .apy-select:hover {
            background-color: #f2f3f5;
          }

          .dropdown-menu {
            margin-right: 200px;
            background-color: white;
            font-size: 16px;
            color: black;
            min-width: 90px;
            top: 100%;
            left: 0;
            padding: 5px;
          }

          .dropdown-item {
            background-color: white;
            color: black;
            padding: 3px 5px 3px 10px;
            line-height: 20px;
            cursor: pointer;
          }

          .dropdown-item:hover {
            background-color: #f2f3f5;
          }

          .downcaret {
            position: absolute;
            left: 42px;
          }
        `}</style>
      </>
    )
  }

  return (
    <>
      <div className="balance-header d-flex flex-column justify-content-start">
        <div className="d-flex flex-column flex-md-row balance-holder justify-content-start w-100">
          <div className="apy-container d-flex justify-content-center">
            <div
              className={`contents d-flex align-items-center justify-content-center box box-black ${
                isMobile ? 'w-50' : ''
              }`}
            >
              <Statistic
                dropdown={<ApySelect />}
                title={fbt('Trailing APY', 'Trailing APY')}
                titleLink="https://analytics.ousd.com/apy"
                value={
                  typeof daysToApy[apyDays] === 'number'
                    ? formatCurrency(daysToApy[apyDays] * 100, 2)
                    : '--.--'
                }
                type={
                  typeof daysToApy[apyDays] === 'number' ? 'percentage' : ''
                }
              />
            </div>
          </div>
          <div className="d-flex flex-column flex-md-row align-items-center justify-content-between box box-narrow w-100">
            <Statistic
              title={fbt('wOUSD Balance', 'wOUSD Balance')}
              value={
                !isNaN(parseFloat(displayedWousdBalance)) && wousdBalanceLoaded
                  ? displayedWousdBalance
                  : '--.--'
              }
              type={'number'}
              marginBottom={true}
            />
            <Statistic
              title={fbt('Current Value (OUSD)', 'Current Value (OUSD)')}
              value={
                walletConnected && !isNaN(wousdValue)
                  ? formatCurrency(wousdValue, 2)
                  : '--.--'
              }
              type={'number'}
              marginBottom={true}
            />
            <Statistic
              title={fbt('Pending yield (OUSD)', 'Pending yield (OUSD)')}
              value={
                walletConnected
                  ? formatCurrency(animatedExpectedIncrease, 2)
                  : '--.--'
              }
              type={'number'}
            />
          </div>
        </div>
      </div>
      <style jsx>{`
        .balance-header {
          margin-bottom: 19px;
        }

        .balance-header .inaccurate-balance {
          border: 2px solid #ed2a28;
          border-radius: 5px;
          color: #ed2a28;
          margin-bottom: 40px;
          padding: 15px;
        }

        .balance-header .inaccurate-balance a {
          text-decoration: underline;
        }

        .balance-header .light-grey-label {
          font-size: 14px;
          font-weight: bold;
          color: #8293a4;
        }

        .balance-header .detail {
          font-size: 12px;
          color: #8293a4;
        }

        .balance-header a:hover {
          color: white;
        }

        .balance-header .ousd-value {
          font-size: 14px;
          color: white;
          text-align: left;
          text-overflow: ellipsis;
          transition: font-size 0.2s cubic-bezier(0.5, -0.5, 0.5, 1.5),
            color 0.2s cubic-bezier(0.5, -0.5, 0.5, 1.5);
          position: relative;
          margin-left: 11px;
        }

        .balance-header .ousd-value.big {
          color: #00d592;
        }

        .balance-header .apy-container {
          height: 100%;
        }

        .balance-header .apy-container .contents {
          z-index: 2;
        }

        .balance-header .apy-container .apy-percentage {
          font-size: 14px;
          color: #ffffff;
          font-weight: bold;
          margin-left: 8px;
        }

        .balance-header .apy-container .apy-percentage::after {
          content: '%';
          font-weight: bold;
          padding-left: 2px;
        }

        .balance-header .expected-increase {
          font-size: 12px;
          color: #8293a4;
        }

        .balance-header .expected-increase p {
          margin: auto;
        }

        .balance-header .expected-increase .dropdown {
          justify-content: center !important;
        }

        .balance-header .expected-increase .dropdown .disclaimer-tooltip {
          display: flex !important;
        }

        .balance-header .expected-increase .collect {
          color: #1a82ff;
          cursor: pointer;
        }

        .box {
          padding: 30px;
          min-width: 210px;
          min-height: 118px;
          border-radius: 10px;
          box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.1);
          border: solid 1px black;
          color: white;
        }

        .box-narrow {
          padding: 30px 50px;
        }

        .box.box-black {
          background-color: black;
          margin-right: 10px;
          min-width: 230px;
        }

        @media (max-width: 767px) {
          .balance-header {
            align-items: center;
            text-align: center;
            padding: 0px 20px;
            min-height: 80px;
          }

          .apy-container {
            margin-bottom: 10px;
          }

          .balance-header .gradient-border {
            width: 100px;
            height: 100px;
            margin-right: 20px;
            padding-right: 20px;
          }

          .box {
            padding: 20px;
            min-width: auto;
            min-height: 90px;
          }

          .box.box-black {
            min-width: 100%;
            margin-right: 0px;
          }

          .balance-header .ousd-value.mio-club {
            font-size: 20px;
          }

          .balance-header .ousd-value .grey {
            color: #8293a4;
          }

          .balance-header .ousd-value-holder {
            white-space: nowrap;
          }

          .balance-header .apy-container .apy-label {
            font-family: Lato;
            font-size: 11px;
            font-weight: bold;
            text-align: center;
            color: #8293a4;
          }

          .balance-header .apy-container .apy-percentage {
            font-family: Lato;
            font-weight: normal;
          }

          .balance-header .ousd-value::after {
            content: '';
          }

          .balance-header .light-grey-label {
            font-family: Lato;
            font-size: 11px;
            font-weight: bold;
            color: #8293a4;
            margin-bottom: -2px;
          }

          .balance-holder {
            width: 100%;
          }
        }
      `}</style>
    </>
  )
}
Example #6
Source File: Canvas.js    From hivemind with Apache License 2.0 4 votes vote down vote up
Canvas = ({ data, timestamp, events }) => {
  const { cyWrapper, poppers } = useContext(GlobalContext)
  const [output, setOutput] = useState(null)
  const [els, setEls] = useState([])
  const prevEls = usePrevious(els)

  useEffect(() => {
    if (cyWrapper.cy && prevEls !== els) {
      const commonEls = intersectionBy(prevEls, els, 'data.id')
      const celMap = zipObject(map(commonEls, 'data.id'), commonEls)

      cyWrapper.cy
        .elements()
        .filter((el) => celMap[el.id()])
        .forEach((el) => {
          el.removeData('summary content audio lastUpdatedBy')
          el.data(celMap[el.id()].data)
        })
    }
  }, [cyWrapper.cy, els, prevEls])

  useEffect(() => {
    if (get(data, 'ok') && typeof window !== 'undefined') {
      setEls(CytoscapeComponent.normalizeElements(data.data.elements))
    }
  }, [data])

  useEffect(() => {
    function initCy(cyInternal) {
      cyWrapper.cy = cyInternal

      cyInternal.nodes().forEach((node) => {
        node.scratch('style', node.style())
      })
    }

    const nodes = els.filter((el) => !el.data.id.startsWith('links'))
    const fit = shouldFit(nodes)
    const options = getOptions(fit)

    setOutput(
      <CytoscapeComponent
        cy={initCy}
        style={{ width: '100%', height: '100%' }}
        stylesheet={style}
        layout={options}
        elements={els}
      />
    )
  }, [cyWrapper, els])

  useEffect(() => {
    function configurePlugins(access) {
      function buildMenu() {
        const { viewApi } = cyWrapper

        return function (node) {
          const menu = []

          view(menu, poppers)
          if (!node.data('isRoot')) {
            hide(menu, viewApi)
          }
          if (node.scratch('showReveal')) {
            reveal(menu, viewApi)
          }

          if (access && ['admin', 'write'].includes(access.access)) {
            add(menu, poppers)
            if (!node.data('isRoot')) {
              del(menu, poppers)
            }
            edit(menu, poppers)
          }

          return menu
        }
      }

      const { cy } = cyWrapper
      const minRadius = Math.min(cy.width(), cy.height()) / 8

      const viewOpts = {
        highlightStyles: [
          {
            node: { 'border-color': '#0b9bcd', 'border-width': 3 },
            edge: {
              'line-color': '#0b9bcd',
              'source-arrow-color': '#0b9bcd',
              'target-arrow-color': '#0b9bcd',
              width: 3,
            },
          },
          {
            node: { 'border-color': '#04f06a', 'border-width': 3 },
            edge: {
              'line-color': '#04f06a',
              'source-arrow-color': '#04f06a',
              'target-arrow-color': '#04f06a',
              width: 3,
            },
          },
        ],
        selectStyles: {
          node: {
            'border-color': 'white',
            'border-width': 3,
            'background-color': 'lightgrey',
          },
          edge: {
            'line-color': 'white',
            'source-arrow-color': 'white',
            'target-arrow-color': 'white',
            width: 3,
          },
        },
        setVisibilityOnHide: false, // whether to set visibility on hide/show
        setDisplayOnHide: true, // whether to set display on hide/show
        zoomAnimationDuration: 500, //default duration for zoom animation speed
        neighbor: function (node) {
          return node.successors()
        },
        neighborSelectTime: 500,
      }
      cyWrapper.viewApi = cy.viewUtilities(viewOpts)

      const cxtMenu = {
        menuRadius: minRadius + 50, // the radius of the circular menu in pixels
        selector: 'node', // elements matching this Cytoscape.js selector will trigger cxtmenus
        commands: buildMenu(), // function( ele ){ return [
        // /*...*/ ] }, // a function
        // that returns
        // commands or a promise of commands
        fillColor: 'rgba(0, 0, 0, 0.75)', // the background colour of the menu
        activeFillColor: 'rgba(100, 100, 100, 0.5)', // the colour used to indicate the selected
        // command
        activePadding: 10, // additional size in pixels for the active command
        indicatorSize: 16, // the size in pixels of the pointer to the active command
        separatorWidth: 3, // the empty spacing in pixels between successive commands
        spotlightPadding: 4, // extra spacing in pixels between the element and the spotlight
        minSpotlightRadius: minRadius - 40, // the minimum radius in pixels of the spotlight
        maxSpotlightRadius: minRadius - 20, // the maximum radius in pixels of the spotlight
        openMenuEvents: 'tap', // space-separated cytoscape events that will open the menu; only
        // `cxttapstart` and/or `taphold` work here
        itemColor: 'white', // the colour of text in the command's content
        itemTextShadowColor: 'transparent', // the text shadow colour of the command's content
        // zIndex: 9999, // the z-index of the ui div
        atMouse: false, // draw menu at mouse position
      }
      cyWrapper.menu = cy.cxtmenu(cxtMenu)
    }

    function setHandlers() {
      const { viewApi, cy } = cyWrapper

      cy.on(
        'boxend',
        throttle(() => defer(() => viewApi.zoomToSelected(cy.$(':selected')))),
        1000
      )

      cy.on('mouseover', 'node', () => {
        document.getElementById('cy').style.cursor = 'pointer'
      })

      cy.on('mouseout', 'node', () => {
        document.getElementById('cy').style.cursor = 'default'
      })

      cy.on('mouseover', 'edge', (e) => {
        e.target.style({
          width: 4,
          'line-color': '#007bff',
          'target-arrow-color': '#007bff',
        })
      })

      cy.on('unselect mouseout', 'edge', (e) => {
        const edge = e.target

        if (!edge.selected()) {
          edge.style({
            width: 2,
            'line-color': '#ccc',
            'target-arrow-color': '#ccc',
          })
        }
      })

      cy.on('add', 'node', (e) => {
        const node = e.target

        node.scratch('style', node.style())
      })

      cy.on(
        'add data remove',
        'node',
        throttle(() => {
          if (timestamp) {
            const idx = findIndex(events.data, { lctime: timestamp })
            const event = events.data[idx]
            const { viewApi } = cyWrapper

            viewApi.removeHighlights(cy.elements())
            if (event && event.event !== 'deleted') {
              const nid = event.nids[0]
              const node = cy.$id(nid)

              viewApi.highlight(node)
            }
          }
        }, 100)
      )

      cy.on('mouseover', 'node', (e) => {
        e.target.style('background-color', '#007bff')
      })

      cy.on('unselect mouseout', 'node', (e) => {
        const node = e.target

        viewApi.removeHighlights(node)

        if (!node.selected()) {
          node.style(
            'background-color',
            node.scratch('style')['background-color']
          )
        }
      })
    }

    if (cyWrapper.cy && get(data, 'ok') && get(events, 'ok')) {
      configurePlugins(data.data.access)
      setHandlers()
    }

    return () => {
      if (cyWrapper.menu) {
        cyWrapper.menu.destroy()
      }
    }
  }, [data, events, cyWrapper.menu, cyWrapper, timestamp, poppers, els])

  return (
    <div
      className={`border border-${
        timestamp ? 'secondary' : 'danger'
      } rounded w-100`}
      id="cy-container"
    >
      <div className="m-1" id="cy">
        {output}
      </div>
    </div>
  )
}
Example #7
Source File: index.js    From datapass with GNU Affero General Public License v3.0 4 votes vote down vote up
DonneesSection = ({
  DonneesDescription,
  availableScopes = [],
  AvailableScopesDescription,
  accessModes,
  enableFileSubmissionForScopeSelection = false,
}) => {
  const {
    disabled,
    onChange,
    enrollment: {
      scopes = {},
      data_recipients = '',
      data_retention_period = '',
      data_retention_comment = '',
      additional_content = {},
      documents = [],
      documents_attributes = [],
    },
  } = useContext(FormContext);

  useEffect(() => {
    if (!isEmpty(availableScopes) && isEmpty(scopes)) {
      onChange({
        target: {
          name: 'scopes',
          value: zipObject(
            availableScopes.map(({ value }) => value),
            availableScopes.map(
              ({ required, checkedByDefault }) =>
                !!required || !!checkedByDefault
            )
          ),
        },
      });
    }
  });

  const groupTitleScopesGroup = groupBy(
    availableScopes,
    (e) => e.groupTitle || 'default'
  );

  // {'a': true, 'b': false, 'c': true} becomes ['a', 'c']
  const scopesAsArray = chain(scopes)
    .omitBy((e) => !e)
    .keys()
    .value();
  const availableScopesAsArray = availableScopes.map(({ value }) => value);
  const outdatedScopes = difference(scopesAsArray, availableScopesAsArray);

  const [isFileInputExpanded, setFileInputExpanded] = useState(
    enableFileSubmissionForScopeSelection &&
      !isEmpty(
        documents.filter(
          ({ type }) => type === 'Document::ExpressionBesoinSpecifique'
        )
      )
  );

  useEffect(() => {
    const hasDocument = !isEmpty(
      documents.filter(
        ({ type }) => type === 'Document::ExpressionBesoinSpecifique'
      )
    );
    if (
      enableFileSubmissionForScopeSelection &&
      !isFileInputExpanded &&
      hasDocument
    ) {
      setFileInputExpanded(true);
    }
  }, [enableFileSubmissionForScopeSelection, isFileInputExpanded, documents]);

  return (
    <ScrollablePanel scrollableId={SECTION_ID}>
      <h2>Les données nécessaires</h2>
      {DonneesDescription && (
        <ExpandableQuote title="Comment choisir les données ?">
          <DonneesDescription />
        </ExpandableQuote>
      )}
      {AvailableScopesDescription && <AvailableScopesDescription />}
      {!isEmpty(availableScopes) && (
        <>
          <h3>À quelles données souhaitez-vous avoir accès ?</h3>
          {Object.keys(groupTitleScopesGroup).map((group) => (
            <Scopes
              key={group}
              title={group === 'default' ? null : group}
              scopes={groupTitleScopesGroup[group]}
              selectedScopes={scopes}
              disabledApplication={disabled}
              handleChange={onChange}
            />
          ))}
          {disabled && !isEmpty(outdatedScopes) && (
            <Scopes
              title="Les données suivantes ont été sélectionnées mais ne sont plus disponibles :"
              scopes={outdatedScopes.map((value) => ({ value, label: value }))}
              selectedScopes={zipObject(
                outdatedScopes,
                Array(outdatedScopes.length).fill(true)
              )}
              disabledApplication
              handleChange={() => null}
            />
          )}
        </>
      )}
      {enableFileSubmissionForScopeSelection && (
        <>
          <ExpandableQuote title="J’ai une expression de besoin spécifique ?">
            <p>
              Les partenaires ayant convenu avec la DGFiP un périmètre de
              données particulier peuvent rattacher leur expression de besoin
              listant l’ensemble des données strictement nécessaires à leur cas
              d’usage. Si vous n'avez pas encore contacté la DGFiP, vous pouvez
              les joindre à l'adresse{' '}
              <Link
                inline
                href="mailto:[email protected]?subject=Expression%20de%20besoin%20spécifique"
              >
                [email protected]
              </Link>
            </p>
            <CheckboxInput
              label="J’ai une expression de besoin spécifique"
              value={isFileInputExpanded}
              onChange={() => setFileInputExpanded(!isFileInputExpanded)}
              disabled={disabled}
            />
          </ExpandableQuote>
          {isFileInputExpanded && (
            <>
              <FileInput
                label="Joindre l’expression de besoin"
                meta={
                  'Attention : seule l’expression de besoin en données ayant déjà été partagée avec la DGFiP peut être rattachée à votre habilitation.'
                }
                mimeTypes="*"
                disabled={disabled}
                uploadedDocuments={documents}
                documentsToUpload={documents_attributes}
                documentType={'Document::ExpressionBesoinSpecifique'}
                onChange={onChange}
              />
            </>
          )}
        </>
      )}
      {!isEmpty(accessModes) && (
        <>
          <h3>Comment souhaitez-vous y accéder ?</h3>
          {accessModes.map(({ id, label }) => (
            <CheckboxInput
              key={id}
              label={label}
              name={`additional_content.${id}`}
              value={additional_content[id] || false}
              disabled={disabled}
              onChange={onChange}
            />
          ))}
        </>
      )}
      <h3>Comment seront traitées ces données personnelles ?</h3>
      <TextInput
        label="Destinataires des données"
        placeholder={
          '« agents instructeurs des demandes d’aides », « usagers des ' +
          'services publics de la ville », etc.'
        }
        meta={
          <Link
            inline
            newTab
            href="https://www.cnil.fr/fr/definition/destinataire"
            aria-label="Voir la définition CNIL du destinataire des données"
          >
            Plus d’infos
          </Link>
        }
        name="data_recipients"
        value={data_recipients}
        disabled={disabled}
        onChange={onChange}
        required
      />
      <NumberInput
        label="Durée de conservation des données en mois"
        meta={
          <Link
            inline
            newTab
            href="https://www.cnil.fr/fr/les-durees-de-conservation-des-donnees"
            aria-label="Voir l’explication CNIL sur les durées de conservation des données"
          >
            Plus d’infos
          </Link>
        }
        name="data_retention_period"
        value={data_retention_period}
        disabled={disabled}
        onChange={onChange}
        required
      />
      {data_retention_period > 36 && (
        <>
          <Alert type="warning" title="Attention">
            Cette durée excède la durée communément constatée (36 mois).
          </Alert>
          <TextInput
            label="Veuillez justifier cette durée dans le champ ci-après :"
            name="data_retention_comment"
            value={data_retention_comment}
            disabled={disabled}
            onChange={onChange}
          />
        </>
      )}
    </ScrollablePanel>
  );
}