Skip to content

feat: countdown &timer i18n#5622

Merged
cuzz-venus merged 6 commits into
mainfrom
feat/prime-end-of-cycle
Jun 17, 2026
Merged

feat: countdown &timer i18n#5622
cuzz-venus merged 6 commits into
mainfrom
feat/prime-end-of-cycle

Conversation

@cuzz-venus

@cuzz-venus cuzz-venus commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Jira ticket(s)

VPD-1331

Changes

  • add count down
  • update timer

implementation

image

@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a34a35

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@venusprotocol/evm Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dapp-preview Ready Ready Preview Jun 16, 2026 8:31am
dapp-testnet Ready Ready Preview Jun 16, 2026 8:31am
venus.io Ready Ready Preview Jun 16, 2026 8:31am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires up i18n and a live countdown for the Prime leaderboard's End-of-Cycle card. The placeholder card is replaced with a react-countdown-driven component showing days/hours/minutes/seconds, localized unit labels, and a formatted deadline string across all seven supported locales.

  • EndOfCycle renders either an active countdown or an ended state depending on whether endDate is provided; a module-level endOfMonth(new Date()) placeholder is left with a TODO for the future API value.
  • Timer is a new sub-component that renders four zero-padded segments with translated labels.
  • Translation keys are added to all seven locale files, including a deadline key with an inline date format.

Confidence Score: 5/5

Safe to merge — changes are scoped to the Prime leaderboard End-of-Cycle card and its i18n strings, with no impact on shared infrastructure or other pages.

The new Timer and EndOfCycle components are self-contained, the countdown logic is delegated to a well-tested library, and the translation additions are purely additive. The only findings are quality nits around using translated strings as React keys and the inline date format bypassing locale-aware formatting already in the codebase.

Timer/index.tsx — uses translated labels as React list keys; consider switching to stable string keys to avoid unnecessary remounts on locale change.

Important Files Changed

Filename Overview
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/Timer/index.tsx New Timer component with i18n labels and zero-padded segments; uses translated text as React keys which can cause unnecessary remounts on locale change
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/index.tsx Refactored to use react-countdown with i18n support; active/completed branching and graceful fallback when no endDate are correct
apps/evm/src/pages/PrimeLeaderboard/index.tsx Wired EndOfCycle with a temporary module-level date placeholder; TODO comments present; tablesRefreshNote i18n added
apps/evm/src/libs/translations/translations/en.json New endOfCycle and tablesRefreshNote keys added; inline date format h:mm a (12-hour) is correct
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/tests/index.spec.tsx New tests covering active and ended countdown states; straightforward and correct
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/Timer/tests/index.spec.tsx Tests zero-padding for all four time segments; correct and sufficient for the component's logic

Reviews (2): Last reviewed commit: "feat: update date" | Re-trigger Greptile

Comment on lines +50 to +53
components={{
bold: <span className="text-b1s text-white" />,
summaryLink: <span className="text-blue underline" />,
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 summaryLink renders as a non-interactive <span> instead of an anchor element. The text is styled like a link (text-blue underline) and the translation copy says "See last cycle's Prime summary", so users will click it expecting navigation — but nothing happens. It should be an <a> (or a router Link) pointing to the Prime summary page.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a navigation link — "See last cycle's Prime summary" opens the last-cycle summary modal (PRD §6.5), not a page, so an / router Link would be the wrong semantic here. It's an intentional placeholder rendered as inline text for now (kept as a so the copy can wrap mid-phrase to match the design). It'll be wired up as an accessible trigger in the summary-modal ticket.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — modal triggers shouldn't use <a> or Link. When the summary-modal ticket lands, consider using a <button> with appropriate styling rather than a <span>, since <button> gives you keyboard accessibility and the correct ARIA role for a clickable element out of the box. The <span> is fine as a placeholder for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cuzz-venus Your point is fair, though I agree with greptile that the CTA will need to use a button tag (and to open the last cycle's modal when clicked).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @therealemjy, you are right, actually I have updated the span to the button at https://github.com/VenusProtocol/venus-protocol-interface/pull/5630/changes#diff-4df7a2b008795f28a1f81865b97c60a1efa765a95984d98cd6adb22af9878030R55

forgot to update here. but now I have updated in this branch :)

export const EndOfCycle: React.FC<EndOfCycleProps> = ({ endDate, className }) => {
const { t, Trans } = useTranslation();

const deadline = endDate ? t('primeLeaderboard.endOfCycle.deadline', { date: endDate }) : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 deadline is set to '' when endDate is undefined, but the helperEnded string uses {{deadline}} inline — so when !endDate the rendered text becomes "Stay in the top 500 until to receive Prime rewards during the next cycle." with a blank gap. Either provide a fallback label, omit the placeholder from the ended copy, or keep two separate translation keys (one with and one without the date).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endDate is always provided in the product (the page derives it, and it'll ultimately come from the API), so the blank-deadline case won't occur — the optional path is just a defensive fallback. I'll tighten the type when wiring it to the API.

},
"primeLeaderboard": {
"description": "Stake XVS into the governance vault and receive Prime rewards on top of your yields.",
"endOfCycle": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The date format string HH:mm a mixes the 24-hour hour token (HH, range 00–23) with the meridiem marker (a, AM/PM). This produces output like "13:00 PM" for 1 pm, which is invalid. The same format is copied into all seven locale files. Use either HH:mm (24-hour, no meridiem) or h:mm a (12-hour with meridiem).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — switched to 12-hour with a meridiem (h:mm a) across all locales, so it now reads e.g. "March 31, 2026 11:59 PM" (localized AM/PM per locale) instead of the invalid 24h + meridiem combo.

<div className="absolute inset-x-0 -top-40 flex justify-center">
<div className="h-200 w-200 rounded-full bg-blue/30 blur-3xl" />
// TODO: use the cycle end date returned by the API
const endOfCycleDate = endOfMonth(new Date());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 endOfCycleDate is evaluated once at module-load time, not inside the component. If the module is cached across a month boundary (e.g. a long-lived tab) the countdown will tick past the real end of month and the next render will still target the old date. Moving the call inside the component body, or inside a useMemo/useState, would at least re-evaluate on mount. The TODO comment acknowledges this will come from the API eventually, but the current placement can produce silently incorrect state today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a placeholder — the cycle end date will come from the API and be read at fetch/render time, replacing this module-level computation. Left a TODO to that effect.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./apps/evm

Status Category Percentage Covered / Total
🔵 Lines 81.5% 47552 / 58345
🔵 Statements 81.5% 47552 / 58345
🔵 Functions 62.29% 656 / 1053
🔵 Branches 72.95% 5420 / 7429
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/evm/src/pages/PrimeLeaderboard/index.tsx 100% 33.33% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/index.tsx 100% 83.33% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/Timer/index.tsx 100% 50% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/EndOfCycle/Timer/pad/index.ts 100% 0% 100% 100%
Generated in workflow #13632 for commit 2a34a35 by the Vitest Coverage Report Action

@cuzz-venus

Copy link
Copy Markdown
Contributor Author

@greptile review again

@cuzz-venus cuzz-venus requested a review from therealemjy June 11, 2026 12:30
"endOfCycle": {
"days": "Days",
"deadline": "{{date, MMMM d, yyyy h:mm a}}",
"helper": "Stay in the top 500 until <bold>{{deadline}}</bold> to receive Prime rewards during the next cycle. Check the <summaryLink>See last cycle's Prime summary</summaryLink> now to view your ranking and stay competitive.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The content in the Figma document is incorrect (I left a comment about this but it has not been updated):

Suggested change
"helper": "Stay in the top 500 until <bold>{{deadline}}</bold> to receive Prime rewards during the next cycle. Check the <summaryLink>See last cycle's Prime summary</summaryLink> now to view your ranking and stay competitive.",
"helper": "Stay in the top 500 until <bold>{{deadline}}</bold> to receive Prime rewards during the next cycle. <summaryLink>See last cycle's Prime summary</summaryLink>.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! uodate!

<div className="flex w-full flex-col items-center">
<div className="flex w-full items-center justify-between text-center text-b2s text-light-grey">
{segments.map(segment => (
<p key={segment.label} className="w-9">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to add a key prop (perhaps using the label property of each segment) to each element for React to differentiate them when rendering the DOM.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. already have the key prop.

export const Timer: React.FC<TimerProps> = ({ days, hours, minutes, seconds }) => {
const { t } = useTranslation();

const pad = (value: number) => String(value).padStart(2, '0');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility function should be inside its own file and directory (inside the Timer directory).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

import { Timer } from './Timer';

export interface EndOfCycleProps {
endDate?: Date;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think endDate should be required, so that we don't end up with an odd state where the timer displays 00:00:00:00 and the description underneath is incomplete ("...in the top 500 until (empty) to receive").

If an end date hasn't been fetched from the page yet, then this page can either decide to show a spinner or simply not display this component.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. have fixed this one, and add spinner for this

"sec": "Sec",
"title": "End of cycle"
},
"tablesRefreshNote": "Refreshed hourly · Last refresh: {{distance}} ago",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codebase has a built-in way of displaying distances in time:

Suggested change
"tablesRefreshNote": "Refreshed hourly · Last refresh: {{distance}} ago",
"tablesRefreshNote": "Refreshed hourly · Last refresh: {{date, distanceToNow}} ago",

You might have to check how it handles past dates, but the idea is that the date will be converted into a distance in time from now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update!

Comment thread .gitignore Outdated
@therealemjy

Copy link
Copy Markdown
Member

The PR looks good to me. The only work left on it is for the "See last cycle's summary" button to open the last cycle's summary modal.

@cuzz-venus cuzz-venus merged commit 23ca826 into main Jun 17, 2026
5 checks passed
@cuzz-venus cuzz-venus deleted the feat/prime-end-of-cycle branch June 17, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants