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.
chajia/components/mothership/mothership-weekday-date-pic...

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>
);
}