Skip to content

Commit 28b4e7a

Browse files
committed
feat(web): add optimistic UI updates for mutations (task-37)
1 parent 5c00042 commit 28b4e7a

File tree

3 files changed

+424
-32
lines changed

3 files changed

+424
-32
lines changed

apps/web/src/hooks/use-user-interactions.ts

Lines changed: 149 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { useLocation } from "@tanstack/react-router";
1515
import { useCallback, useEffect, useRef, useState } from "react";
1616

17+
import { useNotifications } from "@/contexts/NotificationContext";
1718
import { serializeSearch } from "@/utils/url-decoding";
1819

1920
const USER_INTERACTIONS_LOGGER_CONTEXT = "user-interactions";
@@ -108,6 +109,9 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
108109
// Get router location - must be called unconditionally at top level (React hooks rules)
109110
const location = useLocation();
110111

112+
// Get notifications for user feedback
113+
const { showNotification } = useNotifications();
114+
111115
// State
112116
const [recentHistory, setRecentHistory] = useState<CatalogueEntity[]>([]);
113117
const [bookmarks, setBookmarks] = useState<CatalogueEntity[]>([]);
@@ -381,16 +385,28 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
381385
throw new Error("Entity ID and type are required to bookmark");
382386
}
383387

388+
// Optimistic update
389+
const wasBookmarked = isBookmarked;
390+
setIsBookmarked(true);
391+
384392
try {
385393
await catalogueService.addBookmark({
386394
entityType: entityType as EntityType,
387395
entityId: entityId,
388396
notes: tags ? `${notes || ''}\n\nTags: ${tags.join(', ')}` : notes,
389397
});
390398

391-
setIsBookmarked(true);
399+
showNotification({
400+
title: "Success",
401+
message: "Added to bookmarks",
402+
category: "success",
403+
});
404+
392405
await refreshData();
393406
} catch (error) {
407+
// Rollback optimistic update
408+
setIsBookmarked(wasBookmarked);
409+
394410
logger.error(
395411
USER_INTERACTIONS_LOGGER_CONTEXT,
396412
"Failed to bookmark entity",
@@ -400,6 +416,13 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
400416
error,
401417
},
402418
);
419+
420+
showNotification({
421+
title: "Error",
422+
message: `Failed to bookmark: ${error instanceof Error ? error.message : 'Unknown error'}`,
423+
category: "error",
424+
});
425+
403426
throw error;
404427
}
405428
},
@@ -409,6 +432,8 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
409432
location.pathname,
410433
location.search,
411434
refreshData,
435+
isBookmarked,
436+
showNotification,
412437
],
413438
);
414439

@@ -417,6 +442,10 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
417442
throw new Error("Entity ID and type are required to unbookmark");
418443
}
419444

445+
// Optimistic update
446+
const wasBookmarked = isBookmarked;
447+
setIsBookmarked(false);
448+
420449
try {
421450
// Find the bookmark record for this entity
422451
const allBookmarks = await catalogueService.getBookmarks();
@@ -426,10 +455,19 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
426455

427456
if (bookmark?.id) {
428457
await catalogueService.removeBookmark(bookmark.id);
429-
setIsBookmarked(false);
458+
459+
showNotification({
460+
title: "Success",
461+
message: "Removed from bookmarks",
462+
category: "success",
463+
});
464+
430465
await refreshData();
431466
}
432467
} catch (error) {
468+
// Rollback optimistic update
469+
setIsBookmarked(wasBookmarked);
470+
433471
logger.error(
434472
USER_INTERACTIONS_LOGGER_CONTEXT,
435473
"Failed to unbookmark entity",
@@ -439,9 +477,16 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
439477
error,
440478
},
441479
);
480+
481+
showNotification({
482+
title: "Error",
483+
message: `Failed to remove bookmark: ${error instanceof Error ? error.message : 'Unknown error'}`,
484+
category: "error",
485+
});
486+
442487
throw error;
443488
}
444-
}, [entityId, entityType, refreshData]);
489+
}, [entityId, entityType, refreshData, isBookmarked, showNotification]);
445490

446491
const bookmarkSearch = useCallback(
447492
async ({
@@ -468,6 +513,13 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
468513
});
469514

470515
setIsBookmarked(true);
516+
517+
showNotification({
518+
title: "Success",
519+
message: "Saved search to bookmarks",
520+
category: "success",
521+
});
522+
471523
await refreshData();
472524
} catch (error) {
473525
logger.error(
@@ -479,10 +531,17 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
479531
error,
480532
},
481533
);
534+
535+
showNotification({
536+
title: "Error",
537+
message: `Failed to save search: ${error instanceof Error ? error.message : 'Unknown error'}`,
538+
category: "error",
539+
});
540+
482541
throw error;
483542
}
484543
},
485-
[location.pathname, location.search, refreshData],
544+
[location.pathname, location.search, refreshData, showNotification],
486545
);
487546

488547
const bookmarkList = useCallback(
@@ -508,6 +567,13 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
508567
});
509568

510569
setIsBookmarked(true);
570+
571+
showNotification({
572+
title: "Success",
573+
message: "Saved list to bookmarks",
574+
category: "success",
575+
});
576+
511577
await refreshData();
512578
} catch (error) {
513579
logger.error(
@@ -518,17 +584,27 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
518584
error,
519585
},
520586
);
587+
588+
showNotification({
589+
title: "Error",
590+
message: `Failed to save list: ${error instanceof Error ? error.message : 'Unknown error'}`,
591+
category: "error",
592+
});
593+
521594
throw error;
522595
}
523596
},
524-
[refreshData],
597+
[refreshData, showNotification],
525598
);
526599

527600
const unbookmarkSearch = useCallback(async () => {
528601
if (!searchQuery) {
529602
throw new Error("Search query is required to unbookmark");
530603
}
531604

605+
const wasBookmarked = isBookmarked;
606+
setIsBookmarked(false);
607+
532608
try {
533609
// Find the bookmark record for this search
534610
const allBookmarks = await catalogueService.getBookmarks();
@@ -539,10 +615,19 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
539615

540616
if (bookmark?.id) {
541617
await catalogueService.removeBookmark(bookmark.id);
542-
setIsBookmarked(false);
618+
619+
showNotification({
620+
title: "Success",
621+
message: "Removed saved search from bookmarks",
622+
category: "success",
623+
});
624+
543625
await refreshData();
544626
}
545627
} catch (error) {
628+
// Rollback optimistic update
629+
setIsBookmarked(wasBookmarked);
630+
546631
logger.error(
547632
USER_INTERACTIONS_LOGGER_CONTEXT,
548633
"Failed to unbookmark search",
@@ -552,15 +637,25 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
552637
error,
553638
},
554639
);
640+
641+
showNotification({
642+
title: "Error",
643+
message: `Failed to remove: ${error instanceof Error ? error.message : 'Unknown error'}`,
644+
category: "error",
645+
});
646+
555647
throw error;
556648
}
557-
}, [searchQuery, filters, refreshData]);
649+
}, [searchQuery, filters, refreshData, isBookmarked, showNotification]);
558650

559651
const unbookmarkList = useCallback(async () => {
560652
if (!url) {
561653
throw new Error("URL is required to unbookmark list");
562654
}
563655

656+
const wasBookmarked = isBookmarked;
657+
setIsBookmarked(false);
658+
564659
try {
565660
// Find the bookmark record for this list
566661
const allBookmarks = await catalogueService.getBookmarks();
@@ -571,10 +666,19 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
571666

572667
if (bookmark?.id) {
573668
await catalogueService.removeBookmark(bookmark.id);
574-
setIsBookmarked(false);
669+
670+
showNotification({
671+
title: "Success",
672+
message: "Removed saved list from bookmarks",
673+
category: "success",
674+
});
675+
575676
await refreshData();
576677
}
577678
} catch (error) {
679+
// Rollback optimistic update
680+
setIsBookmarked(wasBookmarked);
681+
578682
logger.error(
579683
USER_INTERACTIONS_LOGGER_CONTEXT,
580684
"Failed to unbookmark list",
@@ -583,9 +687,16 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
583687
error,
584688
},
585689
);
690+
691+
showNotification({
692+
title: "Error",
693+
message: `Failed to remove: ${error instanceof Error ? error.message : 'Unknown error'}`,
694+
category: "error",
695+
});
696+
586697
throw error;
587698
}
588-
}, [url, refreshData]);
699+
}, [url, refreshData, isBookmarked, showNotification]);
589700

590701
const updateBookmark = useCallback(
591702
async (
@@ -680,6 +791,12 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
680791
});
681792
}
682793

794+
showNotification({
795+
title: "Success",
796+
message: `Removed ${success} bookmark${success !== 1 ? 's' : ''}${failed > 0 ? ` (${failed} failed)` : ''}`,
797+
category: "success",
798+
});
799+
683800
await refreshData(); // Refresh data after bulk operation
684801
return { success, failed };
685802
} catch (error) {
@@ -691,25 +808,46 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
691808
error,
692809
},
693810
);
811+
812+
showNotification({
813+
title: "Error",
814+
message: `Bulk remove failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
815+
category: "error",
816+
});
817+
694818
throw error;
695819
}
696820
},
697-
[refreshData],
821+
[refreshData, showNotification],
698822
);
699823

700824
const clearHistory = useCallback(async () => {
701825
try {
702826
await catalogueService.clearHistory();
827+
828+
showNotification({
829+
title: "Success",
830+
message: "History cleared",
831+
category: "success",
832+
});
833+
703834
await refreshData();
704835
} catch (error) {
705836
logger.error(
706837
USER_INTERACTIONS_LOGGER_CONTEXT,
707838
"Failed to clear history",
708839
{ error }
709840
);
841+
842+
showNotification({
843+
title: "Error",
844+
message: `Failed to clear history: ${error instanceof Error ? error.message : 'Unknown error'}`,
845+
category: "error",
846+
});
847+
710848
throw error;
711849
}
712-
}, [refreshData]);
850+
}, [refreshData, showNotification]);
713851

714852
return {
715853
// Page visit tracking (now using catalogue history)

0 commit comments

Comments
 (0)