mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 19:56:37 +08:00
fix(frontend): polish batch actions and sharing page guidance (#157)
* fix(frontend): polish batch actions and sharing page guidance * fix(frontend): add top spacing above sharing section banners * fix(frontend): allow manual and calendar expiry date input * fix(frontend): switch batch action buttons to warning tone * fix(frontend): switch batch action buttons to default tone
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||
|
||||
@@ -11,6 +12,50 @@ interface CalendarDateLike {
|
||||
year: number;
|
||||
}
|
||||
|
||||
function isValidCalendarDate(year: number, month: number, day: number) {
|
||||
if (
|
||||
!Number.isInteger(year) ||
|
||||
!Number.isInteger(month) ||
|
||||
!Number.isInteger(day)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
||||
return false;
|
||||
}
|
||||
const candidate = new Date(year, month - 1, day);
|
||||
|
||||
return (
|
||||
candidate.getFullYear() === year &&
|
||||
candidate.getMonth() === month - 1 &&
|
||||
candidate.getDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
function parseDateText(value: string) {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matched = trimmed.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/);
|
||||
|
||||
if (!matched) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const year = Number(matched[1]);
|
||||
const month = Number(matched[2]);
|
||||
const day = Number(matched[3]);
|
||||
|
||||
if (!isValidCalendarDate(year, month, day)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { day, month, year };
|
||||
}
|
||||
|
||||
export interface DatePickerProps extends FieldMetaProps {
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
@@ -39,9 +84,105 @@ export function DatePicker({
|
||||
isRequired,
|
||||
label,
|
||||
onChange,
|
||||
showMonthAndYearPickers,
|
||||
value,
|
||||
}: DatePickerProps) {
|
||||
const id = React.useId();
|
||||
const nativePickerRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const formattedValue = React.useMemo(() => formatDateValue(value), [value]);
|
||||
const [textValue, setTextValue] = React.useState(formattedValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTextValue(formattedValue);
|
||||
}, [formattedValue]);
|
||||
|
||||
const shouldUseTextInput = Boolean(showMonthAndYearPickers);
|
||||
|
||||
const notifyNativeDateChange = (rawValue: string) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rawValue) {
|
||||
onChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const [yearText, monthText, dayText] = rawValue.split("-");
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
|
||||
if (!isValidCalendarDate(year, month, day)) {
|
||||
onChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ day, month, year });
|
||||
};
|
||||
|
||||
const notifyTextDateChange = (rawValue: string) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rawValue.trim()) {
|
||||
onChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseDateText(rawValue);
|
||||
|
||||
if (parsed) {
|
||||
onChange(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
const commitTextInput = () => {
|
||||
const parsed = parseDateText(textValue);
|
||||
|
||||
if (parsed) {
|
||||
setTextValue(formatDateValue(parsed));
|
||||
onChange?.(parsed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!textValue.trim()) {
|
||||
onChange?.(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setTextValue(formattedValue);
|
||||
};
|
||||
|
||||
const openNativePicker = () => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
const picker = nativePickerRef.current;
|
||||
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pickerWithShow = picker as HTMLInputElement & {
|
||||
showPicker?: () => void;
|
||||
};
|
||||
|
||||
if (typeof pickerWithShow.showPicker === "function") {
|
||||
pickerWithShow.showPicker();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
picker.focus();
|
||||
picker.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldContainer
|
||||
@@ -52,43 +193,71 @@ export function DatePicker({
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
>
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn(className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
required={isRequired}
|
||||
type="date"
|
||||
value={formatDateValue(value)}
|
||||
onChange={(event) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
{shouldUseTextInput ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn("flex-1", className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
inputMode="numeric"
|
||||
placeholder="YYYY-MM-DD"
|
||||
required={isRequired}
|
||||
type="text"
|
||||
value={textValue}
|
||||
onBlur={commitTextInput}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
|
||||
if (!event.target.value) {
|
||||
onChange(null);
|
||||
setTextValue(nextValue);
|
||||
notifyTextDateChange(nextValue);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
commitTextInput();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
aria-label="打开日历选择器"
|
||||
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-input bg-background text-default-600 shadow-sm transition-colors hover:bg-default-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isDisabled}
|
||||
type="button"
|
||||
onClick={openNativePicker}
|
||||
>
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={nativePickerRef}
|
||||
aria-hidden="true"
|
||||
className="sr-only"
|
||||
disabled={isDisabled}
|
||||
tabIndex={-1}
|
||||
type="date"
|
||||
value={formattedValue}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const [yearText, monthText, dayText] = event.target.value.split("-");
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
|
||||
if (
|
||||
!Number.isFinite(year) ||
|
||||
!Number.isFinite(month) ||
|
||||
!Number.isFinite(day)
|
||||
) {
|
||||
onChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ day, month, year });
|
||||
}}
|
||||
/>
|
||||
setTextValue(nextValue);
|
||||
notifyNativeDateChange(nextValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn(className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
required={isRequired}
|
||||
type="date"
|
||||
value={formattedValue}
|
||||
onChange={(event) => {
|
||||
notifyNativeDateChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FieldContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user