feat: Add modal dragHandlers and prevent background scroll
All checks were successful
Deploy Brew Application / deploy (push) Successful in 11s

This commit is contained in:
2026-06-06 20:04:13 +05:30
parent bf96fb9763
commit de9cbb14d0
4 changed files with 246 additions and 13 deletions

View File

@@ -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()}
>
<div
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8`}
onClick={e => e.stopPropagation()}
style={{
transform: isDragging
? `translateY(${dragY}px)`
: active
? `translateY(${dragY}px)`
: "translateY(100%)",
transition: isDragging ? "none" : "transform 0.2s ease-out"
}}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div
className="w-full py-4 -mt-2 cursor-grab active:cursor-grabbing flex justify-center select-none"
onTouchStart={(e) => 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}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded" />
</div>
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">{initial ? "Edit Bean" : "Add Bean"}</div>
<div className="mb-4"><label className={labelCls}>Bean Name *</label>
<input className={inputCls} placeholder="e.g. Ethiopia Yirgacheffe" value={form.name} onChange={e => set("name", e.target.value)} /></div>

View File

@@ -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 (
<div
@@ -30,10 +61,31 @@ export default function BrewForm({ beans, onSave, onClose }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8`}
onClick={e => e.stopPropagation()}
style={{
transform: isDragging
? `translateY(${dragY}px)`
: active
? `translateY(${dragY}px)`
: "translateY(100%)",
transition: isDragging ? "none" : "transform 0.2s ease-out"
}}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div
className="w-full py-4 -mt-2 cursor-grab active:cursor-grabbing flex justify-center select-none"
onTouchStart={(e) => 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}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded" />
</div>
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">Log a Brew</div>
<div className="text-center py-12 text-[#9C8B7A] dark:text-[#C8B9A6]">
<div className="text-4xl mb-3 opacity-50 dark:opacity-80">🫘</div>
@@ -75,10 +127,31 @@ export default function BrewForm({ beans, onSave, onClose }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8`}
onClick={e => e.stopPropagation()}
style={{
transform: isDragging
? `translateY(${dragY}px)`
: active
? `translateY(${dragY}px)`
: "translateY(100%)",
transition: isDragging ? "none" : "transform 0.2s ease-out"
}}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div
className="w-full py-4 -mt-2 cursor-grab active:cursor-grabbing flex justify-center select-none"
onTouchStart={(e) => 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}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded" />
</div>
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">Log a Brew</div>
<div className="flex gap-1.5 mb-5">
{METHODS.map(m => (

View File

@@ -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 (
<div
className={`fixed inset-0 bg-[rgba(44,24,16,0.5)] z-[100] flex items-end justify-center transition-opacity duration-200 ease-in-out ${active ? "opacity-100" : "opacity-0"}`}
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-5 pb-10 transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-5 pb-10 transition-transform duration-200 ease-in-out`}
onClick={e => e.stopPropagation()}
style={{
transform: isDragging
? `translateY(${dragY}px)`
: active
? `translateY(${dragY}px)`
: "translateY(100%)",
transition: isDragging ? "none" : "transform 0.2s ease-out"
}}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto mt-3 mb-6" />
<div
className="w-full py-4 -mt-2 cursor-grab active:cursor-grabbing flex justify-center select-none"
onTouchStart={(e) => 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}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded" />
</div>
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-6">What would you like to add?</div>
<div className="flex flex-col gap-3">
<button

View File

@@ -3,6 +3,9 @@ import { useState, useEffect } from "react";
export default function IosPromptModal() {
const [show, setShow] = useState(false);
const [active, setActive] = useState(false);
const [dragY, setDragY] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [startY, setStartY] = useState(0);
useEffect(() => {
// Detect if user is on iOS or iPadOS
@@ -23,6 +26,15 @@ export default function IosPromptModal() {
}
}, []);
useEffect(() => {
if (show) {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}
}, [show]);
if (!show) return null;
const handleClose = () => {
@@ -33,17 +45,61 @@ export default function IosPromptModal() {
}, 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 (
<div
className={`fixed inset-0 bg-[rgba(44,24,16,0.5)] z-[200] flex items-end justify-center transition-opacity duration-200 ease-in-out ${active ? "opacity-100" : "opacity-0"}`}
onClick={handleClose}
>
<div
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-6 pb-8 pt-4 relative shadow-[0_-8px_30px_rgba(44,24,16,0.15)] dark:shadow-none transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-6 pb-8 pt-4 relative shadow-[0_-8px_30px_rgba(44,24,16,0.15)] dark:shadow-none transition-transform duration-200 ease-in-out`}
onClick={e => e.stopPropagation()}
style={{
transform: isDragging
? `translateY(${dragY}px)`
: active
? `translateY(${dragY}px)`
: "translateY(100%)",
transition: isDragging ? "none" : "transform 0.2s ease-out"
}}
>
{/* Handle bar */}
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto mb-6" />
<div
className="w-full py-4 -mt-4 cursor-grab active:cursor-grabbing flex justify-center select-none"
onTouchStart={(e) => 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}
>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded" />
</div>
{/* Close Button */}
<button