From 93689d4486d5ac067b6fc002650a916169a164bc Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sun, 29 Mar 2026 15:38:16 -0400 Subject: [PATCH] v0.12.38 event form bug fixes --- backend/package.json | 2 +- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/MobileEventForm.jsx | 49 ++++++++++++++++++-- frontend/src/components/SchedulePage.jsx | 36 ++++++++------ frontend/src/components/UserManagerModal.jsx | 4 ++ frontend/src/pages/UserManagerPage.jsx | 14 ++++-- 7 files changed, 83 insertions(+), 26 deletions(-) diff --git a/backend/package.json b/backend/package.json index b3b183b..b59fd71 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.37", + "version": "0.12.38", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index b88a254..6183c6a 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.37}" +VERSION="${1:-0.12.38}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 5c9eb0d..b5760be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.37", + "version": "0.12.38", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/MobileEventForm.jsx b/frontend/src/components/MobileEventForm.jsx index 8b2a089..ac028ab 100644 --- a/frontend/src/components/MobileEventForm.jsx +++ b/frontend/src/components/MobileEventForm.jsx @@ -68,15 +68,48 @@ function fmt12(val) { return `${h}:${String(mm).padStart(2,'0')} ${ampm}`; } -// Mobile TimeInput — same behaviour as desktop but styled for mobile inline use +// Mobile TimeInput — free-text time entry with smart-positioned scrollable dropdown function TimeInputMobile({ value, onChange }) { const [open, setOpen] = useState(false); const [inputVal, setInputVal] = useState(fmt12(value)); + const [keyboardOffset, setKeyboardOffset] = useState(0); + const [dropdownPos, setDropdownPos] = useState({ top: 0, bottom: 'auto', left: 0 }); const wrapRef = useRef(null); const listRef = useRef(null); useEffect(() => { setInputVal(fmt12(value)); }, [value]); + // Detect keyboard height via Visual Viewport API + useEffect(() => { + const handleViewportChange = () => { + if (window.visualViewport) { + const offset = window.innerHeight - window.visualViewport.height; + setKeyboardOffset(offset > 0 ? offset : 0); + } + }; + if (open) { + handleViewportChange(); + window.visualViewport?.addEventListener('resize', handleViewportChange); + return () => window.visualViewport?.removeEventListener('resize', handleViewportChange); + } else { + setKeyboardOffset(0); + } + }, [open]); + + // Calculate dropdown pixel position using getBoundingClientRect (required for position:fixed) + useEffect(() => { + if (open && wrapRef.current) { + const rect = wrapRef.current.getBoundingClientRect(); + const dropdownHeight = 5 * 40; + const spaceBelow = window.innerHeight - rect.bottom - keyboardOffset; + if (spaceBelow >= dropdownHeight) { + setDropdownPos({ top: rect.bottom, bottom: 'auto', left: rect.left }); + } else { + setDropdownPos({ top: 'auto', bottom: window.innerHeight - rect.top + keyboardOffset, left: rect.left }); + } + } + }, [open, keyboardOffset]); + useEffect(() => { if (!open || !listRef.current) return; const idx = TIME_SLOTS.findIndex(s => s.value === value); @@ -100,22 +133,30 @@ function TimeInputMobile({ value, onChange }) { return (
setInputVal(e.target.value)} onFocus={() => setOpen(true)} onBlur={e => setTimeout(() => commit(e.target.value), 150)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }} autoComplete="off" + inputMode="text" + enterKeyHint="done" style={{ fontSize: 15, color: 'var(--primary)', fontWeight: 600, background: 'transparent', border: 'none', outline: 'none', cursor: 'text', width: 90 }} /> {open && (
{TIME_SLOTS.map(s => ( @@ -520,8 +561,8 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
{/* End date/time */} -
setShowEndDate(true)} style={{ display:'flex',alignItems:'center',padding:'6px 20px 14px 56px',cursor:'pointer',borderBottom:'1px solid var(--border)' }}> - {fmtDateDisplay(ed)} +
+ setShowEndDate(true)} style={{ flex:1,fontSize:15,color:'var(--text-secondary)',cursor:'pointer' }}>{fmtDateDisplay(ed)} {!allDay && ( { setEt(newEt); diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index d000507..a10ec94 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -153,7 +153,7 @@ function TimeInput({ value, onChange, style }) { // Keep display in sync when value changes externally useEffect(() => { setInputVal(fmt12(value)); }, [value]); - // Scroll the dropdown so the selected slot is near the top + // Scroll the dropdown so that selected slot is near the top useEffect(() => { if (!open || !listRef.current) return; const idx = TIME_SLOTS.findIndex(s => s.value === value); @@ -185,6 +185,7 @@ function TimeInput({ value, onChange, style }) { return (
setInputVal(e.target.value)} @@ -196,17 +197,20 @@ function TimeInput({ value, onChange, style }) { onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }} style={{ width: '100%', cursor: 'text' }} autoComplete="off" + inputMode="text" + enterKeyHint="done" placeholder="9:00 AM" /> {open && (
{TIME_SLOTS.map(s => ( @@ -350,7 +354,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
- onFilterKeyword(e.target.value)} autoComplete="off" onFocus={onInputFocus} onBlur={onInputBlur} + onFilterKeyword(e.target.value)} autoComplete="new-password" onFocus={onInputFocus} onBlur={onInputBlur} placeholder="Search events…" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{width:'100%',padding:'7px 8px 7px 28px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:13,boxSizing:'border-box'}}/>
@@ -389,7 +393,7 @@ function EventTypePopup({ userGroups, onSave, onClose, editing=null }) { }; return (
-
setName(e.target.value)} autoComplete="off" autoCorrect="off" style={{marginTop:4}} autoFocus/>
+
setName(e.target.value)} autoComplete="new-password" autoCorrect="off" style={{marginTop:4}} autoFocus/>
setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/>
@@ -453,7 +457,7 @@ function CustomRecurrenceFields({ rule, onChange }) {
Every - upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="off" style={{width:60,textAlign:'center'}}/> + upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="new-password" style={{width:60,textAlign:'center'}}/> @@ -475,8 +479,8 @@ function CustomRecurrenceFields({ rule, onChange }) { ))}
@@ -663,10 +667,13 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc return ( <>
+ {/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password" + on individual inputs is ignored by Chrome but respected on the form element */} +
e.preventDefault()}>
{if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA') e.preventDefault();}}> {/* Title */}
- setTitle(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="sentences" style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/> + setTitle(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="sentences" style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
{/* Event Type */} @@ -685,7 +692,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
- setSd(e.target.value)} autoComplete="off" style={{width:150,flexShrink:0}}/> + setSd(e.target.value)} autoComplete="new-password" style={{width:150,flexShrink:0}}/> {!allDay&&( <> @@ -694,7 +701,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc setEt(newEt); userSetEndTime.current=true; if(sd===ed && newEt<=st){ const d=new Date(buildISO(sd,st)); d.setDate(d.getDate()+1); const p=n=>String(n).padStart(2,'0'); setEd(`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`); } }} style={{width:120,flexShrink:0}}/> - {setEd(e.target.value);userSetEndTime.current=true;}} autoComplete="off" style={{width:150,flexShrink:0}}/> + {setEd(e.target.value);userSetEndTime.current=true;}} autoComplete="new-password" style={{width:150,flexShrink:0}}/> )}
@@ -752,12 +759,12 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc {/* Location */} - setLoc(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="off" /> + setLoc(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" /> {/* Description */} -