From de9cbb14d0c6002ccd400d97401edb4166f633f8 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 6 Jun 2026 20:04:13 +0530 Subject: [PATCH] feat: Add modal dragHandlers and prevent background scroll --- src/components/BeanForm.jsx | 58 +++++++++++++++++++-- src/components/BrewForm.jsx | 83 +++++++++++++++++++++++++++++-- src/components/CreateModal.jsx | 58 +++++++++++++++++++-- src/components/IosPromptModal.jsx | 60 +++++++++++++++++++++- 4 files changed, 246 insertions(+), 13 deletions(-) diff --git a/src/components/BeanForm.jsx b/src/components/BeanForm.jsx index 68b3131..c096b74 100644 --- a/src/components/BeanForm.jsx +++ b/src/components/BeanForm.jsx @@ -4,12 +4,20 @@ import { ROAST_TYPES, inputCls, labelCls } from "../constants"; export default function BeanForm({ onSave, onClose, initial }) { const [form, setForm] = useState(initial || { name: "", roastery: "", roastDate: "", roastType: "", image: "", tastingNotes: "" }); const [active, setActive] = useState(false); + const [dragY, setDragY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [startY, setStartY] = useState(0); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); const canSave = form.name.trim().length > 0; useEffect(() => { + // Prevent background scroll when mounted + document.body.style.overflow = "hidden"; const raf = requestAnimationFrame(() => setActive(true)); - return () => cancelAnimationFrame(raf); + return () => { + cancelAnimationFrame(raf); + document.body.style.overflow = ""; + }; }, []); const handleClose = (callback) => { @@ -23,6 +31,29 @@ export default function BeanForm({ onSave, onClose, initial }) { }, 200); }; + const handleDragStart = (clientY) => { + setIsDragging(true); + setStartY(clientY); + }; + + const handleDragMove = (clientY) => { + if (!isDragging) return; + const deltaY = clientY - startY; + if (deltaY > 0) { + setDragY(deltaY); + } + }; + + const handleDragEnd = () => { + if (!isDragging) return; + setIsDragging(false); + if (dragY > 100) { + handleClose(); + } else { + setDragY(0); + } + }; + const handleImage = (e) => { const file = e.target.files?.[0]; if (!file) return; @@ -38,10 +69,31 @@ export default function BeanForm({ onSave, onClose, initial }) { onClick={() => handleClose()} >
e.stopPropagation()} + style={{ + transform: isDragging + ? `translateY(${dragY}px)` + : active + ? `translateY(${dragY}px)` + : "translateY(100%)", + transition: isDragging ? "none" : "transform 0.2s ease-out" + }} > -
+
handleDragStart(e.touches[0].clientY)} + onTouchMove={(e) => handleDragMove(e.touches[0].clientY)} + onTouchEnd={handleDragEnd} + onMouseDown={(e) => handleDragStart(e.clientY)} + onMouseMove={(e) => { + if (e.buttons === 1) handleDragMove(e.clientY); + }} + onMouseUp={handleDragEnd} + onMouseLeave={handleDragEnd} + > +
+
{initial ? "Edit Bean" : "Add Bean"}
set("name", e.target.value)} />
diff --git a/src/components/BrewForm.jsx b/src/components/BrewForm.jsx index 4b25c62..6973b1d 100644 --- a/src/components/BrewForm.jsx +++ b/src/components/BrewForm.jsx @@ -5,11 +5,19 @@ export default function BrewForm({ beans, onSave, onClose }) { const [method, setMethod] = useState("pourover"); const [form, setForm] = useState({ beanId: beans[0]?.id || "" }); const [active, setActive] = useState(false); + const [dragY, setDragY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [startY, setStartY] = useState(0); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); useEffect(() => { + // Prevent background scroll when mounted + document.body.style.overflow = "hidden"; const raf = requestAnimationFrame(() => setActive(true)); - return () => cancelAnimationFrame(raf); + return () => { + cancelAnimationFrame(raf); + document.body.style.overflow = ""; + }; }, []); const handleClose = (callback) => { @@ -23,6 +31,29 @@ export default function BrewForm({ beans, onSave, onClose }) { }, 200); }; + const handleDragStart = (clientY) => { + setIsDragging(true); + setStartY(clientY); + }; + + const handleDragMove = (clientY) => { + if (!isDragging) return; + const deltaY = clientY - startY; + if (deltaY > 0) { + setDragY(deltaY); + } + }; + + const handleDragEnd = () => { + if (!isDragging) return; + setIsDragging(false); + if (dragY > 100) { + handleClose(); + } else { + setDragY(0); + } + }; + if (beans.length === 0) { return (
handleClose()} >
e.stopPropagation()} + style={{ + transform: isDragging + ? `translateY(${dragY}px)` + : active + ? `translateY(${dragY}px)` + : "translateY(100%)", + transition: isDragging ? "none" : "transform 0.2s ease-out" + }} > -
+
handleDragStart(e.touches[0].clientY)} + onTouchMove={(e) => handleDragMove(e.touches[0].clientY)} + onTouchEnd={handleDragEnd} + onMouseDown={(e) => handleDragStart(e.clientY)} + onMouseMove={(e) => { + if (e.buttons === 1) handleDragMove(e.clientY); + }} + onMouseUp={handleDragEnd} + onMouseLeave={handleDragEnd} + > +
+
Log a Brew
🫘
@@ -75,10 +127,31 @@ export default function BrewForm({ beans, onSave, onClose }) { onClick={() => handleClose()} >
e.stopPropagation()} + style={{ + transform: isDragging + ? `translateY(${dragY}px)` + : active + ? `translateY(${dragY}px)` + : "translateY(100%)", + transition: isDragging ? "none" : "transform 0.2s ease-out" + }} > -
+
handleDragStart(e.touches[0].clientY)} + onTouchMove={(e) => handleDragMove(e.touches[0].clientY)} + onTouchEnd={handleDragEnd} + onMouseDown={(e) => handleDragStart(e.clientY)} + onMouseMove={(e) => { + if (e.buttons === 1) handleDragMove(e.clientY); + }} + onMouseUp={handleDragEnd} + onMouseLeave={handleDragEnd} + > +
+
Log a Brew
{METHODS.map(m => ( diff --git a/src/components/CreateModal.jsx b/src/components/CreateModal.jsx index 7915dc0..504f958 100644 --- a/src/components/CreateModal.jsx +++ b/src/components/CreateModal.jsx @@ -2,11 +2,19 @@ import { useState, useEffect } from "react"; export default function CreateModal({ onClose, onAddBean, onAddBrew }) { const [active, setActive] = useState(false); + const [dragY, setDragY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [startY, setStartY] = useState(0); useEffect(() => { + // Prevent background scroll when mounted + document.body.style.overflow = "hidden"; // Trigger transition shortly after mounting const raf = requestAnimationFrame(() => setActive(true)); - return () => cancelAnimationFrame(raf); + return () => { + cancelAnimationFrame(raf); + document.body.style.overflow = ""; + }; }, []); const handleClose = (callback) => { @@ -19,16 +27,60 @@ export default function CreateModal({ onClose, onAddBean, onAddBrew }) { }, 200); }; + const handleDragStart = (clientY) => { + setIsDragging(true); + setStartY(clientY); + }; + + const handleDragMove = (clientY) => { + if (!isDragging) return; + const deltaY = clientY - startY; + if (deltaY > 0) { + setDragY(deltaY); + } + }; + + const handleDragEnd = () => { + if (!isDragging) return; + setIsDragging(false); + if (dragY > 100) { + handleClose(); + } else { + setDragY(0); + } + }; + return (
handleClose()} >
e.stopPropagation()} + style={{ + transform: isDragging + ? `translateY(${dragY}px)` + : active + ? `translateY(${dragY}px)` + : "translateY(100%)", + transition: isDragging ? "none" : "transform 0.2s ease-out" + }} > -
+
handleDragStart(e.touches[0].clientY)} + onTouchMove={(e) => handleDragMove(e.touches[0].clientY)} + onTouchEnd={handleDragEnd} + onMouseDown={(e) => handleDragStart(e.clientY)} + onMouseMove={(e) => { + if (e.buttons === 1) handleDragMove(e.clientY); + }} + onMouseUp={handleDragEnd} + onMouseLeave={handleDragEnd} + > +
+
What would you like to add?