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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(null)
|
||||
const [showCamera, setShowCamera] = useState(false)
|
||||
const [hasGetUserMedia, setHasGetUserMedia] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const cameraInputRef = useRef<HTMLInputElement>(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 (
|
||||
<CameraCapture
|
||||
onCapture={(file) => {
|
||||
setShowCamera(false)
|
||||
handleFile(file)
|
||||
}}
|
||||
onClose={() => setShowCamera(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
@@ -125,6 +151,21 @@ export function DrinkImageUpload({
|
||||
) : (
|
||||
<>
|
||||
<ImagePlus className="h-8 w-8 text-muted-foreground" />
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
// Without getUserMedia, fall back to the file input's capture
|
||||
// hint, which opens the camera app on mobile.
|
||||
if (hasGetUserMedia) setShowCamera(true)
|
||||
else cameraInputRef.current?.click()
|
||||
}}
|
||||
>
|
||||
<Camera className="h-4 w-4" />
|
||||
Take Photo
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -133,6 +174,7 @@ export function DrinkImageUpload({
|
||||
>
|
||||
Upload Photo
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
or drag and drop an image here
|
||||
</p>
|
||||
@@ -143,6 +185,17 @@ export function DrinkImageUpload({
|
||||
|
||||
{error && <p className="text-sm text-destructive mt-1.5">{error}</p>}
|
||||
|
||||
<input
|
||||
ref={cameraInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,image/heic"
|
||||
capture="environment"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) handleFile(file)
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
||||
@@ -82,7 +82,7 @@ export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
||||
<div className="flex flex-col items-center gap-4 py-12 text-center">
|
||||
<Camera className="h-12 w-12 text-muted-foreground" />
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -103,6 +103,7 @@ export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
||||
{/* Overlay controls */}
|
||||
<div className="absolute bottom-0 inset-x-0 flex items-center justify-center gap-4 p-4 bg-gradient-to-t from-black/60 to-transparent">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:bg-white/20"
|
||||
@@ -111,6 +112,7 @@ export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="rounded-full h-16 w-16 bg-white hover:bg-white/90"
|
||||
onClick={handleCapture}
|
||||
@@ -118,6 +120,7 @@ export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
||||
<Camera className="h-6 w-6 text-black" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:bg-white/20"
|
||||
|
||||
Reference in New Issue
Block a user