This repository has no description
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 { BsCheck, BsExclamation } from 'react-icons/bs';
18
19export default function useAddCard(
20 analyticsContext?: CardSaveAnalyticsContext,
21) {
22 const queryClient = useQueryClient();
23
24 const mutation = useMutation({
25 mutationFn: async (newCard: {
26 url: string;
27 note?: string;
28 collectionIds?: string[];
29 viaCardId?: string;
30 notificationId?: string;
31 }) => {
32 return addUrlToLibrary(newCard.url, {
33 note: newCard.note,
34 collectionIds: newCard.collectionIds,
35 viaCardId: newCard.viaCardId,
36 });
37 },
38
39 // Generally, do UI things (redirects, toasts) in mutate-level callbacks so they
40 // don't fire if the user navigated away. But loading toasts that need .update()
41 // must be handled here — Suspense re-renders can unmount the caller and drop
42 // mutate-level callbacks, leaving the loading toast stuck forever.
43 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire
44 onSuccess: (_data, variables) => {
45 if (variables.notificationId) {
46 notifications.update({
47 id: variables.notificationId,
48 color: 'green',
49 title: 'Success!',
50 message: 'Card added',
51 position: 'top-center',
52 loading: false,
53 autoClose: 2000,
54 icon: <BsCheck />,
55 });
56 }
57
58 queryClient.invalidateQueries({ queryKey: cardKeys.all() });
59 queryClient.invalidateQueries({ queryKey: noteKeys.all() });
60 queryClient.invalidateQueries({ queryKey: feedKeys.all() });
61 queryClient.invalidateQueries({ queryKey: sembleKeys.all() });
62 queryClient.invalidateQueries({ queryKey: collectionKeys.mine() });
63 queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
64 queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
65 queryClient.invalidateQueries({ queryKey: connectionKeys.all() });
66 queryClient.invalidateQueries({ queryKey: profileKeys.all() });
67 queryClient.invalidateQueries({
68 queryKey: collectionKeys.bySembleUrl(variables.url),
69 });
70 // Invalidate URL metadata with stats to update tab counts
71 queryClient.invalidateQueries({
72 queryKey: cardKeys.urlMetadata(variables.url, { includeStats: true }),
73 });
74
75 // invalidate each collection query individually
76 variables.collectionIds?.forEach((id) => {
77 queryClient.invalidateQueries({
78 queryKey: collectionKeys.collection(id),
79 });
80 queryClient.invalidateQueries({
81 queryKey: collectionKeys.infinite(id),
82 });
83 });
84
85 // Track card save event in PostHog
86 if (shouldCaptureAnalytics() && analyticsContext) {
87 const eventProperties: CardSaveEventProperties = {
88 save_source: analyticsContext.saveSource,
89 is_new_card: true,
90 has_note: !!variables.note,
91 collection_count: variables.collectionIds?.length || 0,
92 active_filters: analyticsContext.activeFilters
93 ? {
94 url_type: analyticsContext.activeFilters.urlType,
95 sort: analyticsContext.activeFilters.sort,
96 search_query: analyticsContext.activeFilters.searchQuery,
97 profile_filter: analyticsContext.activeFilters.profileFilter,
98 }
99 : undefined,
100 via_card_id: variables.viaCardId,
101 page_path: analyticsContext.pagePath,
102 };
103
104 posthog.capture('card_saved', eventProperties);
105
106 // Clear super properties after capture
107 posthog.unregister('original_save_source');
108 posthog.unregister('original_active_filters');
109 }
110 },
111
112 onError: (_error, variables) => {
113 if (variables.notificationId) {
114 notifications.update({
115 id: variables.notificationId,
116 color: 'red',
117 title: 'Error',
118 message: 'Could not add card',
119 position: 'top-center',
120 loading: false,
121 autoClose: 5000,
122 withCloseButton: true,
123 icon: <BsExclamation />,
124 });
125 }
126 },
127 });
128
129 return mutation;
130}