import { useMutation, useQueryClient } from '@tanstack/react-query'; import { addUrlToLibrary } from '../dal'; import { cardKeys } from '../cardKeys'; import { collectionKeys } from '@/features/collections/lib/collectionKeys'; import { feedKeys } from '@/features/feeds/lib/feedKeys'; import { noteKeys } from '@/features/notes/lib/noteKeys'; import { sembleKeys } from '@/features/semble/lib/sembleKeys'; import posthog from 'posthog-js'; import { CardSaveAnalyticsContext, CardSaveEventProperties, } from '@/features/analytics/types'; import { shouldCaptureAnalytics } from '@/features/analytics/utils'; import { notifications } from '@mantine/notifications'; import { BsCheck, BsExclamation } from 'react-icons/bs'; export default function useAddCard( analyticsContext?: CardSaveAnalyticsContext, ) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async (newCard: { url: string; note?: string; collectionIds?: string[]; viaCardId?: string; notificationId?: string; }) => { return addUrlToLibrary(newCard.url, { note: newCard.note, collectionIds: newCard.collectionIds, viaCardId: newCard.viaCardId, }); }, // Generally, do UI things (redirects, toasts) in mutate-level callbacks so they // don't fire if the user navigated away. But loading toasts that need .update() // must be handled here — Suspense re-renders can unmount the caller and drop // mutate-level callbacks, leaving the loading toast stuck forever. // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire onSuccess: (_data, variables) => { if (variables.notificationId) { notifications.update({ id: variables.notificationId, color: 'green', title: 'Success!', message: 'Card added', position: 'top-center', loading: false, autoClose: 2000, icon: , }); } queryClient.invalidateQueries({ queryKey: cardKeys.all() }); queryClient.invalidateQueries({ queryKey: noteKeys.all() }); queryClient.invalidateQueries({ queryKey: feedKeys.all() }); queryClient.invalidateQueries({ queryKey: sembleKeys.all() }); queryClient.invalidateQueries({ queryKey: collectionKeys.mine() }); queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() }); queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); queryClient.invalidateQueries({ queryKey: collectionKeys.bySembleUrl(variables.url), }); // Invalidate URL metadata with stats to update tab counts queryClient.invalidateQueries({ queryKey: cardKeys.urlMetadata(variables.url, { includeStats: true }), }); // invalidate each collection query individually variables.collectionIds?.forEach((id) => { queryClient.invalidateQueries({ queryKey: collectionKeys.collection(id), }); queryClient.invalidateQueries({ queryKey: collectionKeys.infinite(id), }); }); // Track card save event in PostHog if (shouldCaptureAnalytics() && analyticsContext) { const eventProperties: CardSaveEventProperties = { save_source: analyticsContext.saveSource, is_new_card: true, has_note: !!variables.note, collection_count: variables.collectionIds?.length || 0, active_filters: analyticsContext.activeFilters ? { url_type: analyticsContext.activeFilters.urlType, sort: analyticsContext.activeFilters.sort, search_query: analyticsContext.activeFilters.searchQuery, profile_filter: analyticsContext.activeFilters.profileFilter, } : undefined, via_card_id: variables.viaCardId, page_path: analyticsContext.pagePath, }; posthog.capture('card_saved', eventProperties); // Clear super properties after capture posthog.unregister('original_save_source'); posthog.unregister('original_active_filters'); } }, onError: (_error, variables) => { if (variables.notificationId) { notifications.update({ id: variables.notificationId, color: 'red', title: 'Error', message: 'Could not add card', position: 'top-center', loading: false, autoClose: 5000, withCloseButton: true, icon: , }); } }, }); return mutation; }