))}
@@ -40,16 +44,19 @@ const FavoritesTab = () => {
}
return (
-
- {favoriteResources.map(resource => (
+
+ {favoriteResources.map(resource => {
+ const resourceUrl = getResourceUrl(resource);
+ return (
- ))}
+ );
+ })}
);
};
-export default FavoritesTab;
\ No newline at end of file
+export default FavoritesTab;
diff --git a/src/components/resources/ResourceCard.tsx b/src/components/resources/ResourceCard.tsx
index aa0af7a..558806f 100644
--- a/src/components/resources/ResourceCard.tsx
+++ b/src/components/resources/ResourceCard.tsx
@@ -11,10 +11,9 @@ import {
IconHeart,
IconBoxModel,
} from "@tabler/icons-react";
-import { Resource } from "@/types/resources";
+import { getResourceUrl, Resource } from "@/types/resources";
import { cn } from "@/lib/utils";
-import { useUserFavorites } from "@/hooks/useUserFavorites";
-import { useAuth } from "@/hooks/useAuth";
+import { useHeartedResources } from "@/hooks/useHeartedResources";
import AudioPlayer from "@/components/AudioPlayer";
interface ResourceCardProps {
@@ -29,15 +28,14 @@ const ResourceCard = ({ resource, onClick }: ResourceCardProps) => {
const stored = localStorage.getItem('hoverToPlay');
return stored === null ? true : stored === 'true';
});
- const { user } = useAuth();
-
// Reset image loaded state when resource changes
useEffect(() => {
setIsImageLoaded(false);
}, [resource.id]);
- const { toggleFavorite, isFavorited } = useUserFavorites();
- const isFavorite = isFavorited(String(resource.id));
+ const { toggleHeart, isHearted } = useHeartedResources();
+ const resourceUrl = getResourceUrl(resource);
+ const isFavorite = isHearted(resourceUrl);
const getPreviewUrl = (resource: Resource) => {
if (resource.download_url) return resource.download_url;
@@ -141,9 +139,9 @@ const ResourceCard = ({ resource, onClick }: ResourceCardProps) => {
const handleFavoriteClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
- toggleFavorite(String(resource.id));
+ toggleHeart(resourceUrl);
},
- [toggleFavorite, resource.id],
+ [toggleHeart, resourceUrl],
);
const handlePreviewClick = (e: React.MouseEvent) => {
diff --git a/src/hooks/useHeartedResources.ts b/src/hooks/useHeartedResources.ts
index 0dda402..3195027 100644
--- a/src/hooks/useHeartedResources.ts
+++ b/src/hooks/useHeartedResources.ts
@@ -1,56 +1,81 @@
+import { useCallback, useEffect, useState } from 'react';
import { useUserFavorites } from './useUserFavorites';
import { useAuth } from './useAuth';
+import { getResourceUrl, Resource } from '@/types/resources';
export const useHeartedResources = () => {
const { user } = useAuth();
const userFavorites = useUserFavorites();
-
- // If user is logged in, use user favorites, otherwise fall back to localStorage
- if (user) {
- return {
- heartedResources: userFavorites.favorites,
- toggleHeart: userFavorites.toggleFavorite,
- isHearted: userFavorites.isFavorited
- };
- }
-
- // Legacy localStorage fallback for non-authenticated users
const localStorageKey = 'heartedResources';
- const getLocalHeartedResources = (): string[] => {
+ const getLocalHeartedResources = useCallback((): string[] => {
try {
const stored = localStorage.getItem(localStorageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
- };
+ }, []);
const setLocalHeartedResources = (resources: string[]) => {
localStorage.setItem(localStorageKey, JSON.stringify(resources));
};
- const heartedResources = getLocalHeartedResources();
+ const [heartedResources, setHeartedResources] = useState
(() => getLocalHeartedResources());
- const toggleHeart = (resourceId: string) => {
+ useEffect(() => {
+ const handleLocalUpdate = () => {
+ setHeartedResources(getLocalHeartedResources());
+ };
+
+ const handleStorage = (event: StorageEvent) => {
+ if (event.key === localStorageKey) {
+ setHeartedResources(getLocalHeartedResources());
+ }
+ };
+
+ window.addEventListener('localFavoritesChanged', handleLocalUpdate);
+ window.addEventListener('storage', handleStorage);
+
+ return () => {
+ window.removeEventListener('localFavoritesChanged', handleLocalUpdate);
+ window.removeEventListener('storage', handleStorage);
+ };
+ }, [getLocalHeartedResources]);
+
+ const toggleHeart = (resource: Resource | string) => {
+ const resourceUrl = typeof resource === 'string' ? resource : getResourceUrl(resource);
+ if (!resourceUrl) return;
const current = getLocalHeartedResources();
- const newHearted = current.includes(resourceId)
- ? current.filter(id => id !== resourceId)
- : [...current, resourceId];
+ const newHearted = current.includes(resourceUrl)
+ ? current.filter(id => id !== resourceUrl)
+ : [...current, resourceUrl];
setLocalHeartedResources(newHearted);
- // Force re-render by dispatching a custom event
+ setHeartedResources(newHearted);
window.dispatchEvent(new CustomEvent('localFavoritesChanged'));
};
- const isHearted = (resourceId: string) => {
- return getLocalHeartedResources().includes(resourceId);
+ const isHearted = (resource: Resource | string) => {
+ const resourceUrl = typeof resource === 'string' ? resource : getResourceUrl(resource);
+ if (!resourceUrl) return false;
+ return getLocalHeartedResources().includes(resourceUrl);
};
+ if (user) {
+ return {
+ heartedResources: userFavorites.favorites,
+ toggleHeart: userFavorites.toggleFavorite,
+ isHearted: userFavorites.isFavorited,
+ isLoading: userFavorites.isLoading,
+ };
+ }
+
return {
heartedResources,
toggleHeart,
- isHearted
+ isHearted,
+ isLoading: false,
};
};
diff --git a/src/hooks/useUserFavorites.ts b/src/hooks/useUserFavorites.ts
index 30f05b9..0ceb940 100644
--- a/src/hooks/useUserFavorites.ts
+++ b/src/hooks/useUserFavorites.ts
@@ -1,4 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from './useAuth';
import { toast } from 'sonner';
@@ -6,49 +7,60 @@ import { toast } from 'sonner';
export const useUserFavorites = () => {
const { user } = useAuth();
const queryClient = useQueryClient();
+ const [isSchemaReady, setIsSchemaReady] = useState(true);
const { data: favorites = [], isLoading } = useQuery({
queryKey: ['userFavorites', user?.id],
queryFn: async () => {
if (!user?.id) return [];
+ if (!isSchemaReady) return [];
const { data, error } = await supabase
.from('user_favorites')
- .select('resource_id')
+ .select('resource_url')
.eq('user_id', user.id);
if (error) {
+ if (error.code === '42703' || error.message.includes('resource_url')) {
+ setIsSchemaReady(false);
+ toast.error('Favorites storage needs a database update');
+ return [];
+ }
console.error('Error fetching favorites:', error);
toast.error('Failed to load favorites');
throw error;
}
- return data?.map(fav => fav.resource_id.toString()) || [];
+ return data?.map(fav => fav.resource_url.toString()) || [];
},
enabled: !!user?.id,
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
});
const toggleMutation = useMutation({
- mutationFn: async (resourceId: string) => {
+ mutationFn: async (resourceUrl: string) => {
if (!user) throw new Error('User not authenticated');
+ if (!isSchemaReady) throw new Error('Favorites storage needs a database update');
- const isFavorited = favorites.includes(resourceId);
+ const isFavorited = favorites.includes(resourceUrl);
if (isFavorited) {
const { error } = await supabase
.from('user_favorites')
.delete()
.eq('user_id', user.id)
- .eq('resource_id', resourceId);
+ .eq('resource_url', resourceUrl);
if (error) throw error;
- return { action: 'removed', resourceId };
+ return { action: 'removed', resourceUrl };
} else {
const { error } = await supabase
.from('user_favorites')
- .insert({ user_id: user.id, resource_id: resourceId });
+ .upsert(
+ { user_id: user.id, resource_url: resourceUrl },
+ { onConflict: 'user_id,resource_url', ignoreDuplicates: true }
+ );
if (error) throw error;
- return { action: 'added', resourceId };
+ return { action: 'added', resourceUrl };
}
},
onSuccess: (data) => {
@@ -56,20 +68,33 @@ export const useUserFavorites = () => {
toast.success(data.action === 'added' ? 'Added to favorites' : 'Removed from favorites');
},
onError: (error) => {
+ const errorMessage = error instanceof Error ? error.message : '';
+ if (errorMessage.includes('database update')) {
+ toast.error('Favorites storage needs a database update');
+ return;
+ }
console.error('Error toggling favorite:', error);
toast.error('Failed to update favorites');
}
});
- const toggleFavorite = (resourceId: string) => {
+ const toggleFavorite = (resourceUrl: string) => {
if (!user) {
toast.error('Please sign in to save favorites');
return;
}
- toggleMutation.mutate(resourceId);
+ if (!resourceUrl) {
+ toast.error('Unable to favorite this resource');
+ return;
+ }
+ if (!isSchemaReady) {
+ toast.error('Favorites storage needs a database update');
+ return;
+ }
+ toggleMutation.mutate(resourceUrl);
};
- const isFavorited = (resourceId: string) => favorites.includes(resourceId);
+ const isFavorited = (resourceUrl: string) => favorites.includes(resourceUrl);
return {
favorites,
@@ -77,4 +102,4 @@ export const useUserFavorites = () => {
toggleFavorite,
isFavorited,
};
-};
\ No newline at end of file
+};
diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts
index cc12314..5bb01ae 100644
--- a/src/integrations/supabase/types.ts
+++ b/src/integrations/supabase/types.ts
@@ -138,19 +138,19 @@ export type Database = {
Row: {
created_at: string
id: string
- resource_id: string
+ resource_url: string
user_id: string
}
Insert: {
created_at?: string
id?: string
- resource_id: string
+ resource_url: string
user_id: string
}
Update: {
created_at?: string
id?: string
- resource_id?: string
+ resource_url?: string
user_id?: string
}
Relationships: []
diff --git a/src/pages/ResourcesHub.tsx b/src/pages/ResourcesHub.tsx
index b86e561..f4652c6 100644
--- a/src/pages/ResourcesHub.tsx
+++ b/src/pages/ResourcesHub.tsx
@@ -9,6 +9,7 @@ import { Resource } from '@/types/resources';
import ResourceFilters from '@/components/resources/ResourceFilters';
import SortSelector from '@/components/resources/SortSelector';
import ResourcesList from '@/components/resources/ResourcesList';
+import FavoritesTab from '@/components/resources/FavoritesTab';
import AuthDialog from '@/components/auth/AuthDialog';
import { Button } from '@/components/ui/button';
import { IconArrowUp, IconHeart, IconSearch } from '@tabler/icons-react';
@@ -137,6 +138,26 @@ const ResourcesHub = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.5 }}
>
+
+
+
+
{showFavorites ? (
{
transition={{ duration: 0.3 }}
className="text-center"
>
-
+
) : (
{
);
};
-export default ResourcesHub;
\ No newline at end of file
+export default ResourcesHub;
diff --git a/src/pages/Showcase.tsx b/src/pages/Showcase.tsx
index df03ac8..c007b11 100644
--- a/src/pages/Showcase.tsx
+++ b/src/pages/Showcase.tsx
@@ -78,7 +78,7 @@ const ShowcaseCard: React.FC<{ item: ShowcaseWithAssets }> = ({ item }) => {
if (isFont) {
const fontName = `font-${a.id}`;
return (
-