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