'use client' import { useState, useRef, useCallback, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Camera, X, RotateCcw, Check, Loader2, SunMedium, Move, Focus } from 'lucide-react' import type { CameraQualityFeedback } from '@/types' interface ReceiptCameraProps { onCapture: (imageData: string, mimeType: string) => Promise onClose: () => void } export default function ReceiptCamera({ onCapture, onClose }: ReceiptCameraProps) { const videoRef = useRef(null) const canvasRef = useRef(null) const streamRef = useRef(null) const [isStreaming, setIsStreaming] = useState(false) const [capturedImage, setCapturedImage] = useState(null) const [isProcessing, setIsProcessing] = useState(false) const [error, setError] = useState(null) const [quality, setQuality] = useState({ lightingOk: false, distanceOk: false, focusOk: false, readyToCapture: false, }) // Start camera stream const startCamera = useCallback(async () => { try { setError(null) const constraints: MediaStreamConstraints = { video: { facingMode: 'environment', // Use back camera on mobile width: { ideal: 1920 }, height: { ideal: 1080 }, }, } const stream = await navigator.mediaDevices.getUserMedia(constraints) streamRef.current = stream if (videoRef.current) { videoRef.current.srcObject = stream try { await videoRef.current.play() setIsStreaming(true) } catch (playErr) { // Ignore AbortError - this happens when stream is interrupted (e.g., component unmount) if (playErr instanceof Error && playErr.name === 'AbortError') { console.log('Video play interrupted - this is normal during navigation') return } throw playErr } } } catch (err) { console.error('Camera access error:', err) // Don't show error for AbortError if (err instanceof Error && err.name === 'AbortError') return setError('Kunde inte komma åt kameran. Kontrollera att du har gett tillåtelse.') } }, []) // Stop camera stream const stopCamera = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()) streamRef.current = null } setIsStreaming(false) }, []) // Analyze video frame for quality feedback const analyzeQuality = useCallback(() => { if (!videoRef.current || !canvasRef.current || !isStreaming) return const video = videoRef.current const canvas = canvasRef.current const ctx = canvas.getContext('2d') if (!ctx) return // Set canvas size to match video canvas.width = video.videoWidth / 4 // Downsample for analysis canvas.height = video.videoHeight / 4 // Draw current frame ctx.drawImage(video, 0, 0, canvas.width, canvas.height) // Get image data for analysis const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) const data = imageData.data // Calculate average brightness let totalBrightness = 0 for (let i = 0; i < data.length; i += 4) { const r = data[i] const g = data[i + 1] const b = data[i + 2] totalBrightness += (r + g + b) / 3 } const avgBrightness = totalBrightness / (data.length / 4) // Calculate contrast (standard deviation of brightness) let sumSquares = 0 for (let i = 0; i < data.length; i += 4) { const r = data[i] const g = data[i + 1] const b = data[i + 2] const brightness = (r + g + b) / 3 sumSquares += Math.pow(brightness - avgBrightness, 2) } const contrast = Math.sqrt(sumSquares / (data.length / 4)) // Quality assessment const lightingOk = avgBrightness > 80 && avgBrightness < 200 const focusOk = contrast > 30 // Higher contrast suggests better focus const distanceOk = true // Would need edge detection for proper distance check const newQuality: CameraQualityFeedback = { lightingOk, distanceOk, focusOk, readyToCapture: lightingOk && focusOk && distanceOk, } if (!lightingOk) { newQuality.message = avgBrightness < 80 ? 'För mörkt - öka ljuset' : 'För ljust - minska ljuset' } else if (!focusOk) { newQuality.message = 'Försök hålla kameran stilla' } setQuality(newQuality) }, [isStreaming]) // Quality analysis loop useEffect(() => { if (!isStreaming) return const interval = setInterval(analyzeQuality, 500) return () => clearInterval(interval) }, [isStreaming, analyzeQuality]) // Start camera on mount useEffect(() => { startCamera() return () => stopCamera() }, [startCamera, stopCamera]) // Capture image const captureImage = useCallback(() => { if (!videoRef.current || !canvasRef.current) return const video = videoRef.current const canvas = canvasRef.current const ctx = canvas.getContext('2d') if (!ctx) return // Set canvas to full resolution canvas.width = video.videoWidth canvas.height = video.videoHeight // Draw the current video frame ctx.drawImage(video, 0, 0) // Convert to base64 const imageData = canvas.toDataURL('image/jpeg', 0.9) setCapturedImage(imageData) stopCamera() }, [stopCamera]) // Retake photo const retake = useCallback(() => { setCapturedImage(null) startCamera() }, [startCamera]) // Confirm and upload const confirmCapture = async () => { if (!capturedImage) return setIsProcessing(true) try { // Extract base64 data (remove data:image/jpeg;base64, prefix) const base64Data = capturedImage.split(',')[1] await onCapture(base64Data, 'image/jpeg') } catch (err) { console.error('Upload error:', err) setError('Kunde inte ladda upp bilden. Försök igen.') setIsProcessing(false) } } // Render captured image review if (capturedImage) { return (
{/* Header - fixed at top */}

Granska bild

{/* Image preview - centered */}
Captured receipt
{/* Action buttons - fixed at bottom */}
) } // Render camera view return (
{/* Header - fixed at top */}

Skanna kvitto

{/* Video preview - fills the screen */}