'use client' import { useState, useEffect } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' import { Checkbox } from '@/components/ui/checkbox' import { useToast } from '@/hooks/use-toast' import { useFileUpload } from '@/hooks/use-file-upload' import { LoadingSpinner } from './loading-states' import { ErrorBoundary } from './error-boundary' import { Upload, Search, Filter, Grid, List, Eye, Edit, Trash2, Download, Star, Calendar, User, Tag, BarChart3, Image as ImageIcon, Plus, X } from 'lucide-react' import Image from 'next/image' import { PortfolioImage, Artist } from '@/types/database' interface PortfolioStats { totalImages: number totalViews: number totalLikes: number averageRating: number storageUsed: string recentUploads: number } export function PortfolioManager() { const [portfolioImages, setPortfolioImages] = useState([]) const [artists, setArtists] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [searchTerm, setSearchTerm] = useState('') const [selectedArtist, setSelectedArtist] = useState('all') const [selectedCategory, setSelectedCategory] = useState('all') const [selectedImages, setSelectedImages] = useState>(new Set()) const [showUploadDialog, setShowUploadDialog] = useState(false) const { toast } = useToast() const { uploadFiles, isUploading, progress } = useFileUpload({ maxFiles: 20, maxSize: 5 * 1024 * 1024, // 5MB allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], }) useEffect(() => { loadPortfolioData() loadArtists() loadStats() }, []) const loadPortfolioData = async () => { try { const response = await fetch('/api/portfolio') if (!response.ok) throw new Error('Failed to load portfolio') const data = await response.json() setPortfolioImages(data) } catch (error) { toast({ title: 'Error', description: 'Failed to load portfolio images', variant: 'destructive', }) } } const loadArtists = async () => { try { const response = await fetch('/api/artists') if (!response.ok) throw new Error('Failed to load artists') const data = await response.json() setArtists(data) } catch (error) { console.error('Failed to load artists:', error) } } const loadStats = async () => { try { const response = await fetch('/api/portfolio/stats') if (!response.ok) throw new Error('Failed to load stats') const data = await response.json() setStats(data) } catch (error) { console.error('Failed to load stats:', error) } finally { setLoading(false) } } const handleFileUpload = async (files: FileList) => { try { const fileArray = Array.from(files) await uploadFiles(fileArray) await loadPortfolioData() await loadStats() setShowUploadDialog(false) toast({ title: 'Success', description: `Uploaded ${fileArray.length} images successfully`, }) } catch (error) { toast({ title: 'Error', description: 'Failed to upload images', variant: 'destructive', }) } } const handleDeleteImage = async (imageId: string) => { try { const response = await fetch(`/api/portfolio/${imageId}`, { method: 'DELETE', }) if (!response.ok) throw new Error('Failed to delete image') await loadPortfolioData() await loadStats() toast({ title: 'Success', description: 'Image deleted successfully', }) } catch (error) { toast({ title: 'Error', description: 'Failed to delete image', variant: 'destructive', }) } } const handleBulkDelete = async () => { try { const response = await fetch('/api/portfolio/bulk-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageIds: Array.from(selectedImages) }), }) if (!response.ok) throw new Error('Failed to delete images') await loadPortfolioData() await loadStats() setSelectedImages(new Set()) toast({ title: 'Success', description: `Deleted ${selectedImages.size} images successfully`, }) } catch (error) { toast({ title: 'Error', description: 'Failed to delete images', variant: 'destructive', }) } } const toggleImageSelection = (imageId: string) => { const newSelection = new Set(selectedImages) if (newSelection.has(imageId)) { newSelection.delete(imageId) } else { newSelection.add(imageId) } setSelectedImages(newSelection) } const selectAllImages = () => { setSelectedImages(new Set(filteredImages.map(img => img.id))) } const clearSelection = () => { setSelectedImages(new Set()) } const filteredImages = portfolioImages.filter(image => { const matchesSearch = image.caption?.toLowerCase().includes(searchTerm.toLowerCase()) || image.tags?.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())) const matchesArtist = selectedArtist === 'all' || image.artistId === selectedArtist return matchesSearch && matchesArtist }) const categories = ['Traditional', 'Realism', 'Blackwork', 'Watercolor', 'Geometric', 'Japanese'] if (loading) { return } return (
{/* Stats Cards */} {stats && (
Total Images
{stats.totalImages}

+{stats.recentUploads} this week

Total Views
{stats.totalViews.toLocaleString()}

Portfolio engagement

Average Rating
{stats.averageRating.toFixed(1)}

Out of 5.0 stars

Storage Used
{stats.storageUsed}

R2 storage usage

)} {/* Controls */} Portfolio Management Manage your portfolio images, organize galleries, and track performance. {/* Search and Filters */}
setSearchTerm(e.target.value)} className="max-w-sm" />
{/* Action Bar */}
Upload Portfolio Images Select multiple images to upload to the portfolio.
e.target.files && handleFileUpload(e.target.files)} disabled={isUploading} />
{isUploading && (
Uploading... {progress.length > 0 ? Math.round(progress[0].progress || 0) : 0}%
0 ? progress[0].progress || 0 : 0}%` }} />
)}
{selectedImages.size > 0 && ( <> Delete Images Are you sure you want to delete {selectedImages.size} selected images? This action cannot be undone. Cancel Delete )}
{/* Portfolio Grid/List */}

Portfolio Images ({filteredImages.length})

{viewMode === 'grid' ? (
{filteredImages.map((image) => (
{image.caption
toggleImageSelection(image.id)} className="bg-background" />
Delete Image Are you sure you want to delete this image? This action cannot be undone. Cancel handleDeleteImage(image.id)}> Delete

{image.caption || 'Untitled'}

{artists.find(a => a.id === image.artistId)?.name || 'Unknown'} {new Date(image.createdAt).toLocaleDateString()}
{image.tags && image.tags.length > 0 && (
{image.tags.slice(0, 3).map((tag, index) => ( {tag} ))} {image.tags.length > 3 && ( +{image.tags.length - 3} )}
)}
))}
) : (
{filteredImages.map((image) => (
toggleImageSelection(image.id)} />
{image.caption

{image.caption || 'Untitled'}

{artists.find(a => a.id === image.artistId)?.name || 'Unknown Artist'}

Portfolio {new Date(image.createdAt).toLocaleDateString()}
Delete Image Are you sure you want to delete this image? This action cannot be undone. Cancel handleDeleteImage(image.id)}> Delete
))}
)} {filteredImages.length === 0 && (

No images found

{searchTerm || selectedArtist !== 'all' || selectedCategory !== 'all' ? 'Try adjusting your search or filters' : 'Upload your first portfolio images to get started'}

{!searchTerm && selectedArtist === 'all' && selectedCategory === 'all' && ( )}
)}
) }