v0.11.1 various UI bug fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-backend",
|
"name": "jama-backend",
|
||||||
"version": "0.11.0",
|
"version": "0.11.1",
|
||||||
"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.11.0}"
|
VERSION="${1:-0.11.1}"
|
||||||
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.11.0",
|
"version": "0.11.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -238,8 +238,7 @@ function CustomPicker({ initial, onSet, onBack }) {
|
|||||||
width: 110, background: 'var(--surface)',
|
width: 110, background: 'var(--surface)',
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
}}
|
}}
|
||||||
placeholder="#000000"
|
placeholder="#000000" autoComplete="new-password">
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Chosen colour</span>
|
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Chosen colour</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -461,7 +460,7 @@ export default function BrandingModal({ onClose }) {
|
|||||||
className="input flex-1"
|
className="input flex-1"
|
||||||
value={appName}
|
value={appName}
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
onChange={e => setAppName(e.target.value)}
|
onChange={e = autoComplete="new-password"> setAppName(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && handleSaveName()}
|
onKeyDown={e => e.key === 'Enter' && handleSaveName()}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={loading}>{loading ? '...' : 'Save'}</button>
|
<button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={loading}>{loading ? '...' : 'Save'}</button>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function ColourPicker({ value, onChange }) {
|
|||||||
<input value={hexInput} onChange={handleHexInput} maxLength={7} placeholder="#000000"
|
<input value={hexInput} onChange={handleHexInput} maxLength={7} placeholder="#000000"
|
||||||
style={{fontFamily:'monospace',fontSize:14,padding:'6px 10px',borderRadius:8,
|
style={{fontFamily:'monospace',fontSize:14,padding:'6px 10px',borderRadius:8,
|
||||||
border:`1px solid ${hexError?'#e53935':'var(--border)'}`,width:110,
|
border:`1px solid ${hexError?'#e53935':'var(--border)'}`,width:110,
|
||||||
background:'var(--surface)',color:'var(--text-primary)'}}/>
|
background:'var(--surface)',color:'var(--text-primary)'}} autoComplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
<div style={{display:'flex',gap:8,marginTop:12}}>
|
<div style={{display:'flex',gap:8,marginTop:12}}>
|
||||||
<button className="btn btn-primary btn-sm" onClick={()=>{onChange(current);setMode('suggestions');}} disabled={hexError}>Set</button>
|
<button className="btn btn-primary btn-sm" onClick={()=>{onChange(current);setMode('suggestions');}} disabled={hexError}>Set</button>
|
||||||
|
|||||||
@@ -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="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} />
|
<input className="input flex-1" value={newName} onChange={e = autoComplete="new-password"> 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>
|
||||||
@@ -165,7 +165,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
|
|||||||
<input
|
<input
|
||||||
className="input flex-1"
|
className="input flex-1"
|
||||||
value={customName}
|
value={customName}
|
||||||
onChange={e => setCustomName(e.target.value)}
|
onChange={e = autoComplete="new-password"> setCustomName(e.target.value)}
|
||||||
placeholder={group.owner_name_original || group.name}
|
placeholder={group.owner_name_original || group.name}
|
||||||
onKeyDown={e => e.key === 'Enter' && handleCustomName()}
|
onKeyDown={e => e.key === 'Enter' && handleCustomName()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function UserCheckList({ allUsers, selectedIds, onChange }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input className="input" placeholder="Search users…" value={search}
|
<input className="input" placeholder="Search users…" value={search}
|
||||||
onChange={e => setSearch(e.target.value)} style={{ marginBottom: 8 }} />
|
onChange={e => setSearch(e.target.value)} style={{ marginBottom: 8 }}
|
||||||
|
autoComplete="new-password" />
|
||||||
<div style={{ maxHeight: 200, overflowY: 'auto', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
<div style={{ maxHeight: 200, overflowY: 'auto', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
||||||
{filtered.map(u => (
|
{filtered.map(u => (
|
||||||
<label key={u.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', borderBottom: '1px solid var(--border)', cursor: 'pointer' }}>
|
<label key={u.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', borderBottom: '1px solid var(--border)', cursor: 'pointer' }}>
|
||||||
@@ -155,7 +156,8 @@ function AllGroupsTab({ allUsers, onRefresh }) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Group Name</label>
|
<label className="settings-section-label">Group Name</label>
|
||||||
<input className="input" value={editName} onChange={e => setEditName(e.target.value)}
|
<input className="input" value={editName} onChange={e => setEditName(e.target.value)}
|
||||||
placeholder="e.g. Coaches" style={{ marginTop: 6 }} />
|
placeholder="e.g. Coaches" style={{ marginTop: 6 }}
|
||||||
|
autoComplete="new-password" />
|
||||||
{isCreating && <p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 5 }}>A matching Direct Message group will be created automatically.</p>}
|
{isCreating && <p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 5 }}>A matching Direct Message group will be created automatically.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -295,7 +297,8 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey }) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">DM Name</label>
|
<label className="settings-section-label">DM Name</label>
|
||||||
<input className="input" value={dmName} onChange={e => setDmName(e.target.value)}
|
<input className="input" value={dmName} onChange={e => setDmName(e.target.value)}
|
||||||
placeholder="e.g. Coaches + Players" style={{ marginTop: 6 }} />
|
placeholder="e.g. Coaches + Players" style={{ marginTop: 6 }}
|
||||||
|
autoComplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Member Groups</label>
|
<label className="settings-section-label">Member Groups</label>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function FieldGroup({ label, children }) {
|
|||||||
function Field({ label, value, onChange, placeholder, type = 'text', hint, required }) {
|
function Field({ label, value, onChange, placeholder, type = 'text', hint, required }) {
|
||||||
return (
|
return (
|
||||||
<FieldGroup label={label}>
|
<FieldGroup label={label}>
|
||||||
<input type={type} value={value} onChange={e => onChange(e.target.value)}
|
<input type={type} value={value} onChange={e = autoComplete="new-password"> onChange(e.target.value)}
|
||||||
placeholder={placeholder} required={required}
|
placeholder={placeholder} required={required}
|
||||||
autoComplete="new-password" autoCorrect="off" spellCheck={false}
|
autoComplete="new-password" autoCorrect="off" spellCheck={false}
|
||||||
className="input" style={{ fontSize: 13 }} />
|
className="input" style={{ fontSize: 13 }} />
|
||||||
@@ -230,7 +230,7 @@ function DeleteModal({ api, tenant, onClose, onDone }) {
|
|||||||
Type <code style={{ background:'var(--background)', padding:'2px 6px', borderRadius:4 }}>{expected}</code> to confirm:
|
Type <code style={{ background:'var(--background)', padding:'2px 6px', borderRadius:4 }}>{expected}</code> to confirm:
|
||||||
</p>
|
</p>
|
||||||
{error && <div style={{ color:'var(--error)', fontSize:13, marginBottom:10 }}>{error}</div>}
|
{error && <div style={{ color:'var(--error)', fontSize:13, marginBottom:10 }}>{error}</div>}
|
||||||
<input className="input" value={confirm} onChange={e => setConfirm(e.target.value)} placeholder={expected} style={{ marginBottom:16 }} autoComplete="new-password" />
|
<input className="input" value={confirm} onChange={e = autoComplete="new-password"> setConfirm(e.target.value)} placeholder={expected} style={{ marginBottom:16 }} autoComplete="new-password" />
|
||||||
<div style={{ display:'flex', justifyContent:'flex-end', gap:8 }}>
|
<div style={{ display:'flex', justifyContent:'flex-end', gap:8 }}>
|
||||||
<button className="btn btn-secondary" onClick={onClose}>Cancel</button>
|
<button className="btn btn-secondary" onClick={onClose}>Cancel</button>
|
||||||
<button className="btn btn-danger" onClick={handle} disabled={confirm !== expected || deleting}>
|
<button className="btn btn-danger" onClick={handle} disabled={confirm !== expected || deleting}>
|
||||||
@@ -323,7 +323,7 @@ function KeyEntry({ onSubmit }) {
|
|||||||
<h2 style={{ fontSize:18, fontWeight:700, margin:'0 0 4px' }}>Control Panel</h2>
|
<h2 style={{ fontSize:18, fontWeight:700, margin:'0 0 4px' }}>Control Panel</h2>
|
||||||
<p style={{ color:'var(--text-secondary)', fontSize:13, margin:'0 0 20px' }}>Enter your host admin key to continue.</p>
|
<p style={{ color:'var(--text-secondary)', fontSize:13, margin:'0 0 20px' }}>Enter your host admin key to continue.</p>
|
||||||
{error && <div style={{ padding:'8px 12px', background:'#fce8e6', color:'var(--error)', borderRadius:6, fontSize:13, marginBottom:14 }}>{error}</div>}
|
{error && <div style={{ padding:'8px 12px', background:'#fce8e6', color:'var(--error)', borderRadius:6, fontSize:13, marginBottom:14 }}>{error}</div>}
|
||||||
<input type="password" className="input" value={key} onChange={e => setKey(e.target.value)}
|
<input type="password" className="input" value={key} onChange={e = autoComplete="new-password"> setKey(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && handle()} placeholder="Host admin key" autoFocus
|
onKeyDown={e => e.key === 'Enter' && handle()} placeholder="Host admin key" autoFocus
|
||||||
style={{ marginBottom:12, textAlign:'center' }} />
|
style={{ marginBottom:12, textAlign:'center' }} />
|
||||||
<button className="btn btn-primary" onClick={handle} disabled={checking} style={{ width:'100%', justifyContent:'center' }}>
|
<button className="btn btn-primary" onClick={handle} disabled={checking} style={{ width:'100%', justifyContent:'center' }}>
|
||||||
@@ -431,7 +431,7 @@ export default function HostPanel({ onProfile, onHelp, onAbout }) {
|
|||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div style={{ padding:'0 24px 12px', flexShrink:0, display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
|
<div style={{ padding:'0 24px 12px', flexShrink:0, display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
|
||||||
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search tenants…"
|
<input value={search} onChange={e = autoComplete="new-password"> setSearch(e.target.value)} placeholder="Search tenants…"
|
||||||
className="input" style={{ flex:1, minWidth:160, fontSize:13 }} autoComplete="new-password" />
|
className="input" style={{ flex:1, minWidth:160, fontSize:13 }} autoComplete="new-password" />
|
||||||
<button className="btn btn-secondary btn-sm" onClick={load} disabled={loading}>{loading ? '…' : '↻ Refresh'}</button>
|
<button className="btn btn-secondary btn-sm" onClick={load} disabled={loading}>{loading ? '…' : '↻ Refresh'}</button>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={handleMigrateAll} disabled={migrating}>{migrating ? 'Migrating…' : '⬆ Migrate All'}</button>
|
<button className="btn btn-secondary btn-sm" onClick={handleMigrateAll} disabled={migrating}>{migrating ? 'Migrating…' : '⬆ Migrate All'}</button>
|
||||||
|
|||||||
@@ -282,7 +282,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="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' }}/>
|
<input value={title} onChange={e= autoComplete="new-password">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 */}
|
||||||
@@ -364,7 +364,7 @@ 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="new-password" 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= autoComplete="new-password">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 */}
|
||||||
@@ -397,7 +397,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
|||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
value={newTypeName}
|
value={newTypeName}
|
||||||
onChange={e=>setNewTypeName(e.target.value)}
|
onChange={e= autoComplete="new-password">setNewTypeName(e.target.value)}
|
||||||
onKeyDown={e=>e.key==='Enter'&&createEventType()}
|
onKeyDown={e=>e.key==='Enter'&&createEventType()}
|
||||||
placeholder="Type name…" autoComplete="new-password" 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)' }}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function MembersScreen({ group, allUsers, onBack }) {
|
|||||||
<div style={{ position:'relative' }}>
|
<div style={{ position:'relative' }}>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:10,top:'50%',transform:'translateY(-50%)',pointerEvents:'none'}}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:10,top:'50%',transform:'translateY(-50%)',pointerEvents:'none'}}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
<input
|
<input
|
||||||
value={search} onChange={e=>setSearch(e.target.value)}
|
value={search} onChange={e= autoComplete="new-password">setSearch(e.target.value)}
|
||||||
placeholder="Search users…"
|
placeholder="Search users…"
|
||||||
autoComplete="new-password" 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'}}
|
||||||
@@ -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="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' }}/>
|
<input autoFocus value={newName} onChange={e= autoComplete="new-password">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="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 }}/>
|
<input autoFocus value={newName} onChange={e= autoComplete="new-password">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>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function NewChatModal({ onClose, onCreated }) {
|
|||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e = autoComplete="new-password"> setName(e.target.value)}
|
||||||
placeholder={namePlaceholder}
|
placeholder={namePlaceholder}
|
||||||
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function ProfileModal({ onClose }) {
|
|||||||
<input
|
<input
|
||||||
className="input flex-1"
|
className="input flex-1"
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={async e => {
|
onChange={async e = autoComplete="new-password"> {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setDisplayName(val);
|
setDisplayName(val);
|
||||||
setDisplayNameWarning('');
|
setDisplayNameWarning('');
|
||||||
@@ -170,15 +170,15 @@ export default function ProfileModal({ onClose }) {
|
|||||||
<div className="flex-col gap-3">
|
<div className="flex-col gap-3">
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
|
||||||
<input className="input" type="password" value={currentPw} onChange={e => setCurrentPw(e.target.value)} />
|
<input className="input" type="password" value={currentPw} onChange={e = autoComplete="new-password"> setCurrentPw(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)' }}>New Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>New Password</label>
|
||||||
<input className="input" type="password" value={newPw} onChange={e => setNewPw(e.target.value)} />
|
<input className="input" type="password" value={newPw} onChange={e = autoComplete="new-password"> setNewPw(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)' }}>Confirm New Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Confirm New Password</label>
|
||||||
<input className="input" type="password" value={confirmPw} onChange={e => setConfirmPw(e.target.value)} />
|
<input className="input" type="password" value={confirmPw} onChange={e = autoComplete="new-password"> setConfirmPw(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary" onClick={handleChangePassword} disabled={loading || !currentPw || !newPw}>
|
<button className="btn btn-primary" onClick={handleChangePassword} disabled={loading || !currentPw || !newPw}>
|
||||||
{loading ? 'Changing...' : 'Change Password'}
|
{loading ? 'Changing...' : 'Change Password'}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ function EventTypePopup({ userGroups, onSave, onClose, editing = null }) {
|
|||||||
<div style={{ position:'absolute', top:'100%', left:0, zIndex:200, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius)', padding:16, width:280, boxShadow:'0 4px 20px rgba(0,0,0,0.15)' }}>
|
<div style={{ position:'absolute', top:'100%', left:0, zIndex:200, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius)', padding:16, width:280, boxShadow:'0 4px 20px rgba(0,0,0,0.15)' }}>
|
||||||
<div style={{ marginBottom:10 }}>
|
<div style={{ marginBottom:10 }}>
|
||||||
<label className="settings-section-label">Type Name</label>
|
<label className="settings-section-label">Type Name</label>
|
||||||
<input className="input" value={name} onChange={e=>setName(e.target.value)} style={{ marginTop:4 }} autoFocus />
|
<input className="input" value={name} onChange={e= autoComplete="new-password">setName(e.target.value)} style={{ marginTop:4 }} autoFocus />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom:10 }}>
|
<div style={{ marginBottom:10 }}>
|
||||||
<label className="settings-section-label">Colour</label>
|
<label className="settings-section-label">Colour</label>
|
||||||
@@ -195,7 +195,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
|||||||
return (
|
return (
|
||||||
<div style={{ display:'flex', flexDirection:'column', gap:0 }}>
|
<div style={{ display:'flex', flexDirection:'column', gap:0 }}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<input className="input" placeholder="Add title" value={title} onChange={e=>setTitle(e.target.value)}
|
<input className="input" placeholder="Add title" value={title} onChange={e= autoComplete="new-password">setTitle(e.target.value)}
|
||||||
style={{ fontSize:18, fontWeight:600, marginBottom:16, border:'none', borderBottom:'2px solid var(--border)', borderRadius:0, padding:'4px 0' }} />
|
style={{ fontSize:18, fontWeight:600, marginBottom:16, border:'none', borderBottom:'2px solid var(--border)', borderRadius:0, padding:'4px 0' }} />
|
||||||
|
|
||||||
{/* Date/Time */}
|
{/* Date/Time */}
|
||||||
@@ -265,7 +265,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
|||||||
|
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<Row label="Location">
|
<Row label="Location">
|
||||||
<input className="input" placeholder="Add location" value={location} onChange={e=>setLocation(e.target.value)} />
|
<input className="input" placeholder="Add location" value={location} onChange={e= autoComplete="new-password">setLocation(e.target.value)} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
|
|||||||
<div style={{padding:'8px 12px 12px',borderTop:'1px solid var(--border)'}}>
|
<div style={{padding:'8px 12px 12px',borderTop:'1px solid var(--border)'}}>
|
||||||
<div style={{position:'relative',marginBottom:8}}>
|
<div style={{position:'relative',marginBottom:8}}>
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:9,top:'50%',transform:'translateY(-50%)',pointerEvents:'none'}}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:9,top:'50%',transform:'translateY(-50%)',pointerEvents:'none'}}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
<input value={filterKeyword} onChange={e=>onFilterKeyword(e.target.value)} onFocus={onInputFocus} onBlur={onInputBlur}
|
<input value={filterKeyword} onChange={e= autoComplete="new-password">onFilterKeyword(e.target.value)} onFocus={onInputFocus} onBlur={onInputBlur}
|
||||||
placeholder="Search events…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
placeholder="Search events…" 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>
|
||||||
@@ -201,7 +201,7 @@ function EventTypePopup({ userGroups, onSave, onClose, editing=null }) {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div style={{position:'absolute',top:'100%',left:0,zIndex:300,background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:16,width:270,boxShadow:'0 4px 20px rgba(0,0,0,0.2)'}}>
|
<div style={{position:'absolute',top:'100%',left:0,zIndex:300,background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:16,width:270,boxShadow:'0 4px 20px rgba(0,0,0,0.2)'}}>
|
||||||
<div style={{marginBottom:8}}><label className="settings-section-label">Name</label><input className="input" value={name} onChange={e=>setName(e.target.value)} style={{marginTop:4}} autoFocus/></div>
|
<div style={{marginBottom:8}}><label className="settings-section-label">Name</label><input className="input" value={name} onChange={e= autoComplete="new-password">setName(e.target.value)} style={{marginTop:4}} autoFocus/></div>
|
||||||
<div style={{marginBottom:8}}><label className="settings-section-label">Colour</label><input type="color" value={colour} onChange={e=>setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/></div>
|
<div style={{marginBottom:8}}><label className="settings-section-label">Colour</label><input type="color" value={colour} onChange={e=>setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/></div>
|
||||||
<div style={{marginBottom:8}}><label className="settings-section-label">Default Group</label><select className="input" value={groupId} onChange={e=>setGroupId(e.target.value)} style={{marginTop:4}}><option value="">None</option>{userGroups.map(g=><option key={g.id} value={g.id}>{g.name}</option>)}</select></div>
|
<div style={{marginBottom:8}}><label className="settings-section-label">Default Group</label><select className="input" value={groupId} onChange={e=>setGroupId(e.target.value)} style={{marginTop:4}}><option value="">None</option>{userGroups.map(g=><option key={g.id} value={g.id}>{g.name}</option>)}</select></div>
|
||||||
<div style={{marginBottom:12}}>
|
<div style={{marginBottom:12}}>
|
||||||
@@ -412,7 +412,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
|||||||
<div style={{minWidth:500}} onKeyDown={e=>{if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA') e.preventDefault();}}>
|
<div style={{minWidth:500}} onKeyDown={e=>{if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA') e.preventDefault();}}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div style={{marginBottom:20}}>
|
<div style={{marginBottom:20}}>
|
||||||
<input className="input" placeholder="Add title" value={title} onChange={e=>setTitle(e.target.value)}
|
<input className="input" placeholder="Add title" value={title} onChange={e= autoComplete="new-password">setTitle(e.target.value)}
|
||||||
style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
|
style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -500,7 +500,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
|||||||
|
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<FormRow label="Location">
|
<FormRow label="Location">
|
||||||
<input className="input" placeholder="Add location" value={loc} onChange={e=>setLoc(e.target.value)}/>
|
<input className="input" placeholder="Add location" value={loc} onChange={e= autoComplete="new-password">setLoc(e.target.value)}/>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
@@ -673,7 +673,7 @@ function EventTypesPanel({ eventTypes, userGroups, onUpdated, isMobile=false })
|
|||||||
<span style={{fontWeight:700,fontSize:16}}>{sheetMode==='create'?'New Event Type':'Edit Event Type'}</span>
|
<span style={{fontWeight:700,fontSize:16}}>{sheetMode==='create'?'New Event Type':'Edit Event Type'}</span>
|
||||||
<button onClick={closeSheet} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:20,lineHeight:1}}>✕</button>
|
<button onClick={closeSheet} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:20,lineHeight:1}}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<input autoFocus value={sheetName} onChange={e=>setSheetName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&saveSheet()} placeholder="Type name…"
|
<input autoFocus value={sheetName} onChange={e= autoComplete="new-password">setSheetName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&saveSheet()} placeholder="Type name…"
|
||||||
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}}>
|
||||||
<label style={{fontSize:14,color:'var(--text-tertiary)',flexShrink:0}}>Colour</label>
|
<label style={{fontSize:14,color:'var(--text-tertiary)',flexShrink:0}}>Colour</label>
|
||||||
@@ -1201,8 +1201,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
|
|
||||||
if (loading) return <div style={{display:'flex',alignItems:'center',justifyContent:'center',flex:1,color:'var(--text-tertiary)',fontSize:14}}>Loading schedule…</div>;
|
if (loading) return <div style={{display:'flex',alignItems:'center',justifyContent:'center',flex:1,color:'var(--text-tertiary)',fontSize:14}}>Loading schedule…</div>;
|
||||||
|
|
||||||
// ── Sidebar width matches Messages (~280px) ───────────────────────────────
|
// ── Sidebar width matches Messages sidebar (320px) ────────────────────────
|
||||||
const SIDEBAR_W = isMobile ? 0 : 260;
|
const SIDEBAR_W = isMobile ? 0 : 320;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display:'flex', flex:1, overflow:'hidden', minHeight:0 }}>
|
<div style={{ display:'flex', flex:1, overflow:'hidden', minHeight:0 }}>
|
||||||
@@ -1248,7 +1248,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|||||||
className="input"
|
className="input"
|
||||||
placeholder={`Keyword… (space = OR, "phrase")`}
|
placeholder={`Keyword… (space = OR, "phrase")`}
|
||||||
value={filterKeyword}
|
value={filterKeyword}
|
||||||
onChange={e=>setFilterKeyword(e.target.value)}
|
onChange={e= autoComplete="new-password">setFilterKeyword(e.target.value)}
|
||||||
autoComplete="new-password" 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 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function RegistrationTab({ onFeaturesChanged }) {
|
|||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div className="settings-section-label">Serial Number</div>
|
<div className="settings-section-label">Serial Number</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
|
||||||
<input className="input flex-1" value={serialNumber} readOnly style={{ fontFamily: 'monospace', letterSpacing: 1 }} />
|
<input className="input flex-1" value={serialNumber} readOnly style={{ fontFamily: 'monospace', letterSpacing: 1 }} autoComplete="new-password">
|
||||||
<button className="btn btn-secondary btn-sm" onClick={handleCopySerial} style={{ flexShrink: 0 }}>
|
<button className="btn btn-secondary btn-sm" onClick={handleCopySerial} style={{ flexShrink: 0 }}>
|
||||||
{copied ? '✓ Copied' : (
|
{copied ? '✓ Copied' : (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
@@ -167,7 +167,7 @@ function RegistrationTab({ onFeaturesChanged }) {
|
|||||||
<div className="settings-section-label">Registration Code</div>
|
<div className="settings-section-label">Registration Code</div>
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
|
||||||
<input className="input flex-1" placeholder="Enter registration code" value={regCode}
|
<input className="input flex-1" placeholder="Enter registration code" value={regCode}
|
||||||
onChange={e => setRegCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRegister()} />
|
onChange={e = autoComplete="new-password"> setRegCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRegister()} />
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleRegister} disabled={regLoading}>
|
<button className="btn btn-primary btn-sm" onClick={handleRegister} disabled={regLoading}>
|
||||||
{regLoading ? '…' : 'Register'}
|
{regLoading ? '…' : 'Register'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function SupportModal({ onClose }) {
|
|||||||
className="input"
|
className="input"
|
||||||
placeholder="Jane Smith"
|
placeholder="Jane Smith"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e = autoComplete="new-password"> setName(e.target.value)}
|
||||||
|
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +113,7 @@ export default function SupportModal({ onClose }) {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="jane@example.com"
|
placeholder="jane@example.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e = autoComplete="new-password"> setEmail(e.target.value)}
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function UserRow({ u, onUpdated }) {
|
|||||||
className="input"
|
className="input"
|
||||||
style={{ flex: 1, fontSize: 13, padding: '5px 8px' }}
|
style={{ flex: 1, fontSize: 13, padding: '5px 8px' }}
|
||||||
value={nameVal}
|
value={nameVal}
|
||||||
onChange={e => setNameVal(e.target.value)}
|
onChange={e = autoComplete="new-password"> setNameVal(e.target.value)}
|
||||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditName(false); setNameVal(u.name); } }}
|
onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditName(false); setNameVal(u.name); } }}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
@@ -167,7 +167,7 @@ function UserRow({ u, onUpdated }) {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="New password (min 6)"
|
placeholder="New password (min 6)"
|
||||||
value={resetPw}
|
value={resetPw}
|
||||||
onChange={e => setResetPw(e.target.value)}
|
onChange={e = autoComplete="new-password"> setResetPw(e.target.value)}
|
||||||
onKeyDown={e => { if (e.key === 'Enter') handleResetPw(); if (e.key === 'Escape') { setShowReset(false); setResetPw(''); } }}
|
onKeyDown={e => { if (e.key === 'Enter') handleResetPw(); if (e.key === 'Escape') { setShowReset(false); setResetPw(''); } }}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ export default function ChangePassword() {
|
|||||||
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
|
||||||
<input className="input" type="password" value={current} onChange={e => setCurrent(e.target.value)} required />
|
<input className="input" type="password" value={current} onChange={e = autoComplete="new-password"> setCurrent(e.target.value)} required />
|
||||||
</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)' }}>New Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>New Password</label>
|
||||||
<input className="input" type="password" value={next} onChange={e => setNext(e.target.value)} required />
|
<input className="input" type="password" value={next} onChange={e = autoComplete="new-password"> setNext(e.target.value)} required />
|
||||||
</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)' }}>Confirm New Password</label>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Confirm New Password</label>
|
||||||
<input className="input" type="password" value={confirm} onChange={e => setConfirm(e.target.value)} required />
|
<input className="input" type="password" value={confirm} onChange={e = autoComplete="new-password"> setConfirm(e.target.value)} required />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary" type="submit" disabled={loading}>
|
<button className="btn btn-primary" type="submit" disabled={loading}>
|
||||||
{loading ? 'Saving...' : 'Set New Password'}
|
{loading ? 'Saving...' : 'Set New Password'}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import Avatar from '../components/Avatar.jsx';
|
|||||||
|
|
||||||
// ── Shared sub-components (identical logic to modal versions) ─────────────────
|
// ── Shared sub-components (identical logic to modal versions) ─────────────────
|
||||||
|
|
||||||
function UserCheckList({ allUsers, selectedIds, onChange }) {
|
function UserCheckList({ allUsers, selectedIds, onChange, onIF, onIB }) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const filtered = allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase()));
|
const filtered = allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase()));
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
<input className="input" placeholder="Search users…" value={search} onChange={e = autoComplete="new-password"> setSearch(e.target.value)}
|
||||||
style={{ marginBottom:8 }} autoComplete="new-password" />
|
style={{ marginBottom:8 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
<div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
<div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
||||||
{filtered.map(u => (
|
{filtered.map(u => (
|
||||||
<label key={u.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 12px', borderBottom:'1px solid var(--border)', cursor:'pointer' }}>
|
<label key={u.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 12px', borderBottom:'1px solid var(--border)', cursor:'pointer' }}>
|
||||||
@@ -59,7 +59,7 @@ function GroupCheckList({ allGroups, selectedIds, onChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── All Groups tab ────────────────────────────────────────────────────────────
|
// ── All Groups tab ────────────────────────────────────────────────────────────
|
||||||
function AllGroupsTab({ allUsers, onRefresh, isMobile = false }) {
|
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
@@ -139,13 +139,13 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false }) {
|
|||||||
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
|
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Group Name</label>
|
<label className="settings-section-label">Group Name</label>
|
||||||
<input className="input" value={editName} onChange={e => setEditName(e.target.value)}
|
<input className="input" value={editName} onChange={e = autoComplete="new-password"> setEditName(e.target.value)}
|
||||||
placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" />
|
placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
{isCreating && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
|
{isCreating && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Members</label>
|
<label className="settings-section-label">Members</label>
|
||||||
<div style={{ marginTop:6 }}><UserCheckList allUsers={allUsers} selectedIds={members} onChange={setMembers} /></div>
|
<div style={{ marginTop:6 }}><UserCheckList allUsers={allUsers} selectedIds={members} onChange={setMembers} onIF={onIF} onIB={onIB} /></div>
|
||||||
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{members.size} selected</p>
|
<p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>{members.size} selected</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
|
<div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
|
||||||
@@ -173,7 +173,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Direct Messages tab ───────────────────────────────────────────────────────
|
// ── Direct Messages tab ───────────────────────────────────────────────────────
|
||||||
function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = false }) {
|
function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = false, onIF, onIB }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [dms, setDms] = useState([]);
|
const [dms, setDms] = useState([]);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
@@ -248,7 +248,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
|||||||
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
|
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">DM Name</label>
|
<label className="settings-section-label">DM Name</label>
|
||||||
<input className="input" value={dmName} onChange={e => setDmName(e.target.value)} placeholder="e.g. Coaches + Players" style={{ marginTop:6 }} autoComplete="new-password" />
|
<input className="input" value={dmName} onChange={e = autoComplete="new-password"> setDmName(e.target.value)} placeholder="e.g. Coaches + Players" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Member Groups</label>
|
<label className="settings-section-label">Member Groups</label>
|
||||||
@@ -282,7 +282,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
|||||||
|
|
||||||
|
|
||||||
// ── U2U Restrictions tab ──────────────────────────────────────────────────────
|
// ── U2U Restrictions tab ──────────────────────────────────────────────────────
|
||||||
function U2URestrictionsTab({ allUserGroups, isMobile = false }) {
|
function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||||
const [blockedIds, setBlockedIds] = useState(new Set());
|
const [blockedIds, setBlockedIds] = useState(new Set());
|
||||||
@@ -434,8 +434,8 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false }) {
|
|||||||
Allowed Groups <span style={{ fontWeight:400, color:'var(--text-tertiary)' }}>({otherGroups.length - blockedIds.size} of {otherGroups.length} allowed)</span>
|
Allowed Groups <span style={{ fontWeight:400, color:'var(--text-tertiary)' }}>({otherGroups.length - blockedIds.size} of {otherGroups.length} allowed)</span>
|
||||||
</label>
|
</label>
|
||||||
<input className="input" placeholder="Search groups…" value={search}
|
<input className="input" placeholder="Search groups…" value={search}
|
||||||
onChange={e => setSearch(e.target.value)} style={{ marginBottom:8 }}
|
onChange={e = autoComplete="new-password"> setSearch(e.target.value)} style={{ marginBottom:8 }}
|
||||||
autoComplete="new-password" />
|
autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -515,7 +515,9 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
|||||||
const [allUsers, setAllUsers] = useState([]);
|
const [allUsers, setAllUsers] = useState([]);
|
||||||
const [allUserGroups, setAllUserGroups] = useState([]);
|
const [allUserGroups, setAllUserGroups] = useState([]);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const keyboardOpen = useKeyboardOpen();
|
const [inputFocused, setInputFocused] = useState(false);
|
||||||
|
const onIF = () => setInputFocused(true);
|
||||||
|
const onIB = () => setInputFocused(false);
|
||||||
const onRefresh = () => setRefreshKey(k => k+1);
|
const onRefresh = () => setRefreshKey(k => k+1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -559,7 +561,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Right panel ── */}
|
{/* ── Right panel ── */}
|
||||||
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)', height:'100%' }}>
|
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)' }}>
|
||||||
|
|
||||||
{/* Mobile tab bar — only shown on mobile */}
|
{/* Mobile tab bar — only shown on mobile */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
@@ -573,14 +575,16 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 70 : 0 }}>
|
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 70 : 0 }}>
|
||||||
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} />}
|
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||||
{tab==='dm' && <DirectMessagesTab allUserGroups={allUserGroups} onRefresh={onRefresh} refreshKey={refreshKey} isMobile={isMobile} />}
|
{tab==='dm' && <DirectMessagesTab allUserGroups={allUserGroups} onRefresh={onRefresh} refreshKey={refreshKey} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||||
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} />}
|
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile footer — in-flow so it stays at bottom and never floats with keyboard */}
|
{/* Mobile footer — fixed, hidden when any input is focused (keyboard open) */}
|
||||||
{isMobile && (
|
{isMobile && !inputFocused && (
|
||||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
<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} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ function KeyEntry({ onSubmit }) {
|
|||||||
placeholder="Host admin key" autoFocus
|
placeholder="Host admin key" autoFocus
|
||||||
style={{ width: '100%', padding: '10px 12px', border: '1px solid #e0e0e0', borderRadius: 6,
|
style={{ width: '100%', padding: '10px 12px', border: '1px solid #e0e0e0', borderRadius: 6,
|
||||||
fontSize: 14, outline: 'none', boxSizing: 'border-box', marginBottom: 12 }}
|
fontSize: 14, outline: 'none', boxSizing: 'border-box', marginBottom: 12 }}
|
||||||
/>
|
autoComplete="new-password" />
|
||||||
<Btn onClick={handle} variant="primary" style={{ width: '100%', justifyContent: 'center' }}>
|
<Btn onClick={handle} variant="primary" style={{ width: '100%', justifyContent: 'center' }}>
|
||||||
Sign In
|
Sign In
|
||||||
</Btn>
|
</Btn>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function UserRow({ u, onUpdated }) {
|
|||||||
<input className="input" style={{ flex:1, fontSize:13, padding:'5px 8px' }}
|
<input className="input" style={{ flex:1, fontSize:13, padding:'5px 8px' }}
|
||||||
value={nameVal} onChange={e => setNameVal(e.target.value)}
|
value={nameVal} onChange={e => setNameVal(e.target.value)}
|
||||||
onKeyDown={e => { if(e.key==='Enter') handleSaveName(); if(e.key==='Escape'){setEditName(false);setNameVal(u.name);} }}
|
onKeyDown={e => { if(e.key==='Enter') handleSaveName(); if(e.key==='Escape'){setEditName(false);setNameVal(u.name);} }}
|
||||||
autoComplete="new-password" />
|
autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleSaveName}>Save</button>
|
<button className="btn btn-primary btn-sm" onClick={handleSaveName}>Save</button>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}>✕</button>
|
<button className="btn btn-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@ function UserRow({ u, onUpdated }) {
|
|||||||
type="text" placeholder="New password (min 6)" value={resetPw}
|
type="text" placeholder="New password (min 6)" value={resetPw}
|
||||||
onChange={e => setResetPw(e.target.value)}
|
onChange={e => setResetPw(e.target.value)}
|
||||||
onKeyDown={e => { if(e.key==='Enter') handleResetPw(); if(e.key==='Escape'){setShowReset(false);setResetPw('');} }}
|
onKeyDown={e => { if(e.key==='Enter') handleResetPw(); if(e.key==='Escape'){setShowReset(false);setResetPw('');} }}
|
||||||
autoComplete="new-password" />
|
autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleResetPw}>Set</button>
|
<button className="btn btn-primary btn-sm" onClick={handleResetPw}>Set</button>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}>✕</button>
|
<button className="btn btn-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@ function UserRow({ u, onUpdated }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateUserForm({ userPass, onCreated, isMobile }) {
|
function CreateUserForm({ userPass, onCreated, isMobile, onIF, onIB }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [form, setForm] = useState({ name:'', email:'', password:'', role:'member' });
|
const [form, setForm] = useState({ name:'', email:'', password:'', role:'member' });
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -178,15 +178,15 @@ function CreateUserForm({ userPass, onCreated, isMobile }) {
|
|||||||
<div style={{ display:'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap:12, marginBottom:12 }}>
|
<div style={{ display:'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap:12, marginBottom: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="new-password" autoCorrect="off" autoCapitalize="words" value={form.name} onChange={e => set('name')(e.target.value)} />
|
<input className="input" placeholder="Jane Smith" autoComplete="new-password" autoCorrect="off" autoCapitalize="words" value={form.name} onChange={e => set('name')(e.target.value)} onFocus={onIF} onBlur={onIB} />
|
||||||
</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="new-password" value={form.email} onChange={e => set('email')(e.target.value)} />
|
<input className="input" type="email" placeholder="jane@example.com" autoComplete="new-password" value={form.email} onChange={e => set('email')(e.target.value)} onFocus={onIF} onBlur={onIB} />
|
||||||
</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)' }}>Temp Password <span style={{ fontWeight:400, color:'var(--text-tertiary)' }}>(blank = {userPass})</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})</span></label>
|
||||||
<input className="input" type="text" autoComplete="new-password" value={form.password} onChange={e => set('password')(e.target.value)} />
|
<input className="input" type="text" autoComplete="new-password" value={form.password} onChange={e => set('password')(e.target.value)} onFocus={onIF} onBlur={onIB} />
|
||||||
</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>
|
||||||
@@ -272,19 +272,6 @@ function BulkImportForm({ userPass, onCreated }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── useKeyboardOpen — true when software keyboard is visible ─────────────────
|
|
||||||
function useKeyboardOpen() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const vv = window.visualViewport;
|
|
||||||
if (!vv) return;
|
|
||||||
const handler = () => setOpen(vv.height < window.innerHeight * 0.75);
|
|
||||||
vv.addEventListener('resize', handler);
|
|
||||||
return () => vv.removeEventListener('resize', handler);
|
|
||||||
}, []);
|
|
||||||
return open;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||||
export default function UserManagerPage({ isMobile = false, onProfile, onHelp, onAbout }) {
|
export default function UserManagerPage({ isMobile = false, onProfile, onHelp, onAbout }) {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
@@ -293,7 +280,9 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [tab, setTab] = useState('users');
|
const [tab, setTab] = useState('users');
|
||||||
const [userPass, setUserPass] = useState('user@1234');
|
const [userPass, setUserPass] = useState('user@1234');
|
||||||
const keyboardOpen = useKeyboardOpen();
|
const [inputFocused, setInputFocused] = useState(false);
|
||||||
|
const onIF = () => setInputFocused(true);
|
||||||
|
const onIB = () => setInputFocused(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoadError(''); setLoading(true);
|
setLoadError(''); setLoading(true);
|
||||||
@@ -349,7 +338,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Right panel ── */}
|
{/* ── Right panel ── */}
|
||||||
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)', height:'100%' }}>
|
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0, background:'var(--background)' }}>
|
||||||
|
|
||||||
{/* Mobile tab bar — only on mobile since desktop uses left panel */}
|
{/* Mobile tab bar — only on mobile since desktop uses left panel */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
@@ -365,6 +354,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
|||||||
{tab === 'users' && (
|
{tab === 'users' && (
|
||||||
<>
|
<>
|
||||||
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
||||||
|
onFocus={onIF} onBlur={onIB}
|
||||||
autoComplete="new-password" autoCorrect="off" spellCheck={false}
|
autoComplete="new-password" autoCorrect="off" spellCheck={false}
|
||||||
style={{ marginBottom:16, width:'100%', maxWidth: isMobile ? '100%' : 400 }} />
|
style={{ marginBottom:16, width:'100%', maxWidth: isMobile ? '100%' : 400 }} />
|
||||||
<div style={{ background:'var(--surface)', borderRadius:'var(--radius)', boxShadow:'var(--shadow-sm)', overflow:'hidden' }}>
|
<div style={{ background:'var(--surface)', borderRadius:'var(--radius)', boxShadow:'var(--shadow-sm)', overflow:'hidden' }}>
|
||||||
@@ -385,13 +375,15 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tab === 'create' && <CreateUserForm userPass={userPass} onCreated={() => { load(); setTab('users'); }} isMobile={isMobile} />}
|
{tab === 'create' && <CreateUserForm userPass={userPass} onCreated={() => { load(); setTab('users'); }} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||||
{tab === 'bulk' && <BulkImportForm userPass={userPass} onCreated={load} />}
|
{tab === 'bulk' && <BulkImportForm userPass={userPass} onCreated={load} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile footer — in-flow so it stays at bottom and never floats with keyboard */}
|
{/* Mobile footer — fixed, hidden when any input is focused (keyboard open) */}
|
||||||
{isMobile && (
|
{isMobile && !inputFocused && (
|
||||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
<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} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user