feat(markets): new price monitoring bounds panel (#5996)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
parent
72df08851c
commit
88f99031d1
@ -6,6 +6,7 @@ import {
|
||||
LiquiditySLAParametersInfoPanel,
|
||||
MarginScalingFactorsPanel,
|
||||
PriceMonitoringBoundsInfoPanel,
|
||||
PriceMonitoringSettingsInfoPanel,
|
||||
SuccessionLineInfoPanel,
|
||||
getDataSourceSpecForSettlementData,
|
||||
getDataSourceSpecForTradingTermination,
|
||||
@ -21,7 +22,6 @@ import {
|
||||
RiskModelInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketInfoTable } from '@vegaprotocol/markets';
|
||||
import type { DataSourceFragment } from '@vegaprotocol/markets';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
@ -74,27 +74,14 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<MarginScalingFactorsPanel market={market} />
|
||||
<h2 className={headerClassName}>{t('Risk factors')}</h2>
|
||||
<RiskFactorsInfoPanel market={market} />
|
||||
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
|
||||
<>
|
||||
<h2 className={headerClassName}>
|
||||
{t('Price monitoring bounds %s', [(i + 1).toString()])}
|
||||
</h2>
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={market}
|
||||
triggerIndex={i + 1}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
{(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(trigger, i) => (
|
||||
<>
|
||||
<h2 className={headerClassName}>
|
||||
{t('Price monitoring settings %s', [(i + 1).toString()])}
|
||||
</h2>
|
||||
<MarketInfoTable data={trigger} key={i} />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
<h2 className={headerClassName}>{t('Price monitoring bounds')}</h2>
|
||||
<div className="mt-3">
|
||||
<PriceMonitoringBoundsInfoPanel market={market} />
|
||||
</div>
|
||||
<h2 className={headerClassName}>{t('Price monitoring settings')}</h2>
|
||||
<div className="mt-3">
|
||||
<PriceMonitoringSettingsInfoPanel market={market} />
|
||||
</div>
|
||||
<h2 className={headerClassName}>{t('Liquidation strategy')}</h2>
|
||||
<LiquidationStrategyInfoPanel market={market} />
|
||||
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
getSigners,
|
||||
MarginScalingFactorsPanel,
|
||||
marketInfoProvider,
|
||||
PriceMonitoringSettingsInfoPanel,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
Button,
|
||||
@ -245,39 +246,23 @@ export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => {
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
|
||||
{showParentPriceMonitoringBounds &&
|
||||
(
|
||||
parentMarketData?.priceMonitoringSettings?.parameters
|
||||
?.triggers || []
|
||||
).map((_, triggerIndex) => (
|
||||
<>
|
||||
<h2 className={marketDataHeaderStyles}>
|
||||
{t(`Parent price monitoring bounds ${triggerIndex + 1}`)}
|
||||
</h2>
|
||||
|
||||
<div className="text-vega-dark-300 line-through">
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={parentMarketData}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
|
||||
{(
|
||||
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
||||
).map((_, triggerIndex) => (
|
||||
{showParentPriceMonitoringBounds && (
|
||||
// shows bounds for parent market
|
||||
<>
|
||||
<h2 className={marketDataHeaderStyles}>
|
||||
{t(`Price monitoring bounds ${triggerIndex + 1}`)}
|
||||
{t('Parent price monitoring bounds')}
|
||||
</h2>
|
||||
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={marketData}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
<div className="text-vega-dark-300 line-through">
|
||||
<PriceMonitoringBoundsInfoPanel market={parentMarketData} />
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
)}
|
||||
|
||||
<h2 className={marketDataHeaderStyles}>
|
||||
{t('Price monitoring settings')}
|
||||
</h2>
|
||||
<PriceMonitoringSettingsInfoPanel market={marketData} />
|
||||
|
||||
<h2 className={marketDataHeaderStyles}>
|
||||
{t('Liquidity monitoring parameters')}
|
||||
</h2>
|
||||
|
@ -6,6 +6,7 @@ from fixtures.market import setup_continuous_market
|
||||
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
|
||||
|
||||
market_title_test_id = "accordion-title"
|
||||
market_accordion_content = "accordion-content"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@ -225,18 +226,16 @@ def test_market_info_risk_factors(page: Page):
|
||||
def test_market_info_price_monitoring_bounds(page: Page):
|
||||
# 6002-MDET-211
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Price monitoring bounds 1"
|
||||
"Price monitoring bounds"
|
||||
).click()
|
||||
expect(page.locator("p.col-span-1").nth(0)).to_contain_text(
|
||||
"99.9999% probability price bounds"
|
||||
)
|
||||
expect(page.locator("p.col-span-1").nth(1)
|
||||
).to_contain_text("Within 86,400 seconds")
|
||||
fields = [
|
||||
["Highest Price", "138.66685 BTC"],
|
||||
["Lowest Price", "83.11038 BTC"],
|
||||
]
|
||||
validate_info_section(page, fields)
|
||||
expect(page.get_by_test_id(market_accordion_content).locator(
|
||||
".w-full").nth(1)).to_contain_text("99.9999%")
|
||||
expect(page.get_by_test_id(market_accordion_content).locator(".w-full").nth(2)
|
||||
).to_contain_text("within 1d")
|
||||
expect(page.get_by_test_id(market_accordion_content).locator(".text-left")
|
||||
).to_contain_text("83.11038 BTC")
|
||||
expect(page.get_by_test_id(market_accordion_content).locator(".text-right")
|
||||
).to_contain_text("138.66685 BTC")
|
||||
|
||||
|
||||
def test_market_info_liquidity_monitoring_parameters(page: Page):
|
||||
|
@ -87,6 +87,7 @@
|
||||
"oracleInMarkets_other": "Oracle in {{count}} markets",
|
||||
"oracleInMarkets": "Oracle in {{count}} markets",
|
||||
"Price monitoring bounds {{index}}": "Price monitoring bounds {{index}}",
|
||||
"Price monitoring bounds": "Price monitoring bounds",
|
||||
"Price oracle {{index}}": "Price oracle {{index}}",
|
||||
"Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.": "Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.",
|
||||
"Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short": "Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short",
|
||||
@ -97,7 +98,7 @@
|
||||
"Proposal": "Proposal",
|
||||
"Propose a change to market": "Propose a change to market",
|
||||
"Read more": "Read more",
|
||||
"Results in {{auctionExtensionSecs}} seconds auction if breached": "Results in {{auctionExtensionSecs}} seconds auction if breached",
|
||||
"Results in {{duration}} auction if breached": "Results in {{duration}} auction if breached",
|
||||
"Risk factors": "Risk factors",
|
||||
"Risk model": "Risk model",
|
||||
"Settlement": "Settlement",
|
||||
@ -166,6 +167,12 @@
|
||||
"View settlement schedule specification": "View settlement schedule specification",
|
||||
"View specification": "View specification",
|
||||
"View termination specification": "View termination specification",
|
||||
"Within {{horizonSecs}} seconds": "Within {{horizonSecs}} seconds",
|
||||
"Weighted": "Weighted"
|
||||
"Weighted": "Weighted",
|
||||
"within {{duration}}": "within {{duration}}",
|
||||
"Minimum price that isn't currently breaching the specified price monitoring trigger": "Minimum price that isn't currently breaching the specified price monitoring trigger",
|
||||
"Maximum price that isn't currently breaching the specified price monitoring trigger": "Maximum price that isn't currently breaching the specified price monitoring trigger",
|
||||
"{{probability}} of prices must be within the bounds for {{duration}}": "{{probability}} of prices must be within the bounds for {{duration}}",
|
||||
"No price monitoring bounds detected.": "No price monitoring bounds detected.",
|
||||
"triggers_one": "Trigger",
|
||||
"triggers_other": "Chain of {{count}} triggers"
|
||||
}
|
||||
|
19
libs/i18n/src/locales/en/react-helpers.json
Normal file
19
libs/i18n/src/locales/en/react-helpers.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"duration_days_one": "{{count}} day",
|
||||
"duration_days_other": "{{count}} days",
|
||||
"duration_hours_one": "{{count}} hour",
|
||||
"duration_hours_other": "{{count}} hours",
|
||||
"duration_minutes_one": "{{count}} minute",
|
||||
"duration_minutes_other": "{{count}} minutes",
|
||||
"duration_seconds_one": "{{count}} second",
|
||||
"duration_seconds_other": "{{count}} seconds",
|
||||
|
||||
"duration_days_compact_one": "{{count}}d",
|
||||
"duration_days_compact_other": "{{count}}d",
|
||||
"duration_hours_compact_one": "{{count}}h",
|
||||
"duration_hours_compact_other": "{{count}}h",
|
||||
"duration_minutes_compact_one": "{{count}}m",
|
||||
"duration_minutes_compact_other": "{{count}}m",
|
||||
"duration_seconds_compact_one": "{{count}}s",
|
||||
"duration_seconds_compact_other": "{{count}}s"
|
||||
}
|
@ -221,27 +221,11 @@ export const MarketInfoAccordion = ({
|
||||
title={t('Risk factors')}
|
||||
content={<RiskFactorsInfoPanel market={market} />}
|
||||
/>
|
||||
{(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(_, triggerIndex) => {
|
||||
const id = `trigger-${triggerIndex}`;
|
||||
return (
|
||||
<AccordionItem
|
||||
key={id}
|
||||
itemId={id}
|
||||
title={t('Price monitoring bounds {{index}}', {
|
||||
index: triggerIndex + 1,
|
||||
})}
|
||||
content={
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={market}
|
||||
triggerIndex={triggerIndex}
|
||||
key={id}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<AccordionItem
|
||||
itemId="trigger"
|
||||
title={t('Price monitoring bounds')}
|
||||
content={<PriceMonitoringBoundsInfoPanel market={market} />}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="liquidation-strategy"
|
||||
title={t('Liquidation strategy')}
|
||||
|
@ -24,10 +24,10 @@ import {
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
determinePriceStep,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
getDateTimeFormat,
|
||||
getMarketExpiryDateFormatted,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { Get } from 'type-fest';
|
||||
import { MarketInfoTable } from './info-key-value-table';
|
||||
@ -88,6 +88,12 @@ import { formatDuration } from 'date-fns';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { useT } from '../../use-t';
|
||||
import { isPerpetual } from '../../product';
|
||||
import omit from 'lodash/omit';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import min from 'lodash/min';
|
||||
import sum from 'lodash/sum';
|
||||
import { useDuration } from '@vegaprotocol/react-helpers';
|
||||
|
||||
type MarketInfoProps = {
|
||||
market: MarketInfo;
|
||||
@ -779,13 +785,30 @@ export const RiskFactorsInfoPanel = ({
|
||||
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
||||
};
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({
|
||||
market,
|
||||
triggerIndex,
|
||||
}: MarketInfoProps & {
|
||||
triggerIndex: number;
|
||||
}) => {
|
||||
type TriggerInfo = {
|
||||
maxValidPrice: BigNumber;
|
||||
minValidPrice: BigNumber;
|
||||
referencePrice: BigNumber;
|
||||
horizonSecs: number;
|
||||
probability: number;
|
||||
auctionExtensionSecs: number;
|
||||
};
|
||||
|
||||
/** Calculates a trigger info group signature. */
|
||||
const triggerInfoGroupIteratee = (t: TriggerInfo) =>
|
||||
`${t.horizonSecs}|${
|
||||
t.probability
|
||||
}|${t.minValidPrice.toString()}|${t.maxValidPrice.toString()}`;
|
||||
|
||||
type ExtendedTriggerInfo = Omit<TriggerInfo, 'auctionExtensionSecs'> & {
|
||||
minAuctionExtensionSecs: number;
|
||||
maxAuctionExtensionSecs: number;
|
||||
};
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
const t = useT();
|
||||
const duration = useDuration();
|
||||
const compactDuration = useDuration('compact');
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId: market.id },
|
||||
@ -793,45 +816,159 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
|
||||
const quoteUnit = getQuoteName(market);
|
||||
|
||||
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
|
||||
const trigger = bounds?.trigger;
|
||||
let triggers = Object.entries(
|
||||
groupBy(
|
||||
data?.priceMonitoringBounds?.map((b) => ({
|
||||
...omit(b.trigger, '__typename'),
|
||||
maxValidPrice: toBigNum(b.maxValidPrice, market.decimalPlaces),
|
||||
minValidPrice: toBigNum(b.minValidPrice, market.decimalPlaces),
|
||||
referencePrice: toBigNum(b.referencePrice, market.decimalPlaces),
|
||||
})),
|
||||
triggerInfoGroupIteratee
|
||||
)
|
||||
).map(([_, ts]) => {
|
||||
const info = omit(ts[0], 'auctionExtensionSecs');
|
||||
const auctions = ts.map((t) => t.auctionExtensionSecs);
|
||||
const minAuctionExtensionSecs = min(auctions) as number;
|
||||
const maxAuctionExtensionSecs = sum(auctions);
|
||||
const extendedInfo: ExtendedTriggerInfo = {
|
||||
...info,
|
||||
minAuctionExtensionSecs,
|
||||
maxAuctionExtensionSecs,
|
||||
};
|
||||
return extendedInfo;
|
||||
});
|
||||
|
||||
if (!trigger) {
|
||||
return null;
|
||||
triggers = orderBy(
|
||||
triggers,
|
||||
[
|
||||
(bd) => bd.probability,
|
||||
(bd) => bd.horizonSecs,
|
||||
(bd) => bd.minAuctionExtensionSecs,
|
||||
(bd) => bd.maxAuctionExtensionSecs,
|
||||
],
|
||||
['desc', 'asc', 'asc', 'asc']
|
||||
);
|
||||
|
||||
if (!triggers || triggers.length === 0) {
|
||||
return <div>{t('No price monitoring bounds detected.')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 grid grid-cols-2 text-xs">
|
||||
<p className="col-span-1">
|
||||
{t('{{probability}} probability price bounds', {
|
||||
probability: formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p className="col-span-1 text-right">
|
||||
{t('Within {{horizonSecs}} seconds', {
|
||||
horizonSecs: formatNumber(trigger.horizonSecs),
|
||||
const price = (price: string, direction: 'min' | 'max') => (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded px-1 py-[1px] bg-vega-clight-500 dark:bg-vega-cdark-500 relative',
|
||||
'after:absolute after:content-[" "] after:z-10',
|
||||
'after:block after:w-3 after:h-3 after:bg-vega-clight-500 dark:after:bg-vega-cdark-500 after:rotate-45 after:-translate-y-1/2',
|
||||
{
|
||||
'after:top-1/2 after:right-[-6px]': direction === 'min',
|
||||
'after:top-1/2 after:left-[-6px]': direction === 'max',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames('text-[10px]', {
|
||||
'text-left': direction === 'min',
|
||||
'text-right': direction === 'max',
|
||||
})}
|
||||
>
|
||||
{price} <span>{quoteUnit}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return triggers.map((trigger, index) => {
|
||||
const probability = formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
);
|
||||
const within = compactDuration(trigger.horizonSecs * 1000);
|
||||
return (
|
||||
<div key={index} className="mb-2 border-b border-b-vega-clight-500">
|
||||
<div className="font-mono text-xs flex">
|
||||
{/** MIN PRICE */}
|
||||
<Tooltip
|
||||
description={t(
|
||||
"Minimum price that isn't currently breaching the specified price monitoring trigger"
|
||||
)}
|
||||
>
|
||||
{price(trigger.minValidPrice.toString(10), 'min')}
|
||||
</Tooltip>
|
||||
|
||||
{/** TRIGGERS WHEN */}
|
||||
<Tooltip
|
||||
description={t(
|
||||
'{{probability}} of prices must be within the bounds for {{duration}}',
|
||||
{
|
||||
probability: probability,
|
||||
duration: within,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div aria-hidden className="w-full text-center text-[10px]">
|
||||
<div className="border-b-[2px] border-dashed border-vega-clight-500 dark:border-vega-cdark-500 w-full h-1/2 translate-y-[1px]">
|
||||
{probability}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{t('within {{duration}}', {
|
||||
duration: within,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{/** MAX PRICE */}
|
||||
<Tooltip
|
||||
description={t(
|
||||
"Maximum price that isn't currently breaching the specified price monitoring trigger"
|
||||
)}
|
||||
>
|
||||
{price(trigger.maxValidPrice.toString(10), 'max')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="my-2 text-xs leading-none">
|
||||
{t('Results in {{duration}} auction if breached', {
|
||||
duration:
|
||||
trigger.minAuctionExtensionSecs !==
|
||||
trigger.maxAuctionExtensionSecs
|
||||
? `${duration(
|
||||
trigger.minAuctionExtensionSecs * 1000
|
||||
)} ~ ${duration(trigger.maxAuctionExtensionSecs * 1000)}`
|
||||
: duration(trigger.minAuctionExtensionSecs * 1000),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{bounds && (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
/>
|
||||
)}
|
||||
<p className="mt-2 text-xs">
|
||||
{t('Results in {{auctionExtensionSecs}} seconds auction if breached', {
|
||||
auctionExtensionSecs: trigger.auctionExtensionSecs.toString(),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const PriceMonitoringSettingsInfoPanel = ({
|
||||
market,
|
||||
className,
|
||||
}: MarketInfoProps & { className?: classNames.Argument }) => {
|
||||
const t = useT();
|
||||
|
||||
const triggers = groupBy(
|
||||
market.priceMonitoringSettings?.parameters?.triggers?.map((t) =>
|
||||
omit(t, '__typename')
|
||||
) || [],
|
||||
(t) => `${t.horizonSecs}|${t.probability}|${t.auctionExtensionSecs}`
|
||||
);
|
||||
|
||||
return (
|
||||
<dl className="grid grid-cols-3 md:grid-cols-6 gap-3">
|
||||
{Object.entries(triggers).map(([_, trigger], i) => (
|
||||
<span className="border p-3 rounded">
|
||||
<dt className="text-sm font-bold">
|
||||
{t('triggers', {
|
||||
count: trigger.length,
|
||||
})}
|
||||
</dt>
|
||||
<dt>
|
||||
<MarketInfoTable data={trigger[0]} />
|
||||
</dt>
|
||||
</span>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,3 +14,4 @@ export * from './use-yesterday';
|
||||
export * from './use-previous';
|
||||
export { useScript } from './use-script';
|
||||
export { useUserAgent } from './use-user-agent';
|
||||
export * from './use-duration';
|
||||
|
53
libs/react-helpers/src/hooks/use-duration.ts
Normal file
53
libs/react-helpers/src/hooks/use-duration.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { convertToDuration } from '@vegaprotocol/utils';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
enum DurationKeys {
|
||||
Days = 'duration_days',
|
||||
Hours = 'duration_hours',
|
||||
Minutes = 'duration_minutes',
|
||||
Seconds = 'duration_seconds',
|
||||
DaysCompact = 'duration_days_compact',
|
||||
HoursCompact = 'duration_hours_compact',
|
||||
MinutesCompact = 'duration_minutes_compact',
|
||||
SecondsCompact = 'duration_seconds_compact',
|
||||
}
|
||||
|
||||
export const useDuration = (mode: 'normal' | 'compact' = 'normal') => {
|
||||
const t = useT();
|
||||
|
||||
let DAYS = DurationKeys.Days;
|
||||
let HOURS = DurationKeys.Hours;
|
||||
let MINUTES = DurationKeys.Minutes;
|
||||
let SECONDS = DurationKeys.Seconds;
|
||||
|
||||
if (mode === 'compact') {
|
||||
DAYS = DurationKeys.DaysCompact;
|
||||
HOURS = DurationKeys.HoursCompact;
|
||||
MINUTES = DurationKeys.MinutesCompact;
|
||||
SECONDS = DurationKeys.SecondsCompact;
|
||||
}
|
||||
|
||||
return (durationInMilliseconds: number) => {
|
||||
const duration = convertToDuration(durationInMilliseconds);
|
||||
|
||||
const segments = [];
|
||||
|
||||
if (duration.days > 0) {
|
||||
segments.push(t(DAYS, { count: duration.days }));
|
||||
}
|
||||
if (duration.hours > 0) {
|
||||
segments.push(t(HOURS, { count: duration.hours }));
|
||||
}
|
||||
if (duration.minutes > 0) {
|
||||
segments.push(t(MINUTES, { count: duration.minutes }));
|
||||
}
|
||||
if (duration.seconds > 0) {
|
||||
segments.push(t(SECONDS, { count: duration.seconds }));
|
||||
}
|
||||
|
||||
if (segments.length > 0) {
|
||||
return segments.join(' ');
|
||||
}
|
||||
return t(SECONDS, { count: 0 });
|
||||
};
|
||||
};
|
3
libs/react-helpers/src/use-t.ts
Normal file
3
libs/react-helpers/src/use-t.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const ns = 'react-helpers';
|
||||
export const useT = () => useTranslation(ns).t;
|
@ -1,6 +1,6 @@
|
||||
import {
|
||||
convertToCountdown,
|
||||
convertToCountdownString,
|
||||
convertToDuration,
|
||||
getSecondsFromInterval,
|
||||
} from './time';
|
||||
|
||||
@ -60,7 +60,8 @@ describe('convertToCountdown', () => {
|
||||
[30, 3, 12, 3],
|
||||
],
|
||||
])('converts %d ms to %s', (time, countdown) => {
|
||||
expect(convertToCountdown(time)).toEqual(countdown);
|
||||
const duration = convertToDuration(time);
|
||||
expect(Object.values(duration)).toEqual(countdown);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -47,7 +47,14 @@ export function getSecondsFromInterval(str: string) {
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export const convertToCountdown = (time: number) => {
|
||||
type Duration = {
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
export const convertToDuration = (time: number): Duration => {
|
||||
const s = 1000;
|
||||
const m = 1000 * 60;
|
||||
const h = 1000 * 60 * 60;
|
||||
@ -60,7 +67,7 @@ export const convertToCountdown = (time: number) => {
|
||||
const minutes = Math.floor((t - days * d - hours * h) / m);
|
||||
const seconds = Math.floor((t - days * d - hours * h - minutes * m) / s);
|
||||
|
||||
return [days, hours, minutes, seconds];
|
||||
return { days, hours, minutes, seconds };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -70,7 +77,7 @@ export const convertToCountdownString = (
|
||||
time: number,
|
||||
pattern = '0d00h00m00s'
|
||||
) => {
|
||||
const values = convertToCountdown(time);
|
||||
const values = Object.values(convertToDuration(time));
|
||||
|
||||
let i = 0;
|
||||
const countdown = pattern
|
||||
|
@ -5,6 +5,6 @@
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"include": ["src/**/*.ts", "../react-helpers/src/lib/use-duration.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user