fiddy/apps/web/components/confirm-slide-modal.tsx
2026-02-11 23:45:15 -08:00

106 lines
4.2 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
type ConfirmSlideModalProps = {
isOpen: boolean;
title: string;
description?: string;
confirmLabel?: string;
onClose: () => void;
onConfirm: () => void;
};
export default function ConfirmSlideModal({
isOpen,
title,
description,
confirmLabel = "Confirm",
onClose,
onConfirm
}: ConfirmSlideModalProps) {
const trackRef = useRef<HTMLDivElement | null>(null);
const [dragX, setDragX] = useState(0);
const [dragging, setDragging] = useState(false);
const handleSize = 44;
function handlePointerDown(event: React.PointerEvent<HTMLButtonElement>) {
event.preventDefault();
setDragging(true);
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
}
function handlePointerMove(event: React.PointerEvent<HTMLButtonElement>) {
if (!dragging) return;
const track = trackRef.current;
if (!track) return;
const rect = track.getBoundingClientRect();
const next = Math.min(Math.max(0, event.clientX - rect.left - handleSize / 2), rect.width - handleSize);
setDragX(next);
}
function handlePointerUp(event: React.PointerEvent<HTMLButtonElement>) {
if (!dragging) return;
setDragging(false);
(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);
const track = trackRef.current;
if (!track) return;
const threshold = (track.clientWidth - handleSize) * 0.8;
if (dragX >= threshold) {
setDragX(0);
onConfirm();
} else {
setDragX(0);
}
}
useEffect(() => {
if (!isOpen) return;
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") onClose();
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 p-4 !mt-0" onClick={onClose}>
<div className="w-full max-w-sm rounded-2xl border border-accent-weak bg-panel p-5" onClick={event => event.stopPropagation()}>
<div className="text-lg font-semibold">{title}</div>
{description ? <p className="mt-2 text-sm text-muted">{description}</p> : null}
<div className="mt-4">
<div className="text-xs text-soft">Slide to confirm</div>
<div
ref={trackRef}
className="mt-2 h-11 rounded-full border border-accent-weak bg-surface relative overflow-hidden touch-none select-none"
>
<div
className="absolute inset-y-0 left-0 bg-accent-soft rounded-full"
style={{ width: dragX + handleSize }}
/>
<button
type="button"
className="absolute top-0 left-0 h-11 w-11 rounded-full border border-accent bg-panel text-xl font-semibold text-[color:var(--color-text)] touch-none select-none leading-none"
style={{ transform: `translateX(${dragX}px)` }}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
aria-label="Slide to confirm"
>
</button>
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<div className="text-xs text-soft">{confirmLabel}</div>
<button type="button" className="rounded-lg btn-outline-accent px-3 py-1.5 text-sm" onClick={onClose}>
Cancel
</button>
</div>
</div>
</div>
);
}