DateTimePicker.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { useEffect, useMemo, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { CloseCircleFilled } from '@ant-design/icons';
  4. import { DatePicker } from 'antd';
  5. import dayjs from 'dayjs';
  6. import type { Dayjs } from 'dayjs';
  7. import { PersianDateTimePicker } from 'persian-calendar-suite';
  8. import { useDatepicker } from '@/hooks/useDatepicker';
  9. import { useTheme } from '@/hooks/useTheme';
  10. import './DateTimePicker.css';
  11. interface DateTimePickerProps {
  12. value: Dayjs | null;
  13. onChange: (next: Dayjs | null) => void;
  14. showTime?: boolean;
  15. format?: string;
  16. placeholder?: string;
  17. disabled?: boolean;
  18. }
  19. const LIGHT_THEME = {
  20. primaryColor: '#1677ff',
  21. backgroundColor: '#ffffff',
  22. borderColor: '#d9d9d9',
  23. hoverColor: 'rgba(22, 119, 255, 0.10)',
  24. selectedTextColor: '#ffffff',
  25. textColor: 'rgba(0, 0, 0, 0.88)',
  26. };
  27. const DARK_THEME = {
  28. primaryColor: '#1677ff',
  29. backgroundColor: '#23252b',
  30. borderColor: 'rgba(255, 255, 255, 0.12)',
  31. hoverColor: 'rgba(22, 119, 255, 0.18)',
  32. selectedTextColor: '#ffffff',
  33. textColor: 'rgba(255, 255, 255, 0.88)',
  34. };
  35. const ULTRA_DARK_THEME = {
  36. primaryColor: '#1677ff',
  37. backgroundColor: '#101013',
  38. borderColor: 'rgba(255, 255, 255, 0.08)',
  39. hoverColor: 'rgba(22, 119, 255, 0.16)',
  40. selectedTextColor: '#ffffff',
  41. textColor: 'rgba(255, 255, 255, 0.88)',
  42. };
  43. export default function DateTimePicker({
  44. value,
  45. onChange,
  46. showTime = true,
  47. format = 'YYYY-MM-DD HH:mm:ss',
  48. placeholder = '',
  49. disabled = false,
  50. }: DateTimePickerProps) {
  51. const { t } = useTranslation();
  52. const { datepicker } = useDatepicker();
  53. const { isDark, isUltra } = useTheme();
  54. const jalaliRef = useRef<HTMLDivElement>(null);
  55. // Bumped on clear: persian-calendar-suite reads `value` only on mount, so
  56. // remounting via key is the only way to reflect an externally cleared value.
  57. const [clearNonce, setClearNonce] = useState(0);
  58. // Mounted without a value, persian-calendar-suite seeds today and emits it —
  59. // which would instantly undo a clear. Armed across every (re)mount.
  60. const suppressMountEmit = useRef(true);
  61. useEffect(() => {
  62. suppressMountEmit.current = false;
  63. return () => {
  64. suppressMountEmit.current = true;
  65. };
  66. }, [clearNonce]);
  67. const persianTheme = useMemo(() => {
  68. if (isUltra) return ULTRA_DARK_THEME;
  69. if (isDark) return DARK_THEME;
  70. return LIGHT_THEME;
  71. }, [isDark, isUltra]);
  72. // The library hardcodes a Persian placeholder and exposes no working prop to
  73. // override it, so clear it (or apply the caller's) on the input directly so
  74. // the empty field shows no leftover Persian text. No dep array: re-apply
  75. // after every render (incl. clear-remounts).
  76. useEffect(() => {
  77. if (datepicker !== 'jalalian') return;
  78. const input = jalaliRef.current?.querySelector('input');
  79. if (input) input.placeholder = placeholder;
  80. });
  81. if (datepicker === 'jalalian') {
  82. return (
  83. <div
  84. ref={jalaliRef}
  85. className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}${value ? '' : ' jdp-empty'}`}
  86. >
  87. <PersianDateTimePicker
  88. key={clearNonce}
  89. value={value ? value.valueOf() : null}
  90. onChange={(next: number | string | null) => {
  91. if (suppressMountEmit.current) return;
  92. if (next == null || next === '') {
  93. onChange(null);
  94. return;
  95. }
  96. const ms = typeof next === 'number' ? next : Number(next);
  97. if (Number.isFinite(ms)) onChange(dayjs(ms));
  98. }}
  99. showTime={showTime}
  100. outputFormat="timestamp"
  101. persianNumbers
  102. rtlCalendar
  103. theme={persianTheme}
  104. />
  105. {value && !disabled && (
  106. <button
  107. type="button"
  108. className="jdp-clear"
  109. aria-label={t('clear')}
  110. onMouseDown={(e) => e.preventDefault()}
  111. onClick={(e) => {
  112. e.stopPropagation();
  113. onChange(null);
  114. setClearNonce((n) => n + 1);
  115. }}
  116. >
  117. <CloseCircleFilled />
  118. </button>
  119. )}
  120. </div>
  121. );
  122. }
  123. return (
  124. <DatePicker
  125. value={value}
  126. onChange={(next) => onChange(next || null)}
  127. onCalendarChange={(next) => onChange((Array.isArray(next) ? next[0] : next) || null)}
  128. showTime={showTime ? { format: 'HH:mm:ss' } : false}
  129. needConfirm={false}
  130. format={format}
  131. placeholder={placeholder}
  132. disabled={disabled}
  133. style={{ width: '100%' }}
  134. />
  135. );
  136. }