forked from 77media/video-flow
391 lines
14 KiB
TypeScript
391 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Plus,
|
|
Upload,
|
|
Search,
|
|
FolderPlus,
|
|
MoreHorizontal,
|
|
Play,
|
|
Download,
|
|
Trash2,
|
|
Eye,
|
|
Grid3X3,
|
|
List,
|
|
} from 'lucide-react';
|
|
|
|
const mockAlbums = [
|
|
{
|
|
id: 1,
|
|
name: 'Tech Videos',
|
|
itemCount: 12,
|
|
thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
createdAt: '2024-01-15',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Marketing Assets',
|
|
itemCount: 8,
|
|
thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
createdAt: '2024-01-10',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Stock Footage',
|
|
itemCount: 24,
|
|
thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
createdAt: '2024-01-05',
|
|
},
|
|
];
|
|
|
|
const mockMediaFiles = [
|
|
{
|
|
id: 1,
|
|
name: 'intro-video.mp4',
|
|
type: 'video',
|
|
size: '45.2 MB',
|
|
duration: '2:30',
|
|
thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
albumId: 1,
|
|
uploadedAt: '2024-01-15',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'product-demo.mp4',
|
|
type: 'video',
|
|
size: '78.9 MB',
|
|
duration: '4:15',
|
|
thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
albumId: 1,
|
|
uploadedAt: '2024-01-14',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'background-music.mp3',
|
|
type: 'audio',
|
|
size: '8.3 MB',
|
|
duration: '3:45',
|
|
thumbnail: null,
|
|
albumId: 2,
|
|
uploadedAt: '2024-01-12',
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'hero-image.jpg',
|
|
type: 'image',
|
|
size: '2.1 MB',
|
|
duration: null,
|
|
thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
albumId: 2,
|
|
uploadedAt: '2024-01-10',
|
|
},
|
|
];
|
|
|
|
export function MediaLibraryPage() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
const [selectedAlbum, setSelectedAlbum] = useState<number | null>(null);
|
|
|
|
const filteredFiles = mockMediaFiles.filter(file =>
|
|
file.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
|
(selectedAlbum ? file.albumId === selectedAlbum : true)
|
|
);
|
|
|
|
const getFileIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'video':
|
|
return '🎥';
|
|
case 'audio':
|
|
return '🎵';
|
|
case 'image':
|
|
return '🖼️';
|
|
default:
|
|
return '📄';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Media Library</h1>
|
|
<p className="text-muted-foreground">
|
|
Organize and manage your video, audio, and image assets
|
|
</p>
|
|
</div>
|
|
<div className="flex space-x-3">
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline">
|
|
<FolderPlus className="mr-2 h-4 w-4" />
|
|
New Album
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Album</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<Input placeholder="Album name" />
|
|
<div className="flex justify-end space-x-2">
|
|
<Button variant="outline">Cancel</Button>
|
|
<Button>Create Album</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<Button>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
Upload Files
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Search files..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 w-64"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant={selectedAlbum ? 'secondary' : 'ghost'}
|
|
onClick={() => setSelectedAlbum(null)}
|
|
>
|
|
All Files
|
|
</Button>
|
|
{mockAlbums.map((album) => (
|
|
<Button
|
|
key={album.id}
|
|
variant={selectedAlbum === album.id ? 'secondary' : 'ghost'}
|
|
onClick={() => setSelectedAlbum(album.id)}
|
|
>
|
|
{album.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setViewMode('grid')}
|
|
>
|
|
<Grid3X3 className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
onClick={() => setViewMode('list')}
|
|
>
|
|
<List className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Tabs defaultValue="files" className="space-y-6">
|
|
<TabsList>
|
|
<TabsTrigger value="files">Files</TabsTrigger>
|
|
<TabsTrigger value="albums">Albums</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="files" className="space-y-6">
|
|
{viewMode === 'grid' ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{filteredFiles.map((file) => (
|
|
<Card key={file.id} className="group hover:shadow-lg transition-all duration-200">
|
|
<CardHeader className="p-0">
|
|
<div className="relative aspect-video bg-muted rounded-t-lg overflow-hidden">
|
|
{file.thumbnail ? (
|
|
<img
|
|
src={file.thumbnail}
|
|
alt={file.name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<span className="text-4xl">{getFileIcon(file.type)}</span>
|
|
</div>
|
|
)}
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
|
|
<div className="flex space-x-2">
|
|
<Button size="sm" variant="secondary">
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
{file.type === 'video' && (
|
|
<Button size="sm" variant="secondary">
|
|
<Play className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Badge className="absolute top-2 left-2">
|
|
{file.type}
|
|
</Badge>
|
|
{file.duration && (
|
|
<Badge variant="secondary" className="absolute bottom-2 right-2">
|
|
{file.duration}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-4">
|
|
<div className="space-y-2">
|
|
<h3 className="font-medium truncate">{file.name}</h3>
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>{file.size}</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Download
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<div className="space-y-1">
|
|
{filteredFiles.map((file) => (
|
|
<div
|
|
key={file.id}
|
|
className="flex items-center justify-between p-4 hover:bg-muted/50 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
{file.thumbnail ? (
|
|
<img
|
|
src={file.thumbnail}
|
|
alt={file.name}
|
|
className="w-10 h-10 object-cover rounded"
|
|
/>
|
|
) : (
|
|
<div className="w-10 h-10 bg-muted rounded flex items-center justify-center">
|
|
<span className="text-lg">{getFileIcon(file.type)}</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<p className="font-medium">{file.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{file.size} • {file.type}
|
|
{file.duration && ` • ${file.duration}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-muted-foreground">
|
|
{file.uploadedAt}
|
|
</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Download
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="albums" className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{mockAlbums.map((album) => (
|
|
<Card key={album.id} className="hover:shadow-lg transition-all duration-200 cursor-pointer">
|
|
<CardHeader className="p-0">
|
|
<div className="aspect-video bg-muted rounded-t-lg overflow-hidden">
|
|
<img
|
|
src={album.thumbnail}
|
|
alt={album.name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-semibold">{album.name}</h3>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>{album.itemCount} items</span>
|
|
<span>Created {new Date(album.createdAt).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
} |