This commit is contained in:
230
src/pages/Dashboard.jsx
Normal file
230
src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,230 @@
|
||||
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 Modal from '../components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useLanguage();
|
||||
const [rooms, setRooms] = 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);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRooms();
|
||||
}, []);
|
||||
|
||||
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 ? (
|
||||
<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>
|
||||
) : (
|
||||
<div className={
|
||||
viewMode === 'grid'
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'
|
||||
: 'space-y-3'
|
||||
}>
|
||||
{rooms.map(room => (
|
||||
<RoomCard key={room.id} room={room} onDelete={handleDelete} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user