All checks were successful
CI / ci (push) Successful in 15s
Run biome check --write --unsafe to fix tabs, import ordering, and non-null assertions across entire codebase. Disable a11y rules not applicable to this single-user app. Exclude auto-generated routeTree. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
import { useState } from "react";
|
|
import { useDeleteCategory, useUpdateCategory } from "../hooks/useCategories";
|
|
import { formatPrice, formatWeight } from "../lib/formatters";
|
|
import { LucideIcon } from "../lib/iconData";
|
|
import { IconPicker } from "./IconPicker";
|
|
|
|
interface CategoryHeaderProps {
|
|
categoryId: number;
|
|
name: string;
|
|
icon: string;
|
|
totalWeight: number;
|
|
totalCost: number;
|
|
itemCount: number;
|
|
}
|
|
|
|
export function CategoryHeader({
|
|
categoryId,
|
|
name,
|
|
icon,
|
|
totalWeight,
|
|
totalCost,
|
|
itemCount,
|
|
}: CategoryHeaderProps) {
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [editName, setEditName] = useState(name);
|
|
const [editIcon, setEditIcon] = useState(icon);
|
|
const updateCategory = useUpdateCategory();
|
|
const deleteCategory = useDeleteCategory();
|
|
|
|
const isUncategorized = categoryId === 1;
|
|
|
|
function handleSave() {
|
|
if (!editName.trim()) return;
|
|
updateCategory.mutate(
|
|
{ id: categoryId, name: editName.trim(), icon: editIcon },
|
|
{ onSuccess: () => setIsEditing(false) },
|
|
);
|
|
}
|
|
|
|
function handleDelete() {
|
|
if (
|
|
confirm(
|
|
`Delete category "${name}"? Items will be moved to Uncategorized.`,
|
|
)
|
|
) {
|
|
deleteCategory.mutate(categoryId);
|
|
}
|
|
}
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<div className="flex items-center gap-3 py-4">
|
|
<IconPicker value={editIcon} onChange={setEditIcon} size="sm" />
|
|
<input
|
|
type="text"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
className="text-lg font-semibold border border-gray-200 rounded-md px-2 py-1"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") handleSave();
|
|
if (e.key === "Escape") setIsEditing(false);
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
|
|
>
|
|
Save
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsEditing(false)}
|
|
className="text-sm text-gray-400 hover:text-gray-600"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="group flex items-center gap-3 py-4">
|
|
<LucideIcon name={icon} size={22} className="text-gray-500" />
|
|
<h2 className="text-lg font-semibold text-gray-900">{name}</h2>
|
|
<span className="text-sm text-gray-400">
|
|
{itemCount} {itemCount === 1 ? "item" : "items"} ·{" "}
|
|
{formatWeight(totalWeight)} · {formatPrice(totalCost)}
|
|
</span>
|
|
{!isUncategorized && (
|
|
<div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setEditName(name);
|
|
setEditIcon(icon);
|
|
setIsEditing(true);
|
|
}}
|
|
className="p-1 text-gray-400 hover:text-gray-600 rounded"
|
|
title="Edit category"
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleDelete}
|
|
className="p-1 text-gray-400 hover:text-red-500 rounded"
|
|
title="Delete category"
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|