All checks were successful
Build & Push Docker Image / build (push) Successful in 6m18s
281 lines
10 KiB
JavaScript
281 lines
10 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { Plus, Video, Loader2, LayoutGrid, List } from 'lucide-react';
|
|
import api from '../services/api';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import RoomCard from '../components/RoomCard';
|
|
import FederatedRoomCard from '../components/FederatedRoomCard';
|
|
import Modal from '../components/Modal';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function Dashboard() {
|
|
const { t } = useLanguage();
|
|
const [rooms, setRooms] = useState([]);
|
|
const [federatedRooms, setFederatedRooms] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [viewMode, setViewMode] = useState('grid');
|
|
const [creating, setCreating] = useState(false);
|
|
const [newRoom, setNewRoom] = useState({
|
|
name: '',
|
|
welcome_message: '',
|
|
max_participants: 0,
|
|
access_code: '',
|
|
mute_on_join: true,
|
|
record_meeting: true,
|
|
});
|
|
|
|
const fetchRooms = async () => {
|
|
try {
|
|
const res = await api.get('/rooms');
|
|
setRooms(res.data.rooms);
|
|
} catch (err) {
|
|
toast.error(t('dashboard.loadFailed'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchFederatedRooms = async () => {
|
|
try {
|
|
const res = await api.get('/federation/federated-rooms');
|
|
setFederatedRooms(res.data.rooms || []);
|
|
} catch {
|
|
// Federation may not be enabled
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchRooms();
|
|
fetchFederatedRooms();
|
|
}, []);
|
|
|
|
const handleCreate = async (e) => {
|
|
e.preventDefault();
|
|
setCreating(true);
|
|
try {
|
|
await api.post('/rooms', newRoom);
|
|
toast.success(t('dashboard.roomCreated'));
|
|
setShowCreate(false);
|
|
setNewRoom({
|
|
name: '',
|
|
welcome_message: t('dashboard.welcomeMessageDefault'),
|
|
max_participants: 0,
|
|
access_code: '',
|
|
mute_on_join: true,
|
|
record_meeting: true,
|
|
});
|
|
fetchRooms();
|
|
} catch (err) {
|
|
toast.error(err.response?.data?.error || t('dashboard.roomCreateFailed'));
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (room) => {
|
|
if (!confirm(t('dashboard.roomDeleteConfirm', { name: room.name }))) return;
|
|
try {
|
|
await api.delete(`/rooms/${room.uid}`);
|
|
toast.success(t('dashboard.roomDeleted'));
|
|
fetchRooms();
|
|
} catch (err) {
|
|
toast.error(t('dashboard.roomDeleteFailed'));
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 size={32} className="animate-spin text-th-accent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-th-text">{t('dashboard.myRooms')}</h1>
|
|
<p className="text-sm text-th-text-s mt-1">
|
|
{t('dashboard.roomCount', { count: rooms.length })}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{/* View toggle */}
|
|
<div className="hidden sm:flex items-center bg-th-bg-s rounded-lg border border-th-border p-0.5">
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className={`p-1.5 rounded-md transition-colors ${
|
|
viewMode === 'grid' ? 'bg-th-accent text-th-accent-t' : 'text-th-text-s hover:text-th-text'
|
|
}`}
|
|
>
|
|
<LayoutGrid size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`p-1.5 rounded-md transition-colors ${
|
|
viewMode === 'list' ? 'bg-th-accent text-th-accent-t' : 'text-th-text-s hover:text-th-text'
|
|
}`}
|
|
>
|
|
<List size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<button onClick={() => setShowCreate(true)} className="btn-primary">
|
|
<Plus size={18} />
|
|
<span className="hidden sm:inline">{t('dashboard.newRoom')}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Room grid/list */}
|
|
{rooms.length === 0 && federatedRooms.length === 0 ? (
|
|
<div className="card p-12 text-center">
|
|
<Video size={48} className="mx-auto text-th-text-s/40 mb-4" />
|
|
<h3 className="text-lg font-semibold text-th-text mb-2">{t('dashboard.noRooms')}</h3>
|
|
<p className="text-sm text-th-text-s mb-6">
|
|
{t('dashboard.noRoomsSubtitle')}
|
|
</p>
|
|
<button onClick={() => setShowCreate(true)} className="btn-primary">
|
|
<Plus size={18} />
|
|
{t('dashboard.createFirst')}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Own rooms */}
|
|
{rooms.filter(r => !r.shared).length > 0 && (
|
|
<div className={
|
|
viewMode === 'grid'
|
|
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'
|
|
: 'space-y-3'
|
|
}>
|
|
{rooms.filter(r => !r.shared).map(room => (
|
|
<RoomCard key={room.id} room={room} onDelete={handleDelete} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Shared rooms */}
|
|
{rooms.filter(r => r.shared).length > 0 && (
|
|
<div className="mt-8">
|
|
<h2 className="text-lg font-semibold text-th-text mb-4">{t('dashboard.sharedWithMe')}</h2>
|
|
<div className={
|
|
viewMode === 'grid'
|
|
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'
|
|
: 'space-y-3'
|
|
}>
|
|
{rooms.filter(r => r.shared).map(room => (
|
|
<RoomCard key={`shared-${room.id}`} room={room} onDelete={handleDelete} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Federated rooms (from other instances) */}
|
|
{federatedRooms.length > 0 && (
|
|
<div className="mt-8">
|
|
<h2 className="text-lg font-semibold text-th-text mb-1">{t('dashboard.federatedRooms')}</h2>
|
|
<p className="text-sm text-th-text-s mb-4">{t('dashboard.federatedRoomsSubtitle')}</p>
|
|
<div className={
|
|
viewMode === 'grid'
|
|
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'
|
|
: 'space-y-3'
|
|
}>
|
|
{federatedRooms.map(room => (
|
|
<FederatedRoomCard key={room.id} room={room} onRemove={fetchFederatedRooms} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Create Room Modal */}
|
|
{showCreate && (
|
|
<Modal title={t('dashboard.createRoom')} onClose={() => setShowCreate(false)}>
|
|
<form onSubmit={handleCreate} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('dashboard.roomName')} *</label>
|
|
<input
|
|
type="text"
|
|
value={newRoom.name}
|
|
onChange={e => setNewRoom({ ...newRoom, name: e.target.value })}
|
|
className="input-field"
|
|
placeholder={t('dashboard.roomNamePlaceholder')}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('dashboard.welcomeMessage')}</label>
|
|
<textarea
|
|
value={newRoom.welcome_message}
|
|
onChange={e => setNewRoom({ ...newRoom, welcome_message: e.target.value })}
|
|
className="input-field resize-none"
|
|
rows={2}
|
|
placeholder={t('dashboard.welcomeMessageDefault')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('dashboard.maxParticipants')}</label>
|
|
<input
|
|
type="number"
|
|
value={newRoom.max_participants}
|
|
onChange={e => setNewRoom({ ...newRoom, max_participants: parseInt(e.target.value) || 0 })}
|
|
className="input-field"
|
|
min="0"
|
|
placeholder={t('dashboard.maxParticipantsHint')}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('dashboard.accessCode')}</label>
|
|
<input
|
|
type="text"
|
|
value={newRoom.access_code}
|
|
onChange={e => setNewRoom({ ...newRoom, access_code: e.target.value })}
|
|
className="input-field"
|
|
placeholder={t('common.optional')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3 pt-2">
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newRoom.mute_on_join}
|
|
onChange={e => setNewRoom({ ...newRoom, mute_on_join: e.target.checked })}
|
|
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
|
/>
|
|
<span className="text-sm text-th-text">{t('dashboard.muteOnJoin')}</span>
|
|
</label>
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newRoom.record_meeting}
|
|
onChange={e => setNewRoom({ ...newRoom, record_meeting: e.target.checked })}
|
|
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
|
/>
|
|
<span className="text-sm text-th-text">{t('dashboard.allowRecording')}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 pt-4 border-t border-th-border">
|
|
<button type="button" onClick={() => setShowCreate(false)} className="btn-secondary flex-1">
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button type="submit" disabled={creating} className="btn-primary flex-1">
|
|
{creating ? <Loader2 size={18} className="animate-spin" /> : t('common.create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|