This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / features / cards / lib / mutations / useAddCard.tsx
5.7 kB 154 lines
1import { useMutation, useQueryClient } from '@tanstack/react-query'; 2import { addUrlToLibrary } from '../dal'; 3import { cardKeys } from '../cardKeys'; 4import { collectionKeys } from '@/features/collections/lib/collectionKeys'; 5import { feedKeys } from '@/features/feeds/lib/feedKeys'; 6import { noteKeys } from '@/features/notes/lib/noteKeys'; 7import { sembleKeys } from '@/features/semble/lib/sembleKeys'; 8import { connectionKeys } from '@/features/connections/lib/connectionKeys'; 9import { profileKeys } from '@/features/profile/lib/profileKeys'; 10import posthog from 'posthog-js'; 11import { 12 CardSaveAnalyticsContext, 13 CardSaveEventProperties, 14} from '@/features/analytics/types'; 15import { shouldCaptureAnalytics } from '@/features/analytics/utils'; 16import { notifications } from '@mantine/notifications'; 17import { Button, Group, Text } from '@mantine/core'; 18import { BsCheck, BsExclamation } from 'react-icons/bs'; 19import { useRouter } from 'next/navigation'; 20 21export default function useAddCard( 22 analyticsContext?: CardSaveAnalyticsContext, 23) { 24 const queryClient = useQueryClient(); 25 const router = useRouter(); 26 27 const mutation = useMutation({ 28 mutationFn: async (newCard: { 29 url: string; 30 note?: string; 31 collectionIds?: string[]; 32 viaCardId?: string; 33 notificationId?: string; 34 }) => { 35 return addUrlToLibrary(newCard.url, { 36 note: newCard.note, 37 collectionIds: newCard.collectionIds, 38 viaCardId: newCard.viaCardId, 39 }); 40 }, 41 42 // Generally, do UI things (redirects, toasts) in mutate-level callbacks so they 43 // don't fire if the user navigated away. But loading toasts that need .update() 44 // must be handled here — Suspense re-renders can unmount the caller and drop 45 // mutate-level callbacks, leaving the loading toast stuck forever. 46 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 47 onSuccess: (_data, variables) => { 48 if (variables.notificationId) { 49 const notificationId = variables.notificationId; 50 notifications.update({ 51 id: notificationId, 52 color: 'green', 53 title: null, 54 message: ( 55 <Group 56 gap="xs" 57 justify="space-between" 58 wrap="nowrap" 59 align="center" 60 > 61 <Text size="sm">Card added</Text> 62 <Button 63 size="xs" 64 variant="light" 65 color="gray" 66 onClick={() => { 67 notifications.hide(notificationId); 68 router.push(`/url?id=${encodeURIComponent(variables.url)}`); 69 }} 70 > 71 View 72 </Button> 73 </Group> 74 ), 75 position: 'top-center', 76 loading: false, 77 autoClose: 5000, 78 icon: <BsCheck />, 79 }); 80 } 81 82 queryClient.invalidateQueries({ queryKey: cardKeys.all() }); 83 queryClient.invalidateQueries({ queryKey: noteKeys.all() }); 84 queryClient.invalidateQueries({ queryKey: feedKeys.all() }); 85 queryClient.invalidateQueries({ queryKey: sembleKeys.all() }); 86 queryClient.invalidateQueries({ queryKey: collectionKeys.mine() }); 87 queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() }); 88 queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 89 queryClient.invalidateQueries({ queryKey: connectionKeys.all() }); 90 queryClient.invalidateQueries({ queryKey: profileKeys.all() }); 91 queryClient.invalidateQueries({ 92 queryKey: collectionKeys.bySembleUrl(variables.url), 93 }); 94 // Invalidate URL metadata with stats to update tab counts 95 queryClient.invalidateQueries({ 96 queryKey: cardKeys.urlMetadata(variables.url, { includeStats: true }), 97 }); 98 99 // invalidate each collection query individually 100 variables.collectionIds?.forEach((id) => { 101 queryClient.invalidateQueries({ 102 queryKey: collectionKeys.collection(id), 103 }); 104 queryClient.invalidateQueries({ 105 queryKey: collectionKeys.infinite(id), 106 }); 107 }); 108 109 // Track card save event in PostHog 110 if (shouldCaptureAnalytics() && analyticsContext) { 111 const eventProperties: CardSaveEventProperties = { 112 save_source: analyticsContext.saveSource, 113 is_new_card: true, 114 has_note: !!variables.note, 115 collection_count: variables.collectionIds?.length || 0, 116 active_filters: analyticsContext.activeFilters 117 ? { 118 url_type: analyticsContext.activeFilters.urlType, 119 sort: analyticsContext.activeFilters.sort, 120 search_query: analyticsContext.activeFilters.searchQuery, 121 profile_filter: analyticsContext.activeFilters.profileFilter, 122 } 123 : undefined, 124 via_card_id: variables.viaCardId, 125 page_path: analyticsContext.pagePath, 126 }; 127 128 posthog.capture('card_saved', eventProperties); 129 130 // Clear super properties after capture 131 posthog.unregister('original_save_source'); 132 posthog.unregister('original_active_filters'); 133 } 134 }, 135 136 onError: (_error, variables) => { 137 if (variables.notificationId) { 138 notifications.update({ 139 id: variables.notificationId, 140 color: 'red', 141 title: 'Error', 142 message: 'Could not add card', 143 position: 'top-center', 144 loading: false, 145 autoClose: 5000, 146 withCloseButton: true, 147 icon: <BsExclamation />, 148 }); 149 } 150 }, 151 }); 152 153 return mutation; 154}