mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 23:27:14 +00:00
2ddcf53020
* fix(sub): honor client expiry for external links * fix(ui): show client expiry on external links * fix(sub): address external expiry review
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { CloseCircleFilled } from '@ant-design/icons';
|
|
import { DatePicker } from 'antd';
|
|
import dayjs from 'dayjs';
|
|
import type { Dayjs } from 'dayjs';
|
|
import { PersianDateTimePicker } from 'persian-calendar-suite';
|
|
|
|
import { useDatepicker } from '@/hooks/useDatepicker';
|
|
import { useTheme } from '@/hooks/useTheme';
|
|
import './DateTimePicker.css';
|
|
|
|
interface DateTimePickerProps {
|
|
value: Dayjs | null;
|
|
onChange: (next: Dayjs | null) => void;
|
|
showTime?: boolean;
|
|
format?: string;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
allowClear?: boolean;
|
|
maxDate?: Dayjs;
|
|
}
|
|
|
|
const LIGHT_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#ffffff',
|
|
borderColor: '#d9d9d9',
|
|
hoverColor: 'rgba(22, 119, 255, 0.10)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(0, 0, 0, 0.88)',
|
|
};
|
|
|
|
const DARK_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#23252b',
|
|
borderColor: 'rgba(255, 255, 255, 0.12)',
|
|
hoverColor: 'rgba(22, 119, 255, 0.18)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(255, 255, 255, 0.88)',
|
|
};
|
|
|
|
const ULTRA_DARK_THEME = {
|
|
primaryColor: '#1677ff',
|
|
backgroundColor: '#101013',
|
|
borderColor: 'rgba(255, 255, 255, 0.08)',
|
|
hoverColor: 'rgba(22, 119, 255, 0.16)',
|
|
selectedTextColor: '#ffffff',
|
|
textColor: 'rgba(255, 255, 255, 0.88)',
|
|
};
|
|
|
|
export default function DateTimePicker({
|
|
value,
|
|
onChange,
|
|
showTime = true,
|
|
format = 'YYYY-MM-DD HH:mm:ss',
|
|
placeholder = '',
|
|
disabled = false,
|
|
allowClear = true,
|
|
maxDate,
|
|
}: DateTimePickerProps) {
|
|
const { t } = useTranslation();
|
|
const { datepicker } = useDatepicker();
|
|
const { isDark, isUltra } = useTheme();
|
|
const jalaliRef = useRef<HTMLDivElement>(null);
|
|
// Bumped on clear: persian-calendar-suite reads `value` only on mount, so
|
|
// remounting via key is the only way to reflect an externally cleared value.
|
|
const [clearNonce, setClearNonce] = useState(0);
|
|
// Mounted without a value, persian-calendar-suite seeds today and emits it —
|
|
// which would instantly undo a clear. Armed across every (re)mount.
|
|
const suppressMountEmit = useRef(true);
|
|
|
|
useEffect(() => {
|
|
suppressMountEmit.current = false;
|
|
return () => {
|
|
suppressMountEmit.current = true;
|
|
};
|
|
}, [clearNonce]);
|
|
|
|
const persianTheme = useMemo(() => {
|
|
if (isUltra) return ULTRA_DARK_THEME;
|
|
if (isDark) return DARK_THEME;
|
|
return LIGHT_THEME;
|
|
}, [isDark, isUltra]);
|
|
|
|
const commitChange = (next: Dayjs | null) => {
|
|
if (next && maxDate && next.isAfter(maxDate)) {
|
|
if (datepicker === 'jalalian') setClearNonce((n) => n + 1);
|
|
return;
|
|
}
|
|
onChange(next);
|
|
};
|
|
|
|
// The library hardcodes a Persian placeholder and exposes no working prop to
|
|
// override it, so clear it (or apply the caller's) on the input directly so
|
|
// the empty field shows no leftover Persian text. No dep array: re-apply
|
|
// after every render (incl. clear-remounts).
|
|
useEffect(() => {
|
|
if (datepicker !== 'jalalian') return;
|
|
const input = jalaliRef.current?.querySelector('input');
|
|
if (input) input.placeholder = placeholder;
|
|
});
|
|
|
|
if (datepicker === 'jalalian') {
|
|
return (
|
|
<div
|
|
ref={jalaliRef}
|
|
className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}${value ? '' : ' jdp-empty'}`}
|
|
>
|
|
<PersianDateTimePicker
|
|
key={clearNonce}
|
|
value={value ? value.valueOf() : null}
|
|
onChange={(next: number | string | null) => {
|
|
if (suppressMountEmit.current) return;
|
|
if (next == null || next === '') {
|
|
commitChange(null);
|
|
return;
|
|
}
|
|
const ms = typeof next === 'number' ? next : Number(next);
|
|
if (Number.isFinite(ms)) commitChange(dayjs(ms));
|
|
}}
|
|
showTime={showTime}
|
|
outputFormat="timestamp"
|
|
maxDate={maxDate?.toDate()}
|
|
persianNumbers
|
|
rtlCalendar
|
|
theme={persianTheme}
|
|
/>
|
|
{value && allowClear && !disabled && (
|
|
<button
|
|
type="button"
|
|
className="jdp-clear"
|
|
aria-label={t('clear')}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
commitChange(null);
|
|
setClearNonce((n) => n + 1);
|
|
}}
|
|
>
|
|
<CloseCircleFilled />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DatePicker
|
|
value={value}
|
|
onChange={(next) => commitChange(next || null)}
|
|
onCalendarChange={(next) => commitChange((Array.isArray(next) ? next[0] : next) || null)}
|
|
showTime={showTime ? { format: 'HH:mm:ss' } : false}
|
|
needConfirm={false}
|
|
format={format}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
allowClear={allowClear}
|
|
maxDate={maxDate}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
);
|
|
}
|