diff --git a/src/components/AdBlockDetector.tsx b/src/components/AdBlockDetector.tsx index ad5fcfb..a59881a 100644 --- a/src/components/AdBlockDetector.tsx +++ b/src/components/AdBlockDetector.tsx @@ -36,6 +36,10 @@ export function AdBlockDetector() { try { const host = import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com'; + const apiKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY; + if (!apiKey) { + return; + } // Try fetching the /decide endpoint which is critical for PostHog and often blocked // We use string concatenation to ensure the URL is well-formed const url = `${host}/decide?v=3&ip=1&_=`; @@ -45,7 +49,7 @@ export function AdBlockDetector() { await fetch(url + Date.now(), { method: 'POST', // POST requests to tracking endpoints are more likely to be blocked mode: 'no-cors', - body: JSON.stringify({ token: 'test' }) + body: JSON.stringify({ token: apiKey }) }); } catch (error) { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 8335fdc..bd2eb7f 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -179,7 +179,7 @@ const Navbar = () => { const navigate = useNavigate(); const handleShowFavorites = () => { - navigate("/account"); + navigate("/resources?tab=favorites"); }; const handleMobileCollapsibleToggle = (name: string) => { diff --git a/src/components/resources/FavoritesTab.tsx b/src/components/resources/FavoritesTab.tsx index 3856933..4f18d6b 100644 --- a/src/components/resources/FavoritesTab.tsx +++ b/src/components/resources/FavoritesTab.tsx @@ -1,21 +1,25 @@ import { motion } from 'framer-motion'; -import { useUserFavorites } from '@/hooks/useUserFavorites'; +import { useHeartedResources } from '@/hooks/useHeartedResources'; import { useResources } from '@/hooks/useResources'; +import { getResourceUrl } from '@/types/resources'; import ResourceCard from './ResourceCard'; import ResourceCardSkeleton from './ResourceCardSkeleton'; import { IconHeart } from '@tabler/icons-react'; const FavoritesTab = () => { - const { favorites, isLoading: favoritesLoading } = useUserFavorites(); + const { heartedResources, isLoading: favoritesLoading } = useHeartedResources(); const { resources, isLoading: resourcesLoading, setSelectedResource } = useResources(); const isLoading = favoritesLoading || resourcesLoading; - const favoriteResources = resources.filter(resource => favorites.includes(String(resource.id))); + const favoriteResources = resources.filter(resource => { + const resourceUrl = getResourceUrl(resource); + return resourceUrl ? heartedResources.includes(resourceUrl) : false; + }); if (isLoading) { return ( -
+
{Array.from({ length: 8 }).map((_, idx) => ( ))} @@ -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 ( -
+