feat: add item duplication with copy-and-edit workflow

Adds POST /api/items/:id/duplicate endpoint, useDuplicateItem hook, and a
Duplicate button on ItemCard (collection view only) that opens the new item
for editing immediately after creation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 18:07:20 +02:00
parent 818db73432
commit b9a06dd244
5 changed files with 136 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
import { useFormatters } from "../hooks/useFormatters";
import { useDuplicateItem } from "../hooks/useItems";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { ClassificationBadge } from "./ClassificationBadge";
@@ -35,6 +36,7 @@ export function ItemCard({
const { weight, price } = useFormatters();
const openEditPanel = useUIStore((s) => s.openEditPanel);
const openExternalLink = useUIStore((s) => s.openExternalLink);
const duplicateItem = useDuplicateItem();
return (
<button
@@ -42,6 +44,46 @@ export function ItemCard({
onClick={() => openEditPanel(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"
>
{!onRemove && (
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
duplicateItem.mutate(id, {
onSuccess: (newItem) => {
openEditPanel(newItem.id);
},
});
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
duplicateItem.mutate(id, {
onSuccess: (newItem) => {
openEditPanel(newItem.id);
},
});
}
}}
className={`absolute top-2 ${productUrl ? "right-10" : "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="Duplicate item"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</span>
)}
{productUrl && (
<span
role="button"