Add Loading component and use across views
Introduce a reusable Loading component (src/components/ui/loading.tsx) that shows a spinning Loader2 icon and an optional message. Replace ad-hoc loading UIs by importing and using Loading in BrowseView and CastView. In App.tsx, add mediaLoading state (set around fetchAllMedia) and pass it to BrowseView; also add local loading states to MediaDetailRoute and CastDetailRoute to show Loading while fetching details. These changes centralize loading UX and remove duplicated spinner markup.
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -14,6 +14,7 @@ import CastDetailView from './components/CastDetailView';
|
|||||||
import AddMediaView from './components/AddMediaView';
|
import AddMediaView from './components/AddMediaView';
|
||||||
import ImporterView from './components/ImporterView';
|
import ImporterView from './components/ImporterView';
|
||||||
import SettingsView from './components/SettingsView';
|
import SettingsView from './components/SettingsView';
|
||||||
|
import Loading from './components/ui/loading';
|
||||||
import { MOCK_MEDIA, DETAIL_MEDIA } from './data';
|
import { MOCK_MEDIA, DETAIL_MEDIA } from './data';
|
||||||
import { Media, Staff, MediaCategory, UserSettings } from './types';
|
import { Media, Staff, MediaCategory, UserSettings } from './types';
|
||||||
import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api';
|
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)
|
// Load media from API on component mount (only when not on cast routes)
|
||||||
const [apiMedia, setApiMedia] = useState<Media[]>([]);
|
const [apiMedia, setApiMedia] = useState<Media[]>([]);
|
||||||
|
const [mediaLoading, setMediaLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettingsFromApi = async () => {
|
const loadSettingsFromApi = async () => {
|
||||||
@@ -72,11 +74,14 @@ function AppContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMediaFromApi = async () => {
|
const loadMediaFromApi = async () => {
|
||||||
|
setMediaLoading(true);
|
||||||
try {
|
try {
|
||||||
const media = await fetchAllMedia();
|
const media = await fetchAllMedia();
|
||||||
setApiMedia(media);
|
setApiMedia(media);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load media from API:', error);
|
console.error('Failed to load media from API:', error);
|
||||||
|
} finally {
|
||||||
|
setMediaLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,6 +325,7 @@ function AppContent() {
|
|||||||
itemsPerPage={settings?.itemsPerPage}
|
itemsPerPage={settings?.itemsPerPage}
|
||||||
gridItemSize={settings?.gridItemSize}
|
gridItemSize={settings?.gridItemSize}
|
||||||
onGridItemSizeChange={handleGridItemSizeChange}
|
onGridItemSizeChange={handleGridItemSizeChange}
|
||||||
|
loading={mediaLoading}
|
||||||
/>
|
/>
|
||||||
} />
|
} />
|
||||||
<Route path="/media/:id" element={
|
<Route path="/media/:id" element={
|
||||||
@@ -385,10 +391,12 @@ function AppContent() {
|
|||||||
function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonClick }: any) {
|
function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonClick }: any) {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMedia = async () => {
|
const loadMedia = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const fetchedMedia = await fetchMediaById(id);
|
const fetchedMedia = await fetchMediaById(id);
|
||||||
if (fetchedMedia) {
|
if (fetchedMedia) {
|
||||||
@@ -399,12 +407,15 @@ function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonC
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch media:', error);
|
console.error('Failed to fetch media:', error);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadMedia();
|
loadMedia();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
if (loading) return <Loading message="Loading media details..." />;
|
||||||
if (!selectedMedia) return null;
|
if (!selectedMedia) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -419,10 +430,12 @@ function MediaDetailRoute({ selectedMedia, setSelectedMedia, allMedia, onPersonC
|
|||||||
function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) {
|
function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCast = async () => {
|
const loadCast = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const castData = await fetchCastById(id);
|
const castData = await fetchCastById(id);
|
||||||
if (castData) {
|
if (castData) {
|
||||||
@@ -434,12 +447,15 @@ function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load cast:', error);
|
console.error('Failed to load cast:', error);
|
||||||
navigate('/cast');
|
navigate('/cast');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadCast();
|
loadCast();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
if (loading) return <Loading message="Loading cast details..." />;
|
||||||
if (!selectedPerson) return null;
|
if (!selectedPerson) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import MediaCard from './MediaCard';
|
|||||||
import MediaListItem from './MediaListItem';
|
import MediaListItem from './MediaListItem';
|
||||||
import { LayoutGrid, List, Star, ChevronLeft, ChevronRight, ArrowUpDown, Search, Monitor, Users, FolderTree, Tag } from 'lucide-react';
|
import { LayoutGrid, List, Star, ChevronLeft, ChevronRight, ArrowUpDown, Search, Monitor, Users, FolderTree, Tag } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import Loading from '@/components/ui/loading';
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -20,9 +21,10 @@ interface BrowseViewProps {
|
|||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
gridItemSize?: number;
|
gridItemSize?: number;
|
||||||
onGridItemSizeChange?: (size: number) => void;
|
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 [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
|
const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
|
||||||
@@ -309,7 +311,9 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{mediaList.length === 0 ? (
|
{loading ? (
|
||||||
|
<Loading message="Loading media..." />
|
||||||
|
) : mediaList.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
|
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
|
||||||
<Search size={32} />
|
<Search size={32} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Search, ArrowUpDown, User, ChevronLeft, ChevronRight, X, Filter } from
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import Loading from '@/components/ui/loading';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { fetchAllCast } from '@/api';
|
import { fetchAllCast } from '@/api';
|
||||||
@@ -319,10 +320,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<Loading message="Loading cast..." />
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#6d28d9] mb-4" />
|
|
||||||
<p className="text-lg font-bold">Loading cast...</p>
|
|
||||||
</div>
|
|
||||||
) : filteredStaff.length === 0 ? (
|
) : filteredStaff.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
<User size={48} className="mb-4 opacity-20" />
|
<User size={48} className="mb-4 opacity-20" />
|
||||||
|
|||||||
14
src/components/ui/loading.tsx
Normal file
14
src/components/ui/loading.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface LoadingProps {
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Loading({ message = 'Loading...' }: LoadingProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
|
<Loader2 className="animate-spin h-12 w-12 text-[#6d28d9] mb-4" />
|
||||||
|
<p className="text-lg font-bold">{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user