From cac06f22fa70f47175283ef000ef2e3893d6972a Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Tue, 3 Mar 2026 18:00:36 +0300 Subject: [PATCH] feat: add controlled flip functionality to DutyTimelineCard component - Introduced `isFlipped` and `onFlipChange` props to `DutyTimelineCard` for controlled flipping behavior. - Updated `DutyList` to manage the flipped state of duty cards, allowing only one card to be flipped at a time. - Enhanced user interaction by implementing dedicated functions for flipping the card to contacts and back, improving usability. --- webapp-next/src/components/duty/DutyList.tsx | 10 +++- .../src/components/duty/DutyTimelineCard.tsx | 49 +++++++++++++------ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/webapp-next/src/components/duty/DutyList.tsx b/webapp-next/src/components/duty/DutyList.tsx index 3fec87e..83e5cef 100644 --- a/webapp-next/src/components/duty/DutyList.tsx +++ b/webapp-next/src/components/duty/DutyList.tsx @@ -108,6 +108,7 @@ export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) { const todayKey = localDateString(new Date()); const [now, setNow] = useState(() => new Date()); + const [flippedDutyId, setFlippedDutyId] = useState(null); useEffect(() => { const id = setInterval(() => setNow(new Date()), 60_000); return () => clearInterval(id); @@ -209,7 +210,14 @@ export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) { className="min-w-0 overflow-hidden" {...(isCurrent ? { "data-current-duty": true } : {})} > - + + setFlippedDutyId(flip ? duty.id : null) + } + /> ); diff --git a/webapp-next/src/components/duty/DutyTimelineCard.tsx b/webapp-next/src/components/duty/DutyTimelineCard.tsx index 8c2071b..e027172 100644 --- a/webapp-next/src/components/duty/DutyTimelineCard.tsx +++ b/webapp-next/src/components/duty/DutyTimelineCard.tsx @@ -21,6 +21,10 @@ import { Phone, ArrowLeft } from "lucide-react"; export interface DutyTimelineCardProps { duty: DutyWithUser; isCurrent: boolean; + /** When provided, card is controlled: only one card can be flipped at a time (managed by parent). */ + isFlipped?: boolean; + /** Called when user flips to contacts (true) or back (false). Used with isFlipped for controlled mode. */ + onFlipChange?: (flipped: boolean) => void; } function buildTimeStr(duty: DutyWithUser): string { @@ -48,9 +52,14 @@ const borderByType = { * Renders a single duty card. If duty has phone or username, wraps in a flip card * (front: type, name, time + "Contacts" button; back: name, ContactLinks, "Back" button). */ -export function DutyTimelineCard({ duty, isCurrent }: DutyTimelineCardProps) { +export function DutyTimelineCard({ + duty, + isCurrent, + isFlipped, + onFlipChange, +}: DutyTimelineCardProps) { const { t } = useTranslation(); - const [flipped, setFlipped] = useState(false); + const [localFlipped, setLocalFlipped] = useState(false); const frontBtnRef = useRef(null); const backBtnRef = useRef(null); const hasContacts = Boolean( @@ -60,13 +69,31 @@ export function DutyTimelineCard({ duty, isCurrent }: DutyTimelineCardProps) { const typeLabel = isCurrent ? t("duty.now_on_duty") : t(`event_type.${duty.event_type || "duty"}`); - const timeStr = useMemo( - () => buildTimeStr(duty), - [duty.start_at, duty.end_at] - ); + const timeStr = useMemo(() => buildTimeStr(duty), [duty]); const eventType = (duty.event_type || "duty") as keyof typeof borderByType; const borderClass = isCurrent ? "border-l-today" : borderByType[eventType] ?? "border-l-duty"; + const isControlled = onFlipChange != null; + const flipped = isControlled ? (isFlipped ?? false) : localFlipped; + + const handleFlipToBack = () => { + if (isControlled) { + onFlipChange?.(true); + } else { + setLocalFlipped(true); + } + setTimeout(() => backBtnRef.current?.focus(), 310); + }; + + const handleFlipToFront = () => { + if (isControlled) { + onFlipChange?.(false); + } else { + setLocalFlipped(false); + } + setTimeout(() => frontBtnRef.current?.focus(), 310); + }; + if (!hasContacts) { return (
{ - setFlipped(true); - setTimeout(() => backBtnRef.current?.focus(), 310); - }} + onClick={handleFlipToBack} > @@ -159,10 +183,7 @@ export function DutyTimelineCard({ duty, isCurrent }: DutyTimelineCardProps) { size="icon" className="absolute right-2 top-1/2 -translate-y-1/2 size-9 rounded-full bg-surface text-accent hover:bg-accent/20" aria-label={t("contact.back")} - onClick={() => { - setFlipped(false); - setTimeout(() => frontBtnRef.current?.focus(), 310); - }} + onClick={handleFlipToFront} >