160 lines
5.8 KiB
JavaScript
160 lines
5.8 KiB
JavaScript
import { Play, Trash2, Eye, EyeOff, Download, Clock, Users, FileVideo } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import api from '../services/api';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function RecordingList({ recordings, onRefresh }) {
|
|
const [loading, setLoading] = useState({});
|
|
const { t, language } = useLanguage();
|
|
|
|
const formatDuration = (startTime, endTime) => {
|
|
if (!startTime || !endTime) return '—';
|
|
const ms = parseInt(endTime) - parseInt(startTime);
|
|
const minutes = Math.floor(ms / 60000);
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours > 0) return `${hours}h ${mins}m`;
|
|
return `${mins}m`;
|
|
};
|
|
|
|
const formatDate = (timestamp) => {
|
|
if (!timestamp) return '—';
|
|
return new Date(parseInt(timestamp)).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const formatSize = (bytes) => {
|
|
if (!bytes) return '';
|
|
const mb = parseInt(bytes) / (1024 * 1024);
|
|
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`;
|
|
return `${mb.toFixed(1)} MB`;
|
|
};
|
|
|
|
const handleDelete = async (recordID) => {
|
|
if (!confirm(t('recordings.deleteConfirm'))) return;
|
|
setLoading(prev => ({ ...prev, [recordID]: 'deleting' }));
|
|
try {
|
|
await api.delete(`/recordings/${recordID}`);
|
|
toast.success(t('recordings.deleted'));
|
|
onRefresh?.();
|
|
} catch (err) {
|
|
toast.error(t('recordings.deleteFailed'));
|
|
} finally {
|
|
setLoading(prev => ({ ...prev, [recordID]: null }));
|
|
}
|
|
};
|
|
|
|
const handlePublish = async (recordID, publish) => {
|
|
setLoading(prev => ({ ...prev, [recordID]: 'publishing' }));
|
|
try {
|
|
await api.put(`/recordings/${recordID}/publish`, { publish });
|
|
toast.success(publish ? t('recordings.publishSuccess') : t('recordings.unpublishSuccess'));
|
|
onRefresh?.();
|
|
} catch (err) {
|
|
toast.error(t('recordings.publishFailed'));
|
|
} finally {
|
|
setLoading(prev => ({ ...prev, [recordID]: null }));
|
|
}
|
|
};
|
|
|
|
if (!recordings || recordings.length === 0) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<FileVideo size={48} className="mx-auto text-th-text-s/40 mb-3" />
|
|
<p className="text-th-text-s text-sm">{t('recordings.noRecordings')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{recordings.map(rec => (
|
|
<div key={rec.recordID} className="card p-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className="text-sm font-medium text-th-text truncate">
|
|
{rec.name}
|
|
</h4>
|
|
<span
|
|
className={`px-2 py-0.5 rounded-full text-[10px] font-medium ${
|
|
rec.published
|
|
? 'bg-th-success/15 text-th-success'
|
|
: 'bg-th-warning/15 text-th-warning'
|
|
}`}
|
|
>
|
|
{rec.published ? t('recordings.published') : t('recordings.unpublished')}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 text-xs text-th-text-s">
|
|
<span className="flex items-center gap-1">
|
|
<Clock size={12} />
|
|
{formatDate(rec.startTime)}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Play size={12} />
|
|
{formatDuration(rec.startTime, rec.endTime)}
|
|
</span>
|
|
{rec.participants && (
|
|
<span className="flex items-center gap-1">
|
|
<Users size={12} />
|
|
{rec.participants}
|
|
</span>
|
|
)}
|
|
{rec.size && (
|
|
<span>{formatSize(rec.size)}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Playback formats */}
|
|
{rec.formats && rec.formats.length > 0 && (
|
|
<div className="flex items-center gap-2 mt-2">
|
|
{rec.formats.map((format, idx) => (
|
|
<a
|
|
key={idx}
|
|
href={format.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-th-accent/10 text-th-accent text-xs font-medium hover:bg-th-accent/20 transition-colors"
|
|
>
|
|
<Play size={12} />
|
|
{format.type === 'presentation' ? t('recordings.presentation') : format.type}
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-1 flex-shrink-0">
|
|
<button
|
|
onClick={() => handlePublish(rec.recordID, !rec.published)}
|
|
disabled={loading[rec.recordID] === 'publishing'}
|
|
className="btn-ghost text-xs py-1.5 px-2"
|
|
title={rec.published ? t('recordings.hide') : t('recordings.publish')}
|
|
>
|
|
{rec.published ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(rec.recordID)}
|
|
disabled={loading[rec.recordID] === 'deleting'}
|
|
className="btn-ghost text-xs py-1.5 px-2 text-th-error"
|
|
title={t('common.delete')}
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|