v0.9.63 updated for mobile
This commit is contained in:
131
frontend/src/components/MobileGroupManager.jsx
Normal file
131
frontend/src/components/MobileGroupManager.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../utils/api.js';
|
||||
import { useToast } from '../contexts/ToastContext.jsx';
|
||||
import Avatar from './Avatar.jsx';
|
||||
|
||||
export default function MobileGroupManager({ onClose }) {
|
||||
const toast = useToast();
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
const [expanded, setExpanded] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [screen, setScreen] = useState('list'); // list | members
|
||||
const [activeGroup, setActiveGroup] = useState(null);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [ug, us] = await Promise.all([api.getUserGroups(), api.getUsers()]);
|
||||
setGroups(ug.groups || []);
|
||||
setAllUsers(us.users || []);
|
||||
} catch(e) { toast(e.message, 'error'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const createGroup = async () => {
|
||||
if(!newName.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.createUserGroup({ name: newName.trim() });
|
||||
setNewName(''); setCreating(false); load();
|
||||
} catch(e) { toast(e.message,'error'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const deleteGroup = async (g) => {
|
||||
if(!confirm(`Delete "${g.name}"?`)) return;
|
||||
try { await api.deleteUserGroup(g.id); load(); } catch(e) { toast(e.message,'error'); }
|
||||
};
|
||||
|
||||
const toggleMember = async (groupId, userId, isMember) => {
|
||||
try {
|
||||
if(isMember) await api.removeFromUserGroup(groupId, userId);
|
||||
else await api.addToUserGroup(groupId, userId);
|
||||
// Reload group members
|
||||
const ug = await api.getUserGroups();
|
||||
setGroups(ug.groups || []);
|
||||
if(activeGroup) setActiveGroup((ug.groups||[]).find(g=>g.id===activeGroup.id)||null);
|
||||
} catch(e) { toast(e.message,'error'); }
|
||||
};
|
||||
|
||||
if(loading) return (
|
||||
<div style={{ display:'flex',alignItems:'center',justifyContent:'center',height:'100%',color:'var(--text-tertiary)' }}>Loading…</div>
|
||||
);
|
||||
|
||||
// Members screen
|
||||
if(screen==='members' && activeGroup) {
|
||||
const memberIds = new Set((activeGroup.members||[]).map(m=>m.id||m.user_id));
|
||||
return (
|
||||
<div style={{ display:'flex',flexDirection:'column',height:'100%',background:'var(--background)' }}>
|
||||
<div style={{ display:'flex',alignItems:'center',gap:12,padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',flexShrink:0 }}>
|
||||
<button onClick={()=>setScreen('list')} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',display:'flex',alignItems:'center' }}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span style={{ fontWeight:700,fontSize:16,flex:1 }}>{activeGroup.name}</span>
|
||||
<span style={{ fontSize:13,color:'var(--text-tertiary)' }}>{memberIds.size} member{memberIds.size!==1?'s':''}</span>
|
||||
</div>
|
||||
<div style={{ flex:1,overflowY:'auto' }}>
|
||||
<div style={{ padding:'10px 16px 4px',fontSize:12,fontWeight:600,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px' }}>All Users</div>
|
||||
{allUsers.map(u => {
|
||||
const isMember = memberIds.has(u.id);
|
||||
return (
|
||||
<div key={u.id} style={{ display:'flex',alignItems:'center',gap:12,padding:'12px 16px',borderBottom:'1px solid var(--border)' }}>
|
||||
<Avatar user={u} size="sm"/>
|
||||
<div style={{ flex:1,minWidth:0 }}>
|
||||
<div style={{ fontSize:15,fontWeight:500 }}>{u.display_name||u.name}</div>
|
||||
<div style={{ fontSize:12,color:'var(--text-tertiary)' }}>{u.role}</div>
|
||||
</div>
|
||||
<button onClick={()=>toggleMember(activeGroup.id, u.id, isMember)} style={{ padding:'8px 14px',borderRadius:20,border:`1px solid ${isMember?'var(--error)':'var(--primary)'}`,background:'transparent',color:isMember?'var(--error)':'var(--primary)',fontSize:13,fontWeight:600,cursor:'pointer',flexShrink:0 }}>
|
||||
{isMember ? 'Remove' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Group list screen
|
||||
return (
|
||||
<div style={{ display:'flex',flexDirection:'column',height:'100%',background:'var(--background)' }}>
|
||||
<div style={{ display:'flex',alignItems:'center',justifyContent:'space-between',padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',flexShrink:0 }}>
|
||||
<button onClick={onClose} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',display:'flex' }}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span style={{ fontWeight:700,fontSize:16 }}>Group Manager</span>
|
||||
<button onClick={()=>setCreating(true)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:24,lineHeight:1 }}>+</button>
|
||||
</div>
|
||||
|
||||
{creating && (
|
||||
<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…" 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',opacity:saving?0.6:1 }}>{saving?'…':'Create'}</button>
|
||||
<button onClick={()=>{setCreating(false);setNewName('');}} style={{ padding:'8px',background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)' }}>✕</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex:1,overflowY:'auto' }}>
|
||||
{groups.length===0 && <div style={{ textAlign:'center',padding:'60px 20px',color:'var(--text-tertiary)',fontSize:14 }}>No groups yet. Tap + to create one.</div>}
|
||||
{groups.map(g => (
|
||||
<div key={g.id} style={{ borderBottom:'1px solid var(--border)' }}>
|
||||
<div style={{ display:'flex',alignItems:'center',gap:12,padding:'14px 16px',cursor:'pointer' }} onClick={()=>{setActiveGroup(g);setScreen('members');}}>
|
||||
<div style={{ width:42,height:42,borderRadius:10,background:'var(--primary)',display:'flex',alignItems:'center',justifyContent:'center',color:'white',fontWeight:700,fontSize:14,flexShrink:0 }}>
|
||||
{g.name.substring(0,2).toUpperCase()}
|
||||
</div>
|
||||
<div style={{ flex:1,minWidth:0 }}>
|
||||
<div style={{ fontSize:15,fontWeight:600 }}>{g.name}</div>
|
||||
<div style={{ fontSize:12,color:'var(--text-tertiary)' }}>{(g.members||[]).length} member{(g.members||[]).length!==1?'s':''}</div>
|
||||
</div>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user