You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
157 lines
5.1 KiB
157 lines
5.1 KiB
"use client";
|
|
|
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
import { CaretDown, CaretLeft, CaretRight } from "@phosphor-icons/react";
|
|
import {
|
|
minMothershipReadyDateIso,
|
|
parseIsoDateLocal,
|
|
} from "@/lib/mothership/logged-in-constraints";
|
|
import {
|
|
buildMothershipWeekdayGrid,
|
|
formatMothershipWeekdayDateLabel,
|
|
monthLabelZh,
|
|
} from "@/lib/mothership/weekday-date-picker";
|
|
import { quoteInputCls } from "@/components/quote/quote-form-chrome";
|
|
|
|
const WEEKDAYS_ZH = ["日", "一", "二", "三", "四", "五", "六"] as const;
|
|
|
|
export type MothershipWeekdayDatePickerProps = {
|
|
value: string;
|
|
onChange: (iso: string) => void;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
minIso?: string;
|
|
};
|
|
|
|
export function MothershipWeekdayDatePicker({
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
className,
|
|
minIso = minMothershipReadyDateIso(),
|
|
}: MothershipWeekdayDatePickerProps) {
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const listId = useId();
|
|
const [open, setOpen] = useState(false);
|
|
const anchor = parseIsoDateLocal(value) ?? parseIsoDateLocal(minIso)!;
|
|
const [viewYear, setViewYear] = useState(anchor.getFullYear());
|
|
const [viewMonth, setViewMonth] = useState(anchor.getMonth());
|
|
|
|
useEffect(() => {
|
|
const d = parseIsoDateLocal(value);
|
|
if (!d) return;
|
|
setViewYear(d.getFullYear());
|
|
setViewMonth(d.getMonth());
|
|
}, [value]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onDoc = (e: MouseEvent) => {
|
|
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
document.addEventListener("mousedown", onDoc);
|
|
return () => document.removeEventListener("mousedown", onDoc);
|
|
}, [open]);
|
|
|
|
const cells = useMemo(
|
|
() => buildMothershipWeekdayGrid(viewYear, viewMonth, value, minIso),
|
|
[viewYear, viewMonth, value, minIso],
|
|
);
|
|
|
|
const shiftMonth = (delta: number) => {
|
|
const d = new Date(viewYear, viewMonth + delta, 1, 12, 0, 0);
|
|
setViewYear(d.getFullYear());
|
|
setViewMonth(d.getMonth());
|
|
};
|
|
|
|
return (
|
|
<div ref={rootRef} className="relative">
|
|
<button
|
|
type="button"
|
|
id={listId}
|
|
disabled={disabled}
|
|
aria-haspopup="dialog"
|
|
aria-expanded={open}
|
|
onClick={() => setOpen((v) => !v)}
|
|
className={className ?? quoteInputCls + " flex items-center justify-between text-left"}
|
|
>
|
|
<span>{formatMothershipWeekdayDateLabel(value)}</span>
|
|
<CaretDown size={16} className="shrink-0 text-text-secondary" />
|
|
</button>
|
|
{open && !disabled && (
|
|
<div
|
|
role="dialog"
|
|
aria-labelledby={listId}
|
|
className="absolute z-30 mt-1 w-[280px] rounded-lg border border-border bg-surface p-3 shadow-modal"
|
|
>
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
aria-label="上一月"
|
|
className="rounded p-1 text-text-secondary hover:bg-bg"
|
|
onClick={() => shiftMonth(-1)}
|
|
>
|
|
<CaretLeft size={16} />
|
|
</button>
|
|
<span className="text-sm font-medium text-text-primary">
|
|
{monthLabelZh(viewYear, viewMonth)}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
aria-label="下一月"
|
|
className="rounded p-1 text-text-secondary hover:bg-bg"
|
|
onClick={() => shiftMonth(1)}
|
|
>
|
|
<CaretRight size={16} />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-1 text-center text-[11px] text-text-secondary">
|
|
{WEEKDAYS_ZH.map((w) => (
|
|
<div key={w} className="py-1 font-medium">
|
|
{w}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-1 grid grid-cols-7 gap-1">
|
|
{cells.map((cell) => {
|
|
const base =
|
|
"flex h-9 w-9 items-center justify-center rounded-full text-sm transition-colors";
|
|
let cls = base;
|
|
if (!cell.inMonth) {
|
|
cls += " invisible pointer-events-none";
|
|
} else if (cell.disabled) {
|
|
cls += " cursor-not-allowed text-text-disabled opacity-40";
|
|
} else if (cell.isSelected) {
|
|
cls +=
|
|
" bg-[#1890FF] font-semibold text-white hover:bg-[#1580E0]";
|
|
} else {
|
|
cls +=
|
|
" text-text-primary hover:bg-[#1890FF]/10 hover:text-[#1890FF]";
|
|
}
|
|
return (
|
|
<button
|
|
key={cell.iso + String(cell.inMonth)}
|
|
type="button"
|
|
disabled={cell.disabled || !cell.inMonth}
|
|
aria-label={cell.iso}
|
|
aria-selected={cell.isSelected}
|
|
className={cls}
|
|
onClick={() => {
|
|
onChange(cell.iso);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
{cell.day}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="mt-2 text-[11px] text-text-secondary">
|
|
周六、周日不可选
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|