Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion src/components/resources/ResourcesList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Resource } from '@/types/resources';
import ResourceCard from './ResourceCard';
import { IconFolderX, IconX } from '@tabler/icons-react';
import { IconAlertTriangle, IconFolderX, IconRefresh, IconX } from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
Expand All @@ -28,6 +28,8 @@ interface ResourcesListProps {
filteredResources: Resource[];
onMusicLink?: (resource: Resource) => void;
onAnimate?: (resource: Resource) => void;
loadError?: string | null;
onRetry?: () => void;
}

const ResourcesList = ({
Expand All @@ -42,6 +44,8 @@ const ResourcesList = ({
filteredResources,
onMusicLink,
onAnimate,
loadError,
onRetry,
}: ResourcesListProps) => {
const [currentPage, setCurrentPage] = React.useState(1);
const itemsPerPage = 12;
Expand Down Expand Up @@ -70,6 +74,64 @@ const ResourcesList = ({
);
}

// A failed catalog fetch must never be presented as "no resources": that reads
// as an empty library when in fact the request (usually the asset session) failed.
if (loadError && resources.length === 0) {
return (
<motion.div
className="text-center py-16 space-y-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<motion.div
className="flex justify-center mb-4"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.1, type: "spring", stiffness: 200 }}
>
<IconAlertTriangle className="h-12 w-12 text-yellow-500" />
</motion.div>
<motion.h3
className="text-2xl font-semibold"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
Couldn&apos;t load resources
</motion.h3>
<motion.p
className="text-muted-foreground max-w-md mx-auto"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
{loadError}. This is usually a blocked or interrupted connection to our asset
service. Check your connection, VPN or ad-blocker, then try again.
</motion.p>
<motion.div
className="flex flex-col sm:flex-row justify-center gap-4 mt-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Button onClick={onClearFilters} variant="outline">
<IconX className="mr-2 h-4 w-4" />
Clear Filters
</Button>
<Button
className="bg-primary hover:bg-primary/90"
disabled={!onRetry}
onClick={() => onRetry?.()}
>
<IconRefresh className="mr-2 h-4 w-4" />
Try Again
</Button>
</motion.div>
</motion.div>
);
}

const shouldShowNoResourcesMessage =
(!isLoading && filteredResources.length === 0) ||
(!isLoading &&
Expand Down Expand Up @@ -169,6 +231,19 @@ const ResourcesList = ({
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{loadError && resources.length > 0 && (
<div className="mb-4 border border-red-500/40 bg-red-500/10 px-4 py-3 text-left">
<p className="text-sm text-red-300">
Some resources could not be refreshed: {loadError}
</p>
{onRetry && (
<Button variant="outline" size="sm" className="mt-2" onClick={onRetry}>
<IconRefresh className="mr-2 h-4 w-4" />
Retry
</Button>
)}
</div>
)}
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
initial={{ opacity: 0 }}
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/useResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const useResources = () => {
const [loadedFonts, setLoadedFonts] = useState<string[]>([]);
const [fontPreviewText, setFontPreviewText] = useState<string>("Aa Bb Cc");
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
const [loadError, setLoadError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const fetchIdRef = useRef(0);

Expand All @@ -44,6 +45,7 @@ export const useResources = () => {
const loadResources = async () => {
setIsLoading(true);
setResources([]);
setLoadError(null);

try {
const categories = getAvailableCategories();
Expand All @@ -66,6 +68,9 @@ export const useResources = () => {
console.error("Error fetching resources:", error);
if (currentFetchId === fetchIdRef.current) {
setResources([]);
setLoadError(
error instanceof Error && error.message ? error.message : "Unable to load resources"
);
}
} finally {
if (currentFetchId === fetchIdRef.current) {
Expand Down Expand Up @@ -267,6 +272,11 @@ export const useResources = () => {
return cacheImage(url);
}, []);

const retryResources = useCallback(() => {
fetchIdRef.current++;
setRefreshKey(prev => prev + 1);
}, []);

return {
resources,
selectedResource,
Expand All @@ -286,6 +296,7 @@ export const useResources = () => {
availableSubcategories,
availableCategories,
hasCategoryResources,
loadError,
handleSearchSubmit,
handleClearSearch,
handleCategoryChange,
Expand All @@ -302,5 +313,6 @@ export const useResources = () => {
setRefreshKey(prev => prev + 1);
setSelectedCategory(null);
},
retryResources,
};
};
Loading
Loading