diff --git a/package-lock.json b/package-lock.json index efd199e..98a5464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "motion": "^12.38.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.14.0", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", @@ -5846,6 +5847,57 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -6146,6 +6198,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/package.json b/package.json index d146898..4a218a1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "motion": "^12.38.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.14.0", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", diff --git a/src/App.tsx b/src/App.tsx index 740c9ec..f543d0f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { useState, useMemo, useEffect } from 'react'; import { LayoutGroup } from 'motion/react'; +import { BrowserRouter, Routes, Route, useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom'; import Header from './components/Header'; import BrowseView from './components/BrowseView'; import DetailView from './components/DetailView'; @@ -14,19 +15,23 @@ import AddMediaView from './components/AddMediaView'; import ImporterView from './components/ImporterView'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { Media, Staff, MediaCategory } from './types'; -import { fetchAllMedia, fetchMediaById } from './api'; +import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff } from './api'; -export default function App() { - const [currentView, setCurrentView] = useState<'browse' | 'detail' | 'cast' | 'castDetail' | 'add' | 'import'>('browse'); - const [activeCategory, setActiveCategory] = useState('Anime'); +function AppContent() { + const navigate = useNavigate(); + const location = useLocation(); + const [searchParams, setSearchParams] = useSearchParams(); + const [activeCategory, setActiveCategory] = useState( + (searchParams.get('category') as MediaCategory) || 'Anime' + ); const [selectedMedia, setSelectedMedia] = useState(null); const [selectedPerson, setSelectedPerson] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || ''); const [enabledCategories, setEnabledCategories] = useState(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); const [customMedia, setCustomMedia] = useState([]); const [adultMedia, setAdultMedia] = useState([]); - // Load media from API on component mount + // Load media from API on component mount (only when not on cast routes) const [apiMedia, setApiMedia] = useState([]); useEffect(() => { @@ -38,8 +43,12 @@ export default function App() { console.error('Failed to load media from API:', error); } }; - loadMediaFromApi(); - }, []); + + // Only load media if not on cast routes + if (!location.pathname.startsWith('/cast')) { + loadMediaFromApi(); + } + }, [location.pathname]); const toggleCategory = (category: MediaCategory) => { setEnabledCategories(prev => { @@ -59,17 +68,18 @@ export default function App() { const handleCategoryChange = (category: MediaCategory) => { setActiveCategory(category); - setCurrentView('browse'); + setSearchParams({ category }); + navigate('/'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleAddMediaView = () => { - setCurrentView('add'); + navigate('/add'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleImporterView = () => { - setCurrentView('import'); + navigate('/import'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -168,17 +178,17 @@ export default function App() { // For non-adult media, use the original media setSelectedMedia(media); } - setCurrentView('detail'); + navigate(`/media/${media.id}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleBack = () => { - setCurrentView('browse'); + navigate('/'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleCastClick = () => { - setCurrentView('cast'); + navigate('/cast'); window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -192,76 +202,73 @@ export default function App() { occupations: ['Voice Actor', 'Singer', 'Narrator'] }; setSelectedPerson(enrichedPerson); - setCurrentView('castDetail'); + navigate(`/cast/${person.id}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleSearch = (query: string) => { setSearchQuery(query); - if (currentView !== 'browse' && currentView !== 'cast') { - setCurrentView('browse'); + const params = new URLSearchParams(searchParams); + if (query) { + params.set('search', query); + } else { + params.delete('search'); } + setSearchParams(params); + navigate('/'); }; return (
- {currentView === 'browse' ? ( - - ) : currentView === 'cast' ? ( - - ) : currentView === 'castDetail' ? ( - selectedPerson && ( - { - const media = allMedia.find(m => m.id === id); - if (media) handleMediaClick(media); - }} - relatedMedia={allMedia.filter(m => m.staff?.some(s => s.id === selectedPerson.id))} + + - ) - ) : currentView === 'add' ? ( - - ) : currentView === 'import' ? ( - - ) : ( - selectedMedia && ( - + - ) - )} + } /> + + } /> + + } /> + + } /> + + } /> +
@@ -285,3 +292,87 @@ export default function App() {
); } + +// Helper component for media detail route +function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonClick }: any) { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + useEffect(() => { + const loadMedia = async () => { + if (id) { + // First check if media is in allMedia + const media = allMedia.find(m => m.id === id); + if (media) { + setSelectedMedia(media); + } else { + // If not found, fetch from API + try { + const fetchedMedia = await fetchMediaById(id); + if (fetchedMedia) { + setSelectedMedia(fetchedMedia); + } else { + navigate('/'); + } + } catch (error) { + console.error('Failed to fetch media:', error); + navigate('/'); + } + } + } + }; + loadMedia(); + }, [id, allMedia]); + + if (!selectedMedia) return null; + + return ( + + ); +} + +// Helper component for cast detail route +function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + useEffect(() => { + const loadCast = async () => { + if (id) { + try { + const castData = await fetchCastById(id); + if (castData) { + const person = convertApiCastToStaff(castData); + setSelectedPerson(person); + } else { + navigate('/cast'); + } + } catch (error) { + console.error('Failed to load cast:', error); + navigate('/cast'); + } + } + }; + loadCast(); + }, [id]); + + if (!selectedPerson) return null; + + return ( + + ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/src/api.ts b/src/api.ts index d0192d6..296d2f9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -111,6 +111,7 @@ export interface CreateStaffInput { export interface ApiCastItem { id: number; name: string; + cleanname?: string; photo: string | null; bio: string | null; birthDate: string | null; @@ -119,6 +120,33 @@ export interface ApiCastItem { updatedAt: string; occupations?: string[]; filmography?: ApiCastMediaItem[]; + media_types?: string[]; + bust_size?: number | null; + cup_size?: string | null; + waist_size?: number | null; + hip_size?: number | null; + height?: number | null; + weight?: number | null; + hair_color?: string | null; + eye_color?: string | null; + ethnicity?: string | null; + adult_specifics?: { + id: number; + cast_id: number; + bust_size?: number | null; + cup_size?: string | null; + waist_size?: number | null; + hip_size?: number | null; + height?: number | null; + weight?: number | null; + hair_color?: string | null; + eye_color?: string | null; + ethnicity?: string | null; + tattoos?: string | null; + piercings?: string | null; + measurements?: string | null; + shoe_size?: number | null; + }; } export interface ApiCastMediaItem { @@ -129,7 +157,7 @@ export interface ApiCastMediaItem { category: string | null; type: string; role: string; - characterName: string | null; + characterName?: string | null; } export interface CreateCastInput { @@ -144,6 +172,43 @@ export interface CreateCastInput { export interface UpdateCastInput extends Partial {} +export function convertApiCastToStaff(apiItem: ApiCastItem): Staff { + return { + id: apiItem.id.toString(), + name: apiItem.name, + cleanname: apiItem.cleanname, + role: apiItem.occupations?.[0] || 'Actor', + photo: normalizeUrl(apiItem.photo) || `https://picsum.photos/seed/cast-${apiItem.id}/200/200`, + bio: apiItem.bio || undefined, + birthDate: apiItem.birthDate || undefined, + birthPlace: apiItem.birthPlace || undefined, + occupations: apiItem.occupations || ['Actor'], + createdAt: apiItem.createdAt, + updatedAt: apiItem.updatedAt, + bust_size: apiItem.bust_size, + cup_size: apiItem.cup_size, + waist_size: apiItem.waist_size, + hip_size: apiItem.hip_size, + height: apiItem.height, + weight: apiItem.weight, + hair_color: apiItem.hair_color, + eye_color: apiItem.eye_color, + ethnicity: apiItem.ethnicity, + filmography: apiItem.filmography?.map(item => ({ + id: item.id, + title: item.title, + year: item.year, + poster: normalizeUrl(item.poster) || `https://picsum.photos/seed/${item.id}/400/600`, + category: item.category, + type: item.type, + role: item.role, + characterName: item.characterName + })), + media_types: apiItem.media_types, + adult_specifics: apiItem.adult_specifics + }; +} + export function convertApiToMedia(apiItem: ApiMediaItem): Media { // Convert staff from API to Media staff format const staff: Staff[] = (apiItem.staff || []).map((staffMember) => ({ @@ -360,7 +425,7 @@ export async function deleteMedia(id: number | string): Promise { } // Cast API Functions -export async function fetchAllCast(page: number = 1, limit: number = 100000): Promise { +export async function fetchAllCast(page: number = 1, limit: number = 100000): Promise { try { const response = await fetch(`${BASE_URL}/api/cast?page=${page}&limit=${limit}`); if (!response.ok) { @@ -369,7 +434,7 @@ export async function fetchAllCast(page: number = 1, limit: number = 100000): Pr const data: ApiResponse> = await response.json(); if (data.success && data.data.items) { - return data.data.items; + return data.data.items.map(convertApiCastToStaff); } return []; } catch (error) { diff --git a/src/components/AddMediaView.tsx b/src/components/AddMediaView.tsx index fa46f6b..97aee5c 100644 --- a/src/components/AddMediaView.tsx +++ b/src/components/AddMediaView.tsx @@ -3,17 +3,18 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { createMedia, type CreateMediaInput } from '@/api'; import { ArrowLeft } from 'lucide-react'; import { cn } from '@/lib/utils'; interface AddMediaViewProps { activeCategory: MediaCategory; - onBack: () => void; onAddComplete: () => void; } -export default function AddMediaView({ activeCategory, onBack, onAddComplete }: AddMediaViewProps) { +export default function AddMediaView({ activeCategory, onAddComplete }: AddMediaViewProps) { + const navigate = useNavigate(); const [newMedia, setNewMedia] = useState({ title: '', year: '', @@ -149,7 +150,7 @@ export default function AddMediaView({ activeCategory, onBack, onAddComplete }:
+ + {(person.ethnicity || person.adult_specifics?.ethnicity) && ( +
+
+ +
+
+

Ethnicity

+

{person.adult_specifics?.ethnicity || person.ethnicity}

+
+
+ )} + +
+

Measurements

+ +
+ +
+
+ +
+
+

Height

+

{person.adult_specifics?.height || person.height} cm

+
+
+ + + {(person.weight || person.adult_specifics?.weight) && ( +
+
+ +
+
+

Weight

+

{person.adult_specifics?.weight || person.weight} kg

+
+
+ )} + + {(person.adult_specifics?.measurements || person.bust_size || person.cup_size || person.waist_size || person.hip_size) && ( +
+
+ +
+
+

Measurements

+

+ {person.adult_specifics?.measurements || ( + <> + {person.bust_size && `${person.bust_size}`} + {person.cup_size && person.cup_size} + {person.bust_size || person.cup_size ? '-' : ''} + {person.waist_size && `${person.waist_size}`} + {person.waist_size ? '-' : ''} + {person.hip_size && `${person.hip_size}`} + + )} +

+
+
+ )} + + {(person.hair_color || person.adult_specifics?.hair_color) && ( +
+
+ +
+
+

Hair Color

+

{person.adult_specifics?.hair_color || person.hair_color}

+
+
+ )} + + {(person.eye_color || person.adult_specifics?.eye_color) && ( +
+
+ +
+
+

Eye Color

+

{person.adult_specifics?.eye_color || person.eye_color}

+
+
+ )} + + {person.adult_specifics?.tattoos && ( +
+
+ +
+
+

Tattoos

+

{person.adult_specifics.tattoos}

+
+
+ )} + + {person.adult_specifics?.piercings && ( +
+
+ +
+
+

Piercings

+

{person.adult_specifics.piercings}

+
+
+ )} +
+
{/* Main Bio & Roles */}
-
-

- Biography -

-

- {person.bio || `${person.name} is a talented ${person.role} known for their work in various media productions. They have brought numerous characters to life with their unique performances.`} -

-
+ {person.bio && ( +
+

+ Biography +

+

+ {person.bio} +

+
+ )} -
-

- - Characters -

-
- {relatedMedia.map(media => { - const character = media.staff?.find(s => s.id === person.id); - if (!character) return null; - return ( + {person.filmography && person.filmography.length > 0 && ( +
+

+ + Characters +

+
+ {person.filmography.map(item => (
{character.characterName}

Character

-

{character.characterName}

+

{item.characterName || item.role}

- ); - })} -
-
+ ))} +
+
+ )} -
-

- - Filmography -

-
- {relatedMedia.map(media => ( -
onMediaClick(media.id)} - className="group flex items-center gap-4 p-4 rounded-2xl bg-white border border-zinc-100 hover:border-[#6d28d9]/30 hover:shadow-lg transition-all cursor-pointer" - > -
- {media.title} -
-
-

- {media.title} -

-

- {media.year} -

-
- - {person.role} - + {person.filmography && person.filmography.length > 0 && ( +
+

+ + Filmography +

+
+ {person.filmography.map(item => ( +
handleMediaClick(item.id.toString())} + className="group flex items-center gap-4 p-4 rounded-2xl bg-white border border-zinc-100 hover:border-[#6d28d9]/30 hover:shadow-lg transition-all cursor-pointer" + > +
+ {item.title} +
+
+

+ {item.title} +

+

+ {item.year || 'Unknown'} +

+
+ + {item.role} + +
-
- ))} -
-
+ ))} +
+ + )} diff --git a/src/components/CastView.tsx b/src/components/CastView.tsx index 729fc92..39a44cc 100644 --- a/src/components/CastView.tsx +++ b/src/components/CastView.tsx @@ -1,36 +1,159 @@ -import { Staff } from '@/types'; +import { Staff, MediaCategory } from '@/types'; import { useState, useMemo, useEffect } from 'react'; -import { Search, ArrowUpDown, User, ChevronLeft, ChevronRight } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { Search, ArrowUpDown, User, ChevronLeft, ChevronRight, X, Filter } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; import { motion, AnimatePresence } from 'motion/react'; import { cn } from '@/lib/utils'; +import { fetchAllCast } from '@/api'; interface CastViewProps { - staffList: Staff[]; onPersonClick: (person: Staff) => void; + enabledCategories: MediaCategory[]; } -export default function CastView({ staffList, onPersonClick }: CastViewProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [sortBy, setSortBy] = useState<'name' | 'role'>('name'); +export default function CastView({ onPersonClick, enabledCategories }: CastViewProps) { + const navigate = useNavigate(); + const [staffList, setStaffList] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(() => { + return localStorage.getItem('castSearchQuery') || ''; + }); + const [sortBy, setSortBy] = useState<'name' | 'role' | 'birthDate' | 'height'>(() => { + return (localStorage.getItem('castSortBy') as 'name' | 'role' | 'birthDate' | 'height') || 'name'; + }); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => { + return (localStorage.getItem('castSortOrder') as 'asc' | 'desc') || 'asc'; + }); + const [filterOccupation, setFilterOccupation] = useState(() => { + return localStorage.getItem('castFilterOccupation') || ''; + }); + const [filterMediaType, setFilterMediaType] = useState(() => { + return localStorage.getItem('castFilterMediaType') || ''; + }); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(12); + const [showFilters, setShowFilters] = useState(false); + + // Persist filters and sorts + useEffect(() => { + localStorage.setItem('castSearchQuery', searchQuery); + }, [searchQuery]); + + useEffect(() => { + localStorage.setItem('castSortBy', sortBy); + }, [sortBy]); + + useEffect(() => { + localStorage.setItem('castSortOrder', sortOrder); + }, [sortOrder]); + + useEffect(() => { + localStorage.setItem('castFilterOccupation', filterOccupation); + }, [filterOccupation]); + + useEffect(() => { + localStorage.setItem('castFilterMediaType', filterMediaType); + }, [filterMediaType]); + + const handleResetFilters = () => { + setSearchQuery(''); + setSortBy('name'); + setSortOrder('asc'); + setFilterOccupation(''); + setFilterMediaType(''); + }; + + const hasActiveFilters = searchQuery || filterOccupation || filterMediaType || sortBy !== 'name' || sortOrder !== 'asc'; + + useEffect(() => { + const loadCast = async () => { + try { + const cast = await fetchAllCast(); + setStaffList(cast); + } catch (error) { + console.error('Failed to load cast:', error); + } finally { + setLoading(false); + } + }; + loadCast(); + }, []); const filteredStaff = useMemo(() => { - let list = staffList.filter(s => - s.name.toLowerCase().includes(searchQuery.toLowerCase()) || - s.role.toLowerCase().includes(searchQuery.toLowerCase()) || - s.mediaTitle?.toLowerCase().includes(searchQuery.toLowerCase()) - ); + let list = staffList.filter(s => { + // Hide actors without linked media + if (!s.filmography || s.filmography.length === 0) { + return false; + } + + // Filter by enabled categories based on media_types + if (s.media_types && s.media_types.length > 0) { + const hasEnabledMediaType = s.media_types.some(type => { + const category = type.charAt(0).toUpperCase() + type.slice(1); + return enabledCategories.includes(category as MediaCategory); + }); + if (!hasEnabledMediaType) { + return false; + } + } + + // Filter by occupation + if (filterOccupation && !s.occupations?.includes(filterOccupation)) { + return false; + } + + // Filter by media type + if (filterMediaType && !s.media_types?.includes(filterMediaType)) { + return false; + } + + return s.name.toLowerCase().includes(searchQuery.toLowerCase()) || + s.role.toLowerCase().includes(searchQuery.toLowerCase()) || + s.mediaTitle?.toLowerCase().includes(searchQuery.toLowerCase()); + }); - return list.sort((a, b) => a[sortBy].localeCompare(b[sortBy])); - }, [staffList, searchQuery, sortBy]); + // Sort + list.sort((a, b) => { + let comparison = 0; + + if (sortBy === 'name' || sortBy === 'role') { + comparison = a[sortBy].localeCompare(b[sortBy]); + } else if (sortBy === 'birthDate') { + const dateA = a.birthDate ? new Date(a.birthDate).getTime() : 0; + const dateB = b.birthDate ? new Date(b.birthDate).getTime() : 0; + comparison = dateA - dateB; + } else if (sortBy === 'height') { + const heightA = a.height || 0; + const heightB = b.height || 0; + comparison = heightA - heightB; + } + + return sortOrder === 'desc' ? -comparison : comparison; + }); + + return list; + }, [staffList, searchQuery, sortBy, sortOrder, filterOccupation, filterMediaType, enabledCategories]); + + // Get unique occupations and media types for filters + const uniqueOccupations = useMemo(() => { + const occupations = new Set(); + staffList.forEach(s => s.occupations?.forEach(o => occupations.add(o))); + return Array.from(occupations).sort(); + }, [staffList]); + + const uniqueMediaTypes = useMemo(() => { + const mediaTypes = new Set(); + staffList.forEach(s => s.media_types?.forEach(m => mediaTypes.add(m))); + return Array.from(mediaTypes).sort(); + }, [staffList]); // Reset to first page when filters or sorting change useEffect(() => { setCurrentPage(1); - }, [searchQuery, sortBy, itemsPerPage]); + }, [searchQuery, sortBy, sortOrder, filterOccupation, filterMediaType, itemsPerPage]); const totalPages = Math.ceil(filteredStaff.length / itemsPerPage); @@ -67,18 +190,127 @@ export default function CastView({ staffList, onPersonClick }: CastViewProps) { className="pl-10 w-full md:w-[300px] bg-zinc-100 border-none rounded-full h-11" /> + + {hasActiveFilters && ( + + )} - {filteredStaff.length === 0 ? ( + {showFilters && ( + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {searchQuery && ( + + Search: {searchQuery} + + + )} + {filterOccupation && ( + + Occupation: {filterOccupation} + + + )} + {filterMediaType && ( + + Media Type: {filterMediaType} + + + )} + {(sortBy !== 'name' || sortOrder !== 'asc') && ( + + Sort: {sortBy} ({sortOrder}) + + + )} +
+
+ )} + + {loading ? ( +
+
+

Loading cast...

+
+ ) : filteredStaff.length === 0 ? (

No cast members found

@@ -88,7 +320,7 @@ export default function CastView({ staffList, onPersonClick }: CastViewProps) { {paginatedStaff.map((person) => (
-
-
- {person.characterName} + {person.filmography && person.filmography.length > 0 && ( +
+
+ {person.filmography[0].title} +
+
+

Latest Role

+

{person.filmography[0].title}

+

{person.filmography[0].role}

+
-
-

Character

-

{person.characterName}

-

in {person.mediaTitle}

-
-
+ )} ))} diff --git a/src/components/DetailView.tsx b/src/components/DetailView.tsx index e5faed1..633c066 100644 --- a/src/components/DetailView.tsx +++ b/src/components/DetailView.tsx @@ -1,4 +1,5 @@ import { Media, Staff } from '@/types'; +import { useNavigate } from 'react-router-dom'; import { Play, Bookmark, @@ -17,11 +18,11 @@ import { motion } from 'motion/react'; interface DetailViewProps { media: Media; - onBack: () => void; onPersonClick: (person: Staff) => void; } -export default function DetailView({ media, onBack, onPersonClick }: DetailViewProps) { +export default function DetailView({ media, onPersonClick }: DetailViewProps) { + const navigate = useNavigate(); return (
{/* Banner */} @@ -35,7 +36,7 @@ export default function DetailView({ media, onBack, onPersonClick }: DetailViewP
+
@@ -105,18 +101,18 @@ export default function Header({ > {isSearchOpen ? : } - - + void }) { +export default function ImporterView() { + const navigate = useNavigate(); const [xbvrConfig, setXbvrConfig] = useState({ url: import.meta.env.VITE_XBVR_URL || '' }); const [stashappConfig, setStashappConfig] = useState({ url: import.meta.env.VITE_STASHAPP_URL || '', @@ -159,7 +161,7 @@ export default function ImporterView({ onBack }: { onBack: () => void }) {