date-fns#add TypeScript Examples

The following examples show how to use date-fns#add. 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: wallets-assets.service.ts    From xBull-Wallet with GNU Affero General Public License v3.0 6 votes vote down vote up
shouldRequestAssetInformation$ = this.requestAssetInformation$.asObservable()
    .pipe(switchMap(params => {
      return of(true)
        .pipe(map(_ => {
          if (params.asset._id === 'native') {
            return false;
          }
          if (params.forceUpdate) {
            return true;
          }

          if (!params.asset.lastTimeUpdated) {
            return true;
          }

          const lastUpdate = new Date(params.asset.lastTimeUpdated);
          // TODO: maybe we should make this time dynamic and configurable form the settings
          const nextUpdate = add(lastUpdate, { minutes: 15 });
          const now = new Date();
          return !params.asset.assetFullDataLoaded && isAfter(now, nextUpdate);
        }))
        .pipe(filter(Boolean))
        .pipe(map(_ => ({
          asset: params.asset as IWalletAssetModel,
          horizonApi: params.horizonApi,
        })));
    }))
    .pipe(map(({ asset, horizonApi}) => {
      return {
        _id: asset._id,
        assetIssuer: asset.assetIssuer,
        assetCode: asset.assetCode,
        horizonApi,
      };
    }));
Example #2
Source File: helpers.tsx    From vvs-ui with GNU General Public License v3.0 6 votes vote down vote up
getDateForBlock = async (currentBlock: number, block: number) => {
  const blocksUntilBlock = block - currentBlock
  const secondsUntilStart = blocksUntilBlock * CRONOS_BLOCK_TIME
  // if block already happened we can get timestamp via .getBlock(block)
  if (currentBlock > block) {
    try {
      const { timestamp } = await simpleRpcProvider.getBlock(block)
      return toDate(timestamp * 1000)
    } catch {
      add(new Date(), { seconds: secondsUntilStart })
    }
  }
  return add(new Date(), { seconds: secondsUntilStart })
}
Example #3
Source File: make-stat-on-interval-days-basis.ts    From merged-pr-stat with MIT License 6 votes vote down vote up
async function main(): Promise<void> {
  program
    .requiredOption("--start <date>")
    .requiredOption("--end <date>")
    .requiredOption("--interval-days <days>")
    .requiredOption("--query <query>");

  program.parse(process.argv);

  const startDate = parseISO(program.start);
  const endDate = parseISO(program.end);
  const query = program.query as string;
  const intervalDays = parseInt(program.intervalDays);

  const allStats = [];
  for (let start = startDate; start < endDate; start = addDays(start, intervalDays)) {
    const end = add(start, { days: intervalDays, seconds: -1 });
    console.error(format(start, "yyyy-MM-dd HH:mm:ss"));
    console.error(format(end, "yyyy-MM-dd HH:mm:ss"));

    const stdout = execFileSync(
      "merged-pr-stat",
      ["--start", start.toISOString(), "--end", end.toISOString(), "--query", query],
      { encoding: "utf8" }
    );
    const result = {
      startDate: format(start, "yyyy-MM-dd HH:mm:ss"),
      endDate: format(end, "yyyy-MM-dd HH:mm:ss"),
      ...JSON.parse(stdout),
    };
    allStats.push(result);
  }
  process.stdout.write(csvStringify(allStats, { header: true }));
}
Example #4
Source File: make-rotate-stat-on-interval-days-basis.ts    From merged-pr-stat with MIT License 6 votes vote down vote up
// Make stat between (<date> - <aggregationDays>) and <date>
// and move aggregation period by <intervalDays>.
async function main(): Promise<void> {
  program
    .requiredOption("--start <date>")
    .requiredOption("--end <date>")
    .requiredOption("--interval-days <days>")
    .requiredOption("--aggregation-days <days>")
    .requiredOption("--query <query>");

  program.parse(process.argv);

  const startDate = parseISO(program.start);
  const endDate = parseISO(program.end);
  const query = program.query as string;
  const intervalDays = parseInt(program.intervalDays);
  const aggregationDays = parseInt(program.aggregationDays);

  const allStats = [];
  for (let start = startDate; start < endDate; start = addDays(start, intervalDays)) {
    const aggregateFrom = add(start, { days: -aggregationDays });
    const aggregateTo = start;
    console.error(format(aggregateFrom, "yyyy-MM-dd HH:mm:ss"));
    console.error(format(aggregateTo, "yyyy-MM-dd HH:mm:ss"));

    const stdout = execFileSync(
      "merged-pr-stat",
      ["--start", aggregateFrom.toISOString(), "--end", aggregateTo.toISOString(), "--query", query],
      { encoding: "utf8" }
    );
    const result = {
      startDate: format(aggregateFrom, "yyyy-MM-dd HH:mm:ss"),
      endDate: format(aggregateTo, "yyyy-MM-dd HH:mm:ss"),
      ...JSON.parse(stdout),
    };
    allStats.push(result);
  }
  process.stdout.write(csvStringify(allStats, { header: true }));
}
Example #5
Source File: make-monthly-stat.ts    From merged-pr-stat with MIT License 6 votes vote down vote up
async function main(): Promise<void> {
  program.requiredOption("--start <yyyy/MM>").requiredOption("--end <yyyy/MM>").requiredOption("--query <query>");

  program.parse(process.argv);

  const startDate = parse(program.start, "yyyy/MM", new Date());
  const endDate = parse(program.end, "yyyy/MM", new Date());
  const query = program.query as string;

  const allStats = [];
  for (let start = startDate; start <= endDate; start = addMonths(start, 1)) {
    const end = add(start, { months: 1, seconds: -1 });
    console.error(format(start, "yyyy-MM-dd HH:mm:ss"));
    console.error(format(end, "yyyy-MM-dd HH:mm:ss"));

    const stdout = execFileSync(
      "merged-pr-stat",
      ["--start", start.toISOString(), "--end", end.toISOString(), "--query", query],
      { encoding: "utf8" }
    );
    const result = {
      startDate: format(start, "yyyy-MM-dd HH:mm:ss"),
      endDate: format(end, "yyyy-MM-dd HH:mm:ss"),
      ...JSON.parse(stdout),
    };
    allStats.push(result);
  }
  process.stdout.write(csvStringify(allStats, { header: true }));
}
Example #6
Source File: utils.tsx    From gio-design with Apache License 2.0 6 votes vote down vote up
getDefaultViewDates = (defaultDate: Date) =>
  [startOfDay(defaultDate), add(startOfDay(defaultDate), { months: 1 })] as [Date, Date]
Example #7
Source File: helpers.tsx    From glide-frontend with GNU General Public License v3.0 6 votes vote down vote up
getDateForBlock = async (currentBlock: number, block: number) => {
  const blocksUntilBlock = block - currentBlock
  const secondsUntilStart = blocksUntilBlock * ESC_BLOCK_TIME
  // if block already happened we can get timestamp via .getBlock(block)
  if (currentBlock > block) {
    try {
      const { timestamp } = await simpleRpcProvider.getBlock(block)
      return toDate(timestamp * 1000)
    } catch {
      add(new Date(), { seconds: secondsUntilStart })
    }
  }
  return add(new Date(), { seconds: secondsUntilStart })
}
Example #8
Source File: Account.ts    From remix-hexagonal-architecture with MIT License 6 votes vote down vote up
export function generateResetPasswordToken(
  account: VerifiedAccount,
  generateId: GenerateId,
  clock: Clock
): AccountForgotPassword {
  return {
    type: "forgot-password",
    id: account.id,
    email: account.email,
    passwordResetToken: generateId.generate(),
    passwordResetExpiration: add(clock.now(), { days: 7 }),
  };
}
Example #9
Source File: leveling.ts    From aero-bot with MIT License 5 votes vote down vote up
/**
 * Adds the specified amount of XP to the member.
 * @param guildId the ID of the guild the member is in
 * @param userId the ID of the member to add XP to
 * @param xpToAdd the number of XP to add
 * @param message the message that triggered this
 */
export async function addXP(
    guildId: string,
    userId: string,
    xpToAdd: number,
    message: Message
) {
    const result = (await members.findOneAndUpdate(
        {
            guildId,
            userId,
        },
        {
            guildId,
            userId,
            $inc: {
                xp: xpToAdd,
            },
            nextXPAdd: add(Date.now(), { minutes: 1 }),
        },
        {
            upsert: true,
            new: true,
            setDefaultsOnInsert: true,
        }
    )) as IMemberInfo;

    let needed = getNeededXP(result.level);
    while (result.xp >= needed) {
        result.level++;
        result.xp -= needed;

        await result.save();

        message.channel.send(
            `${message.member} has just advanced to level ${result.level}`
        );

        needed = getNeededXP(result.level);
    }

    return {
        level: result.level,
        xp: result.xp,
        neededXP: getNeededXP(result.level),
    } as LevelData;
}
Example #10
Source File: types-pg.test.ts    From rds-data with MIT License 5 votes vote down vote up
d = add(new Date(), { days: 10 })
Example #11
Source File: helpers.tsx    From glide-frontend with GNU General Public License v3.0 5 votes vote down vote up
processAuctionData = async (auctionId: number, auctionResponse: AuctionsResponse): Promise<Auction> => {
  const processedAuctionData = {
    ...auctionResponse,
    topLeaderboard: auctionResponse.leaderboard.toNumber(),
    initialBidAmount: ethersToBigNumber(auctionResponse.initialBidAmount).div(DEFAULT_TOKEN_DECIMAL).toNumber(),
    leaderboardThreshold: ethersToBigNumber(auctionResponse.leaderboardThreshold),
    startBlock: auctionResponse.startBlock.toNumber(),
    endBlock: auctionResponse.endBlock.toNumber(),
  }

  // Get all required datas and blocks
  const currentBlock = await simpleRpcProvider.getBlockNumber()
  const startDate = await getDateForBlock(currentBlock, processedAuctionData.startBlock)
  const endDate = await getDateForBlock(currentBlock, processedAuctionData.endBlock)
  const farmStartDate = add(endDate, { hours: 12 })
  const blocksToFarmStartDate = hoursToSeconds(12) / ESC_BLOCK_TIME
  const farmStartBlock = processedAuctionData.endBlock + blocksToFarmStartDate
  const farmDurationInBlocks = hoursToSeconds(7 * 24) / ESC_BLOCK_TIME
  const farmEndBlock = farmStartBlock + farmDurationInBlocks
  const farmEndDate = add(farmStartDate, { weeks: 1 })

  const auctionStatus = getAuctionStatus(
    currentBlock,
    processedAuctionData.startBlock,
    processedAuctionData.endBlock,
    processedAuctionData.status,
  )

  return {
    id: auctionId,
    startDate,
    endDate,
    auctionDuration: differenceInHours(endDate, startDate),
    farmStartBlock,
    farmStartDate,
    farmEndBlock,
    farmEndDate,
    ...processedAuctionData,
    status: auctionStatus,
  }
}
Example #12
Source File: helpers.ts    From js-client with MIT License 5 votes vote down vote up
recalculateZoomEnd = (minZoomWindow: number, count: number, start: Date, end: Date): Date => {
	const origDeltaS = end.getTime() / 1000 - start.getTime() / 1000;
	const deltaS = Math.ceil(origDeltaS / (minZoomWindow * count)) * (minZoomWindow * count);
	const newEnd = add(start, { seconds: deltaS });

	return newEnd;
}
Example #13
Source File: YearMonthSelect.tsx    From UUI with MIT License 5 votes vote down vote up
YearMonthSelect = UUIFunctionComponent({
  name: 'YearMonthSelect',
  nodes: {
    Root: 'div',
    Container: 'div',
    Button: UUIButton,
    YearLabel: 'div',
    MonthLabel: 'div',
    ChevronsLeftIcon: Icons.ChevronsLeft,
    ChevronLeftIcon: Icons.ChevronLeft,
    ChevronsRightIcon: Icons.ChevronsRight,
    ChevronRightIcon: Icons.ChevronRight,
  },
  propTypes: YearMonthSelectPropTypes,
}, (props: YearMonthSelectFeatureProps, { nodes, NodeDataProps }) => {
  const {
    Root, Container,
    Button, YearLabel, MonthLabel,
    ChevronLeftIcon, ChevronsLeftIcon, ChevronRightIcon, ChevronsRightIcon,
  } = nodes

  const yearLabel = format(props.value, 'yyyy')
  const monthLabel = format(props.value, 'M')

  const handleChange = useCallback((type: 'years' | 'months', value: number) => {
    props.onChange(add(props.value, { [type]: value }))
  }, [props])

  return (
    <Root>
      <Container {...NodeDataProps({ 'type': 'actions' })}>
        <Button onClick={() => { handleChange('years', -1) }}>
          <ChevronsLeftIcon width={18} height={18} />
        </Button>
        <Button onClick={() => { handleChange('months', -1) }}>
          <ChevronLeftIcon width={14} height={14} />
        </Button>
      </Container>
      <Container {...NodeDataProps({ 'type': 'labels' })}>
        <YearLabel>{yearLabel}</YearLabel>
        <MonthLabel>{monthLabel}</MonthLabel>
      </Container>
      <Container {...NodeDataProps({ 'type': 'actions' })}>
      <Button onClick={() => { handleChange('months', 1) }}>
          <ChevronRightIcon width={14} height={14} />
        </Button>
        <Button onClick={() => { handleChange('years', 1) }}>
          <ChevronsRightIcon width={18} height={18} />
        </Button>
      </Container>
    </Root>
  )
})
Example #14
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
getFormattedUnlockDate = (remainingBlockNumbersToUnstake: number, formatPattern: string) => {
  const unlockDate = add(new Date(), {
    seconds: remainingBlockNumbersToUnstake * BLOCK_TIME
  });

  return format(unlockDate, formatPattern);
}
Example #15
Source File: make-long-term-log.ts    From merged-pr-stat with MIT License 5 votes vote down vote up
async function main(): Promise<void> {
  program.requiredOption("--start <date>").requiredOption("--end <date>").requiredOption("--query <query>");

  program.parse(process.argv);

  const startDate = parseISO(program.start);
  const endDate = parseISO(program.end);
  const query = program.query as string;

  const intervalDays = 7;
  const allLogs = [];
  process.stdout.write(
    "title,author,url,createdAt,mergedAt,additions,deletions,authoredDate,leadTimeSeconds,timeToMergeSeconds\n"
  );
  for (let start = startDate; start < endDate; start = addDays(start, intervalDays)) {
    const end = min([add(start, { days: intervalDays, seconds: -1 }), endDate]);
    console.error(format(start, "yyyy-MM-dd HH:mm:ss"));
    console.error(format(end, "yyyy-MM-dd HH:mm:ss"));

    const stdout = execFileSync(
      "merged-pr-stat",
      ["log", "--start", start.toISOString(), "--end", end.toISOString(), "--query", query],
      { encoding: "utf8" }
    );
    const logs: any[] = JSON.parse(stdout);
    process.stdout.write(
      csvStringify(
        logs.map((l) => [
          l.title,
          l.author,
          l.url,
          l.createdAt,
          l.mergedAt,
          l.additions,
          l.deletions,
          l.authoredDate,
          l.leadTimeSeconds,
          l.timeToMergeSeconds,
        ])
      )
    );
  }
}
Example #16
Source File: go.ts    From logseq-plugin-vim-shortcuts with MIT License 5 votes vote down vote up
parsePageName = async (pageName: string) => {
  const config = await logseq.App.getUserConfigs();
  const page = await logseq.Editor.getCurrentPage();
  switch (pageName) {
    case "@":
    case "@index":
      return "Contents";
    case "@today":
      pageName = format(new Date(), config.preferredDateFormat);
      return pageName;
    case "@yesterday":
      pageName = format(
        sub(new Date(), {
          days: 1,
        }),
        config.preferredDateFormat
      );
      return pageName;
    case "@tomorrow":
      pageName = format(
        add(new Date(), {
          days: 1,
        }),
        config.preferredDateFormat
      );
      return pageName;
    case "@prev":
      if (page && page["journal?"]) {
        pageName = format(
          sub(new Date(page.name), {
            days: 1,
          }),
          config.preferredDateFormat
        );
        return pageName;
      } else {
        pageName = format(
          sub(new Date(), {
            days: 1,
          }),
          config.preferredDateFormat
        );
        return pageName;
      }
    case "@next":
      if (page && page["journal?"]) {
        pageName = format(
          add(new Date(page.name), {
            days: 1,
          }),
          config.preferredDateFormat
        );
        return pageName;
      } else {
        pageName = format(
          add(new Date(), {
            days: 1,
          }),
          config.preferredDateFormat
        );
        return pageName;
      }
    case "@back":
      await backward();
      return;
    case "@forward":
      await forward();
      return;
    default:
      return pageName;
  }
}
Example #17
Source File: FiltersBar.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
FiltersBar: React.FC<ReturnType<typeof useFiltersBar> & FiltersBarProps> = ({
  setVideoWhereInput,
  videoWhereInput,
  activeFilters,
  setOwnedNftWhereInput,
  filters: {
    setIsFiltersOpen,
    isFiltersOpen,
    dateUploadedFilter,
    setDateUploadedFilter,
    excludeMatureContentRatingFilter,
    setExcludeMatureContentRatingFilter,
    excludePaidPromotionalMaterialFilter,
    setExcludePaidPromotionalMaterialFilter,
    videoLengthFilter,
    setVideoLengthFilter,
    nftStatusFilter,
    setNftStatusFilter,
    categoriesFilter,
    setCategoriesFilter,
    language,
    setLanguage,
  },
  canClearFilters: {
    canClearOtherFilters,
    canClearDateUploadedFilter,
    canClearVideoLengthFilter,
    canClearAllFilters,
    canClearNftStatusFilter,
    clearNftStatusFilter,
    clearCategoriesFilter,
    clearAllFilters,
    clearDateUploadedFilter,
    clearVideoLengthFilter,
    clearOtherFilters,
    canClearCategoriesFilter,
  },
}) => {
  const smMatch = useMediaMatch('sm')
  const betweenBaseAndSMMatch = !smMatch
  const categoriesPopoverRef = useRef<PopoverImperativeHandle>(null)
  const datePopoverRef = useRef<PopoverImperativeHandle>(null)
  const lengthPopoverRef = useRef<PopoverImperativeHandle>(null)
  const othersPopoverRef = useRef<PopoverImperativeHandle>(null)
  const { categories } = useCategories()
  const nftStatusInputs = useMemo(
    () => (
      <FilterContentContainer>
        {nftStatuses.map((status) => (
          <Checkbox
            name="nft-status"
            label={status.name}
            key={`nft-status-${status.id}`}
            value={!!nftStatusFilter?.includes(status.id)}
            onChange={(value) => {
              setNftStatusFilter((statuses) =>
                value ? [...(statuses ?? []), status.id] : statuses?.filter((id) => id !== status.id)
              )
            }}
          />
        ))}
      </FilterContentContainer>
    ),
    [nftStatusFilter, setNftStatusFilter]
  )

  const categoriesInputs = useMemo(
    () => (
      <FilterContentContainer>
        {categories &&
          categories.map((category) => (
            <Checkbox
              name="category-filter"
              label={category.name as string}
              key={`category-filter-${category.id}`}
              value={!!categoriesFilter?.includes(category.id)}
              onChange={(value) => {
                setCategoriesFilter((categories) =>
                  value ? [...(categories ?? []), category.id] : categories?.filter((id) => id !== category.id)
                )
              }}
            />
          ))}
      </FilterContentContainer>
    ),
    [categories, categoriesFilter, setCategoriesFilter]
  )

  const dateUploadedInputs = useMemo(
    () => (
      <FilterContentContainer>
        <RadioButton
          onChange={() => {
            setDateUploadedFilter(1)
          }}
          name="date-uploaded"
          label="Last 24 hours"
          value={1}
          selectedValue={dateUploadedFilter}
        />
        <RadioButton
          onChange={() => {
            setDateUploadedFilter(7)
          }}
          name="date-uploaded"
          label="Last 7 days"
          value={7}
          selectedValue={dateUploadedFilter}
        />
        <RadioButton
          onChange={() => {
            setDateUploadedFilter(30)
          }}
          name="date-uploaded"
          label="Last 30 days"
          value={30}
          selectedValue={dateUploadedFilter}
        />
        <RadioButton
          onChange={() => {
            setDateUploadedFilter(365)
          }}
          name="date-uploaded"
          label="Last 365 days"
          value={365}
          selectedValue={dateUploadedFilter}
        />
      </FilterContentContainer>
    ),
    [dateUploadedFilter, setDateUploadedFilter]
  )

  const videoLengthInputs = useMemo(
    () => (
      <FilterContentContainer>
        <RadioButton
          onChange={() => {
            setVideoLengthFilter('0-to-4')
          }}
          name="length"
          label="Less than 4 minutes"
          value="0-to-4"
          selectedValue={videoLengthFilter}
        />
        <RadioButton
          onChange={() => {
            setVideoLengthFilter('4-to-10')
          }}
          name="length"
          label="4 to 10 minutes"
          value="4-to-10"
          selectedValue={videoLengthFilter}
        />
        <RadioButton
          onChange={() => {
            setVideoLengthFilter('10-to-9999')
          }}
          name="length"
          label="More than 10 minutes"
          value="10-to-9999"
          selectedValue={videoLengthFilter}
        />
      </FilterContentContainer>
    ),
    [setVideoLengthFilter, videoLengthFilter]
  )

  const otherFiltersInputs = useMemo(
    () => (
      <FilterContentContainer>
        <Checkbox
          onChange={setExcludePaidPromotionalMaterialFilter}
          name="other-filters"
          label="Paid promotional material"
          value={!!excludePaidPromotionalMaterialFilter}
        />
        <Checkbox
          onChange={setExcludeMatureContentRatingFilter}
          name="other-filters"
          label="Mature content rating"
          value={!!excludeMatureContentRatingFilter}
        />
      </FilterContentContainer>
    ),
    [
      excludeMatureContentRatingFilter,
      excludePaidPromotionalMaterialFilter,
      setExcludeMatureContentRatingFilter,
      setExcludePaidPromotionalMaterialFilter,
    ]
  )

  if (betweenBaseAndSMMatch) {
    return (
      <MobileFilterDialog
        onExitClick={() => setIsFiltersOpen(false)}
        show={isFiltersOpen}
        title="Filters"
        content={
          <>
            {activeFilters.includes('language') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Language
                </Text>
                <Select
                  items={[{ name: 'All languages', value: 'undefined' }, ...languages]}
                  placeholder="Any language"
                  size="small"
                  value={language}
                  onChange={setLanguage}
                />
              </MobileFilterContainer>
            )}
            {activeFilters.includes('nftStatus') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Status
                </Text>
                {nftStatusInputs}
              </MobileFilterContainer>
            )}
            {categories && activeFilters.includes('categories') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Categories
                </Text>
                {categoriesInputs}
              </MobileFilterContainer>
            )}
            {activeFilters.includes('date') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Date uploaded
                </Text>
                {dateUploadedInputs}
              </MobileFilterContainer>
            )}
            {activeFilters.includes('date-minted') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Date minted
                </Text>
                {dateUploadedInputs}
              </MobileFilterContainer>
            )}
            {activeFilters.includes('length') && (
              <MobileFilterContainer>
                <Text secondary variant="h100">
                  Length
                </Text>
                {videoLengthInputs}
              </MobileFilterContainer>
            )}
            {activeFilters.includes('other') && (
              <MobileFilterContainer>
                <OtherFilterStyledText secondary variant="h100">
                  <OtherFilterStyledIcon />
                  Exclude:
                </OtherFilterStyledText>
                {otherFiltersInputs}
              </MobileFilterContainer>
            )}
          </>
        }
        primaryButton={{
          text: 'Apply',
          onClick: () => {
            setVideoWhereInput((value) => ({
              ...value,
              createdAt_gte: dateUploadedFilter
                ? add(new Date(), {
                    days: -dateUploadedFilter,
                  })
                : undefined,
              hasMarketing_eq: excludePaidPromotionalMaterialFilter ? !excludePaidPromotionalMaterialFilter : undefined,
              isExplicit_eq: excludeMatureContentRatingFilter ? !excludeMatureContentRatingFilter : undefined,
              category: categoriesFilter
                ? {
                    id_in: categoriesFilter,
                  }
                : undefined,
              language:
                language !== 'undefined'
                  ? {
                      iso_eq: language as string,
                    }
                  : undefined,
              ...getDurationRules(),
            }))
            setOwnedNftWhereInput((value) => {
              return {
                ...value,
                OR: [
                  nftStatusFilter?.includes('AuctionTypeEnglish')
                    ? {
                        transactionalStatusAuction: {
                          auctionType_json: { isTypeOf_eq: 'AuctionTypeEnglish' },
                        },
                      }
                    : {},
                  nftStatusFilter?.includes('AuctionTypeOpen')
                    ? {
                        transactionalStatusAuction: {
                          auctionType_json: { isTypeOf_eq: 'AuctionTypeOpen' },
                        },
                      }
                    : {},
                  nftStatusFilter?.includes('TransactionalStatusBuyNow')
                    ? {
                        transactionalStatus_json: { isTypeOf_eq: 'TransactionalStatusBuyNow' },
                      }
                    : {},
                  nftStatusFilter?.includes('TransactionalStatusIdle')
                    ? {
                        transactionalStatus_json: { isTypeOf_eq: 'TransactionalStatusIdle' },
                      }
                    : {},
                ],
              }
            })
            setIsFiltersOpen(false)
          },
        }}
        secondaryButton={{
          text: 'Clear',
          disabled: !canClearAllFilters,
          onClick: () => {
            clearAllFilters()
            setIsFiltersOpen(false)
          },
        }}
      />
    )
  }
  return (
    <CSSTransition
      in={isFiltersOpen}
      timeout={parseInt(transitions.timings.routing)}
      classNames="filters"
      mountOnEnter
      unmountOnExit
    >
      <FiltersContainer open={true}>
        <FiltersInnerContainer>
          {activeFilters.includes('nftStatus') && (
            <DialogPopover
              ref={categoriesPopoverRef}
              trigger={
                <Button variant="secondary" badge={nftStatusFilter?.length}>
                  Status
                </Button>
              }
              dividers
              primaryButton={{
                text: 'Apply',
                disabled: !nftStatusFilter && !canClearNftStatusFilter,
                onClick: () => {
                  categoriesPopoverRef.current?.hide()
                  setOwnedNftWhereInput((value) => {
                    return {
                      ...value,
                      OR: [
                        nftStatusFilter?.includes('AuctionTypeEnglish')
                          ? {
                              transactionalStatusAuction: {
                                auctionType_json: { isTypeOf_eq: 'AuctionTypeEnglish' },
                              },
                            }
                          : {},
                        nftStatusFilter?.includes('AuctionTypeOpen')
                          ? {
                              transactionalStatusAuction: {
                                auctionType_json: { isTypeOf_eq: 'AuctionTypeOpen' },
                              },
                            }
                          : {},
                        nftStatusFilter?.includes('TransactionalStatusBuyNow')
                          ? {
                              transactionalStatus_json: { isTypeOf_eq: 'TransactionalStatusBuyNow' },
                            }
                          : {},
                        nftStatusFilter?.includes('TransactionalStatusIdle')
                          ? {
                              transactionalStatus_json: { isTypeOf_eq: 'TransactionalStatusIdle' },
                            }
                          : {},
                      ],
                    }
                  })
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearNftStatusFilter,
                disabled: nftStatusFilter === undefined,
              }}
            >
              {nftStatusInputs}
            </DialogPopover>
          )}
          {categories && activeFilters.includes('categories') && (
            <DialogPopover
              ref={categoriesPopoverRef}
              trigger={
                <Button variant="secondary" badge={canClearCategoriesFilter && categoriesFilter?.length}>
                  Categories
                </Button>
              }
              dividers
              primaryButton={{
                text: 'Apply',
                disabled: (!categoriesFilter || !categoriesFilter.length) && !canClearCategoriesFilter,
                onClick: () => {
                  categoriesPopoverRef.current?.hide()
                  setVideoWhereInput((value) => ({
                    ...value,
                    category: {
                      id_in: categoriesFilter,
                    },
                  }))
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearCategoriesFilter,
                disabled: categoriesFilter === undefined,
              }}
            >
              {categoriesInputs}
            </DialogPopover>
          )}
          {activeFilters.includes('date') && (
            <DialogPopover
              ref={datePopoverRef}
              trigger={
                <Button badge={canClearDateUploadedFilter} variant="secondary">
                  Date uploaded
                </Button>
              }
              primaryButton={{
                text: 'Apply',
                disabled: !dateUploadedFilter && !canClearDateUploadedFilter,
                onClick: () => {
                  datePopoverRef.current?.hide()
                  setVideoWhereInput((value) => ({
                    ...value,
                    createdAt_gte: dateUploadedFilter
                      ? add(new Date(), {
                          days: -dateUploadedFilter,
                        })
                      : undefined,
                  }))
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearDateUploadedFilter,
                disabled: dateUploadedFilter === undefined,
              }}
            >
              {dateUploadedInputs}
            </DialogPopover>
          )}
          {activeFilters.includes('date-minted') && (
            <DialogPopover
              ref={datePopoverRef}
              trigger={
                <Button badge={canClearDateUploadedFilter} variant="secondary">
                  Date minted
                </Button>
              }
              primaryButton={{
                text: 'Apply',
                disabled: !dateUploadedFilter && !canClearDateUploadedFilter,
                onClick: () => {
                  datePopoverRef.current?.hide()
                  setVideoWhereInput((value) => ({
                    ...value,
                    createdAt_gte: dateUploadedFilter
                      ? add(new Date(), {
                          days: -dateUploadedFilter,
                        })
                      : undefined,
                  }))
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearDateUploadedFilter,
                disabled: dateUploadedFilter === undefined,
              }}
            >
              {dateUploadedInputs}
            </DialogPopover>
          )}
          {activeFilters.includes('length') && (
            <DialogPopover
              ref={lengthPopoverRef}
              trigger={
                <Button badge={canClearVideoLengthFilter} variant="secondary">
                  Length
                </Button>
              }
              primaryButton={{
                text: 'Apply',
                disabled: !videoLengthFilter && !canClearVideoLengthFilter,
                onClick: () => {
                  lengthPopoverRef.current?.hide()
                  setVideoWhereInput((value) => ({
                    ...value,
                    ...getDurationRules(videoLengthFilter),
                  }))
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearVideoLengthFilter,
                disabled: videoLengthFilter === undefined,
              }}
            >
              {videoLengthInputs}
            </DialogPopover>
          )}
          {activeFilters.includes('other') && (
            <DialogPopover
              ref={othersPopoverRef}
              trigger={
                <Button
                  badge={+(videoWhereInput?.hasMarketing_eq === false) + +(videoWhereInput?.isExplicit_eq === false)}
                  variant="secondary"
                >
                  Other filters
                </Button>
              }
              primaryButton={{
                text: 'Apply',
                disabled:
                  !excludePaidPromotionalMaterialFilter && !excludeMatureContentRatingFilter && !canClearOtherFilters,
                onClick: () => {
                  othersPopoverRef.current?.hide()
                  setVideoWhereInput((value) => ({
                    ...value,
                    hasMarketing_eq: excludePaidPromotionalMaterialFilter
                      ? !excludePaidPromotionalMaterialFilter
                      : undefined,
                    isExplicit_eq: excludeMatureContentRatingFilter ? !excludeMatureContentRatingFilter : undefined,
                  }))
                },
              }}
              secondaryButton={{
                text: 'Clear',
                onClick: clearOtherFilters,
                disabled: !excludePaidPromotionalMaterialFilter && !excludeMatureContentRatingFilter,
              }}
            >
              <OtherFilterStyledText secondary variant="h100">
                <OtherFilterStyledIcon />
                Exclude:
              </OtherFilterStyledText>
              {otherFiltersInputs}
            </DialogPopover>
          )}
        </FiltersInnerContainer>

        {canClearAllFilters && (
          <ClearAllButton onClick={clearAllFilters} variant="tertiary" icon={<SvgActionClose />}>
            Clear all
          </ClearAllButton>
        )}
      </FiltersContainer>
    </CSSTransition>
  )
}
Example #18
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
Staking = (): JSX.Element => {
  const [blockLockTimeExtension, setBlockLockTimeExtension] = React.useState<number>(0);

  const dispatch = useDispatch();
  const { t } = useTranslation();

  const { governanceTokenBalance, bridgeLoaded, address, prices } = useSelector((state: StoreType) => state.general);

  const {
    register,
    handleSubmit,
    watch,
    reset,
    formState: { errors },
    trigger
  } = useForm<StakingFormData>({
    mode: 'onChange', // 'onBlur'
    defaultValues: {
      [LOCKING_AMOUNT]: '0',
      [LOCK_TIME]: '0'
    }
  });
  const lockingAmount = watch(LOCKING_AMOUNT) || '0';
  const lockTime = watch(LOCK_TIME) || '0';

  const {
    isIdle: currentBlockNumberIdle,
    isLoading: currentBlockNumberLoading,
    data: currentBlockNumber,
    error: currentBlockNumberError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(currentBlockNumberError);

  const {
    isIdle: voteGovernanceTokenBalanceIdle,
    isLoading: voteGovernanceTokenBalanceLoading,
    data: voteGovernanceTokenBalance,
    error: voteGovernanceTokenBalanceError,
    refetch: voteGovernanceTokenBalanceRefetch
  } = useQuery<VoteGovernanceTokenMonetaryAmount, Error>(
    [GENERIC_FETCHER, 'escrow', 'votingBalance', address],
    genericFetcher<VoteGovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(voteGovernanceTokenBalanceError);

  // My currently claimable rewards
  const {
    isIdle: claimableRewardAmountIdle,
    isLoading: claimableRewardAmountLoading,
    data: claimableRewardAmount,
    error: claimableRewardAmountError,
    refetch: claimableRewardAmountRefetch
  } = useQuery<GovernanceTokenMonetaryAmount, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewards', address],
    genericFetcher<GovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(claimableRewardAmountError);

  // Projected governance token rewards
  const {
    isIdle: projectedRewardAmountAndAPYIdle,
    isLoading: projectedRewardAmountAndAPYLoading,
    data: projectedRewardAmountAndAPY,
    error: rewardAmountAndAPYError,
    refetch: rewardAmountAndAPYRefetch
  } = useQuery<EstimatedRewardAmountAndAPY, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewardEstimate', address],
    genericFetcher<EstimatedRewardAmountAndAPY>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(rewardAmountAndAPYError);

  // Estimated governance token Rewards & APY
  const monetaryLockingAmount = newMonetaryAmount(lockingAmount, GOVERNANCE_TOKEN, true);
  const {
    isIdle: estimatedRewardAmountAndAPYIdle,
    isLoading: estimatedRewardAmountAndAPYLoading,
    data: estimatedRewardAmountAndAPY,
    error: estimatedRewardAmountAndAPYError
  } = useQuery<EstimatedRewardAmountAndAPY, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewardEstimate', address, monetaryLockingAmount, blockLockTimeExtension],
    genericFetcher<EstimatedRewardAmountAndAPY>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(estimatedRewardAmountAndAPYError);

  const {
    isIdle: stakedAmountAndEndBlockIdle,
    isLoading: stakedAmountAndEndBlockLoading,
    data: stakedAmountAndEndBlock,
    error: stakedAmountAndEndBlockError,
    refetch: stakedAmountAndEndBlockRefetch
  } = useQuery<StakedAmountAndEndBlock, Error>(
    [GENERIC_FETCHER, 'escrow', 'getStakedBalance', address],
    genericFetcher<StakedAmountAndEndBlock>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stakedAmountAndEndBlockError);

  const initialStakeMutation = useMutation<void, Error, LockingAmountAndTime>(
    (variables: LockingAmountAndTime) => {
      if (currentBlockNumber === undefined) {
        throw new Error('Something went wrong!');
      }
      const unlockHeight = currentBlockNumber + convertWeeksToBlockNumbers(variables.time);

      return window.bridge.escrow.createLock(variables.amount, unlockHeight);
    },
    {
      onSuccess: () => {
        voteGovernanceTokenBalanceRefetch();
        stakedAmountAndEndBlockRefetch();
        claimableRewardAmountRefetch();
        rewardAmountAndAPYRefetch();
        reset({
          [LOCKING_AMOUNT]: '0.0',
          [LOCK_TIME]: '0'
        });
      }
    }
  );

  const moreStakeMutation = useMutation<void, Error, LockingAmountAndTime>(
    (variables: LockingAmountAndTime) => {
      return (async () => {
        if (stakedAmountAndEndBlock === undefined) {
          throw new Error('Something went wrong!');
        }

        if (checkIncreaseLockAmountAndExtendLockTime(variables.time, variables.amount)) {
          const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time);

          const txs = [
            window.bridge.api.tx.escrow.increaseAmount(variables.amount.toString(variables.amount.currency.rawBase)),
            window.bridge.api.tx.escrow.increaseUnlockHeight(unlockHeight)
          ];
          const batch = window.bridge.api.tx.utility.batchAll(txs);
          await DefaultTransactionAPI.sendLogged(
            window.bridge.api,
            window.bridge.account as AddressOrPair,
            batch,
            undefined, // don't await success event
            true // don't wait for finalized blocks
          );
        } else if (checkOnlyIncreaseLockAmount(variables.time, variables.amount)) {
          return await window.bridge.escrow.increaseAmount(variables.amount);
        } else if (checkOnlyExtendLockTime(variables.time, variables.amount)) {
          const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time);

          return await window.bridge.escrow.increaseUnlockHeight(unlockHeight);
        } else {
          throw new Error('Something went wrong!');
        }
      })();
    },
    {
      onSuccess: () => {
        voteGovernanceTokenBalanceRefetch();
        stakedAmountAndEndBlockRefetch();
        claimableRewardAmountRefetch();
        rewardAmountAndAPYRefetch();
        reset({
          [LOCKING_AMOUNT]: '0.0',
          [LOCK_TIME]: '0'
        });
      }
    }
  );

  React.useEffect(() => {
    if (!lockTime) return;

    const lockTimeValue = Number(lockTime);
    setBlockLockTimeExtension(convertWeeksToBlockNumbers(lockTimeValue));
  }, [lockTime]);

  React.useEffect(() => {
    reset({
      [LOCKING_AMOUNT]: '',
      [LOCK_TIME]: ''
    });
  }, [address, reset]);

  const votingBalanceGreaterThanZero = voteGovernanceTokenBalance?.gt(ZERO_VOTE_GOVERNANCE_TOKEN_AMOUNT);

  const extendLockTimeSet = votingBalanceGreaterThanZero && parseInt(lockTime) > 0;
  const increaseLockingAmountSet =
    votingBalanceGreaterThanZero && monetaryLockingAmount.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  React.useEffect(() => {
    if (extendLockTimeSet) {
      trigger(LOCKING_AMOUNT);
    }
  }, [lockTime, extendLockTimeSet, trigger]);

  React.useEffect(() => {
    if (increaseLockingAmountSet) {
      trigger(LOCK_TIME);
    }
  }, [lockingAmount, increaseLockingAmountSet, trigger]);

  const getStakedAmount = () => {
    if (stakedAmountAndEndBlockIdle || stakedAmountAndEndBlockLoading) {
      return undefined;
    }
    if (stakedAmountAndEndBlock === undefined) {
      throw new Error('Something went wrong!');
    }

    return stakedAmountAndEndBlock.amount;
  };
  const stakedAmount = getStakedAmount();

  const availableBalance = React.useMemo(() => {
    if (!governanceTokenBalance || stakedAmountAndEndBlockIdle || stakedAmountAndEndBlockLoading) return;
    if (stakedAmount === undefined) {
      throw new Error('Something went wrong!');
    }

    return governanceTokenBalance.sub(stakedAmount).sub(TRANSACTION_FEE_AMOUNT);
  }, [governanceTokenBalance, stakedAmountAndEndBlockIdle, stakedAmountAndEndBlockLoading, stakedAmount]);

  const onSubmit = (data: StakingFormData) => {
    if (!bridgeLoaded) return;
    if (currentBlockNumber === undefined) {
      throw new Error('Something went wrong!');
    }

    const lockingAmountWithFallback = data[LOCKING_AMOUNT] || '0';
    const lockTimeWithFallback = data[LOCK_TIME] || '0'; // Weeks

    const monetaryAmount = newMonetaryAmount(lockingAmountWithFallback, GOVERNANCE_TOKEN, true);
    const numberTime = parseInt(lockTimeWithFallback);

    if (votingBalanceGreaterThanZero) {
      moreStakeMutation.mutate({
        amount: monetaryAmount,
        time: numberTime
      });
    } else {
      initialStakeMutation.mutate({
        amount: monetaryAmount,
        time: numberTime
      });
    }
  };

  const validateLockingAmount = (value: string): string | undefined => {
    const valueWithFallback = value || '0';
    const monetaryLockingAmount = newMonetaryAmount(valueWithFallback, GOVERNANCE_TOKEN, true);

    if (!extendLockTimeSet && monetaryLockingAmount.lte(ZERO_GOVERNANCE_TOKEN_AMOUNT)) {
      return 'Locking amount must be greater than zero!';
    }

    if (availableBalance === undefined) {
      throw new Error('Something went wrong!');
    }
    if (monetaryLockingAmount.gt(availableBalance)) {
      return 'Locking amount must not be greater than available balance!';
    }

    const planckLockingAmount = monetaryLockingAmount.to.Planck();
    const lockBlocks = convertWeeksToBlockNumbers(parseInt(lockTime));
    // This is related to the on-chain implementation where currency values are integers.
    // So less tokens than the period would likely round to 0.
    // So on the UI, as long as you require more planck to be locked than the number of blocks the user locks for,
    // it should be good.
    if (!extendLockTimeSet && planckLockingAmount.lte(Big(lockBlocks))) {
      return 'Planck to be locked must be greater than the number of blocks you lock for!';
    }

    return undefined;
  };

  const validateLockTime = (value: string): string | undefined => {
    const valueWithFallback = value || '0';
    const numericValue = parseInt(valueWithFallback);

    if (votingBalanceGreaterThanZero && numericValue === 0 && monetaryLockingAmount.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT)) {
      return undefined;
    }

    if (availableLockTime === undefined) {
      throw new Error('Something went wrong!');
    }
    if (numericValue < STAKE_LOCK_TIME.MIN || numericValue > availableLockTime) {
      return `Please enter a number between ${STAKE_LOCK_TIME.MIN}-${availableLockTime}.`;
    }

    return undefined;
  };

  const renderVoteStakedAmountLabel = () => {
    if (voteGovernanceTokenBalanceIdle || voteGovernanceTokenBalanceLoading) {
      return '-';
    }
    if (voteGovernanceTokenBalance === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(voteGovernanceTokenBalance);
  };

  const renderProjectedRewardAmountLabel = () => {
    if (projectedRewardAmountAndAPYIdle || projectedRewardAmountAndAPYLoading) {
      return '-';
    }
    if (projectedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(projectedRewardAmountAndAPY.amount);
  };

  const renderStakedAmountLabel = () => {
    return stakedAmount === undefined ? '-' : displayMonetaryAmount(stakedAmount);
  };

  const hasStakedAmount = stakedAmount?.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  const getRemainingBlockNumbersToUnstake = () => {
    if (
      stakedAmountAndEndBlockIdle ||
      stakedAmountAndEndBlockLoading ||
      currentBlockNumberIdle ||
      currentBlockNumberLoading
    ) {
      return undefined;
    }
    if (stakedAmountAndEndBlock === undefined) {
      throw new Error('Something went wrong!');
    }
    if (currentBlockNumber === undefined) {
      throw new Error('Something went wrong!');
    }

    return hasStakedAmount
      ? stakedAmountAndEndBlock.endBlock - currentBlockNumber // If the user has staked
      : null; // If the user has not staked
  };
  const remainingBlockNumbersToUnstake = getRemainingBlockNumbersToUnstake();

  const getAvailableLockTime = () => {
    if (remainingBlockNumbersToUnstake === undefined) {
      return undefined;
    }

    // If the user has staked
    if (hasStakedAmount) {
      if (remainingBlockNumbersToUnstake === null) {
        throw new Error('Something went wrong!');
      }
      const remainingWeeksToUnstake = convertBlockNumbersToWeeks(remainingBlockNumbersToUnstake);

      return Math.floor(STAKE_LOCK_TIME.MAX - remainingWeeksToUnstake);
      // If the user has not staked
    } else {
      return STAKE_LOCK_TIME.MAX;
    }
  };
  const availableLockTime = getAvailableLockTime();

  const renderAvailableBalanceLabel = () => {
    return availableBalance === undefined ? '-' : displayMonetaryAmount(availableBalance);
  };

  const renderUnlockDateLabel = () => {
    if (errors[LOCK_TIME]) {
      return '-';
    }

    const unlockDate = add(new Date(), {
      weeks: parseInt(lockTime)
    });

    return format(unlockDate, YEAR_MONTH_DAY_PATTERN);
  };

  const renderNewUnlockDateLabel = () => {
    if (remainingBlockNumbersToUnstake === undefined) {
      return '-';
    }
    if (errors[LOCK_TIME]) {
      return '-';
    }

    let remainingLockSeconds;
    if (hasStakedAmount) {
      if (remainingBlockNumbersToUnstake === null) {
        throw new Error('Something went wrong!');
      }

      remainingLockSeconds = remainingBlockNumbersToUnstake * BLOCK_TIME;
    } else {
      remainingLockSeconds = 0;
    }
    const unlockDate = add(new Date(), {
      weeks: parseInt(lockTime),
      seconds: remainingLockSeconds
    });

    return format(unlockDate, YEAR_MONTH_DAY_PATTERN);
  };

  const renderNewVoteGovernanceTokenGainedLabel = () => {
    const newTotalStakeAmount = getNewTotalStake();
    if (voteGovernanceTokenBalance === undefined || newTotalStakeAmount === undefined) {
      return '-';
    }

    const newVoteGovernanceTokenAmountGained = newTotalStakeAmount.sub(voteGovernanceTokenBalance);
    const rounded = newVoteGovernanceTokenAmountGained.toBig(VOTE_GOVERNANCE_TOKEN.base).round(5);
    const typed = newMonetaryAmount(rounded, VOTE_GOVERNANCE_TOKEN, true);

    return `${displayMonetaryAmount(typed)} ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const getNewTotalStake = () => {
    if (remainingBlockNumbersToUnstake === undefined || stakedAmount === undefined) {
      return undefined;
    }

    const extendingLockTime = parseInt(lockTime); // Weeks

    let newLockTime: number;
    let newLockingAmount: GovernanceTokenMonetaryAmount;
    if (remainingBlockNumbersToUnstake === null) {
      // If the user has not staked
      newLockTime = extendingLockTime;
      newLockingAmount = monetaryLockingAmount;
    } else {
      // If the user has staked
      const currentLockTime = convertBlockNumbersToWeeks(remainingBlockNumbersToUnstake); // Weeks

      // New lock-time that is applied to the entire staked governance token
      newLockTime = currentLockTime + extendingLockTime; // Weeks

      // New total staked governance token
      newLockingAmount = monetaryLockingAmount.add(stakedAmount);
    }

    // Multiplying the new total staked governance token with the staking time divided by the maximum lock time
    return newLockingAmount.mul(newLockTime).div(STAKE_LOCK_TIME.MAX);
  };

  const renderNewTotalStakeLabel = () => {
    const newTotalStakeAmount = getNewTotalStake();
    if (newTotalStakeAmount === undefined) {
      return '-';
    }

    return `${displayMonetaryAmount(newTotalStakeAmount)} ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const renderEstimatedAPYLabel = () => {
    if (
      estimatedRewardAmountAndAPYIdle ||
      estimatedRewardAmountAndAPYLoading ||
      errors[LOCK_TIME] ||
      errors[LOCKING_AMOUNT]
    ) {
      return '-';
    }
    if (estimatedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return `${safeRoundTwoDecimals(estimatedRewardAmountAndAPY.apy.toString())} %`;
  };

  const renderEstimatedRewardAmountLabel = () => {
    if (
      estimatedRewardAmountAndAPYIdle ||
      estimatedRewardAmountAndAPYLoading ||
      errors[LOCK_TIME] ||
      errors[LOCKING_AMOUNT]
    ) {
      return '-';
    }
    if (estimatedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return `${displayMonetaryAmount(estimatedRewardAmountAndAPY.amount)} ${GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const renderClaimableRewardAmountLabel = () => {
    if (claimableRewardAmountIdle || claimableRewardAmountLoading) {
      return '-';
    }
    if (claimableRewardAmount === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(claimableRewardAmount);
  };

  const handleConfirmClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    // TODO: should be handled based on https://kentcdodds.com/blog/application-state-management-with-react
    if (!accountSet) {
      dispatch(showAccountModalAction(true));
      event.preventDefault();
    }
  };

  const valueInUSDOfLockingAmount = getUsdAmount(monetaryLockingAmount, prices.governanceToken?.usd);

  const claimRewardsButtonEnabled = claimableRewardAmount?.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  const unlockFirst =
    hasStakedAmount &&
    // eslint-disable-next-line max-len
    // `remainingBlockNumbersToUnstake !== null` is redundant because if `hasStakedAmount` is truthy `remainingBlockNumbersToUnstake` cannot be null
    remainingBlockNumbersToUnstake !== null &&
    remainingBlockNumbersToUnstake !== undefined &&
    remainingBlockNumbersToUnstake <= 0;

  const accountSet = !!address;

  const lockTimeFieldDisabled =
    votingBalanceGreaterThanZero === undefined ||
    remainingBlockNumbersToUnstake === undefined ||
    availableLockTime === undefined ||
    availableLockTime <= 0 ||
    unlockFirst;

  const lockingAmountFieldDisabled = availableBalance === undefined;

  const initializing =
    currentBlockNumberIdle ||
    currentBlockNumberLoading ||
    voteGovernanceTokenBalanceIdle ||
    voteGovernanceTokenBalanceLoading ||
    claimableRewardAmountIdle ||
    claimableRewardAmountLoading ||
    projectedRewardAmountAndAPYIdle ||
    projectedRewardAmountAndAPYLoading ||
    estimatedRewardAmountAndAPYIdle ||
    estimatedRewardAmountAndAPYLoading ||
    stakedAmountAndEndBlockIdle ||
    stakedAmountAndEndBlockLoading;

  let submitButtonLabel: string;
  if (initializing) {
    submitButtonLabel = 'Loading...';
  } else {
    if (accountSet) {
      // TODO: should improve readability by handling nested conditions
      if (votingBalanceGreaterThanZero) {
        const numericLockTime = parseInt(lockTime);
        if (checkIncreaseLockAmountAndExtendLockTime(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Add more stake and extend lock time';
        } else if (checkOnlyIncreaseLockAmount(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Add more stake';
        } else if (checkOnlyExtendLockTime(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Extend lock time';
        } else {
          submitButtonLabel = 'Stake';
        }
      } else {
        submitButtonLabel = 'Stake';
      }
    } else {
      submitButtonLabel = t('connect_wallet');
    }
  }

  return (
    <>
      <MainContainer>
        {process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA && (
          <WarningBanner
            className={SHARED_CLASSES}
            message='Block times are currently higher than expected. Lock times may be longer than expected.'
          />
        )}
        <Panel className={SHARED_CLASSES}>
          <form className={clsx('p-8', 'space-y-8')} onSubmit={handleSubmit(onSubmit)}>
            <TitleWithUnderline text={`Stake ${GOVERNANCE_TOKEN_SYMBOL}`} />
            <BalancesUI
              stakedAmount={renderStakedAmountLabel()}
              voteStakedAmount={renderVoteStakedAmountLabel()}
              projectedRewardAmount={renderProjectedRewardAmountLabel()}
            />
            <ClaimRewardsButton
              claimableRewardAmount={renderClaimableRewardAmountLabel()}
              disabled={claimRewardsButtonEnabled === false}
            />
            {/* eslint-disable-next-line max-len */}
            {/* `remainingBlockNumbersToUnstake !== null` is redundant because if `hasStakedAmount` is truthy `remainingBlockNumbersToUnstake` cannot be null */}
            {hasStakedAmount && remainingBlockNumbersToUnstake !== null && (
              <WithdrawButton
                stakedAmount={renderStakedAmountLabel()}
                remainingBlockNumbersToUnstake={remainingBlockNumbersToUnstake}
              />
            )}
            <TotalsUI />
            <div className='space-y-2'>
              <AvailableBalanceUI
                label='Available balance'
                balance={renderAvailableBalanceLabel()}
                tokenSymbol={GOVERNANCE_TOKEN_SYMBOL}
              />
              <TokenField
                id={LOCKING_AMOUNT}
                name={LOCKING_AMOUNT}
                label={GOVERNANCE_TOKEN_SYMBOL}
                min={0}
                ref={register({
                  required: {
                    value: extendLockTimeSet ? false : true,
                    message: 'This field is required!'
                  },
                  validate: (value) => validateLockingAmount(value)
                })}
                approxUSD={`≈ $ ${valueInUSDOfLockingAmount}`}
                error={!!errors[LOCKING_AMOUNT]}
                helperText={errors[LOCKING_AMOUNT]?.message}
                disabled={lockingAmountFieldDisabled}
              />
            </div>
            <LockTimeField
              id={LOCK_TIME}
              name={LOCK_TIME}
              min={0}
              ref={register({
                required: {
                  value: votingBalanceGreaterThanZero ? false : true,
                  message: 'This field is required!'
                },
                validate: (value) => validateLockTime(value)
              })}
              error={!!errors[LOCK_TIME]}
              helperText={errors[LOCK_TIME]?.message}
              optional={votingBalanceGreaterThanZero}
              disabled={lockTimeFieldDisabled}
            />
            {votingBalanceGreaterThanZero ? (
              <InformationUI
                label='New unlock Date'
                value={renderNewUnlockDateLabel()}
                tooltip='Your original lock date plus the extended lock time.'
              />
            ) : (
              <InformationUI
                label='Unlock Date'
                value={renderUnlockDateLabel()}
                tooltip='Your staked amount will be locked until this date.'
              />
            )}
            <InformationUI
              label={t('staking_page.new_vote_governance_token_gained', {
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
              value={renderNewVoteGovernanceTokenGainedLabel()}
              tooltip={t('staking_page.the_increase_in_your_vote_governance_token_balance', {
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
            />
            {votingBalanceGreaterThanZero && (
              <InformationUI
                label='New total Stake'
                value={`${renderNewTotalStakeLabel()}`}
                tooltip='Your total stake after this transaction'
              />
            )}
            <InformationUI
              label='Estimated APY'
              value={renderEstimatedAPYLabel()}
              tooltip={`The APY may change as the amount of total ${VOTE_GOVERNANCE_TOKEN_SYMBOL} changes.`}
            />
            <InformationUI
              label={`Estimated ${GOVERNANCE_TOKEN_SYMBOL} Rewards`}
              value={renderEstimatedRewardAmountLabel()}
              tooltip={t('staking_page.the_estimated_amount_of_governance_token_you_will_receive_as_rewards', {
                governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL,
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
            />
            <SubmitButton
              disabled={initializing || unlockFirst}
              pending={initialStakeMutation.isLoading || moreStakeMutation.isLoading}
              onClick={handleConfirmClick}
              endIcon={
                unlockFirst ? (
                  <InformationTooltip label='Please unstake first.' forDisabledAction={unlockFirst} />
                ) : null
              }
            >
              {submitButtonLabel}
            </SubmitButton>
          </form>
        </Panel>
      </MainContainer>
      {(initialStakeMutation.isError || moreStakeMutation.isError) && (
        <ErrorModal
          open={initialStakeMutation.isError || moreStakeMutation.isError}
          onClose={() => {
            initialStakeMutation.reset();
            moreStakeMutation.reset();
          }}
          title='Error'
          description={initialStakeMutation.error?.message || moreStakeMutation.error?.message || ''}
        />
      )}
    </>
  );
}
Example #19
Source File: DateTimeRangePicker.tsx    From UUI with MIT License 4 votes vote down vote up
DateTimeRangePicker = UUIFunctionComponent({
  name: 'DateTimeRangePicker',
  nodes: {
    Root: 'div',
    ConnectIcon: Icons.ArrowRight,
    CalendarIcon: Icons.Calendar,
    Popover: UUIPopover,
    TextField: UUITextField,
    Activator: 'div',
    Container: 'div',
    Toolbar: 'div',
    Main: 'div',
    StartSection: 'div',
    EndSection: 'div',
    Section: 'div',
    PickerButtons: UUIPickerButtons,
    YearMonthSelect: UUIYearMonthSelect,
    DateSelect: UUIDateSelect,
    TimeSelect: UUITimeSelect,
    DateTimeShortcut: UUIDateTimeShortcut,
  },
  propTypes: DateTimeRangePickerPropTypes,
}, (props: DateTimeRangePickerFeatureProps, { nodes }) => {
  const {
    Root, ConnectIcon, CalendarIcon, Popover, TextField,
    Activator, Container, Main, StartSection, EndSection, Section,
    YearMonthSelect, DateSelect, TimeSelect, DateTimeShortcut, PickerButtons,
  } = nodes

  const startTimeSelectRef = useRef<any | null>(null)
  const endTimeSelectRef = useRef<any | null>(null)
  const startInputRef = useRef<HTMLInputElement | null>(null)
  const endInputRef = useRef<HTMLInputElement | null>(null)
  const [active, setActive] = useState(false)
  const [whichFocusing, setWhichFocusing] = useState<'start' | 'end'>()
  const [hoverDate, setHoverDate] = useState<Date>()

  const initialInnerValue = useMemo(() => {
    if (props.value === null) {
      return {
        startDate: null,
        endDate: null,
        startInput: '',
        endInput: '',
        startYearMonth: startOfMonth(new Date),
        endYearMonth: add(startOfMonth(new Date), { months: 1 }),
      }
    }
    return {
      startDate: props.value[0],
      endDate: props.value[1],
      startInput: formatDateTime(props.value[0]),
      endInput: formatDateTime(props.value[1]),
      startYearMonth: startOfMonth(props.value[0]),
      endYearMonth: isSameMonth(props.value[0], props.value[1]) ? add(startOfMonth(props.value[1]), { months: 1 }) : props.value[1],
    }
  }, [props.value])
  const [innerValue, setInnerValue, resetInnerValue] = usePendingValue<DateTimeRangePickerInnerValue>(initialInnerValue, (value) => {
    if (value.startDate && value.endDate) {
      handleValueOnChange([value.startDate, value.endDate])
      closePopover()
    }
  }, { resetWhenInitialValueChanged: true })

  const selectedDates = useMemo(() => {
    return compact([innerValue.startDate, innerValue.endDate])
  }, [innerValue.endDate, innerValue.startDate])

  const timeSelectScrollToValue = useCallback((type: 'start' | 'end', value: Date, animate?: boolean) => {
    if (type === 'start' && startTimeSelectRef.current) {
      startTimeSelectRef.current.scrollToValue(value, animate)
    }
    if (type === 'end' && endTimeSelectRef.current) {
      endTimeSelectRef.current.scrollToValue(value, animate)
    }
  }, [])
  const openPopover = useCallback(() => { setActive(true) }, [])
  const closePopover = useCallback(() => { setActive(false) }, [])
  const handleValueOnChange = useCallback((value: DateTimeRangePickerValue | null) => {
    const sortedValue = value?.sort((i, j) => Number(i) - Number(j)) || null
    props.onChange(sortedValue)
  }, [props])

  /**
   *
   */
  const handleInputOnSubmit = useCallback((type: 'start' | 'end') => {
    if (innerValue.startDate && innerValue.endDate) {
      const originalInput = formatDateTime(type === 'start' ? innerValue.startDate : innerValue.endDate)
      const input = type === 'start' ? innerValue.startInput : innerValue.endInput
      if (originalInput === input) return;
      try {
        if (input === '') {
          handleValueOnChange(null)
        } else {
          const result = tryParseDateTimeFromString(input)
          handleValueOnChange(type === 'start' ? [result, innerValue.endDate] : [innerValue.startDate, result])
        }
      } catch {
        resetInnerValue()
      }
    }
  }, [handleValueOnChange, innerValue.endInput, innerValue.endDate, innerValue.startInput, innerValue.startDate, resetInnerValue])
    /**
   * handle user change year or month in YearMonthSelect.
   */
  const handleStartYearMonthSelect = useCallback((value: Date) => {
    setInnerValue((oldValue) => {
      const startYearMonthDate = value
      let endYearMonthDate = oldValue.endYearMonth
      if (!isBefore(startYearMonthDate, endYearMonthDate)) {
        endYearMonthDate = add(startYearMonthDate, { months: 1 })
      }
      return {
        ...oldValue,
        startYearMonth: startYearMonthDate,
        endYearMonth: endYearMonthDate,
      }
    })
  }, [setInnerValue])
  const handleEndYearMonthSelect = useCallback((value: Date) => {
    setInnerValue((oldValue) => {
      const endYearMonthDate = value
      let startYearMonthDate = oldValue.startYearMonth
      if (!isAfter(endYearMonthDate, startYearMonthDate)) {
        startYearMonthDate = add(endYearMonthDate, { months: -1 })
      }
      return {
        ...oldValue,
        startYearMonth: startYearMonthDate,
        endYearMonth: endYearMonthDate,
      }
    })
  }, [setInnerValue])
  /**
   * handle user select date in DateSelect.
   */
  const handleDateSelect = useCallback((value: Date) => {
    let newStartValue = innerValue.startDate
    let newEndValue = innerValue.endDate
    if (
      (newStartValue !== null && newEndValue !== null) ||
      (newStartValue === null && newEndValue === null)
    ) {
      if (whichFocusing === 'end') {
        newStartValue = null
        newEndValue = value
      } else {
        newStartValue = value
        newEndValue = null
      }
    } else {
      if (newStartValue === null) newStartValue = value
      if (newEndValue === null) newEndValue = value
      if (isAfter(newStartValue, newEndValue)) {
        const tmp = new Date(newStartValue)
        newStartValue = new Date(newEndValue)
        newEndValue = tmp
      }
    }
    setInnerValue((oldValue) => {
      return {
        ...oldValue,
        startDate: newStartValue,
        startInput: formatDateTime(newStartValue),
        endDate: newEndValue,
        endInput: formatDateTime(newEndValue),
      }
    })
  }, [innerValue.endDate, innerValue.startDate, setInnerValue, whichFocusing])
  /**
   * handle user select date in TimeSelect.
   */
  const handleTimeSelect = useCallback((type: 'start' | 'end') => {
    return (value: Date) => {
      setInnerValue((oldValue) => {
        const oldDate = type === 'start' ? oldValue.startDate : oldValue.endDate
        const newDate = set(oldDate || getZeroDate(), {
          hours: value.getHours(),
          minutes: value.getMinutes(),
          seconds: value.getSeconds(),
        })
        const newInput = formatDateTime(newDate)
        return {
          ...oldValue,
          ...(type === 'start' ? {
            startDate: newDate,
            startInput: newInput,
          } : {}),
          ...(type === 'end' ? {
            endDate: newDate,
            endInput: newInput,
          } : {}),
        }
      })
    }
  }, [setInnerValue])

  return (
    <Root>
      <Popover
        placement={'bottom-start'}
        active={active}
        onClickAway={() => {
          resetInnerValue();
          timeSelectScrollToValue('start', props.value ? props.value[0] : getZeroDate(), false)
          timeSelectScrollToValue('end', props.value ? props.value[1] : getZeroDate(), false)
          setTimeout(() => { closePopover() }, 10)
        }}
        activator={
          <Activator
            onClick={() => {
              openPopover()
              setTimeout(() => {
                const focusedElement = ReactHelper.document?.activeElement
                if (startInputRef.current === focusedElement || endInputRef.current === focusedElement) return;
                if (startInputRef.current) {
                  startInputRef.current.focus()
                }
              }, 0)
            }}
          >
            <TextField
              placeholder={props.startPlaceholder}
              value={innerValue.startInput}
              onChange={(value) => { setInnerValue((oldValue) => ({ ...oldValue, startInput: value })) }}
              customize={{
                Input: {
                  ref: startInputRef,
                  onFocus: () => {
                    setWhichFocusing('start')
                  },
                  onBlur: () => {
                    setWhichFocusing(undefined)
                    handleInputOnSubmit('start')
                  },
                  onKeyDown: (event) => {
                    if (event.key === 'Enter') {
                      handleInputOnSubmit('start')
                    }
                  }
                }
              }}
            />
            <ConnectIcon />
            <TextField
              placeholder={props.endPlaceholder}
              value={innerValue.endInput}
              onChange={(value) => { setInnerValue((oldValue) => ({ ...oldValue, endInput: value })) }}
              customize={{
                Input: {
                  ref: endInputRef,
                  onFocus: () => {
                    setWhichFocusing('end')
                  },
                  onBlur: () => {
                    setWhichFocusing(undefined)
                    handleInputOnSubmit('end')
                  },
                  onKeyDown: (event) => {
                    if (event.key === 'Enter') {
                      handleInputOnSubmit('end')
                    }
                  }
                }
              }}
            />
            <CalendarIcon />
          </Activator>
        }
      >
        <Container>
          <Main tabIndex={-1}>
            {props.shortcuts && (
              <DateTimeShortcut
                options={props.shortcuts}
                onSelect={(value) => {
                  handleValueOnChange(value)
                  timeSelectScrollToValue('start', value ? value[0] : getZeroDate(), false)
                  timeSelectScrollToValue('end', value ? value[1] : getZeroDate(), false)
                  closePopover()
                }}
              />
            )}
            <StartSection>
              <YearMonthSelect
                value={innerValue.startYearMonth}
                onChange={handleStartYearMonthSelect}
              />
              <Section>
                <DateSelect
                  yearMonth={innerValue.startYearMonth}
                  selectedDates={selectedDates}
                  onSelect={handleDateSelect}
                  hoverDate={hoverDate}
                  onHoverDateChange={(date) => { setHoverDate(date) }}
                />
                <TimeSelect
                  ref={startTimeSelectRef}
                  value={innerValue.startDate || getZeroDate()}
                  onChange={handleTimeSelect('start')}
                />
              </Section>
            </StartSection>
            <EndSection>
              <YearMonthSelect
                value={innerValue.endYearMonth}
                onChange={handleEndYearMonthSelect}
              />
              <Section>
                <DateSelect
                  yearMonth={innerValue.endYearMonth}
                  selectedDates={selectedDates}
                  onSelect={handleDateSelect}
                  hoverDate={hoverDate}
                  onHoverDateChange={(date) => { setHoverDate(date) }}
                />
                <TimeSelect
                  ref={endTimeSelectRef}
                  value={innerValue.endDate || getZeroDate()}
                  onChange={handleTimeSelect('end')}
                />
              </Section>
            </EndSection>
          </Main>
          <PickerButtons
            confirmLabel={props.confirmLabel}
            cancelLabel={props.cancelLabel}
            onCancel={() => {
              resetInnerValue()
              timeSelectScrollToValue('start', props.value ? props.value[0] : getZeroDate(), false)
              timeSelectScrollToValue('end', props.value ? props.value[1] : getZeroDate(), false)
              setTimeout(() => { closePopover() }, 10)
            }}
            onConfirm={() => {
              setInnerValue((value) => value, true)
              if (innerValue.startDate && innerValue.endDate) {
                let data = [innerValue.startDate, innerValue.endDate]
                if (isAfter(innerValue.startDate, innerValue.endDate)) {
                  data = data.reverse()
                }
                timeSelectScrollToValue('start', data[0], false)
                timeSelectScrollToValue('end', data[1], false)
              } else {
                timeSelectScrollToValue('start', getZeroDate(), false)
                timeSelectScrollToValue('end', getZeroDate(), false)
              }
              setTimeout(() => { closePopover() }, 10)
            }}
          />
        </Container>
      </Popover>
    </Root>
  )
})
Example #20
Source File: DateSelect.tsx    From UUI with MIT License 4 votes vote down vote up
DateSelect = UUIFunctionComponent({
  name: 'DateSelect',
  nodes: {
    Root: 'div',
    Calendar: 'div',
    WeekGrid: 'div',
    WeekItem: 'div',
    DayGrid: 'div',
    DayItem: 'div',
  },
  propTypes: DateSelectPropTypes,
}, (props: DateSelectFeatureProps, { nodes, NodeDataProps }) => {
  const {
    Root, Calendar,
    WeekGrid, WeekItem,
    DayGrid, DayItem,
  } = nodes

  const betweenIncludeDates = useCallback((date: Date, range: [Date, Date] | Date[]) => {
    return !isBefore(date, range[0]) && !isAfter(date, range[1])
  }, [])

  const dateInfo = useMemo(() => {
    const firstDayInMonth = startOfMonth(props.yearMonth)
    const weekdayOfFirstDayInMonth = getDay(firstDayInMonth)

    const weekdays = range(0, 7).map((i) => {
      let date = new Date(props.yearMonth)
      date = startOfWeek(date)
      date = add(date, { days: i })
      return {
        key: format(date, 'yyyy-MM-dd'),
        date: date,
        label: format(date, 'EEEEEE', { locale: zhCN }),
      };
    });

    const days = range(
      1 - weekdayOfFirstDayInMonth,
      1 - weekdayOfFirstDayInMonth + 6*7,
    ).map((i) => {
      const date = add(firstDayInMonth, { days: i - 1 })
      const selected = props.selectedDates.findIndex((i) => isSameDay(date, i)) !== -1
      const inSelectedRange = (() => {
        if (props.selectedDates.length >= 2) {
          return betweenIncludeDates(date, props.selectedDates)
        }
        return false;
      })()
      return {
        key: format(date, 'yyyy-MM-dd'),
        date: date,
        label: getDate(date),
        active: isSameMonth(props.yearMonth, date),
        selected: selected,
        inSelectedRange: inSelectedRange,
      }
    })

    return {
      weekdays,
      days
    }
  }, [props.yearMonth, props.selectedDates, betweenIncludeDates])

  return (
    <Root>
      <Calendar>
        <WeekGrid>
          {dateInfo.weekdays.map((weekday) => {
            return (
              <WeekItem key={weekday.key}>
                {weekday.label}
              </WeekItem>
            );
          })}
        </WeekGrid>
        <DayGrid
          onMouseLeave={() => {
            props.onHoverDateChange && props.onHoverDateChange(undefined)
          }}
        >
          {dateInfo.days.map((day) => {
            const hovering = props.hoverDate ? isSameDay(day.date, props.hoverDate) : false
            const inHoverRange = (() => {
              if (props.selectedDates.length === 1 && props.hoverDate) {
                return betweenIncludeDates(day.date, [props.selectedDates[0], props.hoverDate].sort((i, j) => Number(i) - Number(j)))
              }
              return false
            })()
            return (
              <DayItem
                {...NodeDataProps({
                  'active': day.active,
                  'selected': day.selected,
                  'in-selected-range': day.inSelectedRange,
                  'in-hover-range': inHoverRange,
                  'hovering': hovering,
                })}
                key={day.key}
                onClick={() => {
                  props.onSelect(day.date)
                }}
                onMouseEnter={() => {
                  props.onHoverDateChange && props.onHoverDateChange(day.date)
                }}
                onMouseLeave={() => {
                  props.onHoverDateChange && props.onHoverDateChange(undefined)
                }}
              >
                {day.label}
              </DayItem>
            )
          })}
        </DayGrid>
      </Calendar>
    </Root>
  )
})
Example #21
Source File: DateRangePicker.tsx    From UUI with MIT License 4 votes vote down vote up
DateRangePicker = UUIFunctionComponent({
  name: 'DateRangePicker',
  nodes: {
    Root: 'div',
    ConnectIcon: Icons.ArrowRight,
    CalendarIcon: Icons.Calendar,
    Popover: UUIPopover,
    TextField: UUITextField,
    Activator: 'div',
    Container: 'div',
    Toolbar: 'div',
    Main: 'div',
    StartSection: 'div',
    EndSection: 'div',
    YearMonthSelect: UUIYearMonthSelect,
    DateSelect: UUIDateSelect,
    DateTimeShortcut: UUIDateTimeShortcut,
  },
  propTypes: DateRangePickerPropTypes,
}, (props: DateRangePickerFeatureProps, { nodes }) => {
  const {
    Root, ConnectIcon, CalendarIcon, Popover, TextField,
    Activator, Container, Toolbar, Main, StartSection, EndSection,
    YearMonthSelect, DateSelect, DateTimeShortcut,
  } = nodes

  const startInputRef = useRef<HTMLInputElement | null>(null)
  const endInputRef = useRef<HTMLInputElement | null>(null)
  const [whichFocusing, setWhichFocusing] = useState<'start' | 'end'>()
  const [active, setActive] = useState(false)

  const [hoverDate, setHoverDate] = useState<Date>()

  const initialInnerValue = useMemo<DateRangePickerInnerValue>(() => {
    if (props.value === null) {
      return {
        startDate: null,
        endDate: null,
        startInput: '',
        endInput: '',
        startYearMonth: startOfMonth(new Date),
        endYearMonth: add(startOfMonth(new Date), { months: 1 }),
      }
    }
    return {
      startDate: props.value[0],
      endDate: props.value[1],
      startInput: formatDate(props.value[0]),
      endInput: formatDate(props.value[1]),
      startYearMonth: startOfMonth(props.value[0]),
      endYearMonth: isSameMonth(props.value[0], props.value[1]) ? add(startOfMonth(props.value[1]), { months: 1 }) : props.value[1],
    }
  }, [props.value])
  const [innerValue, setInnerValue, resetInnerValue] = usePendingValue<DateRangePickerInnerValue>(initialInnerValue, (value) => {
    if (value.startDate && value.endDate) {
      handleValueOnChange([value.startDate, value.endDate])
      setActive(false)
    }
  }, { resetWhenInitialValueChanged: true })

  const selectedDates = useMemo(() => {
    return compact([innerValue.startDate, innerValue.endDate])
  }, [innerValue.endDate, innerValue.startDate])

  const handleValueOnChange = useCallback((value: [Date, Date] | null) => {
    const sortedValue = value?.sort((i, j) => Number(i) - Number(j)) || null
    props.onChange(sortedValue)
  }, [props])
  /**
   *
   */
  const handleInputOnSubmit = useCallback((type: 'start' | 'end') => {
    if (innerValue.startDate && innerValue.endDate) {
      const originalInput = formatDate(props.value && (type === 'start' ? props.value[0] : props.value[1]))
      const input = type === 'start' ? innerValue.startInput : innerValue.endInput
      if (originalInput === input) return;
      try {
        if (input === '') {
          handleValueOnChange(null)
        } else {
          const result = tryParseDateFromString(input)
          handleValueOnChange(type === 'start' ? [result, innerValue.endDate] : [innerValue.startDate, result])
        }
      } catch {
        resetInnerValue()
      }
    }
  }, [handleValueOnChange, innerValue.endInput, innerValue.endDate, innerValue.startInput, innerValue.startDate, props.value, resetInnerValue])
  /**
   * handle user change year or month in YearMonthSelect.
   */
  const handleStartYearMonthSelect = useCallback((value: Date) => {
    setInnerValue((oldValue) => {
      const startYearMonthDate = value
      let endYearMonthDate = oldValue.endYearMonth
      if (!isBefore(startYearMonthDate, endYearMonthDate)) {
        endYearMonthDate = add(startYearMonthDate, { months: 1 })
      }
      return {
        ...oldValue,
        startYearMonth: startYearMonthDate,
        endYearMonth: endYearMonthDate,
      }
    })
  }, [setInnerValue])
  const handleEndYearMonthSelect = useCallback((value: Date) => {
    setInnerValue((oldValue) => {
      const endYearMonthDate = value
      let startYearMonthDate = oldValue.startYearMonth
      if (!isAfter(endYearMonthDate, startYearMonthDate)) {
        startYearMonthDate = add(endYearMonthDate, { months: -1 })
      }
      return {
        ...oldValue,
        startYearMonth: startYearMonthDate,
        endYearMonth: endYearMonthDate,
      }
    })
  }, [setInnerValue])
  /**
   * handle user select date in DateSelect.
   */
  const handleDateSelect = useCallback((value: Date) => {
    let shouldSubmit = false
    let newStartValue = innerValue.startDate
    let newEndValue = innerValue.endDate
    if (
      (newStartValue !== null && newEndValue !== null) ||
      (newStartValue === null && newEndValue === null)
    ) {
      if (whichFocusing === 'end') {
        newStartValue = null
        newEndValue = value
      } else {
        newStartValue = value
        newEndValue = null
      }
    } else {
      if (newStartValue === null) newStartValue = value
      if (newEndValue === null) newEndValue = value
      if (isAfter(newStartValue, newEndValue)) {
        const tmp = new Date(newStartValue)
        newStartValue = new Date(newEndValue)
        newEndValue = tmp
      }
      shouldSubmit = true
    }
    setInnerValue((oldValue) => {
      return {
        ...oldValue,
        startDate: newStartValue,
        startInput: formatDate(newStartValue),
        endDate: newEndValue,
        endInput: formatDate(newEndValue),
      }
    }, shouldSubmit)
  }, [innerValue.endDate, innerValue.startDate, setInnerValue, whichFocusing])

  return (
    <Root>
      <Popover
        placement={'bottom-start'}
        active={active}
        onClickAway={() => { setActive(false); resetInnerValue(); }}
        activator={
          <Activator
            onClick={() => {
              setActive(true)
              setTimeout(() => {
                if (whichFocusing === undefined && startInputRef.current) {
                  startInputRef.current.focus()
                }
              }, 0)
            }}
          >
            <TextField
              placeholder={props.startPlaceholder}
              value={innerValue.startInput}
              onChange={(value) => { setInnerValue((oldValue) => ({ ...oldValue, startInput: value })) }}
              customize={{
                Input: {
                  ref: startInputRef,
                  onFocus: () => {
                    setWhichFocusing('start')
                  },
                  onBlur: () => {
                    setWhichFocusing(undefined)
                    handleInputOnSubmit('start')
                  },
                  onKeyDown: (event) => {
                    if (event.key === 'Enter') {
                      handleInputOnSubmit('start')
                    }
                  }
                }
              }}
            />
            <ConnectIcon />
            <TextField
              placeholder={props.endPlaceholder}
              value={innerValue.endInput}
              onChange={(value) => { setInnerValue((oldValue) => ({ ...oldValue, endInput: value })) }}
              customize={{
                Input: {
                  ref: endInputRef,
                  onFocus: () => {
                    setWhichFocusing('end')
                  },
                  onBlur: () => {
                    setWhichFocusing(undefined)
                    handleInputOnSubmit('end')
                  },
                  onKeyDown: (event) => {
                    if (event.key === 'Enter') {
                      handleInputOnSubmit('end')
                    }
                  }
                }
              }}
            />
            <CalendarIcon />
          </Activator>
        }
      >
        <Container>
          <Toolbar>
            {props.shortcuts && (
              <DateTimeShortcut
                options={props.shortcuts}
                onSelect={(value) => {
                  handleValueOnChange(value)
                  setActive(false)
                }}
              />
            )}
          </Toolbar>
          <Main tabIndex={-1}>
            <StartSection>
              <YearMonthSelect
                value={innerValue.startYearMonth}
                onChange={handleStartYearMonthSelect}
              />
              <DateSelect
                yearMonth={innerValue.startYearMonth}
                selectedDates={selectedDates}
                onSelect={handleDateSelect}
                hoverDate={hoverDate}
                onHoverDateChange={(date) => { setHoverDate(date) }}
              />
            </StartSection>
            <EndSection>
              <YearMonthSelect
                value={innerValue.endYearMonth}
                onChange={handleEndYearMonthSelect}
              />
              <DateSelect
                yearMonth={innerValue.endYearMonth}
                selectedDates={selectedDates}
                onSelect={handleDateSelect}
                hoverDate={hoverDate}
                onHoverDateChange={(date) => { setHoverDate(date) }}
              />
            </EndSection>
          </Main>
        </Container>
      </Popover>
    </Root>
  )
})
Example #22
Source File: tracker.spec.ts    From tempomat with MIT License 4 votes vote down vote up
describe('tracker', () => {
    test('can be started for an issue', async () => {
        await clearStore()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: baseDate.getTime(),
            isActive: true,
            intervals: []
        })
    })

    test('can be started with description', async () => {
        await clearStore()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'description',
            now: baseDate
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: 'description',
            activeTimestamp: baseDate.getTime(),
            isActive: true,
            intervals: []
        })
    })

    test('cannot be started twice', async () => {
        await clearStore()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'description',
            now: baseDate
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: baseDate.getTime(),
            isActive: true,
            intervals: []
        })
    })

    test('can be started for an alias', async () => {
        await clearStore()
        await aliases.set('lunch', 'ABC-123')

        await tempo.startTracker({
            issueKeyOrAlias: 'lunch',
            now: baseDate
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: baseDate.getTime(),
            isActive: true,
            intervals: []
        })
    })

    test('can be stopped', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'start description',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            description: 'start description',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
    })

    test('can be started after being stopped', async () => {
        await clearStore()
        authenticate()
        mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'second start',
            now: add(baseDate, { minutes: 60 })
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: 'second start',
            activeTimestamp: baseDate.getTime() + 60 * 60 * 1_000,
            isActive: true,
            intervals: []
        })
    })

    test('cannot be stopped if not started', async () => {
        await clearStore()
        const apiMock = mockWorklogResponse()

        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })

        expect(apiMock).not.toBeCalled()
    })

    test('can be stopped for an alias', async () => {
        await clearStore()
        authenticate()
        await aliases.set('lunch', 'ABC-123')
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'lunch',
            now: add(baseDate, { minutes: 10 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
    })

    test('can be stopped with remaining estimate for each interval', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            remainingEstimate: '1h',
            now: add(baseDate, { minutes: 30 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00',
            remainingEstimateSeconds: 3600
        })
        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:20:00',
            remainingEstimateSeconds: 3600
        })
    })

    test('does not log intervals below 1 minute', async () => {
        await clearStore()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { seconds: 59 })
        })

        expect(apiMock).not.toBeCalled()
    })

    test('can be paused and resumed', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 30 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:20:00'
        })
    })

    test('does not have to be resumed to be stopped', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 30 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
    })

    test('can be paused consecutively multiple times', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()
        const pauses = Array.from(Array(5), (_, i) => (i + 2))

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        for (const pause of pauses) {
            await tempo.pauseTracker({
                issueKeyOrAlias: 'ABC-123',
                now: add(baseDate, { minutes: pause })
            })
        }
        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })
        for (const pause of pauses) {
            await tempo.pauseTracker({
                issueKeyOrAlias: 'ABC-123',
                now: add(baseDate, { minutes: 20 + pause })
            })
        }
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 40 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 120,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 120,
            startDate: '2020-02-28',
            startTime: '12:20:00'
        })
    })

    test('does not have to be paused to be resumed', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 1200,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
    })

    test('can be resumed consecutively multiple times', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()
        const resumes = Array.from(Array(5), (_, i) => (i + 2))

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        for (const resume of resumes) {
            await tempo.resumeTracker({
                issueKeyOrAlias: 'ABC-123',
                now: add(baseDate, { minutes: resume })
            })
        }
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })
        for (const resume of resumes) {
            await tempo.resumeTracker({
                issueKeyOrAlias: 'ABC-123',
                now: add(baseDate, { minutes: 20 + resume })
            })
        }
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 40 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 1200,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 1080,
            startDate: '2020-02-28',
            startTime: '12:22:00'
        })
    })

    test('is not removed if worklog fails to update', async () => {
        await clearStore()
        authenticate()
        mockWorklogFailure()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: baseDate.getTime(),
            isActive: false,
            intervals: [{
                start: baseDate.getTime(),
                end: add(baseDate, { minutes: 10 }).getTime()
            }]
        })
    })

    test('overwrites description for stop', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'start description',
            now: baseDate
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            description: 'stop description',
            now: add(baseDate, { minutes: 10 })
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            description: 'stop description',
            timeSpentSeconds: 600,
            startDate: '2020-02-28',
            startTime: '12:00:00'
        })
    })

    test('removes only logged intervals for failure', async () => {
        await clearStore()
        authenticate()
        api.addWorklog = jest.fn()
            .mockReturnValueOnce({ issue: { key: 'ABC-123', self: 'https://example.atlassian.net/rest/api/2/issue/ABC-123' } })
            .mockRejectedValueOnce(new Error('Failed to upload worklog!'))
            .mockRejectedValueOnce(new Error('Failed to upload worklog!'))
            .mockReturnValueOnce({ issue: { key: 'ABC-123', self: 'https://example.atlassian.net/rest/api/2/issue/ABC-123' } })

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 5 })
        })

        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 10 })
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 15 })
        })

        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 20 })
        })
        await tempo.pauseTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 25 })
        })

        await tempo.resumeTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 30 })
        })
        await tempo.stopTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 60 })
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: add(baseDate, { minutes: 30 }).getTime(),
            isActive: false,
            intervals: [
                {
                    start: add(baseDate, { minutes: 10 }).getTime(),
                    end: add(baseDate, { minutes: 15 }).getTime()
                },
                {
                    start: add(baseDate, { minutes: 20 }).getTime(),
                    end: add(baseDate, { minutes: 25 }).getTime()
                }]
        })
    })

    test('can be deleted for an issue', async () => {
        await clearStore()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.deleteTracker({ issueKeyOrAlias: 'ABC-123' })

        expect(await getTracker('ABC-123')).toBeUndefined()
    })

    test('can be deleted for an alias', async () => {
        await clearStore()
        await aliases.set('lunch', 'ABC-123')

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.deleteTracker({ issueKeyOrAlias: 'lunch' })

        expect(await getTracker('ABC-123')).toBeUndefined()
    })

    test('is not deleted for different key', async () => {
        await clearStore()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.deleteTracker({ issueKeyOrAlias: 'ABC-124' })

        expect(await getTracker('ABC-123')).not.toBeUndefined()
    })

    test('stop previous flag can start a tracker for the same issue', async () => {
        await clearStore()
        authenticate()
        const apiMock = mockWorklogResponse()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 30 }),
            stopPreviousTracker: true,
            description: 'second tracker'
        })

        expect(apiMock).toBeCalledWith({
            issueKey: 'ABC-123',
            timeSpentSeconds: 1800,
            startDate: '2020-02-28',
            startTime: '12:00:00',
            description: undefined
        })
        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: 'second tracker',
            activeTimestamp: add(baseDate, { minutes: 30 }).getTime(),
            isActive: true,
            intervals: []
        })
    })

    test('stop previous flag does not start a tracker for the same issue for log failures', async () => {
        await clearStore()
        authenticate()
        mockWorklogFailure()

        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: baseDate
        })
        await tempo.startTracker({
            issueKeyOrAlias: 'ABC-123',
            now: add(baseDate, { minutes: 30 }),
            stopPreviousTracker: true
        })

        expect(await getTracker('ABC-123')).toEqual({
            issueKey: 'ABC-123',
            description: undefined,
            activeTimestamp: baseDate.getTime(),
            isActive: false,
            intervals: [{
                start: baseDate.getTime(),
                end: add(baseDate, { minutes: 30 }).getTime()
            }]
        })
    })
})
Example #23
Source File: networkRevenueProvider.ts    From akashlytics with GNU General Public License v3.0 4 votes vote down vote up
getWeb3IndexRevenue = async (debug: boolean) => {
  if (!debug && cachedRevenue && cachedRevenueDate && Math.abs(differenceInMinutes(cachedRevenueDate, new Date())) < 30) {
    return cachedRevenue;
  }

  while (isCalculatingRevenue || isSyncingPrices) {
    await sleep(5000);
  }

  const dailyNetworkRevenues = await getDailyRevenue();
  // console.log(dailyNetworkRevenues[0]);
  let days = dailyNetworkRevenues.map((r) => ({
    date: r.date.getTime() / 1000,
    revenue: round(r.usd, 2),
    revenueUAkt: r.uakt,
    aktPrice: r.aktPrice,
    dateStr: r.date
  }));

  const today = getTodayUTC();
  const oneDayAgo = add(today, { days: -1 });
  const twoDaysAgo = add(today, { days: -2 });
  const oneWeekAgo = add(today, { weeks: -1 });
  const twoWeeksAgo = add(today, { weeks: -2 });
  const thirtyDaysAgo = add(today, { days: -30 });
  const sixtyDaysAgo = add(today, { days: -60 });
  const ninetyDaysAgo = add(today, { days: -90 });
  let totalRevenue: number = 0,
    oneDayAgoRevenue: number = 0,
    twoDaysAgoRevenue: number = 0,
    oneWeekAgoRevenue: number = 0,
    twoWeeksAgoRevenue: number = 0,
    thirtyDaysAgoRevenue: number = 0,
    sixtyDaysAgoRevenue: number = 0,
    ninetyDaysAgoRevenue: number = 0;
  let totalRevenueUAkt: number = 0,
    oneDayAgoRevenueUAkt: number = 0,
    twoDaysAgoRevenueUAkt: number = 0,
    oneWeekAgoRevenueUAkt: number = 0,
    twoWeeksAgoRevenueUAkt: number = 0,
    thirtyDaysAgoRevenueUAkt: number = 0,
    sixtyDaysAgoRevenueUAkt: number = 0,
    ninetyDaysAgoRevenueUAkt: number = 0;

  days.forEach((b) => {
    const date = new Date(b.date * 1000);

    if (date <= ninetyDaysAgo) {
      ninetyDaysAgoRevenue += b.revenue;
      ninetyDaysAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= sixtyDaysAgo) {
      sixtyDaysAgoRevenue += b.revenue;
      sixtyDaysAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= thirtyDaysAgo) {
      thirtyDaysAgoRevenue += b.revenue;
      thirtyDaysAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= twoWeeksAgo) {
      twoWeeksAgoRevenue += b.revenue;
      twoWeeksAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= oneWeekAgo) {
      oneWeekAgoRevenue += b.revenue;
      oneWeekAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= twoDaysAgo) {
      twoDaysAgoRevenue += b.revenue;
      twoDaysAgoRevenueUAkt += b.revenueUAkt;
    }
    if (date <= oneDayAgo) {
      oneDayAgoRevenue += b.revenue;
      oneDayAgoRevenueUAkt += b.revenueUAkt;
    }

    totalRevenue += b.revenue;
    totalRevenueUAkt += b.revenueUAkt;
  }, 0);

  if (!debug) {
    days = days.map(({ dateStr, revenueUAkt, aktPrice, ...others }) => others) as any;
  }

  let revenueStats = {
    now: round(totalRevenue),
    oneDayAgo: round(oneDayAgoRevenue),
    twoDaysAgo: round(twoDaysAgoRevenue),
    oneWeekAgo: round(oneWeekAgoRevenue),
    twoWeeksAgo: round(twoWeeksAgoRevenue),
    thirtyDaysAgo: round(thirtyDaysAgoRevenue),
    sixtyDaysAgo: round(sixtyDaysAgoRevenue),
    ninetyDaysAgo: round(ninetyDaysAgoRevenue)
  };

  if (debug) {
    revenueStats = {
      ...revenueStats,
      nowAkt: uaktToAKT(totalRevenueUAkt, 6),
      oneDayAgoAkt: uaktToAKT(oneDayAgoRevenueUAkt, 6),
      twoDaysAgoAkt: uaktToAKT(twoDaysAgoRevenueUAkt, 6),
      oneWeekAgoAkt: uaktToAKT(oneWeekAgoRevenueUAkt, 6),
      twoWeeksAgAkt: uaktToAKT(twoWeeksAgoRevenueUAkt, 6),
      thirtyDaysAgoAkt: uaktToAKT(thirtyDaysAgoRevenueUAkt, 6),
      sixtyDaysAgoAkt: uaktToAKT(sixtyDaysAgoRevenueUAkt, 6),
      ninetyDaysAgoAkt: uaktToAKT(ninetyDaysAgoRevenueUAkt, 6)
    } as any;
  }

  const responseObj = {
    revenue: revenueStats,
    days
  };

  cachedRevenue = responseObj;
  cachedRevenueDate = new Date();

  return responseObj;
}