- Remove duplicate back arrow/header from ManualEntryForm (overlay already shows it) - Move ImageUpload to top of ManualEntryForm for visual cohesion - Change "Submit to Catalog?" from text link to checkbox-style toggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
228 lines
6.3 KiB
TypeScript
228 lines
6.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useCategories } from "../hooks/useCategories";
|
|
import { useCreateItem } from "../hooks/useItems";
|
|
import { CategoryPicker } from "./CategoryPicker";
|
|
import { ImageUpload } from "./ImageUpload";
|
|
|
|
interface ManualEntryFormProps {
|
|
initialName?: string;
|
|
onSuccess: (itemName: string) => void;
|
|
}
|
|
|
|
export function ManualEntryForm({
|
|
initialName,
|
|
onSuccess,
|
|
}: ManualEntryFormProps) {
|
|
const { data: categories } = useCategories();
|
|
const createItem = useCreateItem();
|
|
|
|
const [name, setName] = useState(initialName ?? "");
|
|
const [categoryId, setCategoryId] = useState<number | null>(null);
|
|
const [weightGrams, setWeightGrams] = useState("");
|
|
const [priceDollars, setPriceDollars] = useState("");
|
|
const [purchasePrice, setPurchasePrice] = useState("");
|
|
const [notes, setNotes] = useState("");
|
|
const [productUrl, setProductUrl] = useState("");
|
|
const [imageFilename, setImageFilename] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Pre-select first category when categories load
|
|
useEffect(() => {
|
|
if (categories && categories.length > 0 && categoryId === null) {
|
|
setCategoryId(categories[0].id);
|
|
}
|
|
}, [categories, categoryId]);
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
|
|
if (!name.trim()) {
|
|
setError("Name is required");
|
|
return;
|
|
}
|
|
if (categoryId === null) {
|
|
setError("Please select a category");
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
|
|
createItem.mutate(
|
|
{
|
|
name: name.trim(),
|
|
categoryId,
|
|
weightGrams: weightGrams ? Number(weightGrams) : undefined,
|
|
priceCents: priceDollars
|
|
? Math.round(Number(priceDollars) * 100)
|
|
: undefined,
|
|
purchasePriceCents: purchasePrice
|
|
? Math.round(Number(purchasePrice) * 100)
|
|
: undefined,
|
|
notes: notes || undefined,
|
|
productUrl: productUrl || undefined,
|
|
imageFilename: imageFilename ?? undefined,
|
|
},
|
|
{
|
|
onSuccess: (item) => {
|
|
onSuccess(item.name);
|
|
},
|
|
onError: (err) => {
|
|
setError(err instanceof Error ? err.message : "Failed to save");
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Image upload — hero position */}
|
|
<div>
|
|
<ImageUpload
|
|
value={imageFilename}
|
|
onChange={(filename) => setImageFilename(filename)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Name */}
|
|
<div>
|
|
<label
|
|
htmlFor="manual-name"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Name <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
id="manual-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Item name"
|
|
required
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Category
|
|
</label>
|
|
<CategoryPicker
|
|
value={categoryId ?? 0}
|
|
onChange={(id) => setCategoryId(id)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Weight and Price row */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label
|
|
htmlFor="manual-weight"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Weight (g)
|
|
</label>
|
|
<input
|
|
id="manual-weight"
|
|
type="number"
|
|
value={weightGrams}
|
|
onChange={(e) => setWeightGrams(e.target.value)}
|
|
placeholder="0"
|
|
min="0"
|
|
step="1"
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="manual-price"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
MSRP ($)
|
|
</label>
|
|
<input
|
|
id="manual-price"
|
|
type="number"
|
|
value={priceDollars}
|
|
onChange={(e) => setPriceDollars(e.target.value)}
|
|
placeholder="0.00"
|
|
min="0"
|
|
step="0.01"
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Purchase Price */}
|
|
<div>
|
|
<label
|
|
htmlFor="manual-purchase-price"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Purchase Price ($)
|
|
</label>
|
|
<input
|
|
id="manual-purchase-price"
|
|
type="number"
|
|
value={purchasePrice}
|
|
onChange={(e) => setPurchasePrice(e.target.value)}
|
|
placeholder="0.00"
|
|
min="0"
|
|
step="0.01"
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Product URL */}
|
|
<div>
|
|
<label
|
|
htmlFor="manual-product-url"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Product Link
|
|
</label>
|
|
<input
|
|
id="manual-product-url"
|
|
type="url"
|
|
value={productUrl}
|
|
onChange={(e) => setProductUrl(e.target.value)}
|
|
placeholder="https://..."
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div>
|
|
<label
|
|
htmlFor="manual-notes"
|
|
className="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Notes
|
|
</label>
|
|
<textarea
|
|
id="manual-notes"
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
placeholder="Optional notes..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent resize-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={createItem.isPending || !name.trim() || categoryId === null}
|
|
className="w-full px-4 py-2.5 text-sm font-medium text-white bg-gray-900 rounded-lg hover:bg-gray-800 disabled:opacity-50 transition-colors"
|
|
>
|
|
{createItem.isPending ? "Saving..." : "Add to Collection"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|