feat(21-03): rewire card click handlers to navigate to detail pages

- ItemCard navigates to /items/$itemId instead of opening edit panel
- CandidateCard navigates to /threads/$threadId/candidates/$candidateId
- CandidateListItem navigates to candidate detail page
- CatalogSearchOverlay cards navigate to /global-items/$globalItemId
- Add button on catalog cards uses stopPropagation to prevent navigation
This commit is contained in:
2026-04-06 15:07:51 +02:00
parent 2d71ce15af
commit 1f79c5ca3c
4 changed files with 55 additions and 10 deletions

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
import type { CandidateDelta } from "../hooks/useImpactDeltas"; import type { CandidateDelta } from "../hooks/useImpactDeltas";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
@@ -46,7 +47,7 @@ export function CandidateCard({
delta, delta,
}: CandidateCardProps) { }: CandidateCardProps) {
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const navigate = useNavigate();
const openConfirmDeleteCandidate = useUIStore( const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate, (s) => s.openConfirmDeleteCandidate,
); );
@@ -56,7 +57,12 @@ export function CandidateCard({
return ( return (
<button <button
type="button" type="button"
onClick={() => openCandidateEditPanel(id)} onClick={() =>
navigate({
to: "/threads/$threadId/candidates/$candidateId",
params: { threadId: String(threadId), candidateId: String(id) },
})
}
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group" className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
> >
{/* Hover-reveal action buttons */} {/* Hover-reveal action buttons */}

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { Reorder } from "framer-motion"; import { Reorder } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
@@ -60,7 +61,7 @@ export function CandidateListItem({
}: CandidateListItemProps) { }: CandidateListItemProps) {
const isDragging = useRef(false); const isDragging = useRef(false);
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const navigate = useNavigate();
const openConfirmDeleteCandidate = useUIStore( const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate, (s) => s.openConfirmDeleteCandidate,
); );
@@ -104,7 +105,13 @@ export function CandidateListItem({
type="button" type="button"
onClick={() => { onClick={() => {
if (isDragging.current) return; if (isDragging.current) return;
openCandidateEditPanel(candidate.id); navigate({
to: "/threads/$threadId/candidates/$candidateId",
params: {
threadId: String(candidate.threadId),
candidateId: String(candidate.id),
},
});
}} }}
className="flex-1 min-w-0 text-left" className="flex-1 min-w-0 text-left"
> >

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -7,6 +8,7 @@ import { useTags } from "../hooks/useTags";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
export function CatalogSearchOverlay() { export function CatalogSearchOverlay() {
const navigate = useNavigate();
const catalogSearchOpen = useUIStore((s) => s.catalogSearchOpen); const catalogSearchOpen = useUIStore((s) => s.catalogSearchOpen);
const catalogSearchMode = useUIStore((s) => s.catalogSearchMode); const catalogSearchMode = useUIStore((s) => s.catalogSearchMode);
const closeCatalogSearch = useUIStore((s) => s.closeCatalogSearch); const closeCatalogSearch = useUIStore((s) => s.closeCatalogSearch);
@@ -138,7 +140,25 @@ export function CatalogSearchOverlay() {
{items.map((item) => ( {items.map((item) => (
<div <div
key={item.id} key={item.id}
className="bg-white rounded-xl border border-gray-100 overflow-hidden" className="bg-white rounded-xl border border-gray-100 overflow-hidden cursor-pointer hover:border-gray-200 hover:shadow-sm transition-all"
onClick={() => {
closeCatalogSearch();
navigate({
to: "/global-items/$globalItemId",
params: { globalItemId: String(item.id) },
});
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
closeCatalogSearch();
navigate({
to: "/global-items/$globalItemId",
params: { globalItemId: String(item.id) },
});
}
}}
role="button"
tabIndex={0}
> >
<div className="aspect-[4/3] bg-gray-50"> <div className="aspect-[4/3] bg-gray-50">
{item.imageUrl ? ( {item.imageUrl ? (
@@ -191,7 +211,10 @@ export function CatalogSearchOverlay() {
</div> </div>
<button <button
type="button" type="button"
onClick={handleAddStub} onClick={(e) => {
e.stopPropagation();
handleAddStub();
}}
className="bg-gray-700 text-white rounded-lg px-3 py-1.5 text-xs font-medium hover:bg-gray-800 transition-colors" className="bg-gray-700 text-white rounded-lg px-3 py-1.5 text-xs font-medium hover:bg-gray-800 transition-colors"
> >
Add Add

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
import { useDuplicateItem } from "../hooks/useItems"; import { useDuplicateItem } from "../hooks/useItems";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
@@ -36,14 +37,16 @@ export function ItemCard({
onClassificationCycle, onClassificationCycle,
}: ItemCardProps) { }: ItemCardProps) {
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openEditPanel = useUIStore((s) => s.openEditPanel); const navigate = useNavigate();
const openExternalLink = useUIStore((s) => s.openExternalLink); const openExternalLink = useUIStore((s) => s.openExternalLink);
const duplicateItem = useDuplicateItem(); const duplicateItem = useDuplicateItem();
return ( return (
<button <button
type="button" type="button"
onClick={() => openEditPanel(id)} onClick={() =>
navigate({ to: "/items/$itemId", params: { itemId: String(id) } })
}
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group" className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
> >
{!onRemove && ( {!onRemove && (
@@ -54,7 +57,10 @@ export function ItemCard({
e.stopPropagation(); e.stopPropagation();
duplicateItem.mutate(id, { duplicateItem.mutate(id, {
onSuccess: (newItem) => { onSuccess: (newItem) => {
openEditPanel(newItem.id); navigate({
to: "/items/$itemId",
params: { itemId: String(newItem.id) },
});
}, },
}); });
}} }}
@@ -63,7 +69,10 @@ export function ItemCard({
e.stopPropagation(); e.stopPropagation();
duplicateItem.mutate(id, { duplicateItem.mutate(id, {
onSuccess: (newItem) => { onSuccess: (newItem) => {
openEditPanel(newItem.id); navigate({
to: "/items/$itemId",
params: { itemId: String(newItem.id) },
});
}, },
}); });
} }