Files
MediaCollectorLibaryFrontend/src/pages/Adult copy.tsx
Lars Behrends 4853b860fc first commit
2026-01-21 21:40:09 +01:00

435 lines
16 KiB
TypeScript

import { useState, useContext } from 'react'
import { motion } from 'framer-motion'
import { useNavigate } from 'react-router-dom'
import {
Play,
Plus,
Eye,
Lock,
AlertTriangle,
Star,
Heart,
Circle,
CheckCircle2
} from 'lucide-react'
import { useAdults } from '../hooks/useApi'
import { Tooltip } from '../components/MicroInteractions'
import { ViewContext } from '../components/Layout'
// Import from alternative frontend
import { MediaListView } from '../../../frontend/components/MediaListView'
interface PaginatedResponse<T> {
items: T[]
pagination: {
total: number
per_page: number
current_page: number
last_page: number
}
available_filters?: {
sources?: string[]
genres?: string[]
years?: string[]
}
}
interface AdultContent {
id: number
title: string
overview?: string
poster_url?: string
poster_aspect_ratio?: number
backdrop_url?: string
screenshot_url?: string
release_date?: string
rating?: number
runtime_minutes?: number
watched?: boolean
source_name?: string
genre?: string
studio?: string
cast?: string[]
director?: string
year?: number
}
export default function Adult() {
// Get view context from Layout
const viewContext = useContext(ViewContext)
// Extract values from context or use defaults
const viewMode = viewContext?.viewMode || 'grid'
const gridColumns = viewContext?.gridColumns || 5
const coverSize = viewContext?.coverSize || 200
const PaginationComp = viewContext?.PaginationComponent
const FiltersComp = viewContext?.FiltersComponent
// Load preferences from localStorage
const getStoredPreferences = () => {
const stored = localStorage.getItem('adultPreferences')
return stored ? JSON.parse(stored) : {}
}
const [searchTerm, setSearchTerm] = useState('')
const [selectedGenre, setSelectedGenre] = useState('')
const [selectedYear, setSelectedYear] = useState('')
const [selectedSource, setSelectedSource] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const navigate = useNavigate()
const [itemsPerPage, setItemsPerPage] = useState(() => getStoredPreferences().itemsPerPage || 20)
// Save preferences to localStorage
const savePreferences = (updates: any) => {
const preferences = {
itemsPerPage,
...updates
}
localStorage.setItem('adultPreferences', JSON.stringify(preferences))
}
// Fetch adult content
const { data: adultData, isLoading, error } = useAdults({
search: searchTerm || undefined,
genre: selectedGenre || undefined,
year: selectedYear ? parseInt(selectedYear) : undefined,
source: selectedSource || undefined,
page: currentPage,
per_page: itemsPerPage
}) as { data?: PaginatedResponse<AdultContent>; isLoading: boolean; error: any }
const adultContent = adultData?.items || []
const pagination = adultData?.pagination
const availableFilters = adultData?.available_filters || {}
const availableSources = availableFilters.sources || ['XBVR', 'Stash', 'Other']
// Convert adult content to MediaItem format for MediaListView
const mediaItems = adultContent.map(content => ({
id: content.id.toString(),
title: content.title || 'Untitled Adult Content',
type: 'adult' as const,
coverUrl: content.poster_url || content.screenshot_url || '',
rating: Number(content.rating) || 0,
status: 'completed' as const,
releaseYear: content.year || (content.release_date ? new Date(content.release_date).getFullYear() : new Date().getFullYear()),
addedAt: new Date().toISOString(),
favorite: false,
platform: content.source_name || 'Unknown',
description: content.overview || '',
genres: content.genre ? [content.genre] : []
}))
// State for MediaListView
const [selectedId, setSelectedId] = useState<string | null>(null)
const [multiSelectedIds, setMultiSelectedIds] = useState<Set<string>>(new Set())
const handleSelect = (item: any) => {
setSelectedId(item.id)
}
const handleToggleSelect = (id: string) => {
const newSelected = new Set(multiSelectedIds)
if (newSelected.has(id)) {
newSelected.delete(id)
} else {
newSelected.add(id)
}
setMultiSelectedIds(newSelected)
}
// Pagination handlers
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
const handleItemsPerPageChange = (newItemsPerPage: number) => {
setItemsPerPage(newItemsPerPage)
setCurrentPage(1)
savePreferences({ itemsPerPage: newItemsPerPage })
}
const handleContentClick = (content: AdultContent) => {
navigate(`/adult/${content.id}`)
}
// Filter handlers
const handleFiltersChange = (filters: any) => {
setSelectedSource(filters.source || '')
setSelectedGenre(filters.genre || '')
setSelectedYear(filters.year || '')
setCurrentPage(1)
}
const handleSearch = (term: string) => {
setSearchTerm(term)
setCurrentPage(1)
}
// Mock genres for adult content
const genres = [
'All', 'Action', 'Adventure', 'Comedy', 'Drama', 'Fantasy',
'Horror', 'Mystery', 'Romance', 'Sci-Fi', 'Thriller', 'Other'
]
// Mock years
const currentYear = new Date().getFullYear()
const years = ['All', ...Array.from({ length: 10 }, (_, i) => (currentYear - i).toString())]
// Prepare filter data for FiltersComponent
const filterState = {
search: searchTerm,
source: selectedSource,
genre: selectedGenre,
year: selectedYear
}
const filterOptions = {
sources: availableSources || [],
genres: availableFilters.genres || genres.slice(1),
years: availableFilters.years || years.slice(1)
}
// Helper function to get grid classes based on column count
const getGridClass = (columns: number) => {
const columnClasses = {
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
5: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5',
6: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-6',
7: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-7',
8: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-8'
}
return columnClasses[columns as keyof typeof columnClasses] || columnClasses[5]
}
const formatYear = (date?: string) => {
if (!date) return 'Unknown'
return new Date(date).getFullYear()
}
const renderRating = (rating?: number | string) => {
const numericRating = typeof rating === 'string' ? parseFloat(rating) : rating
if (!numericRating || typeof numericRating !== 'number' || isNaN(numericRating)) return null
return (
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${
i < Math.floor(numericRating) ? 'text-yellow-400 dark:text-yellow-500 fill-yellow-400' : 'text-gray-300 dark:text-gray-600'
}`}
/>
))}
<span className="text-sm text-gray-600 dark:text-gray-400 ml-1">{numericRating.toFixed(1)}</span>
</div>
)
}
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="animate-pulse">
<div className="h-48 bg-gray-200 dark:bg-gray-800"></div>
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{[...Array(8)].map((_, i) => (
<div key={i} className="bg-gray-200 dark:bg-gray-800 rounded-xl h-64"></div>
))}
</div>
</div>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="text-center">
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">Content Error</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">Failed to load adult content. Please try again later.</p>
<button className="btn btn-primary" onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen transition-colors duration-300">
{/* Filters Component */}
{FiltersComp && (
<FiltersComp
filters={filterState}
availableFilters={filterOptions}
onFiltersChange={handleFiltersChange}
onSearchChange={handleSearch}
className="mb-6"
/>
)}
{/* Content Grid/List/Cover */}
{adultContent.length === 0 ? (
<div className="text-center py-12">
<Lock className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">No Content Found</h3>
<p className="text-gray-600 dark:text-gray-400">
Try adjusting your search or filters to find what you're looking for.
</p>
</div>
) : viewMode === 'list' ? (
// Use MediaListView for list mode
<div className="h-[600px] border border-slate-200 dark:border-slate-700 rounded-lg overflow-hidden">
<MediaListView
items={mediaItems}
onSelect={handleSelect}
selectedId={selectedId}
onToggleSelect={handleToggleSelect}
multiSelectedIds={multiSelectedIds}
/>
</div>
) : viewMode === 'cover' ? (
// Cover View (Shelf-style) - use dynamic cover size
<div className="flex flex-wrap gap-4 justify-center">
{adultContent.map((content: AdultContent, index: number) => (
<div className="relative group" key={content.id}>
{(() => {
const aspectRatio = content.poster_aspect_ratio || 1.5;
const height = coverSize / aspectRatio;
return (
<div
className="relative overflow-hidden rounded-t-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1"
style={{
width: `${coverSize}px`,
height: `${height}px`
}}
>
{content.poster_url ? (
<img
src={content.poster_url.startsWith('http') ? content.poster_url : `${(import.meta as any).env?.VITE_API_URL?.replace('/api', '') || ''}${content.poster_url}`}
alt={content.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center">
<Lock className="w-8 h-8 text-white/50" />
</div>
)}
{/* Shelf effect - bottom shadow */}
<div className="absolute bottom-0 left-0 right-0 h-2 bg-gradient-to-t from-black/30 to-transparent"></div>
{/* Hover overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute top-2 right-2">
<motion.button
className="p-1 bg-purple-600 rounded-full hover:bg-purple-700 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Play className="w-3 h-3 text-white" />
</motion.button>
</div>
{content.watched && (
<div className="absolute top-2 left-2">
<Eye className="w-4 h-4 text-green-400" />
</div>
)}
</div>
</div>
);
})()}
</div>
))}
</div>
) : (
<motion.div
layout
className={`grid gap-6 ${getGridClass(gridColumns)}`}
>
{adultContent.map((content: AdultContent, index: number) => (
<motion.div
key={content.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
onClick={() => handleContentClick(content)}
className="group cursor-pointer"
>
{/* Grid View */}
<div className="relative overflow-hidden rounded-xl bg-gray-200 dark:bg-gray-800">
{content.poster_url || content.screenshot_url ? (
<img
src={
(content.poster_url || content.screenshot_url)?.startsWith('http')
? (content.poster_url || content.screenshot_url)
: `${(import.meta as any).env?.VITE_API_URL?.replace('/api', '') || ''}${content.poster_url || content.screenshot_url}`
}
alt={content.title}
className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="w-full h-64 bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center">
<Lock className="w-16 h-16 text-white/50" />
</div>
)}
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="flex items-center gap-2 mb-2">
<motion.button
className="p-2 bg-purple-600 rounded-full hover:bg-purple-700 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Play className="w-4 h-4 text-white" />
</motion.button>
<motion.button
className="p-2 bg-white/20 backdrop-blur-sm rounded-full hover:bg-white/30 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Plus className="w-4 h-4 text-white" />
</motion.button>
{content.watched && (
<Tooltip text="Watched">
<Eye className="w-5 h-5 text-green-400" />
</Tooltip>
)}
</div>
<h3 className="text-white font-semibold text-sm line-clamp-2 mb-1">{content.title}</h3>
<div className="flex items-center gap-2 flex-wrap">
{content.source_name && (
<div className="px-2 py-1 bg-blue-500/80 text-white rounded text-xs font-medium">
{content.source_name}
</div>
)}
{content.rating && renderRating(content.rating)}
</div>
</div>
</div>
</div>
</motion.div>
))}
</motion.div>
)}
{/* Pagination Component */}
{PaginationComp && pagination && pagination.last_page > 1 && (
<PaginationComp
currentPage={currentPage}
lastPage={pagination.last_page}
total={pagination.total}
onPageChange={handlePageChange}
itemsPerPage={itemsPerPage}
onItemsPerPageChange={handleItemsPerPageChange}
/>
)}
</div>
)
}