From 6d5397505a61627d8dc37b1d5e32ce526a536254 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Thu, 9 Apr 2026 13:02:58 +0200 Subject: [PATCH] add media page --- src/App.tsx | 20 +- src/api.ts | 8 +- src/components/AddMediaView.tsx | 421 ++++++++++++++++++++++++++++++++ src/components/BrowseView.tsx | 387 +---------------------------- src/components/Header.tsx | 10 +- src/types.ts | 2 +- 6 files changed, 452 insertions(+), 396 deletions(-) create mode 100644 src/components/AddMediaView.tsx diff --git a/src/App.tsx b/src/App.tsx index 68012d9..3dfe66a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,17 +10,18 @@ import BrowseView from './components/BrowseView'; import DetailView from './components/DetailView'; import CastView from './components/CastView'; import CastDetailView from './components/CastDetailView'; +import AddMediaView from './components/AddMediaView'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { Media, Staff, MediaCategory } from './types'; import { fetchAllMedia, fetchMediaById } from './api'; export default function App() { - const [currentView, setCurrentView] = useState<'browse' | 'detail' | 'cast' | 'castDetail'>('browse'); + const [currentView, setCurrentView] = useState<'browse' | 'detail' | 'cast' | 'castDetail' | 'add'>('browse'); const [activeCategory, setActiveCategory] = useState('Anime'); const [selectedMedia, setSelectedMedia] = useState(null); const [selectedPerson, setSelectedPerson] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - const [enabledCategories, setEnabledCategories] = useState(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); + const [enabledCategories, setEnabledCategories] = useState(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); const [customMedia, setCustomMedia] = useState([]); const [adultMedia, setAdultMedia] = useState([]); @@ -61,6 +62,11 @@ export default function App() { window.scrollTo({ top: 0, behavior: 'smooth' }); }; + const handleAddMediaView = () => { + setCurrentView('add'); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + const allMedia = useMemo(() => { // Use API data if available, otherwise fall back to mock data let list: Media[] = []; @@ -83,7 +89,7 @@ export default function App() { return list.filter(m => m.category === activeCategory && enabledCategories.includes(m.category)); }, [activeCategory, enabledCategories, customMedia, apiMedia]); - const handleAddMedia = async (newMedia: Media) => { + const handleAddMedia = async () => { // Reload all media from API to get the newly added item try { const media = await fetchAllMedia(); @@ -196,6 +202,7 @@ export default function App() {
) : currentView === 'cast' ? ( @@ -230,6 +236,12 @@ export default function App() { relatedMedia={allMedia.filter(m => m.staff?.some(s => s.id === selectedPerson.id))} /> ) + ) : currentView === 'add' ? ( + ) : ( selectedMedia && ( void; + onAddComplete: () => void; +} + +export default function AddMediaView({ activeCategory, onBack, onAddComplete }: AddMediaViewProps) { + const [newMedia, setNewMedia] = useState({ + title: '', + year: '', + poster: '', + banner: '', + description: '', + rating: '', + category: activeCategory, + type: 'Movie' as string, + status: 'Released' as string, + aspectRatio: '2/3' as '2/3' | '16/9' | '1/1', + runtime: '', + director: '', + writer: '', + releaseDate: '', + genres: '' as string, + tags: '' as string, + studios: '' as string + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + // Update category, default aspect ratio, and default type when activeCategory changes + useEffect(() => { + let defaultAspect: '2/3' | '16/9' | '1/1' = '2/3'; + let defaultType = 'Movie'; + + if (activeCategory === 'Music') { + defaultAspect = '1/1'; + defaultType = 'Album'; + } else if (activeCategory === 'Games') { + defaultAspect = '16/9'; + defaultType = 'Game'; + } else if (activeCategory === 'Adult') { + defaultAspect = '16/9'; + defaultType = 'Movie'; + } else if (activeCategory === 'Anime') { + defaultType = 'TV'; + } else if (activeCategory === 'TV Series') { + defaultType = 'TV'; + } else if (activeCategory === 'Books') { + defaultType = 'Hardcover'; + } else if (activeCategory === 'Consoles') { + defaultType = 'Console'; + } + + setNewMedia(prev => ({ + ...prev, + category: activeCategory, + aspectRatio: defaultAspect, + type: defaultType + })); + }, [activeCategory]); + + const handleAddSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newMedia.title || !newMedia.poster) return; + + setIsSubmitting(true); + setSubmitStatus('idle'); + setErrorMessage(''); + + // Convert category from plural to singular to match API format + const categoryMap: Record = { + 'Anime': 'Anime', + 'Movies': 'Movie', + 'TV Series': 'TV', + 'Music': 'Music', + 'Books': 'Book', + 'Consoles': 'Console', + 'Games': 'Game', + 'Adult': 'Adult' + }; + + const mediaInput: CreateMediaInput = { + title: newMedia.title, + year: parseInt(newMedia.year) || new Date().getFullYear(), + poster: newMedia.poster, + banner: newMedia.banner || null, + description: newMedia.description || null, + rating: newMedia.rating ? parseFloat(newMedia.rating) : null, + category: categoryMap[newMedia.category] || newMedia.category, + type: newMedia.type, + status: newMedia.status, + aspectRatio: newMedia.aspectRatio, + runtime: newMedia.runtime ? parseInt(newMedia.runtime) : null, + director: newMedia.director || null, + writer: newMedia.writer || null, + releaseDate: newMedia.releaseDate || null, + genres: newMedia.genres ? newMedia.genres.split(',').map(g => g.trim()) : [], + tags: newMedia.tags ? newMedia.tags.split(',').map(t => t.trim()) : [], + studios: newMedia.studios ? newMedia.studios.split(',').map(s => s.trim()) : [] + }; + + try { + const createdMedia = await createMedia(mediaInput); + + if (createdMedia) { + setSubmitStatus('success'); + onAddComplete(); + + // Reset form after successful submission + setNewMedia({ + title: '', + year: '', + poster: '', + banner: '', + description: '', + rating: '', + category: activeCategory, + type: 'Movie', + status: 'Released', + aspectRatio: '2/3', + runtime: '', + director: '', + writer: '', + releaseDate: '', + genres: '', + tags: '', + studios: '' + }); + } + } catch (error) { + setSubmitStatus('error'); + setErrorMessage(error instanceof Error ? error.message : 'Failed to add media. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ + +
+

Add New Media

+

+ Add a new item to your {activeCategory} library. +

+ + {submitStatus === 'success' && ( +
+

✓ Successfully added to library!

+
+ )} + + {submitStatus === 'error' && ( +
+

✗ Error: {errorMessage}

+
+ )} + +
+
+ + setNewMedia(prev => ({ ...prev, title: e.target.value }))} + placeholder="e.g. Mob Psycho 100" + className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" + required + /> +
+
+
+ + setNewMedia(prev => ({ ...prev, year: e.target.value }))} + placeholder="2024" + className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" + /> +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + setNewMedia(prev => ({ ...prev, poster: e.target.value }))} + placeholder="https://example.com/poster.jpg" + className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" + required + /> +
+
+ + setNewMedia(prev => ({ ...prev, banner: e.target.value }))} + placeholder="https://example.com/banner.jpg" + className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" + /> +
+
+ +