Files
motovaultpro/frontend/src/shared/components/CameraCapture/CameraViewfinder.tsx
Eric Gullickson 7c8b6fda2a
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m14s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 33s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m19s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
feat: add camera capture component (refs #66)
Implements a reusable React camera capture component with:
- getUserMedia API for camera access on mobile and desktop
- Translucent aspect-ratio guidance overlays (VIN ~6:1, receipt ~2:3)
- Post-capture crop tool with draggable handles
- File input fallback for desktop and unsupported browsers
- Support for HEIC, JPEG, PNG (sent as-is to server)
- Full mobile responsiveness (320px - 1920px)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:05:18 -06:00

179 lines
4.5 KiB
TypeScript

/**
* @ai-summary Camera viewfinder with live preview and capture controls
* @ai-context Displays video stream with guidance overlay and capture/cancel buttons
*/
import React, { useRef, useEffect } from 'react';
import { Box, IconButton, Typography } from '@mui/material';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
import CloseIcon from '@mui/icons-material/Close';
import CameraswitchIcon from '@mui/icons-material/Cameraswitch';
import type { CameraViewfinderProps } from './types';
import { GuidanceOverlay } from './GuidanceOverlay';
export const CameraViewfinder: React.FC<CameraViewfinderProps> = ({
stream,
guidanceType,
onCapture,
onCancel,
onSwitchCamera,
canSwitchCamera,
facingMode,
}) => {
const videoRef = useRef<HTMLVideoElement>(null);
// Attach stream to video element
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
return () => {
if (videoRef.current) {
videoRef.current.srcObject = null;
}
};
}, [stream]);
return (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
backgroundColor: 'black',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* Video preview */}
<Box
sx={{
flex: 1,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<video
ref={videoRef}
autoPlay
playsInline
muted
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
// Mirror front camera for natural preview
transform: facingMode === 'user' ? 'scaleX(-1)' : 'none',
}}
/>
{/* Guidance overlay */}
<GuidanceOverlay type={guidanceType} />
{/* Top bar with close and switch camera */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
background: 'linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)',
}}
>
<IconButton
onClick={onCancel}
aria-label="Cancel capture"
sx={{
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
}}
>
<CloseIcon />
</IconButton>
{canSwitchCamera && (
<IconButton
onClick={onSwitchCamera}
aria-label="Switch camera"
sx={{
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
}}
>
<CameraswitchIcon />
</IconButton>
)}
</Box>
</Box>
{/* Bottom controls */}
<Box
sx={{
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1,
py: 3,
px: 2,
background: 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.5) 100%)',
}}
>
{/* Capture button */}
<IconButton
onClick={onCapture}
aria-label="Take photo"
sx={{
width: 72,
height: 72,
backgroundColor: 'white',
border: '4px solid rgba(255, 255, 255, 0.5)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.9)',
},
'&:active': {
transform: 'scale(0.95)',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
},
transition: 'transform 0.1s ease',
}}
>
<CameraAltIcon
sx={{
fontSize: 32,
color: 'black',
}}
/>
</IconButton>
<Typography
variant="caption"
sx={{
color: 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
}}
>
Tap to capture
</Typography>
</Box>
</Box>
);
};
export default CameraViewfinder;