22
33import React , { useId } from "react" ;
44import { motion , AnimatePresence , useReducedMotion , type Variants } from "framer-motion" ;
5+ import { useTranslations , useLocale } from "next-intl" ;
56import { useBalanceSync } from "@/hooks/useBalanceSync" ;
67
78interface RealTimeBalanceSyncProps {
@@ -52,6 +53,8 @@ export function RealTimeBalanceSync({
5253 className = "" ,
5354} : RealTimeBalanceSyncProps ) {
5455 const liveId = useId ( ) ;
56+ const locale = useLocale ( ) ;
57+ const t = useTranslations ( "realTimeBalanceSync" ) ;
5558 const shouldReduceMotion = useReducedMotion ( ) ;
5659
5760 const {
@@ -60,23 +63,34 @@ export function RealTimeBalanceSync({
6063 lastUpdated,
6164 error,
6265 refresh,
63- liveRegionText,
64- ariaLabel,
6566 } = useBalanceSync ( merchantId , apiKey , {
6667 address,
6768 horizonUrl,
6869 pollingInterval,
6970 enabled : true ,
7071 } ) ;
7172
73+ const liveRegionText = isLoading
74+ ? t ( "liveRegion.syncing" )
75+ : error
76+ ? t ( "liveRegion.error" , { error } )
77+ : lastUpdated
78+ ? t ( "liveRegion.updatedAt" , {
79+ time : lastUpdated . toLocaleTimeString ( locale , {
80+ hour : "numeric" ,
81+ minute : "2-digit" ,
82+ } ) ,
83+ } )
84+ : "" ;
85+
7286 const animProps = shouldReduceMotion
7387 ? { initial : false , animate : { } , variants : undefined }
7488 : { variants : containerVariants , initial : "hidden" , animate : "visible" } ;
7589
7690 return (
7791 < motion . section
78- className = { `rounded-2xl border border-gray -200 bg-white p-4 shadow-sm ${ className } ` }
79- aria-label = { ariaLabel }
92+ className = { `w-full rounded-3xl border border-slate -200 bg-white p-4 shadow-sm transition-all duration-200 ease-out ${ className } ` }
93+ aria-label = { t ( "sectionAriaLabel" ) }
8094 aria-busy = { isLoading }
8195 { ...animProps }
8296 >
@@ -90,19 +104,19 @@ export function RealTimeBalanceSync({
90104 { liveRegionText }
91105 </ div >
92106
93- < div className = "mb-3 flex items-center justify-between" >
94- < h2 className = "text-sm font-semibold text-gray-800 " >
95- Real-time Balances
107+ < div className = "mb-3 flex flex-col gap-3 sm:flex-row sm: items-center sm: justify-between" >
108+ < h2 className = "text-sm font-semibold text-slate-900 " >
109+ { t ( "title" ) }
96110 </ h2 >
97111 < motion . button
98112 onClick = { refresh }
99113 disabled = { isLoading }
100114 whileTap = { shouldReduceMotion ? undefined : { scale : 0.92 } }
101- aria-label = "Refresh balances"
115+ aria-label = { t ( "refreshButton" ) }
102116 aria-describedby = { liveId }
103- className = "rounded-md px-2 py-1 text-xs text-blue -600 hover:bg-blue-50 disabled:opacity- 50 focus:outline-none focus:ring-2 focus:ring-blue -400"
117+ className = "inline-flex items-center justify-center rounded-xl border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-semibold text-sky -600 transition hover:border-sky-300 hover:bg-sky- 50 focus:outline-none focus:ring-2 focus:ring-sky -400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 "
104118 >
105- { isLoading ? "Syncing\u2026" : "Refresh" }
119+ { isLoading ? t ( "syncing" ) : t ( "refreshButton" ) }
106120 </ motion . button >
107121 </ div >
108122
@@ -127,62 +141,72 @@ export function RealTimeBalanceSync({
127141 key = "empty"
128142 initial = { shouldReduceMotion ? undefined : { opacity : 0 } }
129143 animate = { shouldReduceMotion ? undefined : { opacity : 1 } }
130- className = "text-xs text-gray -500"
144+ className = "rounded-2xl border border-slate-100 bg-slate-50 px-3 py-3 text-xs text-slate -500"
131145 aria-live = "polite"
132146 >
133- No balances available.
147+ { t ( "emptyState" ) }
134148 </ motion . p >
135149 ) : (
136150 < motion . ul
137151 role = "list"
138- aria-label = "Account balances"
139- className = "divide-y divide-gray -100"
152+ aria-label = { t ( "balancesListAriaLabel" ) }
153+ className = "overflow-hidden rounded-2xl border border-slate-100 bg-slate-50 divide-y divide-slate -100"
140154 variants = { shouldReduceMotion ? undefined : listVariants }
141155 initial = "hidden"
142156 animate = "visible"
143157 >
144158 < AnimatePresence mode = "popLayout" >
145- { balances . map ( ( b ) => (
146- < motion . li
147- key = { b . code }
148- layout = { shouldReduceMotion ? undefined : true }
149- variants = { shouldReduceMotion ? undefined : itemVariants }
150- initial = "hidden"
151- animate = "visible"
152- exit = "exit"
153- transition = { { duration : 0.25 , ease : "easeOut" } }
154- className = "flex items-center justify-between py-2"
155- aria-label = { `${ b . code } balance: ${ b . balance } ` }
156- >
157- < span className = "text-sm font-medium text-gray-700" > { b . code } </ span >
158- < motion . span
159- className = "text-sm tabular-nums text-gray-900"
160- key = { `${ b . code } -${ b . balance } ` }
161- initial = { shouldReduceMotion ? undefined : { opacity : 0 } }
162- animate = { shouldReduceMotion ? undefined : { opacity : 1 } }
163- transition = { { duration : 0.3 } }
164- >
165- { parseFloat ( b . balance ) . toLocaleString ( undefined , {
166- minimumFractionDigits : 2 ,
167- maximumFractionDigits : 7 ,
159+ { balances . map ( ( b ) => {
160+ const formattedBalance = parseFloat ( b . balance ) . toLocaleString ( locale , {
161+ minimumFractionDigits : 2 ,
162+ maximumFractionDigits : 7 ,
163+ } ) ;
164+
165+ return (
166+ < motion . li
167+ key = { b . code }
168+ layout = { shouldReduceMotion ? undefined : true }
169+ variants = { shouldReduceMotion ? undefined : itemVariants }
170+ initial = "hidden"
171+ animate = "visible"
172+ exit = "exit"
173+ transition = { { duration : 0.25 , ease : "easeOut" } }
174+ className = "flex items-center justify-between gap-4 py-3 px-3"
175+ aria-label = { t ( "balanceItemAriaLabel" , {
176+ asset : b . code ,
177+ balance : formattedBalance ,
168178 } ) }
169- </ motion . span >
170- </ motion . li >
171- ) ) }
179+ >
180+ < span className = "text-sm font-medium text-slate-700" > { b . code } </ span >
181+ < motion . span
182+ className = "text-sm tabular-nums text-slate-900"
183+ key = { `${ b . code } -${ b . balance } ` }
184+ initial = { shouldReduceMotion ? undefined : { opacity : 0 } }
185+ animate = { shouldReduceMotion ? undefined : { opacity : 1 } }
186+ transition = { { duration : 0.3 } }
187+ >
188+ { formattedBalance }
189+ </ motion . span >
190+ </ motion . li >
191+ ) ;
192+ } ) }
172193 </ AnimatePresence >
173194 </ motion . ul >
174195 ) }
175196
176197 { lastUpdated && (
177198 < motion . p
178- className = "mt-3 text-xs text-gray -400"
199+ className = "mt-3 text-xs text-slate -400"
179200 initial = { shouldReduceMotion ? undefined : { opacity : 0 } }
180201 animate = { shouldReduceMotion ? undefined : { opacity : 1 } }
181202 transition = { { delay : 0.2 , duration : 0.3 } }
182203 >
183- < span aria-hidden = "true" > Updated </ span >
204+ < span className = "font-medium text-slate-500" > { t ( "updatedLabel" ) } </ span > { " " }
184205 < time dateTime = { lastUpdated . toISOString ( ) } >
185- { lastUpdated . toLocaleTimeString ( ) }
206+ { lastUpdated . toLocaleTimeString ( locale , {
207+ hour : "numeric" ,
208+ minute : "2-digit" ,
209+ } ) }
186210 </ time >
187211 </ motion . p >
188212 ) }
0 commit comments