diff --git a/src/App.tsx b/src/App.tsx index b076f0f..8d1c345 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import CastDetailView from './components/CastDetailView'; import AddMediaView from './components/AddMediaView'; import ImporterView from './components/ImporterView'; import SettingsView from './components/SettingsView'; +import Loading from './components/ui/loading'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { Media, Staff, MediaCategory, UserSettings } from './types'; import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api'; @@ -37,6 +38,7 @@ function AppContent() { // Load media from API on component mount (only when not on cast routes) const [apiMedia, setApiMedia] = useState([]); + const [mediaLoading, setMediaLoading] = useState(true); useEffect(() => { const loadSettingsFromApi = async () => { @@ -72,11 +74,14 @@ function AppContent() { useEffect(() => { const loadMediaFromApi = async () => { + setMediaLoading(true); try { const media = await fetchAllMedia(); setApiMedia(media); } catch (error) { console.error('Failed to load media from API:', error); + } finally { + setMediaLoading(false); } }; @@ -320,6 +325,7 @@ function AppContent() { itemsPerPage={settings?.itemsPerPage} gridItemSize={settings?.gridItemSize} onGridItemSizeChange={handleGridItemSizeChange} + loading={mediaLoading} /> } /> (); const navigate = useNavigate(); + const [loading, setLoading] = useState(true); useEffect(() => { const loadMedia = async () => { if (id) { + setLoading(true); try { const fetchedMedia = await fetchMediaById(id); if (fetchedMedia) { @@ -399,12 +407,15 @@ function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonC } catch (error) { console.error('Failed to fetch media:', error); navigate('/'); + } finally { + setLoading(false); } } }; loadMedia(); }, [id]); + if (loading) return ; if (!selectedMedia) return null; return ( @@ -419,10 +430,12 @@ function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonC function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const [loading, setLoading] = useState(true); useEffect(() => { const loadCast = async () => { if (id) { + setLoading(true); try { const castData = await fetchCastById(id); if (castData) { @@ -434,12 +447,15 @@ function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) { } catch (error) { console.error('Failed to load cast:', error); navigate('/cast'); + } finally { + setLoading(false); } } }; loadCast(); }, [id]); + if (loading) return ; if (!selectedPerson) return null; return ( diff --git a/src/components/BrowseView.tsx b/src/components/BrowseView.tsx index 14f128d..5ba456c 100644 --- a/src/components/BrowseView.tsx +++ b/src/components/BrowseView.tsx @@ -3,6 +3,7 @@ import MediaCard from './MediaCard'; import MediaListItem from './MediaListItem'; import { LayoutGrid, List, Star, ChevronLeft, ChevronRight, ArrowUpDown, Search, Monitor, Users, FolderTree, Tag } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import Loading from '@/components/ui/loading'; import React, { useState, useMemo, useEffect } from 'react'; import { DropdownMenu, @@ -20,9 +21,10 @@ interface BrowseViewProps { itemsPerPage?: number; gridItemSize?: number; onGridItemSizeChange?: (size: number) => void; + loading?: boolean; } -export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12, gridItemSize: initialGridItemSize = 5, onGridItemSizeChange }: BrowseViewProps) { +export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12, gridItemSize: initialGridItemSize = 5, onGridItemSizeChange, loading = false }: BrowseViewProps) { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); @@ -309,7 +311,9 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it {/* Content */} - {mediaList.length === 0 ? ( + {loading ? ( + + ) : mediaList.length === 0 ? (
diff --git a/src/components/CastView.tsx b/src/components/CastView.tsx index d762aed..f9d4c34 100644 --- a/src/components/CastView.tsx +++ b/src/components/CastView.tsx @@ -5,6 +5,7 @@ import { Search, ArrowUpDown, User, ChevronLeft, ChevronRight, X, Filter } from import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import Loading from '@/components/ui/loading'; import { motion, AnimatePresence } from 'motion/react'; import { cn } from '@/lib/utils'; import { fetchAllCast } from '@/api'; @@ -319,10 +320,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag )} {loading ? ( -
-
-

Loading cast...

-
+ ) : filteredStaff.length === 0 ? (
diff --git a/src/components/ui/loading.tsx b/src/components/ui/loading.tsx new file mode 100644 index 0000000..34d87fd --- /dev/null +++ b/src/components/ui/loading.tsx @@ -0,0 +1,14 @@ +import { Loader2 } from 'lucide-react'; + +interface LoadingProps { + message?: string; +} + +export default function Loading({ message = 'Loading...' }: LoadingProps) { + return ( +
+ +

{message}

+
+ ); +}