v0.9.81 bugs fixes

This commit is contained in:
2026-03-18 19:14:34 -04:00
parent de22432cc5
commit ca2d472837
6 changed files with 27 additions and 23 deletions

View File

@@ -10,7 +10,7 @@
PROJECT_NAME=jama PROJECT_NAME=jama
# Image version to run (set by build.sh, or use 'latest') # Image version to run (set by build.sh, or use 'latest')
JAMA_VERSION=0.9.80 JAMA_VERSION=0.9.81
# App port — the host port Docker maps to the container # App port — the host port Docker maps to the container
PORT=3000 PORT=3000

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-backend", "name": "jama-backend",
"version": "0.9.80", "version": "0.9.81",
"description": "TeamChat backend server", "description": "TeamChat backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.9.80}" VERSION="${1:-0.9.81}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-frontend", "name": "jama-frontend",
"version": "0.9.80", "version": "0.9.81",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -277,7 +277,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
<div style={{ flex:1,overflowY:'auto' }}> <div style={{ flex:1,overflowY:'auto' }}>
{/* Title */} {/* Title */}
<div style={{ padding:'16px 20px',borderBottom:'1px solid var(--border)' }}> <div style={{ padding:'16px 20px',borderBottom:'1px solid var(--border)' }}>
<input value={title} onChange={e=>setTitle(e.target.value)} placeholder="Add title" style={{ width:'100%',border:'none',background:'transparent',fontSize:22,fontWeight:700,color:'var(--text-primary)',outline:'none' }}/> <input value={title} onChange={e=>setTitle(e.target.value)} placeholder="Add title" autoComplete="off" autoCorrect="off" autoCapitalize="sentences" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:22,fontWeight:700,color:'var(--text-primary)',outline:'none' }}/>
</div> </div>
{/* Event Type */} {/* Event Type */}
@@ -359,12 +359,12 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
{/* Location */} {/* Location */}
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}> <MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}>
<input value={location} onChange={e=>setLocation(e.target.value)} placeholder="Add location" style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/> <input value={location} onChange={e=>setLocation(e.target.value)} placeholder="Add location" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/>
</MobileRow> </MobileRow>
{/* Description */} {/* Description */}
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}> <MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}>
<textarea value={description} onChange={e=>setDescription(e.target.value)} placeholder="Add description" rows={3} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none',resize:'none' }}/> <textarea value={description} onChange={e=>setDescription(e.target.value)} placeholder="Add description" rows={3} autoComplete="off" autoCorrect="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none',resize:'none' }}/>
</MobileRow> </MobileRow>
{/* Delete */} {/* Delete */}
@@ -394,7 +394,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
value={newTypeName} value={newTypeName}
onChange={e=>setNewTypeName(e.target.value)} onChange={e=>setNewTypeName(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&createEventType()} onKeyDown={e=>e.key==='Enter'&&createEventType()}
placeholder="Type name…" placeholder="Type name…" autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false}
style={{ width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)' }} style={{ width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)' }}
/> />
<div style={{ display:'flex',alignItems:'center',gap:12,marginBottom:16 }}> <div style={{ display:'flex',alignItems:'center',gap:12,marginBottom:16 }}>

View File

@@ -159,6 +159,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
value={filterKeyword} value={filterKeyword}
onChange={e=>onFilterKeyword(e.target.value)} onChange={e=>onFilterKeyword(e.target.value)}
placeholder="Search events…" placeholder="Search events…"
autoComplete="off" 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'}} 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'}}
/> />
</div> </div>
@@ -806,17 +807,18 @@ function DayView({ events, selectedDate, onSelect, onSwipe }) {
const handleTouchEnd = e => { const handleTouchEnd = e => {
const dx = e.changedTouches[0].clientX - touchRef.current.x; const dx = e.changedTouches[0].clientX - touchRef.current.x;
const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y); const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y);
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture) // Only trigger horizontal swipe if clearly horizontal (dx > dy) and > 60px
if(Math.abs(dx) > 60 && dy < 80 && touchRef.current.x > 30) { // and not from left edge (< 30px = OS back gesture)
if(Math.abs(dx) > 60 && Math.abs(dx) > dy * 1.5 && touchRef.current.x > 30) {
onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day
} }
}; };
return( return(
<div style={{display:'flex',flexDirection:'column',height:'100%'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}> <div style={{display:'flex',flexDirection:'column',height:'100%',touchAction:'pan-y'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
<div style={{display:'flex',borderBottom:'1px solid var(--border)',padding:'8px 0 8px 60px',fontSize:13,fontWeight:600,color:'var(--primary)',flexShrink:0}}> <div style={{display:'flex',borderBottom:'1px solid var(--border)',padding:'8px 0 8px 60px',fontSize:13,fontWeight:600,color:'var(--primary)',flexShrink:0}}>
<div style={{textAlign:'center'}}><div>{DAYS[selectedDate.getDay()]}</div><div style={{fontSize:28,fontWeight:700}}>{selectedDate.getDate()}</div></div> <div style={{textAlign:'center'}}><div>{DAYS[selectedDate.getDay()]}</div><div style={{fontSize:28,fontWeight:700}}>{selectedDate.getDate()}</div></div>
</div> </div>
<div ref={scrollRef} style={{flex:1,overflowY:'auto',position:'relative'}}> <div ref={scrollRef} style={{flex:1,overflowY:'auto',position:'relative',touchAction:'pan-y'}}>
<div style={{position:'relative'}}> <div style={{position:'relative'}}>
{hours.map(h=>( {hours.map(h=>(
<div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',height:HOUR_H}}> <div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',height:HOUR_H}}>
@@ -861,20 +863,21 @@ function WeekView({ events, selectedDate, onSelect }) {
const handleTouchEnd = e => { const handleTouchEnd = e => {
const dx = e.changedTouches[0].clientX - touchRef.current.x; const dx = e.changedTouches[0].clientX - touchRef.current.x;
const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y); const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y);
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture) // Only trigger horizontal swipe if clearly horizontal (dx > dy) and > 60px
if(Math.abs(dx) > 60 && dy < 80 && touchRef.current.x > 30) { // and not from left edge (< 30px = OS back gesture)
if(Math.abs(dx) > 60 && Math.abs(dx) > dy * 1.5 && touchRef.current.x > 30) {
onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day
} }
}; };
return( return(
<div style={{display:'flex',flexDirection:'column',height:'100%'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}> <div style={{display:'flex',flexDirection:'column',height:'100%',touchAction:'pan-y'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
{/* Day headers */} {/* Day headers */}
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)',background:'var(--surface)',flexShrink:0}}> <div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',borderBottom:'1px solid var(--border)',background:'var(--surface)',flexShrink:0}}>
<div/> <div/>
{days.map((d,i)=><div key={i} style={{textAlign:'center',padding:'6px 4px',fontSize:12,fontWeight:600,color:sameDay(d,today)?'var(--primary)':'var(--text-secondary)'}}>{DAYS[d.getDay()]} {d.getDate()}</div>)} {days.map((d,i)=><div key={i} style={{textAlign:'center',padding:'6px 4px',fontSize:12,fontWeight:600,color:sameDay(d,today)?'var(--primary)':'var(--text-secondary)'}}>{DAYS[d.getDay()]} {d.getDate()}</div>)}
</div> </div>
{/* Scrollable time grid */} {/* Scrollable time grid */}
<div ref={scrollRef} style={{flex:1,overflowY:'auto'}}> <div ref={scrollRef} style={{flex:1,overflowY:'auto',touchAction:'pan-y'}}>
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',position:'relative'}}> <div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',position:'relative'}}>
{/* Time labels column */} {/* Time labels column */}
<div> <div>
@@ -1049,8 +1052,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
</button> </button>
{createOpen && ( {createOpen && (
<div style={{ position:'absolute', top:'100%', left:0, right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', marginTop:4, boxShadow:'0 4px 16px rgba(0,0,0,0.18)' }}> <div style={{ position:'absolute', top:'100%', left:0, right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', marginTop:4, boxShadow:'0 4px 16px rgba(0,0,0,0.18)' }}>
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);}], {[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);}], ['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
['Bulk Event Import', ()=>{setPanel('bulkImport');setCreateOpen(false);}] ['Bulk Event Import', ()=>{setPanel('bulkImport');setCreateOpen(false);}]
].map(([label,action])=>( ].map(([label,action])=>(
<button key={label} onClick={action} style={{display:'block',width:'100%',padding:'9px 16px',textAlign:'left',fontSize:14,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)'}} <button key={label} onClick={action} style={{display:'block',width:'100%',padding:'9px 16px',textAlign:'left',fontSize:14,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)'}}
@@ -1077,6 +1080,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
placeholder={`Keyword… (space = OR, "phrase")`} placeholder={`Keyword… (space = OR, "phrase")`}
value={filterKeyword} value={filterKeyword}
onChange={e=>setFilterKeyword(e.target.value)} onChange={e=>setFilterKeyword(e.target.value)}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
style={{ marginBottom:8, fontSize:13 }} style={{ marginBottom:8, fontSize:13 }}
/> />
<select <select
@@ -1128,7 +1132,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{allowedViews.map(v => { {allowedViews.map(v => {
const labels = { schedule:'Schedule', day:'Day', week:'Week', month:'Month' }; const labels = { schedule:'Schedule', day:'Day', week:'Week', month:'Month' };
return ( return (
<button key={v} onClick={()=>{setView(v);setPanel('calendar');}} style={{ padding:'4px 10px', borderRadius:5, border:'none', cursor:'pointer', fontSize:12, fontWeight:600, background:view===v?'var(--surface)':'transparent', color:view===v?'var(--text-primary)':'var(--text-tertiary)', boxShadow:view===v?'0 1px 3px rgba(0,0,0,0.1)':'none', transition:'all 0.15s', whiteSpace:'nowrap' }}> <button key={v} onClick={()=>{setView(v);setPanel('calendar');setSelDate(new Date());setFilterKeyword('');setFilterTypeId('');}} style={{ padding:'4px 10px', borderRadius:5, border:'none', cursor:'pointer', fontSize:12, fontWeight:600, background:view===v?'var(--surface)':'transparent', color:view===v?'var(--text-primary)':'var(--text-tertiary)', boxShadow:view===v?'0 1px 3px rgba(0,0,0,0.1)':'none', transition:'all 0.15s', whiteSpace:'nowrap' }}>
{labels[v]} {labels[v]}
</button> </button>
); );
@@ -1165,7 +1169,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
<div style={{ padding:28, maxWidth:1024 }}> <div style={{ padding:28, maxWidth:1024 }}>
<h2 style={{ fontSize:17, fontWeight:700, marginBottom:24 }}>{editingEvent?'Edit Event':'New Event'}</h2> <h2 style={{ fontSize:17, fontWeight:700, marginBottom:24 }}>{editingEvent?'Edit Event':'New Event'}</h2>
<EventForm event={editingEvent} userGroups={userGroups} eventTypes={eventTypes} selectedDate={selDate} isToolManager={isToolManager} <EventForm event={editingEvent} userGroups={userGroups} eventTypes={eventTypes} selectedDate={selDate} isToolManager={isToolManager}
onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);}} onDelete={handleDelete}/> onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}} onDelete={handleDelete}/>
</div> </div>
)} )}
@@ -1212,7 +1216,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
selectedDate={selDate} selectedDate={selDate}
isToolManager={isToolManager} isToolManager={isToolManager}
onSave={handleSaved} onSave={handleSaved}
onCancel={()=>{setPanel('calendar');setEditingEvent(null);}} onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}}
onDelete={handleDelete} onDelete={handleDelete}
/> />
</div> </div>
@@ -1228,8 +1232,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
</button> </button>
{createOpen && ( {createOpen && (
<div style={{ position:'absolute', bottom:'calc(100% + 8px)', right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', boxShadow:'0 -4px 16px rgba(0,0,0,0.15)', minWidth:180 }}> <div style={{ position:'absolute', bottom:'calc(100% + 8px)', right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', boxShadow:'0 -4px 16px rgba(0,0,0,0.15)', minWidth:180 }}>
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);}], {[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);}], ['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
].map(([label,action])=>( ].map(([label,action])=>(
<button key={label} onClick={action} style={{display:'block',width:'100%',padding:'12px 16px',textAlign:'left',fontSize:15,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)',borderBottom:'1px solid var(--border)'}} <button key={label} onClick={action} style={{display:'block',width:'100%',padding:'12px 16px',textAlign:'left',fontSize:15,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)',borderBottom:'1px solid var(--border)'}}
onMouseEnter={e=>e.currentTarget.style.background='var(--background)'} onMouseLeave={e=>e.currentTarget.style.background=''}>{label}</button> onMouseEnter={e=>e.currentTarget.style.background='var(--background)'} onMouseLeave={e=>e.currentTarget.style.background=''}>{label}</button>