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 { 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}