diff --git a/.changeset/seven-feet-bathe.md b/.changeset/seven-feet-bathe.md
new file mode 100644
index 00000000..b8b4716d
--- /dev/null
+++ b/.changeset/seven-feet-bathe.md
@@ -0,0 +1,5 @@
+---
+"@stakekit/widget": patch
+---
+
+feat: unstaking time format improved
diff --git a/packages/widget/package.json b/packages/widget/package.json
index 95f6c004..6f39bda7 100644
--- a/packages/widget/package.json
+++ b/packages/widget/package.json
@@ -115,6 +115,7 @@
"chartjs-plugin-annotation": "^3.1.0",
"clsx": "^2.1.1",
"cosmjs-types": "^0.9.0",
+ "date-fns": "^4.1.0",
"eventemitter3": "^5.0.1",
"i18next": "^25.6.2",
"i18next-browser-languagedetector": "^8.2.0",
diff --git a/packages/widget/src/pages/position-details/components/position-balances.tsx b/packages/widget/src/pages/position-details/components/position-balances.tsx
index 73290cbc..1322140a 100644
--- a/packages/widget/src/pages/position-details/components/position-balances.tsx
+++ b/packages/widget/src/pages/position-details/components/position-balances.tsx
@@ -1,12 +1,13 @@
import type { YieldBalanceDto, YieldDto } from "@stakekit/api-hooks";
import BigNumber from "bignumber.js";
+import { isPast } from "date-fns";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Box } from "../../../components/atoms/box";
import { TokenIcon } from "../../../components/atoms/token-icon";
import { Text } from "../../../components/atoms/typography/text";
import { defaultFormattedNumber } from "../../../utils";
-import { daysUntilDate } from "../../../utils/date";
+import { formatDurationUntilDate } from "../../../utils/date";
export const PositionBalances = ({
yieldBalance,
@@ -17,14 +18,30 @@ export const PositionBalances = ({
}) => {
const { t } = useTranslation();
- const daysRemaining = useMemo(() => {
- return (yieldBalance.type === "unstaking" ||
- yieldBalance.type === "unlocking" ||
- yieldBalance.type === "preparing") &&
- yieldBalance.date
- ? daysUntilDate(new Date(yieldBalance.date))
- : null;
- }, [yieldBalance.date, yieldBalance.type]);
+ const durationUntilDate = useMemo(() => {
+ if (
+ !yieldBalance.date ||
+ (yieldBalance.type !== "unstaking" &&
+ yieldBalance.type !== "unlocking" &&
+ yieldBalance.type !== "preparing")
+ ) {
+ return null;
+ }
+
+ const date = new Date(yieldBalance.date);
+
+ if (isPast(date)) {
+ return t("position_details.unstaking_imminent");
+ }
+
+ const duration = formatDurationUntilDate(date);
+
+ if (!duration) {
+ return null;
+ }
+
+ return t("position_details.unstaking_duration", { duration });
+ }, [yieldBalance.date, yieldBalance.type, t]);
const yieldType = integrationData.metadata.type;
@@ -68,11 +85,9 @@ export const PositionBalances = ({
- {typeof daysRemaining === "number" && (
+ {!!durationUntilDate && (
- {t("position_details.unstaking_days", {
- count: daysRemaining,
- })}
+ {durationUntilDate}
)}
diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json
index 3a99c19b..f4f13e8d 100644
--- a/packages/widget/src/translation/English/translations.json
+++ b/packages/widget/src/translation/English/translations.json
@@ -475,8 +475,8 @@
"locked": "Locked",
"unlocking": "Unlocking"
},
- "unstaking_days_one": "{{count}} day remaining",
- "unstaking_days_other": "{{count}} days remaining",
+ "unstaking_duration": "{{duration}} remaining",
+ "unstaking_imminent": "Imminent",
"pending_action": {
"stake": "Stake",
"unstake": "Unstake",
diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json
index 3c098389..0ca0efac 100644
--- a/packages/widget/src/translation/French/translations.json
+++ b/packages/widget/src/translation/French/translations.json
@@ -425,8 +425,8 @@
"locked": "Bloqué",
"unlocking": "En cours de déblocage"
},
- "unstaking_days_one": "{{count}} jour restant",
- "unstaking_days_other": "{{count}} jours restants",
+ "unstaking_duration": "{{duration}} restant",
+ "unstaking_imminent": "Prochain",
"pending_action": {
"stake": "Staker",
"unstake": "Déstaker",
diff --git a/packages/widget/src/translation/index.ts b/packages/widget/src/translation/index.ts
index ad572a12..74d5bc46 100644
--- a/packages/widget/src/translation/index.ts
+++ b/packages/widget/src/translation/index.ts
@@ -1,4 +1,6 @@
import { useQuery } from "@tanstack/react-query";
+import { setDefaultOptions } from "date-fns";
+import { enUS as dateFnsEN, fr as dateFnsFR } from "date-fns/locale";
import { createInstance } from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { EitherAsync } from "purify-ts";
@@ -25,8 +27,17 @@ i18nInstance
fallbackLng: "en",
interpolation: { escapeValue: false },
detection: { order: ["navigator", "localStorage"] },
+ })
+ .then(() => {
+ setDefaultOptions({
+ locale: i18nInstance.language === "fr" ? dateFnsFR : dateFnsEN,
+ });
});
+i18nInstance.on("languageChanged", (lng) => {
+ setDefaultOptions({ locale: lng === "fr" ? dateFnsFR : dateFnsEN });
+});
+
i18nInstance.services.formatter?.add("lowercase", (value, _, __) =>
value.toLowerCase()
);
diff --git a/packages/widget/src/utils/date.ts b/packages/widget/src/utils/date.ts
index 12288f63..53a337c3 100644
--- a/packages/widget/src/utils/date.ts
+++ b/packages/widget/src/utils/date.ts
@@ -1,15 +1,42 @@
-export const daysUntilDate = (futureDate: Date) => {
- const now = new Date();
- const _MS_PER_DAY = 1000 * 60 * 60 * 24;
+import {
+ type FormatDurationOptions,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
- const utc1 = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
- const utc2 = Date.UTC(
- futureDate.getFullYear(),
- futureDate.getMonth(),
- futureDate.getDate()
- );
+const getFormat = ({
+ days,
+ hours,
+ minutes,
+}: {
+ days: number;
+ hours: number;
+ minutes: number;
+}): FormatDurationOptions["format"] => {
+ if (days >= 1) {
+ return ["days"];
+ }
+ if (hours >= 1) {
+ return ["hours"];
+ }
+ if (minutes >= 1) {
+ return ["minutes"];
+ }
+ return ["seconds"];
+};
- return Math.floor((utc2 - utc1) / _MS_PER_DAY);
+export const formatDurationUntilDate = (futureDate: Date) => {
+ const {
+ days = 0,
+ hours = 0,
+ minutes = 0,
+ seconds = 0,
+ } = intervalToDuration({ start: new Date(), end: futureDate });
+
+ return formatDuration(
+ { days, hours, minutes, seconds },
+ { format: getFormat({ days, hours, minutes }) }
+ );
};
export const dateOlderThen7Days = (date: string): boolean => {
diff --git a/packages/widget/vite/vite.config.base.ts b/packages/widget/vite/vite.config.base.ts
index 41aacd79..a6a33531 100644
--- a/packages/widget/vite/vite.config.base.ts
+++ b/packages/widget/vite/vite.config.base.ts
@@ -28,6 +28,7 @@ export const getConfig = (overides?: Partial): UserConfigFnObject =>
"vite-plugin-node-polyfills/shims/process",
"@vanilla-extract/recipes/createRuntimeFn",
"@vanilla-extract/sprinkles/createRuntimeSprinkles",
+ "date-fns/locale",
],
},
test: {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a60b9976..01f5353c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -287,6 +287,9 @@ importers:
cosmjs-types:
specifier: ^0.9.0
version: 0.9.0
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
@@ -6096,6 +6099,9 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
+ date-fns@4.1.0:
+ resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -19818,6 +19824,8 @@ snapshots:
dependencies:
'@babel/runtime': 7.28.4
+ date-fns@4.1.0: {}
+
dayjs@1.11.13: {}
debug@2.6.9: