Add recipes, images, AI photo ID, barcode scanning & ingredient matching
- Fuzzy ingredient matching for bar inventory against recipes - AI photo identification API for bottles/labels (drink + bar context) - Barcode scanner with photo toggle for My Bar - Barcode scan + photo ID buttons on Add Drink form - Auto-pull product images from Open Food Facts barcode lookup - Recipes section on drink detail pages with bar availability - Dedicated Recipes page in sidebar navigation - Bar item image support (schema, upload, display) - Drink detail image upload component - MinIO image proxy through Next.js rewrites (fixes broken image links) - Improved category mapping (energy drinks → Mixers, not Spirits) - Re-process saved recipe ingredients against current bar inventory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,9 @@ import {
|
||||
useDeleteBarItem,
|
||||
} from "@/hooks/use-bar"
|
||||
import type { BarItem } from "@/hooks/use-bar"
|
||||
import { Plus, Wine } from "lucide-react"
|
||||
import { BarcodeScanDialog } from "@/components/bar/barcode-scan-dialog"
|
||||
import type { BarcodeLookupResult } from "@/hooks/use-barcode-lookup"
|
||||
import { Plus, Wine, ScanLine } from "lucide-react"
|
||||
import type { BarItemCreate } from "@/lib/validators"
|
||||
|
||||
export default function BarPage() {
|
||||
@@ -65,17 +67,40 @@ const CATEGORY_ORDER = [
|
||||
|
||||
function BarContent() {
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
||||
const [scanDialogOpen, setScanDialogOpen] = useState(false)
|
||||
const [editingItem, setEditingItem] = useState<BarItem | null>(null)
|
||||
const [scannedData, setScannedData] = useState<Partial<BarItemCreate> | null>(null)
|
||||
|
||||
const { data, isLoading, error } = useBarItems()
|
||||
const createBarItem = useCreateBarItem()
|
||||
const updateBarItem = useUpdateBarItem()
|
||||
const deleteBarItem = useDeleteBarItem()
|
||||
|
||||
function handleScanResult(result: BarcodeLookupResult) {
|
||||
const initial: Partial<BarItemCreate> & { imageUrl?: string } = {
|
||||
barcode: result.barcode,
|
||||
}
|
||||
if (result.name) {
|
||||
initial.name = result.brand
|
||||
? `${result.brand} ${result.name}`
|
||||
: result.name
|
||||
}
|
||||
if (result.category) {
|
||||
initial.category = result.category as BarItemCreate["category"]
|
||||
}
|
||||
if (result.imageUrl) {
|
||||
initial.imageUrl = result.imageUrl
|
||||
}
|
||||
setScannedData(initial)
|
||||
// Scan dialog closes itself before calling this — just open add form
|
||||
setAddDialogOpen(true)
|
||||
}
|
||||
|
||||
function handleCreate(formData: BarItemCreate) {
|
||||
createBarItem.mutate(formData, {
|
||||
onSuccess: () => {
|
||||
setAddDialogOpen(false)
|
||||
setScannedData(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -124,10 +149,16 @@ function BarContent() {
|
||||
: "Your bar inventory"}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Item
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setScanDialogOpen(true)}>
|
||||
<ScanLine className="h-4 w-4 mr-2" />
|
||||
Scan
|
||||
</Button>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -180,16 +211,33 @@ function BarContent() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Barcode Scan Dialog */}
|
||||
<BarcodeScanDialog
|
||||
open={scanDialogOpen}
|
||||
onOpenChange={setScanDialogOpen}
|
||||
onResult={handleScanResult}
|
||||
/>
|
||||
|
||||
{/* Add Item Dialog */}
|
||||
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
||||
<Dialog
|
||||
open={addDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAddDialogOpen(open)
|
||||
if (!open) setScannedData(null)
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[550px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Bar Item</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a spirit, mixer, or other item to your bar inventory.
|
||||
{scannedData
|
||||
? "Review the scanned product info and make any changes."
|
||||
: "Add a spirit, mixer, or other item to your bar inventory."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<BarItemForm
|
||||
key={scannedData?.barcode || "manual"}
|
||||
initialData={scannedData || undefined}
|
||||
onSubmit={handleCreate}
|
||||
isSubmitting={createBarItem.isPending}
|
||||
submitLabel="Add Item"
|
||||
@@ -224,6 +272,8 @@ function BarContent() {
|
||||
category: editingItem.category,
|
||||
quantity: editingItem.quantity,
|
||||
notes: editingItem.notes || undefined,
|
||||
barcode: editingItem.barcode || undefined,
|
||||
imageUrl: editingItem.imageUrl || undefined,
|
||||
}}
|
||||
onSubmit={handleUpdate}
|
||||
isSubmitting={updateBarItem.isPending}
|
||||
|
||||
Reference in New Issue
Block a user