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?