mirror of
https://github.com/arkorty/Osborne.git
synced 2026-03-18 00:57:14 +00:00
init
This commit is contained in:
311
client/components/MediaPanel.tsx
Normal file
311
client/components/MediaPanel.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Upload,
|
||||
File,
|
||||
ImageIcon,
|
||||
Video,
|
||||
Music,
|
||||
FileText,
|
||||
Download,
|
||||
Play,
|
||||
Pause,
|
||||
Trash2,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
interface MediaFile {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
url: string;
|
||||
uploadedAt: Date;
|
||||
uploadedBy: string;
|
||||
}
|
||||
|
||||
interface MediaPanelProps {
|
||||
isVisible: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MediaPanel: React.FC<MediaPanelProps> = ({
|
||||
isVisible,
|
||||
className = ''
|
||||
}) => {
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([
|
||||
// Mock data for demonstration
|
||||
{
|
||||
id: '1',
|
||||
name: 'demo-video.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 12456789,
|
||||
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||
uploadedAt: new Date(Date.now() - 300000),
|
||||
uploadedBy: 'Alice'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'background-music.mp3',
|
||||
type: 'audio/mpeg',
|
||||
size: 3456789,
|
||||
url: 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav',
|
||||
uploadedAt: new Date(Date.now() - 600000),
|
||||
uploadedBy: 'Bob'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'screenshot.png',
|
||||
type: 'image/png',
|
||||
size: 234567,
|
||||
url: 'https://picsum.photos/800/600',
|
||||
uploadedAt: new Date(Date.now() - 900000),
|
||||
uploadedBy: 'You'
|
||||
}
|
||||
]);
|
||||
|
||||
const [playingAudio, setPlayingAudio] = useState<string | null>(null);
|
||||
const [playingVideo, setPlayingVideo] = useState<string | null>(null);
|
||||
const audioRefs = useRef<{ [key: string]: HTMLAudioElement }>({});
|
||||
const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({});
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const timeDiff = Date.now() - date.getTime();
|
||||
if (timeDiff < 60000) {
|
||||
return 'Just now';
|
||||
} else if (timeDiff < 3600000) {
|
||||
const minutes = Math.floor(timeDiff / 60000);
|
||||
return `${minutes}m ago`;
|
||||
} else {
|
||||
const hours = Math.floor(timeDiff / 3600000);
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
};
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
if (type.startsWith('image/')) return <ImageIcon size={16} className="text-blue-500" />;
|
||||
if (type.startsWith('video/')) return <Video size={16} className="text-purple-500" />;
|
||||
if (type.startsWith('audio/')) return <Music size={16} className="text-green-500" />;
|
||||
if (type.includes('text') || type.includes('json') || type.includes('xml'))
|
||||
return <FileText size={16} className="text-yellow-500" />;
|
||||
return <File size={16} className="text-gray-500" />;
|
||||
};
|
||||
|
||||
const getFileTypeColor = (type: string) => {
|
||||
if (type.startsWith('image/')) return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
|
||||
if (type.startsWith('video/')) return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
|
||||
if (type.startsWith('audio/')) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
|
||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
|
||||
};
|
||||
|
||||
const handlePlayAudio = (fileId: string, url: string) => {
|
||||
// Stop any currently playing audio
|
||||
if (playingAudio && audioRefs.current[playingAudio]) {
|
||||
audioRefs.current[playingAudio].pause();
|
||||
}
|
||||
|
||||
if (playingAudio === fileId) {
|
||||
setPlayingAudio(null);
|
||||
} else {
|
||||
if (!audioRefs.current[fileId]) {
|
||||
audioRefs.current[fileId] = new Audio(url);
|
||||
audioRefs.current[fileId].addEventListener('ended', () => {
|
||||
setPlayingAudio(null);
|
||||
});
|
||||
}
|
||||
audioRefs.current[fileId].play();
|
||||
setPlayingAudio(fileId);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlayVideo = (fileId: string) => {
|
||||
if (playingVideo === fileId) {
|
||||
setPlayingVideo(null);
|
||||
} else {
|
||||
setPlayingVideo(fileId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = (fileId: string) => {
|
||||
setMediaFiles(prev => prev.filter(file => file.id !== fileId));
|
||||
|
||||
// Clean up audio/video refs
|
||||
if (audioRefs.current[fileId]) {
|
||||
audioRefs.current[fileId].pause();
|
||||
delete audioRefs.current[fileId];
|
||||
}
|
||||
if (playingAudio === fileId) setPlayingAudio(null);
|
||||
if (playingVideo === fileId) setPlayingVideo(null);
|
||||
};
|
||||
|
||||
const handleDownload = (file: MediaFile) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = file.url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-64 bg-card border border-border rounded-md shadow-lg flex flex-col ${className}`}>
|
||||
{/* Header */}
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Media Files</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
{/* Files List */}
|
||||
<CardContent className="p-0 flex-1">
|
||||
<div className="max-h-80 overflow-y-auto p-4 space-y-3">
|
||||
{mediaFiles.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Upload size={24} className="mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No files uploaded</p>
|
||||
<p className="text-xs">Use upload button in toolbar</p>
|
||||
</div>
|
||||
) : (
|
||||
mediaFiles.map((file) => (
|
||||
<Card key={file.id} className="bg-background border-border">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
{getFileIcon(file.type)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatFileSize(file.size)} • {formatTimeAgo(file.uploadedAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${getFileTypeColor(file.type)}`}
|
||||
>
|
||||
{file.type.split('/')[1] || 'file'}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
by {file.uploadedBy}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Media preview/player */}
|
||||
{file.type.startsWith('image/') && (
|
||||
<div className="mt-2">
|
||||
<div
|
||||
className="w-full h-24 bg-cover bg-center rounded border"
|
||||
style={{ backgroundImage: `url(${file.url})` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{file.type.startsWith('video/') && (
|
||||
<div className="mt-2">
|
||||
{playingVideo === file.id ? (
|
||||
<div className="relative">
|
||||
<video
|
||||
ref={(el) => {
|
||||
if (el) videoRefs.current[file.id] = el;
|
||||
}}
|
||||
src={file.url}
|
||||
controls
|
||||
className="w-full h-24 rounded border"
|
||||
onEnded={() => setPlayingVideo(null)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-1 right-1 h-6 w-6 p-0 bg-black/50 hover:bg-black/70"
|
||||
onClick={() => setPlayingVideo(null)}
|
||||
>
|
||||
<X size={12} className="text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-24 bg-muted rounded border flex items-center justify-center cursor-pointer hover:bg-muted/80"
|
||||
onClick={() => handlePlayVideo(file.id)}
|
||||
>
|
||||
<Play size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{file.type.startsWith('audio/') && (
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePlayAudio(file.id, file.url)}
|
||||
>
|
||||
{playingAudio === file.id ? (
|
||||
<Pause size={12} />
|
||||
) : (
|
||||
<Play size={12} />
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex-1 h-2 bg-muted rounded-full">
|
||||
<div className="h-2 bg-primary rounded-full w-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex justify-end mt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => handleDownload(file)}
|
||||
>
|
||||
<Download size={12} className="mr-1" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Footer with file count */}
|
||||
<div className="p-3 border-t border-border bg-muted/50 rounded-b-md">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{mediaFiles.length} files</span>
|
||||
<span>
|
||||
{formatFileSize(mediaFiles.reduce((total, file) => total + file.size, 0))} total
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user