diff --git a/docs/testing-env.md b/docs/testing-env.md index 19a472c8b..4a65a8adc 100644 --- a/docs/testing-env.md +++ b/docs/testing-env.md @@ -3,7 +3,7 @@ ### Branches - `main` - - branch `main` is the base branch for current developement + - branch `main` is the base branch for current development - every feature branch should be merged into `main` via pull request - `stage-live` - base for running the staging environment that is running on the live chain diff --git a/src/redux-state/selectors/population.ts b/src/redux-state/selectors/population.ts index a13a8f625..44ab70435 100644 --- a/src/redux-state/selectors/population.ts +++ b/src/redux-state/selectors/population.ts @@ -1,22 +1,33 @@ import { createSelector } from "@reduxjs/toolkit" -import { RealmData } from "shared/types" +import { + getDisplayedPopulationOfRealms, + getPopulationOfRealms, +} from "redux-state/utils/population" import { selectRealmById, selectRealms } from "./realm" -const getPopulationOfRealms = (realms: RealmData[]) => - realms.map((realm) => realm.population) +type PopulationKey = "population" | "displayedPopulation" -export const selectSortedPopulation = createSelector(selectRealms, (realms) => { - const realmsData = Object.entries(realms).map(([id, data]) => ({ - id, - ...data, - })) +const getPopulationById = (key: PopulationKey) => + createSelector(selectRealmById, (realm) => realm?.[key] ?? 0) - return realmsData.sort((a, b) => a.population - b.population) -}) +const sortPopulation = (key: PopulationKey) => + createSelector(selectRealms, (realms) => { + const realmsData = Object.entries(realms).map(([id, data]) => ({ + id, + ...data, + })) -export const selectPopulationById = createSelector( - selectRealmById, - (realm) => realm?.population ?? 0 + return realmsData.sort((a, b) => a[key] - b[key]) + }) + +export const selectSortedPopulation = sortPopulation("population") +export const selectSortedDisplayedPopulation = sortPopulation( + "displayedPopulation" +) + +export const selectPopulationById = getPopulationById("population") +export const selectDisplayedPopulationById = getPopulationById( + "displayedPopulation" ) export const selectTotalPopulation = createSelector( @@ -25,7 +36,24 @@ export const selectTotalPopulation = createSelector( realms.length ? getPopulationOfRealms(realms).reduce((a, b) => a + b) : 0 ) +export const selectTotalDisplayedPopulation = createSelector( + selectSortedDisplayedPopulation, + (realms) => + realms.length + ? getDisplayedPopulationOfRealms(realms).reduce((a, b) => a + b) + : 0 +) + export const selectMaxPopulation = createSelector( selectSortedPopulation, - (realms) => (realms.length ? Math.max(...getPopulationOfRealms(realms)) : 0) + (realms) => + realms.length ? getPopulationOfRealms(realms)[realms.length - 1] : 0 +) + +export const selectMaxDisplayedPopulation = createSelector( + selectSortedDisplayedPopulation, + (realms) => + realms.length + ? getDisplayedPopulationOfRealms(realms)[realms.length - 1] + : 0 ) diff --git a/src/redux-state/slices/island.ts b/src/redux-state/slices/island.ts index 0ef26e9ee..a2c980e48 100644 --- a/src/redux-state/slices/island.ts +++ b/src/redux-state/slices/island.ts @@ -3,9 +3,9 @@ import { LeaderboardData, UnclaimedXpData, OverlayType, - RealmData, RealmDataWithId, SeasonInfo, + RealmDataById, } from "shared/types" export type IslandModeType = "default" | "join-realm" @@ -13,7 +13,7 @@ export type IslandModeType = "default" | "join-realm" export type IslandState = { mode: IslandModeType overlay: OverlayType - realms: { [id: string]: RealmData } + realms: RealmDataById leaderboards: { [id: string]: LeaderboardData } unclaimedXp: { [id: string]: UnclaimedXpData[] } stakingRealmId: string | null @@ -75,6 +75,15 @@ const islandSlice = createSlice({ immerState.realms[realmPopulation.id].population = realmPopulation.population }, + setRealmDisplayedPopulation: ( + immerState, + { + payload: realmPopulation, + }: { payload: { id: string; population: number } } + ) => { + immerState.realms[realmPopulation.id].displayedPopulation = + realmPopulation.population + }, setRealmXpAllocatable: ( immerState, { @@ -139,6 +148,7 @@ export const { resetIslandDisplay, resetIslandAccount, setRealmPopulation, + setRealmDisplayedPopulation, setRealmXpAllocatable, setRealmsData, setDisplayedRealmId, diff --git a/src/redux-state/thunks/island.ts b/src/redux-state/thunks/island.ts index c2baa2d86..dc42771d8 100644 --- a/src/redux-state/thunks/island.ts +++ b/src/redux-state/thunks/island.ts @@ -6,6 +6,7 @@ import { setRealmXpAllocatable, setRealmsData, setSeasonInfo, + setRealmDisplayedPopulation, } from "redux-state/slices/island" import { REALMS_WITH_CONTRACT_NAME, @@ -29,7 +30,11 @@ import { UnclaimedXpData, } from "shared/types" import { updateTransactionStatus } from "redux-state/slices/wallet" -import { bigIntToUserAmount, getAllowanceTransactionID } from "shared/utils" +import { + bigIntToUserAmount, + getAllowanceTransactionID, + isDisplayedPopulationAvailable, +} from "shared/utils" import { getRealmLeaderboardData, getUserLeaderboardRank, @@ -107,8 +112,14 @@ export const fetchPopulation = createDappAsyncThunk( realmsWithAddress, }) + const displayedPopulationAvailable = isDisplayedPopulationAvailable(realms) + if (result) { - result.forEach((data) => dispatch(setRealmPopulation(data))) + result.forEach((data) => { + dispatch(setRealmPopulation(data)) + if (!displayedPopulationAvailable) + dispatch(setRealmDisplayedPopulation(data)) + }) } return !!result diff --git a/src/redux-state/utils/population.ts b/src/redux-state/utils/population.ts new file mode 100644 index 000000000..ece3e6637 --- /dev/null +++ b/src/redux-state/utils/population.ts @@ -0,0 +1,7 @@ +import { RealmData } from "shared/types" + +export const getPopulationOfRealms = (realms: RealmData[]) => + realms.map((realm) => realm.population) + +export const getDisplayedPopulationOfRealms = (realms: RealmData[]) => + realms.map((realm) => realm.displayedPopulation) diff --git a/src/shared/assets/partners/arbitrum-population.svg b/src/shared/assets/partners/arbitrum-population.svg new file mode 100644 index 000000000..278a2ea3e --- /dev/null +++ b/src/shared/assets/partners/arbitrum-population.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/partners/cyberconnect-population.svg b/src/shared/assets/partners/cyberconnect-population.svg new file mode 100644 index 000000000..9bd71ee12 --- /dev/null +++ b/src/shared/assets/partners/cyberconnect-population.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/partners/frax-population.svg b/src/shared/assets/partners/frax-population.svg new file mode 100644 index 000000000..aee020d86 --- /dev/null +++ b/src/shared/assets/partners/frax-population.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/partners/galxe-population.svg b/src/shared/assets/partners/galxe-population.svg new file mode 100644 index 000000000..ceb4c4326 --- /dev/null +++ b/src/shared/assets/partners/galxe-population.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/assets/partners/gitcoin-population.svg b/src/shared/assets/partners/gitcoin-population.svg new file mode 100644 index 000000000..4ec576b7e --- /dev/null +++ b/src/shared/assets/partners/gitcoin-population.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/components/Input.tsx b/src/shared/components/Input.tsx index 4fabd2509..bba0e2b2e 100644 --- a/src/shared/components/Input.tsx +++ b/src/shared/components/Input.tsx @@ -51,6 +51,7 @@ export default function SharedInput({
(null) + + useEffect(() => { + const icon = getRealmPopulationIcon(realmId) + setIconSrc(icon) + }, [realmId]) + + const [bubbleProps] = useSpring( + () => ({ + from: STYLE.default, + to: showBubble ? STYLE.highlight : STYLE.default, + config: BUBBLE_CONFIG, + onRest: () => setShowBubble(false), + }), + [showBubble] + ) + + if (!iconSrc) return null + + return ( + +
+ Bubble +
+
+ ) +} diff --git a/src/shared/components/RealmCutout/RealmCutout.tsx b/src/shared/components/RealmCutout/RealmCutout.tsx index 756a3ae9c..b8da2f2e5 100644 --- a/src/shared/components/RealmCutout/RealmCutout.tsx +++ b/src/shared/components/RealmCutout/RealmCutout.tsx @@ -8,6 +8,7 @@ import { useDappSelector, } from "redux-state" import RealmPin from "./RealmPin" +import Bubble from "./Bubble" const CUTOUT_HEIGHT = 208 const CUTOUT_WIDTH = 356 @@ -27,6 +28,7 @@ export default function RealmCutout() { return ( <>
+ {isStakedRealm && } Population - {separateThousandsByComma(realm?.population ?? 0)} + + {separateThousandsByComma(realm?.displayedPopulation ?? 0)} +
selectTokenBalanceByAddress(state, displayedRealmVeTokenAddress) ) - const veTahoUserAmount = bigIntToDisplayUserAmount(veTahoBalance) + const veTahoUserAmount = bigIntToDisplayUserAmount(veTahoBalance, 18, 5) return ( <> diff --git a/src/shared/components/TokenAmountInput.tsx b/src/shared/components/TokenAmountInput.tsx index ddd035ef9..52a2ef7de 100644 --- a/src/shared/components/TokenAmountInput.tsx +++ b/src/shared/components/TokenAmountInput.tsx @@ -1,8 +1,8 @@ -import React from "react" +import React, { useCallback, useEffect, useState } from "react" import { userAmountToBigInt, bigIntToDisplayUserAmount, - bigIntToUserAmount, + bigIntToPreciseUserAmount, } from "shared/utils" import { selectTokenBalanceByAddress, @@ -46,12 +46,14 @@ export default function TokenAmountInput({ }: { label?: string inputLabel: string - amount: string + amount: bigint | null tokenAddress: string disabled?: boolean - onChange: (value: string) => void + onChange: (value: bigint | null) => void onValidate?: (value: boolean) => void }) { + const [textAmount, setTextAmount] = useState("") + const balance = useDappSelector((state) => selectTokenBalanceByAddress(state, tokenAddress) ) @@ -59,29 +61,57 @@ export default function TokenAmountInput({ selectTokenSymbolByAddress(state, tokenAddress) ) - const validate = (value: string) => { - const result = handleValidate(value, balance) - const hasError = "error" in result + const validate = useCallback( + (value: string) => { + const result = handleValidate(value, balance) + const hasError = "error" in result - onValidate?.(!hasError) - return result - } + onValidate?.(!hasError) + return result + }, + [balance, onValidate] + ) + + useEffect(() => { + const textToBigIntAmount = + textAmount === "" ? null : userAmountToBigInt(textAmount) ?? 0n + + const bigIntToTextAmount = bigIntToPreciseUserAmount(balance) + + // As we may be loosing some precision, we need to compare the values. + // Clicking "Max" button may result in bigint that is too big to be + // represented as a float number. In this case we need to compare values to + // not override the external value that stores the bigint using greater precision. + if (textToBigIntAmount !== amount && textAmount !== bigIntToTextAmount) { + onChange(textToBigIntAmount) + } + + // Make sure this is working only one way: + // from the text provided by input to the parent component + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [textAmount, onChange]) + + useEffect(() => { + // Allow clearing the input from parent componentthis should be the only case + // where parent component is allowed to change the value + if (amount === null) { + setTextAmount("") + } + }, [amount]) + + const parsedBalance = bigIntToDisplayUserAmount(balance, 18, 4) return (
{label && ( -
{`${label} ${bigIntToDisplayUserAmount( - balance, - 18, - 5 - )} ${symbol}`}
+
{`${label} ${parsedBalance} ${symbol}`}
)} { event.preventDefault() - onChange(bigIntToUserAmount(balance, 18, 18)) + setTextAmount(bigIntToPreciseUserAmount(balance)) + onChange(balance) }} > Max diff --git a/src/shared/constants/chains.ts b/src/shared/constants/chains.ts index 3ec53d8ff..8ab31978f 100644 --- a/src/shared/constants/chains.ts +++ b/src/shared/constants/chains.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import { ethers } from "ethers" +import { SECOND } from "./time" export const getArbitrumRpcUrl = () => { if (process.env.USE_LOCALHOST_FORK === "true") { @@ -48,7 +49,8 @@ export const TAHO_ADDRESS = CONTRACT_Taho export const TAHO_SYMBOL = "TAHO" export const ETH_SYMBOL = "ETH" -export const BALANCE_UPDATE_INTERVAL = 30 * 1000 +export const BALANCE_UPDATE_INTERVAL = 30 * SECOND +export const POPULATION_FETCH_INTERVAL = 15 * SECOND export const CONTRACT_DEPLOYMENT_BLOCK_NUMBER = process.env .CONTRACT_DEPLOYMENT_BLOCK_NUMBER diff --git a/src/shared/constants/partners-icons.ts b/src/shared/constants/partners-icons.ts new file mode 100644 index 000000000..11aeaa868 --- /dev/null +++ b/src/shared/constants/partners-icons.ts @@ -0,0 +1,39 @@ +import arbitrum from "shared/assets/partners/arbitrum.svg" +import arbitrumPopulation from "shared/assets/partners/arbitrum-population.svg" +import cyberconnect from "shared/assets/partners/cyberconnect.svg" +import cyberconnectPopulation from "shared/assets/partners/cyberconnect-population.svg" +import frax from "shared/assets/partners/frax.svg" +import fraxPopulation from "shared/assets/partners/frax-population.svg" +import galxe from "shared/assets/partners/galxe.svg" +import galxePopulation from "shared/assets/partners/galxe-population.svg" +import gitcoin from "shared/assets/partners/gitcoin.svg" +import gitcoinPopulation from "shared/assets/partners/gitcoin-population.svg" +import arbitrumShadow from "shared/assets/partners/arbitrum-shadow.svg" +import cyberconnectShadow from "shared/assets/partners/cyberconnect-shadow.svg" +import fraxShadow from "shared/assets/partners/frax-shadow.svg" +import galxeShadow from "shared/assets/partners/galxe-shadow.svg" +import gitcoinShadow from "shared/assets/partners/gitcoin-shadow.svg" + +export default { + arbitrum: { + default: arbitrum, + shadow: arbitrumShadow, + population: arbitrumPopulation, + }, + cyberconnect: { + default: cyberconnect, + shadow: cyberconnectShadow, + population: cyberconnectPopulation, + }, + frax: { default: frax, shadow: fraxShadow, population: fraxPopulation }, + galxe: { + default: galxe, + shadow: galxeShadow, + population: galxePopulation, + }, + gitcoin: { + default: gitcoin, + shadow: gitcoinShadow, + population: gitcoinPopulation, + }, +} diff --git a/src/shared/constants/partners.ts b/src/shared/constants/partners.ts deleted file mode 100644 index 8ab975791..000000000 --- a/src/shared/constants/partners.ts +++ /dev/null @@ -1,18 +0,0 @@ -import arbitrum from "shared/assets/partners/arbitrum.svg" -import cyberconnect from "shared/assets/partners/cyberconnect.svg" -import frax from "shared/assets/partners/frax.svg" -import galxe from "shared/assets/partners/galxe.svg" -import gitcoin from "shared/assets/partners/gitcoin.svg" -import arbitrumShadow from "shared/assets/partners/arbitrum-shadow.svg" -import cyberconnectShadow from "shared/assets/partners/cyberconnect-shadow.svg" -import fraxShadow from "shared/assets/partners/frax-shadow.svg" -import galxeShadow from "shared/assets/partners/galxe-shadow.svg" -import gitcoinShadow from "shared/assets/partners/gitcoin-shadow.svg" - -export default { - arbitrum: { default: arbitrum, shadow: arbitrumShadow }, - cyberconnect: { default: cyberconnect, shadow: cyberconnectShadow }, - frax: { default: frax, shadow: fraxShadow }, - galxe: { default: galxe, shadow: galxeShadow }, - gitcoin: { default: gitcoin, shadow: gitcoinShadow }, -} diff --git a/src/shared/constants/realms-data.ts b/src/shared/constants/realms-data.ts index a6cd44754..d218f6d5c 100644 --- a/src/shared/constants/realms-data.ts +++ b/src/shared/constants/realms-data.ts @@ -1,4 +1,4 @@ -import partners from "./partners" +import partners from "./partners-icons" /* eslint-disable @typescript-eslint/no-loss-of-precision */ export const realm1 = { @@ -71,7 +71,7 @@ export const realm4 = { color: "#D1F5F5", labelX: 380, labelY: 216.5, - partnerLogo: partners.gitcoin, + partnerIcons: partners.gitcoin, partnerColor: "#1D4E56", } @@ -127,7 +127,7 @@ export const realm7 = { color: "#6FE2A5", labelX: 540, labelY: 400, - partnerLogo: partners.cyberconnect, + partnerIcons: partners.cyberconnect, partnerColor: "#fff", } @@ -165,7 +165,7 @@ export const realm9 = { color: "#12AAFF", labelX: 700, labelY: 350, - partnerLogo: partners.arbitrum, + partnerIcons: partners.arbitrum, } export const realm10 = { @@ -357,7 +357,7 @@ export const realm19 = { color: "#1E5DFF", labelX: 264, labelY: 409, - partnerLogo: partners.galxe, + partnerIcons: partners.galxe, } export const realm20 = { @@ -411,7 +411,7 @@ export const realm22 = { color: "#E4EEEE", labelX: 337, labelY: 388, - partnerLogo: partners.frax, + partnerIcons: partners.frax, } export const realm23 = { diff --git a/src/shared/constants/realms.ts b/src/shared/constants/realms.ts index 3036dc32f..ba529ecb8 100644 --- a/src/shared/constants/realms.ts +++ b/src/shared/constants/realms.ts @@ -84,6 +84,17 @@ export function getRealmColor(realmId: string): string { return color } +export function getRealmPopulationIcon(realmId: string): string { + const populationIcon = REALMS_MAP_DATA.find((realm) => realm.id === realmId) + ?.partnerIcons.population + + if (!populationIcon) { + throw new Error(`Missing population icon for realm ${realmId}`) + } + + return populationIcon +} + export const REALM_FONT_SIZE = 78 export const REALM_FONT_FAMILY = "QuincyCF" export const REALM_FONT_STYLE = "bold" diff --git a/src/shared/contracts/game.ts b/src/shared/contracts/game.ts index cf90df8eb..b826ab09e 100644 --- a/src/shared/contracts/game.ts +++ b/src/shared/contracts/game.ts @@ -25,7 +25,7 @@ export const getSeasonInfo: ReadTransactionBuilder = async ( const season = seasonInfo[0].toNumber() - // Season start data is accessible throught .env + // Season start data is accessible through .env // Date requires ms, whereas block.timestamp is in s // const seasonStartTimestamp = seasonInfo[1].toNumber() * 1000 diff --git a/src/shared/contracts/realmsData.ts b/src/shared/contracts/realmsData.ts index 601cd0cd2..afcc39c26 100644 --- a/src/shared/contracts/realmsData.ts +++ b/src/shared/contracts/realmsData.ts @@ -44,6 +44,7 @@ export const getRealmData: ReadTransactionBuilder< questlineUrl, // Population is fetched separately because it should be updated more frequently than other data for the realm. population: 0, + displayedPopulation: 0, // XpAllocatable is fetched after all Realm data is initialized. xpAllocatable: "0", }, diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index cd4099aa1..342fd1b26 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -4,3 +4,4 @@ export * from "./island" export * from "./transactions" export * from "./assistant" export * from "./tenderly" +export * from "./population" diff --git a/src/shared/hooks/population.ts b/src/shared/hooks/population.ts new file mode 100644 index 000000000..d7890698d --- /dev/null +++ b/src/shared/hooks/population.ts @@ -0,0 +1,84 @@ +import { RefObject, useCallback, useEffect, useState } from "react" +import { + selectDisplayedPopulationById, + selectMaxDisplayedPopulation, + selectPopulationById, + selectRealms, + selectSortedDisplayedPopulation, + setRealmDisplayedPopulation, + useDappDispatch, + useDappSelector, +} from "redux-state" +import { SECOND } from "shared/constants" +import { calculatePopulationIconsPositions, randomInteger } from "shared/utils" +import { useInterval } from "./helpers" + +export function useDisplayedPopulation() { + const realms = useDappSelector(selectRealms) + const dispatch = useDappDispatch() + + useEffect(() => { + Object.entries(realms).forEach( + ([id, { population, displayedPopulation }]) => { + if (population < displayedPopulation || !displayedPopulation) { + dispatch(setRealmDisplayedPopulation({ id, population })) + } + } + ) + }, [realms, dispatch]) +} + +export function usePopulationBubble(realmId: string): { + showBubble: boolean + setShowBubble: (newValue: boolean) => void +} { + const population = useDappSelector((state) => + selectPopulationById(state, realmId) + ) + const displayedPopulation = useDappSelector((state) => + selectDisplayedPopulationById(state, realmId) + ) + const dispatch = useDappDispatch() + + const [showBubble, setShowBubble] = useState(false) + const [delay] = useState(randomInteger(5, 15) * SECOND) // Generate random intervals for realms + + const populationCallback = useCallback(() => { + if (population > displayedPopulation) { + dispatch( + setRealmDisplayedPopulation({ + id: realmId, + population: displayedPopulation + 1, + }) + ) + + setShowBubble(true) + } + }, [population, displayedPopulation, dispatch, realmId]) + + useInterval(populationCallback, population ? delay : null) + return { showBubble, setShowBubble } +} + +export function usePopulationIconPositions(ref: RefObject) { + const realmsData = useDappSelector(selectSortedDisplayedPopulation) + const maxPopulation = useDappSelector(selectMaxDisplayedPopulation) + + const [positions, setPositions] = useState([]) + + useEffect(() => { + if (!realmsData.length || !ref.current) return + + const { width } = ref.current.getBoundingClientRect() + + const pos = calculatePopulationIconsPositions( + width, + realmsData, + maxPopulation + ) + + setPositions(pos) + }, [realmsData, maxPopulation, ref]) + + return positions +} diff --git a/src/shared/hooks/wallets.ts b/src/shared/hooks/wallets.ts index 3d5c7fa63..d3c7269a7 100644 --- a/src/shared/hooks/wallets.ts +++ b/src/shared/hooks/wallets.ts @@ -19,6 +19,7 @@ import { ARBITRUM_SEPOLIA_RPC_FALLBACK, BALANCE_UPDATE_INTERVAL, LOCAL_STORAGE_WALLET, + POPULATION_FETCH_INTERVAL, } from "shared/constants" import { Network } from "@ethersproject/networks" import { Logger, defineReadOnly } from "ethers/lib/utils" @@ -119,7 +120,7 @@ export function usePopulationFetch() { useInterval( populationFetchCallback, - account ? BALANCE_UPDATE_INTERVAL * 2 : null + account ? POPULATION_FETCH_INTERVAL : null ) } diff --git a/src/shared/types/island.ts b/src/shared/types/island.ts index d044f00f6..14a5deabd 100644 --- a/src/shared/types/island.ts +++ b/src/shared/types/island.ts @@ -10,6 +10,7 @@ export type RealmAddressesData = { export type RealmContractData = { name: string population: number + displayedPopulation: number xpAllocatable: string xpToken: { name: string; symbol: string; contractAddress: string } } @@ -27,6 +28,8 @@ export type RealmData = RealmAddressesData & export type RealmDataWithId = { id: string; data: RealmData } +export type RealmDataById = { [id: string]: RealmData } + export type RealmContractDataWithId = { id: string data: RealmAddressesData & RealmContractData @@ -78,6 +81,6 @@ export type RealmMapData = { color: string labelX: number labelY: number - partnerLogo: { default: string; shadow: string } + partnerIcons: { default: string; shadow: string; population: string } partnerColor?: string } diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index d7ecf672a..2149d70c9 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -3,6 +3,7 @@ export * from "./address" export * from "./names" export * from "./claim" export * from "./pool" +export * from "./population" export * from "./providers" export * from "./island" export * from "./timers" diff --git a/src/shared/utils/island.ts b/src/shared/utils/island.ts index 97ed73eb6..8dcffdf67 100644 --- a/src/shared/utils/island.ts +++ b/src/shared/utils/island.ts @@ -173,7 +173,7 @@ export function calculatePopulationIconsPositions( const positions: number[] = [] realmsData.forEach((realm, index) => { - const populationShare = realm.population / maxValue + const populationShare = realm.displayedPopulation / maxValue let iconPosition = Math.max( populationShare * width + POPULATION_BAR_GAP, POPULATION_BAR_GAP + index * POPULATION_ICON_SIZE @@ -185,7 +185,7 @@ export function calculatePopulationIconsPositions( } // Realm with biggest population - if (realm.population === maxValue) { + if (realm.displayedPopulation === maxValue) { iconPosition = width - (POPULATION_BAR_GAP + POPULATION_ICON_SIZE) } diff --git a/src/shared/utils/misc.ts b/src/shared/utils/misc.ts index 431f240a1..972471ca1 100644 --- a/src/shared/utils/misc.ts +++ b/src/shared/utils/misc.ts @@ -15,11 +15,26 @@ export function wait(ms: number): Promise { /** * Checks the validity of the entered value from input for token amount. * The value shouldn't be: - * - a empty string - * - a string or other non-numeric value + * - null * - equal or less than zero. + * - an empty string + * - a string or other non-numeric value */ -export function isValidInputAmount(amount: string): boolean { +export function isValidInputAmount( + amount: string | number | bigint | null +): boolean { + if (amount === null) { + return false + } + + if (typeof amount === "bigint") { + return amount > 0n + } + + if (typeof amount === "number") { + return amount > 0 + } + return ( !!amount.trim() && !Number.isNaN(parseFloat(amount)) && diff --git a/src/shared/utils/numbers.ts b/src/shared/utils/numbers.ts index 58982830d..960105934 100644 --- a/src/shared/utils/numbers.ts +++ b/src/shared/utils/numbers.ts @@ -3,10 +3,10 @@ import { FixedPointNumber } from "shared/types/stake" // Matches floating point numbers with optional thousands separators const FLOATING_POINT_REGEX = /^[^0-9]*([0-9,]+)(?:\.([0-9]*))?$/ -export const separateThousandsByComma = ( +export function separateThousandsByComma( value: number | bigint | string, decimals = 2 -): string => { +): string { const adjustedValue = typeof value === "string" ? +value : value return adjustedValue.toLocaleString("en-US", { // @ts-expect-error - `maximumFractionDigits` wants to get number less than 21, @@ -64,6 +64,11 @@ function convertFixedPointNumber( } } +// Generates a random integer in min-max range (inclusively) +export function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + export function userAmountToBigInt( amount: string, decimals = 18 @@ -88,13 +93,41 @@ export function bigIntToUserAmount( const desiredDecimalsAmount = amount / 10n ** BigInt(Math.max(0, decimals - desiredDecimals)) + if (desiredDecimalsAmount > BigInt(Number.MAX_SAFE_INTEGER)) { + // eslint-disable-next-line no-console + console.warn( + `bigIntToUserAmount: amount ${amount} is too big to be represented as a number` + ) + } + return ( Number(desiredDecimalsAmount) / 10 ** Math.min(desiredDecimals, decimals) ).toString() } -// Parse token amount by moving the decimal point and separate thousands by comma +export function bigIntToPreciseUserAmount( + amount: bigint, + decimals = 18 +): string { + let currentPrecision = decimals + + while (currentPrecision >= 0) { + const desiredDecimalsAmount = + amount / 10n ** BigInt(Math.max(0, decimals - currentPrecision)) + + if (desiredDecimalsAmount <= BigInt(Number.MAX_SAFE_INTEGER)) { + return bigIntToUserAmount(amount, decimals, currentPrecision) + } + + currentPrecision -= 1 + } + + return "0" +} + +// Parse token amount by moving the decimal point and separate thousands by comma. +// Gracefully handle amounts smaller than desired precision. export function bigIntToDisplayUserAmount( amount: bigint | string, decimals = 18, @@ -102,8 +135,14 @@ export function bigIntToDisplayUserAmount( ): string { const amountBigInt = typeof amount === "string" ? BigInt(amount) : amount - return separateThousandsByComma( + const parsed = separateThousandsByComma( bigIntToUserAmount(amountBigInt, decimals, desiredDecimals), desiredDecimals ) + + if (parsed === "0" && amountBigInt > 0n) { + return `<${1 / 10 ** desiredDecimals}` + } + + return parsed } diff --git a/src/shared/utils/population.ts b/src/shared/utils/population.ts new file mode 100644 index 000000000..43695a267 --- /dev/null +++ b/src/shared/utils/population.ts @@ -0,0 +1,8 @@ +/* eslint-disable import/prefer-default-export */ +import { RealmDataById } from "shared/types" + +export function isDisplayedPopulationAvailable(realms: RealmDataById) { + return Object.values(realms) + .map((realm) => realm.displayedPopulation) + .some((population) => population !== undefined) +} diff --git a/src/ui/DApps/IslandView.tsx b/src/ui/DApps/IslandView.tsx index 3ef4ca600..fdca58fb3 100644 --- a/src/ui/DApps/IslandView.tsx +++ b/src/ui/DApps/IslandView.tsx @@ -16,6 +16,7 @@ import { } from "redux-state" import FullPageLoader from "shared/components/FullPageLoader" import { Route, Switch } from "react-router-dom" +import { useDisplayedPopulation } from "shared/hooks" export default function IslandView() { const islandMode = useDappSelector(selectIslandMode) @@ -23,6 +24,8 @@ export default function IslandView() { const hasLoadedSeasonInfo = useDappSelector(selectHasLoadedSeasonInfo) const hasBalances = useDappSelector(selectHasLoadedBalances) + useDisplayedPopulation() + return ( <> 0) if (!population) return null return ( <> -
+
{separateThousandsByComma(population)}

-
+
- + setIsTooltipVisible(true)} + onMouseLeave={() => setIsTooltipVisible(false)} + > + + + ) } diff --git a/src/ui/Footer/RealmBar/RealmBarTooltip.tsx b/src/ui/Footer/RealmBar/RealmBarTooltip.tsx index 77938557b..bf71aba3f 100644 --- a/src/ui/Footer/RealmBar/RealmBarTooltip.tsx +++ b/src/ui/Footer/RealmBar/RealmBarTooltip.tsx @@ -36,7 +36,7 @@ export default function RealmBarTooltip({

{name}

- {separateThousandsByComma(population)} + {separateThousandsByComma(population ?? 0)}

diff --git a/src/ui/Footer/RealmBar/index.tsx b/src/ui/Footer/RealmBar/index.tsx index 17671594b..80b600d6a 100644 --- a/src/ui/Footer/RealmBar/index.tsx +++ b/src/ui/Footer/RealmBar/index.tsx @@ -1,88 +1,65 @@ -import React, { useEffect, useRef, useState } from "react" +import React, { useRef } from "react" import populationIcon from "shared/assets/icons/people.svg" import Icon from "shared/components/Icon" import Tooltip from "shared/components/Tooltip" +import { separateThousandsByComma } from "shared/utils" import { - calculatePopulationIconsPositions, - separateThousandsByComma, -} from "shared/utils" -import { - selectMaxPopulation, - selectSortedPopulation, - selectTotalPopulation, + selectSortedDisplayedPopulation, + selectTotalDisplayedPopulation, useDappSelector, } from "redux-state" -import { useVisibilityTransition } from "shared/hooks" -import { animated } from "@react-spring/web" +import { usePopulationIconPositions } from "shared/hooks" import RealmBarIcon from "./RealmBarIcon" export default function RealmsBar() { - const realmsData = useDappSelector(selectSortedPopulation) - const totalPopulation = useDappSelector(selectTotalPopulation) - const maxPopulation = useDappSelector(selectMaxPopulation) + const realmsData = useDappSelector(selectSortedDisplayedPopulation) + const totalPopulation = useDappSelector(selectTotalDisplayedPopulation) - const [positions, setPositions] = useState([]) const progressBarRef = useRef(null) - const transition = useVisibilityTransition(totalPopulation > 0) - - useEffect(() => { - if (!realmsData.length || !progressBarRef.current) return - - const { width } = progressBarRef.current.getBoundingClientRect() - - const pos = calculatePopulationIconsPositions( - width, - realmsData, - maxPopulation - ) - - setPositions(pos) - }, [realmsData, maxPopulation, progressBarRef]) + const positions = usePopulationIconPositions(progressBarRef) return ( <> - -
-
-
- -

- Realms by population -

- - This graph provides a visual overview each realm's standing - in terms of population relative to others. Hover over each - Realm's icon to see the exact population count. - -
-
-

Total:

-

- {separateThousandsByComma(totalPopulation)} -

-
+
+
+
+ +

+ Realms by population +

+ + This graph provides a visual overview each realm's standing + in terms of population relative to others. Hover over each + Realm's icon to see the exact population count. +
-
- {realmsData.map((realm, index) => ( - - ))} +
+

Total:

+

+ {separateThousandsByComma(totalPopulation)} +

- +
+ {realmsData.map((realm, index) => ( + + ))} +
+