v0.9.84 bug fixes
This commit is contained in:
@@ -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.83
|
JAMA_VERSION=0.9.84
|
||||||
|
|
||||||
# App port — the host port Docker maps to the container
|
# App port — the host port Docker maps to the container
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-backend",
|
"name": "jama-backend",
|
||||||
"version": "0.9.83",
|
"version": "0.9.84",
|
||||||
"description": "TeamChat backend server",
|
"description": "TeamChat backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.9.83}"
|
VERSION="${1:-0.9.84}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="jama"
|
IMAGE_NAME="jama"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-frontend",
|
"name": "jama-frontend",
|
||||||
"version": "0.9.83",
|
"version": "0.9.84",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
|||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} />
|
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} />
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button>
|
<button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button>
|
<button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +219,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
|||||||
</div>
|
</div>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<input className="input" placeholder="Search to add member..." autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={addSearch} onChange={e => setAddSearch(e.target.value)} />
|
<input className="input" placeholder="Search to add member..." autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={addSearch} onChange={e => setAddSearch(e.target.value)} />
|
||||||
{addResults.length > 0 && addSearch && (
|
{addResults.length > 0 && addSearch && (
|
||||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--surface)' }}>
|
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--surface)' }}>
|
||||||
{addResults.filter(u => !members.find(m => m.id === u.id)).map(u => (
|
{addResults.filter(u => !members.find(m => m.id === u.id)).map(u => (
|
||||||
|
|||||||
@@ -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" 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' }}/>
|
<input value={title} onChange={e=>setTitle(e.target.value)} placeholder="Add title" autoComplete="new-password" 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" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} 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="new-password" 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} autoComplete="off" autoCorrect="off" spellCheck={false} 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="new-password" 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…" autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
placeholder="Type name…" autoComplete="new-password" 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 }}>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function MembersScreen({ group, allUsers, onBack }) {
|
|||||||
<input
|
<input
|
||||||
value={search} onChange={e=>setSearch(e.target.value)}
|
value={search} onChange={e=>setSearch(e.target.value)}
|
||||||
placeholder="Search users…"
|
placeholder="Search users…"
|
||||||
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
||||||
style={{width:'100%',padding:'8px 10px 8px 32px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:14,boxSizing:'border-box'}}
|
style={{width:'100%',padding:'8px 10px 8px 32px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:14,boxSizing:'border-box'}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +146,7 @@ function MultiGroupDmsScreen({ userGroups, onBack }) {
|
|||||||
/>
|
/>
|
||||||
{creating && (
|
{creating && (
|
||||||
<div style={{ padding:16,background:'var(--surface)',borderBottom:'1px solid var(--border)' }}>
|
<div style={{ padding:16,background:'var(--surface)',borderBottom:'1px solid var(--border)' }}>
|
||||||
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} placeholder="DM name…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',padding:'9px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15,marginBottom:10,boxSizing:'border-box' }}/>
|
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} placeholder="DM name…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',padding:'9px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15,marginBottom:10,boxSizing:'border-box' }}/>
|
||||||
<div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:6 }}>Select groups (min 2):</div>
|
<div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:6 }}>Select groups (min 2):</div>
|
||||||
{userGroups.map(g=>(
|
{userGroups.map(g=>(
|
||||||
<label key={g.id} style={{ display:'flex',alignItems:'center',gap:10,padding:'8px 0',borderBottom:'1px solid var(--border)',cursor:'pointer' }}>
|
<label key={g.id} style={{ display:'flex',alignItems:'center',gap:10,padding:'8px 0',borderBottom:'1px solid var(--border)',cursor:'pointer' }}>
|
||||||
@@ -241,7 +241,7 @@ export default function MobileGroupManager({ onClose }) {
|
|||||||
<>
|
<>
|
||||||
{creating && (
|
{creating && (
|
||||||
<div style={{ padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',display:'flex',gap:10 }}>
|
<div style={{ padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',display:'flex',gap:10 }}>
|
||||||
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ flex:1,padding:'8px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15 }}/>
|
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ flex:1,padding:'8px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15 }}/>
|
||||||
<button onClick={createGroup} disabled={saving||!newName.trim()} style={{ padding:'8px 16px',background:'var(--primary)',color:'white',border:'none',borderRadius:'var(--radius)',fontSize:14,fontWeight:600,cursor:'pointer' }}>{saving?'…':'Create'}</button>
|
<button onClick={createGroup} disabled={saving||!newName.trim()} style={{ padding:'8px 16px',background:'var(--primary)',color:'white',border:'none',borderRadius:'var(--radius)',fontSize:14,fontWeight:600,cursor:'pointer' }}>{saving?'…':'Create'}</button>
|
||||||
<button onClick={()=>{setCreating(false);setNewName('');}} style={{ padding:'8px',background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:18 }}>✕</button>
|
<button onClick={()=>{setCreating(false);setNewName('');}} style={{ padding:'8px',background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:18 }}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export default function NewChatModal({ onClose, onCreated }) {
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => setName(e.target.value)}
|
||||||
placeholder={namePlaceholder}
|
placeholder={namePlaceholder}
|
||||||
|
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -121,7 +122,7 @@ export default function NewChatModal({ onClose, onCreated }) {
|
|||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{isDirect ? 'Direct Message with' : 'Add Members'}
|
{isDirect ? 'Direct Message with' : 'Add Members'}
|
||||||
</label>
|
</label>
|
||||||
<input className="input" placeholder="Search users..." value={search} onChange={e => setSearch(e.target.value)} />
|
<input className="input" placeholder="Search users..." autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={search} onChange={e => setSearch(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selected.length > 0 && (
|
{selected.length > 0 && (
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default function ProfileModal({ onClose }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={user?.name}
|
placeholder={user?.name}
|
||||||
autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
||||||
style={{ borderColor: displayNameWarning ? '#e53935' : undefined }}
|
style={{ borderColor: displayNameWarning ? '#e53935' : undefined }}
|
||||||
/>
|
/>
|
||||||
{displayName !== savedDisplayName ? null : savedDisplayName ? (
|
{displayName !== savedDisplayName ? null : savedDisplayName ? (
|
||||||
@@ -138,7 +138,7 @@ export default function ProfileModal({ onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>About Me</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>About Me</label>
|
||||||
<textarea className="input" value={aboutMe} onChange={e => setAboutMe(e.target.value)} placeholder="Tell your team about yourself..." rows={3} autoComplete="off" autoCorrect="off" spellCheck={false} style={{ resize: 'vertical' }} />
|
<textarea className="input" value={aboutMe} onChange={e => setAboutMe(e.target.value)} placeholder="Tell your team about yourself..." rows={3} autoComplete="new-password" autoCorrect="off" spellCheck={false} style={{ resize: 'vertical' }} />
|
||||||
</div>
|
</div>
|
||||||
{user?.role === 'admin' && (
|
{user?.role === 'admin' && (
|
||||||
<label className="flex items-center gap-2 text-sm pointer" style={{ color: 'var(--text-secondary)', userSelect: 'none' }}>
|
<label className="flex items-center gap-2 text-sm pointer" style={{ color: 'var(--text-secondary)', userSelect: 'none' }}>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function MiniCalendar({ selected, onChange, eventDates=new Set() }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile Filter Bar (Schedule view: keyword+type filters with month nav; Day view: calendar accordion) ──
|
// ── Mobile Filter Bar (Schedule view: keyword+type filters with month nav; Day view: calendar accordion) ──
|
||||||
function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filterKeyword, onFilterKeyword, filterTypeId, onFilterTypeId, eventDates=new Set() }) {
|
function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filterKeyword, onFilterKeyword, filterTypeId, onFilterTypeId, eventDates=new Set(), onInputFocus, onInputBlur }) {
|
||||||
// Day view: keep accordion calendar
|
// Day view: keep accordion calendar
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const y=selected.getFullYear(), m=selected.getMonth();
|
const y=selected.getFullYear(), m=selected.getMonth();
|
||||||
@@ -158,8 +158,10 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
|
|||||||
<input
|
<input
|
||||||
value={filterKeyword}
|
value={filterKeyword}
|
||||||
onChange={e=>onFilterKeyword(e.target.value)}
|
onChange={e=>onFilterKeyword(e.target.value)}
|
||||||
|
onFocus={onInputFocus}
|
||||||
|
onBlur={onInputBlur}
|
||||||
placeholder="Search events…"
|
placeholder="Search events…"
|
||||||
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
autoComplete="new-password" 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>
|
||||||
@@ -976,6 +978,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
const [editingEvent, setEditingEvent] = useState(null);
|
const [editingEvent, setEditingEvent] = useState(null);
|
||||||
const [filterKeyword, setFilterKeyword] = useState('');
|
const [filterKeyword, setFilterKeyword] = useState('');
|
||||||
const [filterTypeId, setFilterTypeId] = useState('');
|
const [filterTypeId, setFilterTypeId] = useState('');
|
||||||
|
const [inputFocused, setInputFocused] = useState(false); // hides footer when keyboard open on mobile
|
||||||
const [detailEvent, setDetailEvent] = useState(null);
|
const [detailEvent, setDetailEvent] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
@@ -1080,7 +1083,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}
|
autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
||||||
style={{ marginBottom:8, fontSize:13 }}
|
style={{ marginBottom:8, fontSize:13 }}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
@@ -1150,6 +1153,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
onFilterKeyword={setFilterKeyword}
|
onFilterKeyword={setFilterKeyword}
|
||||||
filterTypeId={filterTypeId}
|
filterTypeId={filterTypeId}
|
||||||
onFilterTypeId={setFilterTypeId}
|
onFilterTypeId={setFilterTypeId}
|
||||||
|
onInputFocus={()=>setInputFocused(true)}
|
||||||
|
onInputBlur={()=>setInputFocused(false)}
|
||||||
eventDates={eventDates}
|
eventDates={eventDates}
|
||||||
onMonthChange={(dir, exactDate) => {
|
onMonthChange={(dir, exactDate) => {
|
||||||
if(exactDate) { setSelDate(exactDate); }
|
if(exactDate) { setSelDate(exactDate); }
|
||||||
@@ -1193,8 +1198,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile bottom bar — position:fixed so keyboard doesn't push it up */}
|
{/* Mobile bottom bar — hidden when keyboard open to avoid being pushed up */}
|
||||||
{isMobile && (
|
{isMobile && !inputFocused && (
|
||||||
<div style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:20, background:'var(--surface)', borderTop:'1px solid var(--border)' }}>
|
<div style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:20, background:'var(--surface)', borderTop:'1px solid var(--border)' }}>
|
||||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export default function UserManagerModal({ onClose }) {
|
|||||||
{/* Users list — accordion */}
|
{/* Users list — accordion */}
|
||||||
{tab === 'users' && (
|
{tab === 'users' && (
|
||||||
<>
|
<>
|
||||||
<input className="input" style={{ marginBottom: 12 }} placeholder="Search users…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={search} onChange={e => setSearch(e.target.value)} />
|
<input className="input" style={{ marginBottom: 12 }} placeholder="Search users…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={search} onChange={e => setSearch(e.target.value)} />
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center" style={{ padding: 40 }}><div className="spinner" /></div>
|
<div className="flex justify-center" style={{ padding: 40 }}><div className="spinner" /></div>
|
||||||
) : (
|
) : (
|
||||||
@@ -321,17 +321,17 @@ export default function UserManagerModal({ onClose }) {
|
|||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Full Name <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(First Last)</span></label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Full Name <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(First Last)</span></label>
|
||||||
<input className="input" placeholder="Jane Smith" autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false} value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
|
<input className="input" placeholder="Jane Smith" autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false} value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Email</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Email</label>
|
||||||
<input className="input" type="email" placeholder="jane@example.com" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} />
|
<input className="input" type="email" placeholder="jane@example.com" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12 }}>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = {userPass || 'USER_PASS'})</span></label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = {userPass || 'USER_PASS'})</span></label>
|
||||||
<input className="input" type="text" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} />
|
<input className="input" type="text" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Role</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Role</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user