v0.11.1 various UI bug fixes
This commit is contained in:
@@ -40,15 +40,15 @@ export default function ChangePassword() {
|
||||
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div className="flex-col gap-1">
|
||||
<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 className="flex-col gap-1">
|
||||
<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 className="flex-col gap-1">
|
||||
<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>
|
||||
<button className="btn btn-primary" type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : 'Set New Password'}
|
||||
|
||||
@@ -19,13 +19,13 @@ import Avatar from '../components/Avatar.jsx';
|
||||
|
||||
// ── 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 filtered = allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase()));
|
||||
return (
|
||||
<div>
|
||||
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
|
||||
style={{ marginBottom:8 }} autoComplete="new-password" />
|
||||
<input className="input" placeholder="Search users…" value={search} onChange={e = autoComplete="new-password"> setSearch(e.target.value)}
|
||||
style={{ marginBottom:8 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||
<div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
||||
{filtered.map(u => (
|
||||
<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 ────────────────────────────────────────────────────────────
|
||||
function AllGroupsTab({ allUsers, onRefresh, isMobile = false }) {
|
||||
function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
const toast = useToast();
|
||||
const [groups, setGroups] = useState([]);
|
||||
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>
|
||||
<label className="settings-section-label">Group Name</label>
|
||||
<input className="input" value={editName} onChange={e => setEditName(e.target.value)}
|
||||
placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" />
|
||||
<input className="input" value={editName} onChange={e = autoComplete="new-password"> setEditName(e.target.value)}
|
||||
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>}
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
|
||||
@@ -173,7 +173,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false }) {
|
||||
}
|
||||
|
||||
// ── Direct Messages tab ───────────────────────────────────────────────────────
|
||||
function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = false }) {
|
||||
function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = false, onIF, onIB }) {
|
||||
const toast = useToast();
|
||||
const [dms, setDms] = useState([]);
|
||||
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>
|
||||
<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>
|
||||
<label className="settings-section-label">Member Groups</label>
|
||||
@@ -282,7 +282,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
|
||||
|
||||
// ── U2U Restrictions tab ──────────────────────────────────────────────────────
|
||||
function U2URestrictionsTab({ allUserGroups, isMobile = false }) {
|
||||
function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||
const toast = useToast();
|
||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||
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>
|
||||
</label>
|
||||
<input className="input" placeholder="Search groups…" value={search}
|
||||
onChange={e => setSearch(e.target.value)} style={{ marginBottom:8 }}
|
||||
autoComplete="new-password" />
|
||||
onChange={e = autoComplete="new-password"> setSearch(e.target.value)} style={{ marginBottom:8 }}
|
||||
autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -515,7 +515,9 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
const [allUserGroups, setAllUserGroups] = useState([]);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -559,7 +561,7 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
)}
|
||||
|
||||
{/* ── 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 */}
|
||||
{isMobile && (
|
||||
@@ -573,14 +575,16 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 70 : 0 }}>
|
||||
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} />}
|
||||
{tab==='dm' && <DirectMessagesTab allUserGroups={allUserGroups} onRefresh={onRefresh} refreshKey={refreshKey} isMobile={isMobile} />}
|
||||
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} 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} onIF={onIF} onIB={onIB} />}
|
||||
{tab==='u2u' && <U2URestrictionsTab allUserGroups={allUserGroups} isMobile={isMobile} onIF={onIF} onIB={onIB} />}
|
||||
</div>
|
||||
|
||||
{/* Mobile footer — in-flow so it stays at bottom and never floats with keyboard */}
|
||||
{isMobile && (
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
{/* Mobile footer — fixed, hidden when any input is focused (keyboard open) */}
|
||||
{isMobile && !inputFocused && (
|
||||
<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>
|
||||
|
||||
@@ -417,7 +417,7 @@ function KeyEntry({ onSubmit }) {
|
||||
placeholder="Host admin key" autoFocus
|
||||
style={{ width: '100%', padding: '10px 12px', border: '1px solid #e0e0e0', borderRadius: 6,
|
||||
fontSize: 14, outline: 'none', boxSizing: 'border-box', marginBottom: 12 }}
|
||||
/>
|
||||
autoComplete="new-password" />
|
||||
<Btn onClick={handle} variant="primary" style={{ width: '100%', justifyContent: 'center' }}>
|
||||
Sign In
|
||||
</Btn>
|
||||
|
||||
@@ -108,7 +108,7 @@ function UserRow({ u, onUpdated }) {
|
||||
<input className="input" style={{ flex:1, fontSize:13, padding:'5px 8px' }}
|
||||
value={nameVal} onChange={e => setNameVal(e.target.value)}
|
||||
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-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}>✕</button>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ function UserRow({ u, onUpdated }) {
|
||||
type="text" placeholder="New password (min 6)" value={resetPw}
|
||||
onChange={e => setResetPw(e.target.value)}
|
||||
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-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}>✕</button>
|
||||
</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 [form, setForm] = useState({ name:'', email:'', password:'', role:'member' });
|
||||
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 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>
|
||||
<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 className="flex-col gap-1">
|
||||
<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 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>
|
||||
<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 className="flex-col gap-1">
|
||||
<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 ─────────────────────────────────────────────────────────────────
|
||||
export default function UserManagerPage({ isMobile = false, onProfile, onHelp, onAbout }) {
|
||||
const [users, setUsers] = useState([]);
|
||||
@@ -293,7 +280,9 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
const [search, setSearch] = useState('');
|
||||
const [tab, setTab] = useState('users');
|
||||
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 () => {
|
||||
setLoadError(''); setLoading(true);
|
||||
@@ -349,7 +338,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
)}
|
||||
|
||||
{/* ── 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 */}
|
||||
{isMobile && (
|
||||
@@ -365,6 +354,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
|
||||
{tab === 'users' && (
|
||||
<>
|
||||
<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}
|
||||
style={{ marginBottom:16, width:'100%', maxWidth: isMobile ? '100%' : 400 }} />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{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} />}
|
||||
</div>
|
||||
|
||||
{/* Mobile footer — in-flow so it stays at bottom and never floats with keyboard */}
|
||||
{isMobile && (
|
||||
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
|
||||
{/* Mobile footer — fixed, hidden when any input is focused (keyboard open) */}
|
||||
{isMobile && !inputFocused && (
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user