From 87fe94037ea554ccc42df56c53dd3863c02123fb Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 15 Mar 2026 18:48:27 +0100 Subject: [PATCH] feat: add external link confirmation dialog for product URLs Show an external link icon on ItemCard and CandidateCard that opens a confirmation dialog before navigating to product URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/components/CandidateCard.tsx | 33 +++++++++- src/client/components/ExternalLinkDialog.tsx | 65 ++++++++++++++++++++ src/client/components/ItemCard.tsx | 35 +++++++++++ src/client/routes/__root.tsx | 4 ++ src/client/stores/uiStore.ts | 10 +++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/client/components/ExternalLinkDialog.tsx diff --git a/src/client/components/CandidateCard.tsx b/src/client/components/CandidateCard.tsx index 0cec639..30a84ac 100644 --- a/src/client/components/CandidateCard.tsx +++ b/src/client/components/CandidateCard.tsx @@ -10,6 +10,7 @@ interface CandidateCardProps { categoryName: string; categoryIcon: string; imageFilename: string | null; + productUrl?: string | null; threadId: number; isActive: boolean; } @@ -22,6 +23,7 @@ export function CandidateCard({ categoryName, categoryIcon, imageFilename, + productUrl, threadId, isActive, }: CandidateCardProps) { @@ -30,9 +32,38 @@ export function CandidateCard({ (s) => s.openConfirmDeleteCandidate, ); const openResolveDialog = useUIStore((s) => s.openResolveDialog); + const openExternalLink = useUIStore((s) => s.openExternalLink); return ( -
+
+ {productUrl && ( + openExternalLink(productUrl)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + openExternalLink(productUrl); + } + }} + className="absolute top-2 right-2 z-10 w-6 h-6 flex items-center justify-center rounded-full bg-gray-100/80 text-gray-400 hover:bg-blue-100 hover:text-blue-500 opacity-0 group-hover:opacity-100 transition-all cursor-pointer" + title="Open product link" + > + + + + + )}
{imageFilename ? ( s.externalLinkUrl); + const closeExternalLink = useUIStore((s) => s.closeExternalLink); + + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") closeExternalLink(); + } + if (externalLinkUrl) { + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + } + }, [externalLinkUrl, closeExternalLink]); + + if (!externalLinkUrl) return null; + + function handleContinue() { + if (externalLinkUrl) { + window.open(externalLinkUrl, "_blank", "noopener,noreferrer"); + } + closeExternalLink(); + } + + return ( +
+
{ + if (e.key === "Escape") closeExternalLink(); + }} + /> +
+

+ You are about to leave GearBox +

+

+ You will be redirected to: +

+

+ {externalLinkUrl} +

+
+ + +
+
+
+ ); +} diff --git a/src/client/components/ItemCard.tsx b/src/client/components/ItemCard.tsx index 0115d1a..77fd23d 100644 --- a/src/client/components/ItemCard.tsx +++ b/src/client/components/ItemCard.tsx @@ -10,6 +10,7 @@ interface ItemCardProps { categoryName: string; categoryIcon: string; imageFilename: string | null; + productUrl?: string | null; onRemove?: () => void; } @@ -21,9 +22,11 @@ export function ItemCard({ categoryName, categoryIcon, imageFilename, + productUrl, onRemove, }: ItemCardProps) { const openEditPanel = useUIStore((s) => s.openEditPanel); + const openExternalLink = useUIStore((s) => s.openExternalLink); return (