Spaces:
Running
Running
| import { format } from "date-fns"; | |
| import { Calendar } from "lucide-react"; | |
| import { useEffect, useRef, useState } from "react"; | |
| import { createPortal } from "react-dom"; | |
| export const DEFAULT_PHASE_COLOR = "#14b8a6"; | |
| const PHASE_COLOR_OPTIONS = [ | |
| "#14b8a6", | |
| "#0ea5e9", | |
| "#6366f1", | |
| "#a855f7", | |
| "#ec4899", | |
| "#f59e0b", | |
| "#22c55e", | |
| "#64748b", | |
| ]; | |
| export interface PhaseCreateInput { | |
| name: string; | |
| start_date: string; | |
| end_date: string; | |
| color: string; | |
| } | |
| interface PhaseCreatePopoverProps { | |
| anchor: { top: number; left: number }; | |
| startDate: string; | |
| endDate: string; | |
| onCancel: () => void; | |
| onSave: (input: PhaseCreateInput) => void; | |
| isSaving?: boolean; | |
| } | |
| export function PhaseCreatePopover({ | |
| anchor, | |
| startDate, | |
| endDate, | |
| onCancel, | |
| onSave, | |
| isSaving = false, | |
| }: PhaseCreatePopoverProps) { | |
| const popoverRef = useRef<HTMLDivElement>(null); | |
| const [name, setName] = useState("Phase"); | |
| const [color, setColor] = useState(DEFAULT_PHASE_COLOR); | |
| const [showColors, setShowColors] = useState(false); | |
| useEffect(() => { | |
| setName("Phase"); | |
| setColor(DEFAULT_PHASE_COLOR); | |
| setShowColors(false); | |
| }, [startDate, endDate]); | |
| useEffect(() => { | |
| const onKey = (event: KeyboardEvent) => { | |
| if (event.key === "Escape") onCancel(); | |
| }; | |
| const onDown = (event: MouseEvent) => { | |
| if (popoverRef.current?.contains(event.target as Node)) return; | |
| onCancel(); | |
| }; | |
| window.addEventListener("keydown", onKey); | |
| document.addEventListener("mousedown", onDown); | |
| return () => { | |
| window.removeEventListener("keydown", onKey); | |
| document.removeEventListener("mousedown", onDown); | |
| }; | |
| }, [onCancel]); | |
| const rangeLabel = | |
| startDate === endDate | |
| ? format(new Date(startDate), "d MMM yyyy") | |
| : `${format(new Date(startDate), "d MMM")} – ${format(new Date(endDate), "d MMM yyyy")}`; | |
| const style = { | |
| top: Math.min(anchor.top, window.innerHeight - 200), | |
| left: Math.min(Math.max(8, anchor.left), window.innerWidth - 320), | |
| }; | |
| return createPortal( | |
| <div className="phase-create-popover" ref={popoverRef} style={style}> | |
| <input | |
| aria-label="Phase name" | |
| autoFocus | |
| className="phase-create-popover-input" | |
| onChange={(event) => setName(event.target.value)} | |
| onKeyDown={(event) => { | |
| if (event.key === "Enter" && name.trim()) { | |
| onSave({ name: name.trim(), start_date: startDate, end_date: endDate, color }); | |
| } | |
| }} | |
| placeholder="Phase" | |
| type="text" | |
| value={name} | |
| /> | |
| <div className="phase-create-popover-footer"> | |
| <button className="link-button" onClick={onCancel} type="button"> | |
| Cancel | |
| </button> | |
| <div className="phase-create-popover-tools"> | |
| <div className="phase-color-picker"> | |
| <button | |
| aria-label="Phase color" | |
| className="phase-color-trigger" | |
| onClick={() => setShowColors((open) => !open)} | |
| style={{ background: color }} | |
| type="button" | |
| /> | |
| {showColors ? ( | |
| <div className="phase-color-menu"> | |
| {PHASE_COLOR_OPTIONS.map((option) => ( | |
| <button | |
| aria-label={`Color ${option}`} | |
| className={option === color ? "is-selected" : undefined} | |
| key={option} | |
| onClick={() => { | |
| setColor(option); | |
| setShowColors(false); | |
| }} | |
| style={{ background: option }} | |
| type="button" | |
| /> | |
| ))} | |
| </div> | |
| ) : null} | |
| </div> | |
| <span className="phase-date-hint" title={rangeLabel}> | |
| <Calendar aria-hidden size={16} /> | |
| </span> | |
| <button | |
| className="primary-button" | |
| disabled={isSaving || !name.trim()} | |
| onClick={() => | |
| onSave({ name: name.trim(), start_date: startDate, end_date: endDate, color }) | |
| } | |
| type="button" | |
| > | |
| Save | |
| </button> | |
| </div> | |
| </div> | |
| </div>, | |
| document.body, | |
| ); | |
| } | |
| export function phasePopoverAnchorFromTrack( | |
| trackRect: DOMRect, | |
| startIndex: number, | |
| endIndex: number, | |
| totalDays: number, | |
| ): { top: number; left: number } { | |
| const start = Math.min(startIndex, endIndex); | |
| const end = Math.max(startIndex, endIndex); | |
| const span = end - start + 1; | |
| const leftPct = start / totalDays; | |
| const widthPct = span / totalDays; | |
| const centerX = trackRect.left + trackRect.width * (leftPct + widthPct / 2); | |
| return { top: trackRect.top - 12, left: centerX - 150 }; | |
| } | |