From 593f68138cd59e7e74e594233af59767ec73cc3b Mon Sep 17 00:00:00 2001 From: JP Date: Sat, 8 Aug 2026 19:16:20 +0000 Subject: [PATCH] Add a camera option wherever a photo can be attached The bar add-item form could only pick an existing file, so adding a bottle meant taking a photo first and then hunting for it. The scan flow already had a working camera; this reuses that CameraCapture component rather than adding a second implementation. The change is in DrinkImageUpload, which the bar form, the drink form and the drink detail view all share, so all three gain the camera. Falls back to a file input with capture="environment" when getUserMedia is unavailable - it needs a secure context, so it is absent when the app is reached over plain http on the LAN. Also sets type="button" on CameraCapture's controls. They previously had no type, which defaults to submit, so capturing a photo inside the bar item form would have submitted the form. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/drinks/drink-image-upload.tsx | 73 +++++++++++++++++--- src/components/scan/camera-capture.tsx | 5 +- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/components/drinks/drink-image-upload.tsx b/src/components/drinks/drink-image-upload.tsx index 9244f81..bab344f 100644 --- a/src/components/drinks/drink-image-upload.tsx +++ b/src/components/drinks/drink-image-upload.tsx @@ -1,8 +1,9 @@ "use client" -import { useRef, useState, useCallback } from "react" -import { ImagePlus, X, Loader2 } from "lucide-react" +import { useRef, useState, useCallback, useEffect } from "react" +import { Camera, ImagePlus, X, Loader2 } from "lucide-react" import { Button } from "@/components/ui/button" +import { CameraCapture } from "@/components/scan/camera-capture" import { cn } from "@/lib/utils" interface DrinkImageUploadProps { @@ -17,7 +18,19 @@ export function DrinkImageUpload({ const [isUploading, setIsUploading] = useState(false) const [dragActive, setDragActive] = useState(false) const [error, setError] = useState(null) + const [showCamera, setShowCamera] = useState(false) + const [hasGetUserMedia, setHasGetUserMedia] = useState(false) const fileInputRef = useRef(null) + const cameraInputRef = useRef(null) + + // getUserMedia needs a secure context, so it is absent when the app is reached + // over plain http on the LAN. Checked after mount because it is not available + // during server rendering. + useEffect(() => { + setHasGetUserMedia( + typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia + ) + }, []) const uploadFile = useCallback( async (file: File) => { @@ -76,6 +89,19 @@ export function DrinkImageUpload({ onImageChange(null) setError(null) if (fileInputRef.current) fileInputRef.current.value = "" + if (cameraInputRef.current) cameraInputRef.current.value = "" + } + + if (showCamera) { + return ( + { + setShowCamera(false) + handleFile(file) + }} + onClose={() => setShowCamera(false)} + /> + ) } if (imageUrl) { @@ -125,14 +151,30 @@ export function DrinkImageUpload({ ) : ( <> - +
+ + +

or drag and drop an image here

@@ -143,6 +185,17 @@ export function DrinkImageUpload({ {error &&

{error}

} + { + const file = e.target.files?.[0] + if (file) handleFile(file) + }} + />

{error}

- @@ -103,6 +103,7 @@ export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) { {/* Overlay controls */}