Add sharing rooms
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m8s
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m8s
This commit is contained in:
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Play, Square, Users, Settings, FileVideo, Radio,
|
||||
Loader2, Copy, ExternalLink, Lock, Mic, MicOff, UserCheck,
|
||||
Shield, Save,
|
||||
Shield, Save, UserPlus, X, Share2,
|
||||
} from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -25,14 +25,21 @@ export default function RoomDetail() {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [editRoom, setEditRoom] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [sharedUsers, setSharedUsers] = useState([]);
|
||||
const [shareSearch, setShareSearch] = useState('');
|
||||
const [shareResults, setShareResults] = useState([]);
|
||||
const [shareSearching, setShareSearching] = useState(false);
|
||||
|
||||
const isOwner = room && user && room.user_id === user.id;
|
||||
const isShared = room && !!room.shared;
|
||||
const canManage = isOwner || isShared;
|
||||
|
||||
const fetchRoom = async () => {
|
||||
try {
|
||||
const res = await api.get(`/rooms/${uid}`);
|
||||
setRoom(res.data.room);
|
||||
setEditRoom(res.data.room);
|
||||
if (res.data.sharedUsers) setSharedUsers(res.data.sharedUsers);
|
||||
} catch {
|
||||
toast.error(t('room.notFound'));
|
||||
navigate('/dashboard');
|
||||
@@ -144,6 +151,48 @@ export default function RoomDetail() {
|
||||
toast.success(t('room.linkCopied'));
|
||||
};
|
||||
|
||||
// Share functions
|
||||
const searchUsers = async (query) => {
|
||||
setShareSearch(query);
|
||||
if (query.length < 2) {
|
||||
setShareResults([]);
|
||||
return;
|
||||
}
|
||||
setShareSearching(true);
|
||||
try {
|
||||
const res = await api.get(`/rooms/users/search?q=${encodeURIComponent(query)}`);
|
||||
// Filter out already shared users
|
||||
const sharedIds = new Set(sharedUsers.map(u => u.id));
|
||||
setShareResults(res.data.users.filter(u => !sharedIds.has(u.id)));
|
||||
} catch {
|
||||
setShareResults([]);
|
||||
} finally {
|
||||
setShareSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async (userId) => {
|
||||
try {
|
||||
const res = await api.post(`/rooms/${uid}/shares`, { user_id: userId });
|
||||
setSharedUsers(res.data.shares);
|
||||
setShareSearch('');
|
||||
setShareResults([]);
|
||||
toast.success(t('room.shareAdded'));
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error || t('room.shareFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnshare = async (userId) => {
|
||||
try {
|
||||
const res = await api.delete(`/rooms/${uid}/shares/${userId}`);
|
||||
setSharedUsers(res.data.shares);
|
||||
toast.success(t('room.shareRemoved'));
|
||||
} catch {
|
||||
toast.error(t('room.shareFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
@@ -203,7 +252,7 @@ export default function RoomDetail() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isOwner && !status.running && (
|
||||
{canManage && !status.running && (
|
||||
<button onClick={handleStart} disabled={actionLoading === 'start'} className="btn-primary">
|
||||
{actionLoading === 'start' ? <Loader2 size={16} className="animate-spin" /> : <Play size={16} />}
|
||||
{t('room.start')}
|
||||
@@ -215,7 +264,7 @@ export default function RoomDetail() {
|
||||
{t('room.join')}
|
||||
</button>
|
||||
)}
|
||||
{isOwner && status.running && (
|
||||
{canManage && status.running && (
|
||||
<button onClick={handleEnd} disabled={actionLoading === 'end'} className="btn-danger">
|
||||
{actionLoading === 'end' ? <Loader2 size={16} className="animate-spin" /> : <Square size={16} />}
|
||||
{t('room.end')}
|
||||
@@ -448,6 +497,90 @@ export default function RoomDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Share section */}
|
||||
<div className="pt-4 border-t border-th-border space-y-4">
|
||||
<h3 className="text-sm font-semibold text-th-text flex items-center gap-2">
|
||||
<Share2 size={16} />
|
||||
{t('room.shareTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-th-text-s">{t('room.shareDescription')}</p>
|
||||
|
||||
{/* User search */}
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<UserPlus size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={shareSearch}
|
||||
onChange={e => searchUsers(e.target.value)}
|
||||
className="input-field pl-11"
|
||||
placeholder={t('room.shareSearchPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{shareResults.length > 0 && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-th-card border border-th-border rounded-lg shadow-lg max-h-48 overflow-y-auto">
|
||||
{shareResults.map(u => (
|
||||
<button
|
||||
key={u.id}
|
||||
type="button"
|
||||
onClick={() => handleShare(u.id)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-th-hover transition-colors text-left"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{u.avatar_image ? (
|
||||
<img src={`${api.defaults.baseURL}/auth/avatar/${u.avatar_image}`} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.name}</div>
|
||||
<div className="text-xs text-th-text-s truncate">{u.email}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Shared users list */}
|
||||
{sharedUsers.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{sharedUsers.map(u => (
|
||||
<div key={u.id} className="flex items-center justify-between gap-3 p-3 bg-th-bg-s rounded-lg border border-th-border">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{u.avatar_image ? (
|
||||
<img src={`${api.defaults.baseURL}/auth/avatar/${u.avatar_image}`} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.name}</div>
|
||||
<div className="text-xs text-th-text-s truncate">{u.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUnshare(u.id)}
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s hover:text-th-error transition-colors flex-shrink-0"
|
||||
title={t('room.shareRemove')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-th-border">
|
||||
<button type="submit" disabled={saving} className="btn-primary">
|
||||
{saving ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
||||
|
||||
Reference in New Issue
Block a user