feat: countdown &timer i18n#5622
Conversation
🦋 Changeset detectedLatest commit: 2a34a35 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR wires up i18n and a live countdown for the Prime leaderboard's End-of-Cycle card. The placeholder card is replaced with a
Confidence Score: 5/5Safe 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
Reviews (2): Last reviewed commit: "feat: update date" | Re-trigger Greptile |
| components={{ | ||
| bold: <span className="text-b1s text-white" />, | ||
| summaryLink: <span className="text-blue underline" />, | ||
| }} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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).
There was a problem hiding this comment.
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 }) : ''; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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": { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Coverage Report for ./apps/evm
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@greptile review again |
| "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.", |
There was a problem hiding this comment.
The content in the Figma document is incorrect (I left a comment about this but it has not been updated):
| "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>.", |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
This utility function should be inside its own file and directory (inside the Timer directory).
| import { Timer } from './Timer'; | ||
|
|
||
| export interface EndOfCycleProps { | ||
| endDate?: Date; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes. have fixed this one, and add spinner for this
| "sec": "Sec", | ||
| "title": "End of cycle" | ||
| }, | ||
| "tablesRefreshNote": "Refreshed hourly · Last refresh: {{distance}} ago", |
There was a problem hiding this comment.
The codebase has a built-in way of displaying distances in time:
| "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.
fa92f7f to
68a561b
Compare
|
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. |
Jira ticket(s)
VPD-1331
Changes
implementation