feat(timeline): add pulse animation for in-flight drag mutations
Allocation bars that have active optimistic overrides (post-drag, awaiting server confirmation) now pulse subtly via animate-pulse. The pending set is derived from the existing optimisticAllocations map keys, requiring no additional state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
|
||||
import { useState } from "react";
|
||||
import { OrderType, AllocationType, ProjectStatus } from "@capakraken/shared";
|
||||
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
|
||||
import type { Project } from "@capakraken/shared";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { DateInput } from "~/components/ui/DateInput.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
import { toDateInputValue } from "~/lib/format.js";
|
||||
|
||||
const ORDER_TYPE_OPTIONS = [
|
||||
{ value: "BD", label: "BD" },
|
||||
@@ -28,14 +29,6 @@ const STATUS_OPTIONS = [
|
||||
{ value: "CANCELLED", label: "Cancelled" },
|
||||
] as const;
|
||||
|
||||
function formatDateForInput(date: Date | string): string {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
shortCode: string;
|
||||
name: string;
|
||||
@@ -54,7 +47,7 @@ interface FormState {
|
||||
}
|
||||
|
||||
function getDefaultForm(): FormState {
|
||||
const today = formatDateForInput(new Date());
|
||||
const today = toDateInputValue(new Date());
|
||||
return {
|
||||
shortCode: "",
|
||||
name: "",
|
||||
@@ -81,8 +74,8 @@ function projectToForm(project: Project): FormState {
|
||||
allocationType: project.allocationType,
|
||||
winProbability: String(project.winProbability),
|
||||
budgetEur: String(Math.round(project.budgetCents) / 100),
|
||||
startDate: formatDateForInput(project.startDate),
|
||||
endDate: formatDateForInput(project.endDate),
|
||||
startDate: toDateInputValue(project.startDate),
|
||||
endDate: toDateInputValue(project.endDate),
|
||||
status: project.status,
|
||||
responsiblePerson: project.responsiblePerson ?? "",
|
||||
color: (project as unknown as { color?: string | null }).color ?? "",
|
||||
@@ -108,9 +101,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
useFocusTrap(panelRef, true);
|
||||
|
||||
const { data: utilizationCategories } = trpc.utilizationCategory.list.useQuery(undefined, { staleTime: 60_000 });
|
||||
const { data: clientList } = trpc.clientEntity.list.useQuery(undefined, { staleTime: 60_000 });
|
||||
|
||||
@@ -139,15 +129,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
|
||||
|
||||
const isLoading = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
function setField<K extends keyof FormState>(key: K, value: FormState[K]) {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
setErrors((prev) => ({ ...prev, [key]: undefined }));
|
||||
@@ -246,17 +227,8 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
|
||||
const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-xl mx-4"
|
||||
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
|
||||
>
|
||||
<AnimatedModal open={true} onClose={onClose} maxWidth="max-w-xl" className="mx-4">
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
@@ -609,6 +581,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user