DateTimePicker.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. const persianTheme = useMemo(() => {
  59. if (isUltra) return ULTRA_DARK_THEME;
  60. if (isDark) return DARK_THEME;
  61. return LIGHT_THEME;
  62. }, [isDark, isUltra]);
  63. // The library hardcodes a Persian placeholder and exposes no working prop to
  64. // override it, so clear it (or apply the caller's) on the input directly so
  65. // the empty field shows no leftover Persian text. No dep array: re-apply
  66. // after every render (incl. clear-remounts).
  67. useEffect(() => {
  68. if (datepicker !== 'jalalian') return;
  69. const input = jalaliRef.current?.querySelector('input');
  70. if (input) input.placeholder = placeholder;
  71. });
  72. if (datepicker === 'jalalian') {
  73. return (
  74. <div ref={jalaliRef} className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}`}>
  75. <PersianDateTimePicker
  76. key={clearNonce}
  77. value={value ? value.valueOf() : null}
  78. onChange={(next: number | string | null) => {
  79. if (next == null || next === '') {
  80. onChange(null);
  81. return;
  82. }
  83. const ms = typeof next === 'number' ? next : Number(next);
  84. if (Number.isFinite(ms)) onChange(dayjs(ms));
  85. }}
  86. showTime={showTime}
  87. outputFormat="timestamp"
  88. persianNumbers
  89. rtlCalendar
  90. theme={persianTheme}
  91. />
  92. {value && !disabled && (
  93. <button
  94. type="button"
  95. className="jdp-clear"
  96. aria-label={t('clear')}
  97. onMouseDown={(e) => e.preventDefault()}
  98. onClick={(e) => {
  99. e.stopPropagation();
  100. onChange(null);
  101. setClearNonce((n) => n + 1);
  102. }}
  103. >
  104. <CloseCircleFilled />
  105. </button>
  106. )}
  107. </div>
  108. );
  109. }
  110. return (
  111. <DatePicker
  112. value={value}
  113. onChange={(next) => onChange(next || null)}
  114. onCalendarChange={(next) => onChange((Array.isArray(next) ? next[0] : next) || null)}
  115. showTime={showTime ? { format: 'HH:mm:ss' } : false}
  116. needConfirm={false}
  117. format={format}
  118. placeholder={placeholder}
  119. disabled={disabled}
  120. style={{ width: '100%' }}
  121. />
  122. );
  123. }