DateTimePicker.tsx 4.9 KB

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