Initial Commit

This commit is contained in:
2026-03-06 11:54:19 -05:00
parent ee68c4704f
commit 4517746692
36 changed files with 4262 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
import { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext.jsx';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
import Avatar from './Avatar.jsx';
export default function GroupInfoModal({ group, onClose, onUpdated }) {
const { user } = useAuth();
const toast = useToast();
const [members, setMembers] = useState([]);
const [editing, setEditing] = useState(false);
const [newName, setNewName] = useState(group.name);
const [addSearch, setAddSearch] = useState('');
const [addResults, setAddResults] = useState([]);
const [loading, setLoading] = useState(false);
const isOwner = group.owner_id === user.id;
const isAdmin = user.role === 'admin';
const canManage = (group.type === 'private' && isOwner) || (group.type === 'public' && isAdmin);
const canRename = !group.is_default && ((group.type === 'public' && isAdmin) || (group.type === 'private' && isOwner));
useEffect(() => {
if (group.type === 'private') {
api.getMembers(group.id).then(({ members }) => setMembers(members)).catch(() => {});
}
}, [group.id]);
useEffect(() => {
if (addSearch) {
api.searchUsers(addSearch).then(({ users }) => setAddResults(users)).catch(() => {});
}
}, [addSearch]);
const handleRename = async () => {
if (!newName.trim() || newName === group.name) { setEditing(false); return; }
try {
await api.renameGroup(group.id, newName.trim());
toast('Group renamed', 'success');
onUpdated();
setEditing(false);
} catch (e) { toast(e.message, 'error'); }
};
const handleLeave = async () => {
if (!confirm('Leave this group?')) return;
try {
await api.leaveGroup(group.id);
toast('Left group', 'success');
onUpdated();
onClose();
} catch (e) { toast(e.message, 'error'); }
};
const handleTakeOwnership = async () => {
if (!confirm('Take ownership of this private group? You will be able to see all messages.')) return;
try {
await api.takeOwnership(group.id);
toast('Ownership taken', 'success');
onUpdated();
onClose();
} catch (e) { toast(e.message, 'error'); }
};
const handleAdd = async (u) => {
try {
await api.addMember(group.id, u.id);
toast(`${u.display_name || u.name} added`, 'success');
api.getMembers(group.id).then(({ members }) => setMembers(members));
setAddSearch('');
setAddResults([]);
} catch (e) { toast(e.message, 'error'); }
};
const handleDelete = async () => {
if (!confirm('Delete this group? This cannot be undone.')) return;
try {
await api.deleteGroup(group.id);
toast('Group deleted', 'success');
onUpdated();
onClose();
} catch (e) { toast(e.message, 'error'); }
};
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal">
<div className="flex items-center justify-between" style={{ marginBottom: 20 }}>
<h2 className="modal-title" style={{ margin: 0 }}>Group Info</h2>
<button className="btn-icon" onClick={onClose}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
{/* Name */}
<div style={{ marginBottom: 16 }}>
{editing ? (
<div className="flex gap-2">
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && handleRename()} />
<button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button>
<button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button>
</div>
) : (
<div className="flex items-center gap-8" style={{ gap: 12 }}>
<h3 style={{ fontSize: 18, fontWeight: 600, flex: 1 }}>{group.name}</h3>
{canRename && (
<button className="btn-icon" onClick={() => setEditing(true)} title="Rename">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
)}
</div>
)}
<div className="flex items-center gap-6" style={{ gap: 8, marginTop: 4 }}>
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{group.type === 'public' ? 'Public channel' : 'Private group'}
</span>
{group.is_readonly && <span className="readonly-badge" style={{ fontSize: 11, padding: '2px 8px', borderRadius: 10, background: '#fff3e0', color: '#e65100' }}>Read-only</span>}
</div>
</div>
{/* Members (private groups) */}
{group.type === 'private' && (
<div style={{ marginBottom: 16 }}>
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
Members ({members.length})
</div>
<div style={{ maxHeight: 180, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
{members.map(m => (
<div key={m.id} className="flex items-center gap-2" style={{ gap: 10, padding: '6px 0' }}>
<Avatar user={m} size="sm" />
<span className="flex-1 text-sm">{m.display_name || m.name}</span>
{m.id === group.owner_id && <span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>Owner</span>}
</div>
))}
</div>
{canManage && (
<div style={{ marginTop: 12 }}>
<input className="input" placeholder="Search to add member..." value={addSearch} onChange={e => setAddSearch(e.target.value)} />
{addResults.length > 0 && addSearch && (
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto' }}>
{addResults.filter(u => !members.find(m => m.id === u.id)).map(u => (
<button key={u.id} className="flex items-center gap-2 w-full" style={{ gap: 10, padding: '8px 12px', textAlign: 'left', transition: 'background var(--transition)' }} onClick={() => handleAdd(u)} onMouseEnter={e => e.currentTarget.style.background = 'var(--background)'} onMouseLeave={e => e.currentTarget.style.background = ''}>
<Avatar user={u} size="sm" />
<span className="text-sm flex-1">{u.display_name || u.name}</span>
</button>
))}
</div>
)}
</div>
)}
</div>
)}
{/* Actions */}
<div className="flex-col gap-2">
{group.type === 'private' && group.owner_id !== user.id && (
<button className="btn btn-secondary w-full" onClick={handleLeave}>Leave Group</button>
)}
{isAdmin && group.type === 'private' && group.owner_id !== user.id && (
<button className="btn btn-secondary w-full" onClick={handleTakeOwnership}>
Take Ownership (Admin)
</button>
)}
{(isOwner || (isAdmin && group.type === 'public')) && !group.is_default && (
<button className="btn btn-danger w-full" onClick={handleDelete}>Delete Group</button>
)}
</div>
</div>
</div>
);
}