Files
redlight/src/components/RecordingList.jsx
Michelle 54d6ee553a
Some checks failed
Build & Push Docker Image / build (push) Failing after 53s
Init v1.0.0
2026-02-24 18:14:16 +01:00

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>
);
}