RetryCountdown.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { useCallback, useEffect, useMemo, useState } from 'react'
  2. interface RetryCountdownProps {
  3. nextRetryTime: string // RFC3339 formatted date
  4. }
  5. interface TimeRemaining {
  6. days: number
  7. hours: number
  8. minutes: number
  9. seconds: number
  10. isExpired: boolean
  11. isInvalid: boolean
  12. }
  13. export const RetryCountdown = ({ nextRetryTime }: RetryCountdownProps) => {
  14. const [timeRemaining, setTimeRemaining] = useState<TimeRemaining>({
  15. days: 0,
  16. hours: 0,
  17. minutes: 0,
  18. seconds: 0,
  19. isExpired: false,
  20. isInvalid: false,
  21. })
  22. const targetTimestamp = useMemo(() => {
  23. try {
  24. const date = new Date(nextRetryTime)
  25. if (isNaN(date.getTime())) {
  26. return null
  27. }
  28. return date.getTime()
  29. } catch {
  30. return null
  31. }
  32. }, [nextRetryTime])
  33. const calculateTimeRemaining = useCallback((targetTime: number): TimeRemaining => {
  34. const now = Date.now()
  35. const difference = targetTime - now
  36. if (difference <= 0) {
  37. return { days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true, isInvalid: false }
  38. }
  39. const days = Math.floor(difference / (1000 * 60 * 60 * 24))
  40. const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
  41. const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60))
  42. const seconds = Math.floor((difference % (1000 * 60)) / 1000)
  43. return { days, hours, minutes, seconds, isExpired: false, isInvalid: false }
  44. }, [])
  45. useEffect(() => {
  46. if (targetTimestamp === null) return
  47. const updateTimer = () => {
  48. setTimeRemaining(calculateTimeRemaining(targetTimestamp))
  49. }
  50. updateTimer()
  51. const interval = setInterval(updateTimer, 1000)
  52. return () => clearInterval(interval)
  53. }, [targetTimestamp, calculateTimeRemaining])
  54. const { timeDisplay, statusMessage } = useMemo(() => {
  55. if (targetTimestamp === null) {
  56. return {
  57. timeDisplay: 'Invalid retry time format',
  58. statusMessage: '',
  59. }
  60. }
  61. const formatTimeUnit = (value: number, unit: string) => {
  62. if (value === 0) return null
  63. return `${value}${unit.charAt(0)}`
  64. }
  65. let timeDisplay: string
  66. let statusMessage: string
  67. if (timeRemaining.isExpired) {
  68. statusMessage = ''
  69. timeDisplay = 'Retrying soon...'
  70. } else {
  71. const parts = [
  72. formatTimeUnit(timeRemaining.days, 'day'),
  73. formatTimeUnit(timeRemaining.hours, 'hour'),
  74. formatTimeUnit(timeRemaining.minutes, 'minute'),
  75. formatTimeUnit(timeRemaining.seconds, 'second'),
  76. ].filter(Boolean)
  77. statusMessage = parts.length === 0 ? '' : 'Next retry in:'
  78. timeDisplay = parts.length === 0 ? 'Retrying soon...' : parts.join(' ')
  79. }
  80. return { timeDisplay, statusMessage }
  81. }, [targetTimestamp, timeRemaining])
  82. return (
  83. <div role="status" aria-live="polite" aria-label={`${statusMessage} ${timeDisplay}`}>
  84. <span className="text-xs font-medium">{statusMessage}</span>{' '}
  85. {/* [Joshen] It's a bit hard to debug without doing this locally, but we could use CountdownTimerSpan here perhaps */}
  86. <span className="text-xs font-mono">{timeDisplay}</span>
  87. </div>
  88. )
  89. }