feat: add camera capture component (refs #66)
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

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>
This commit is contained in:
Eric Gullickson
2026-02-01 15:05:18 -06:00
parent 42e0fc1fce
commit 7c8b6fda2a
11 changed files with 2439 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/**
* @ai-summary Translucent guidance overlay for camera viewfinder
* @ai-context Displays aspect-ratio guides for VIN (~6:1), receipt (~2:3), and documents
*/
import React from 'react';
import { Box, Typography } from '@mui/material';
import type { GuidanceOverlayProps, GuidanceType } from './types';
import { GUIDANCE_CONFIGS } from './types';
/** Calculate overlay dimensions based on guidance type and container */
function getOverlayStyles(type: GuidanceType): React.CSSProperties {
if (type === 'none') {
return { display: 'none' };
}
const config = GUIDANCE_CONFIGS[type];
// For VIN (wide), use width-constrained layout
// For receipt/document (tall), use height-constrained layout
if (config.aspectRatio > 1) {
// Wide aspect ratio (VIN)
return {
width: '85%',
height: 'auto',
aspectRatio: `${config.aspectRatio}`,
maxHeight: '30%',
};
} else {
// Tall aspect ratio (receipt, document)
return {
height: '70%',
width: 'auto',
aspectRatio: `${config.aspectRatio}`,
maxWidth: '85%',
};
}
}
export const GuidanceOverlay: React.FC<GuidanceOverlayProps> = ({ type }) => {
if (type === 'none') {
return null;
}
const config = GUIDANCE_CONFIGS[type];
const overlayStyles = getOverlayStyles(type);
return (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 1,
}}
>
{/* Guide box */}
<Box
sx={{
...overlayStyles,
border: '2px dashed rgba(255, 255, 255, 0.7)',
borderRadius: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
boxShadow: 'inset 0 0 20px rgba(255, 255, 255, 0.1)',
position: 'relative',
}}
>
{/* Corner indicators */}
<CornerIndicator position="top-left" />
<CornerIndicator position="top-right" />
<CornerIndicator position="bottom-left" />
<CornerIndicator position="bottom-right" />
</Box>
{/* Instruction text */}
<Typography
variant="body2"
sx={{
color: 'white',
mt: 2,
px: 2,
py: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 1,
textAlign: 'center',
}}
>
{config.description}
</Typography>
</Box>
);
};
interface CornerIndicatorProps {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
}
const CornerIndicator: React.FC<CornerIndicatorProps> = ({ position }) => {
const size = 20;
const thickness = 3;
const positionStyles: Record<string, React.CSSProperties> = {
'top-left': {
top: -thickness / 2,
left: -thickness / 2,
borderTop: `${thickness}px solid white`,
borderLeft: `${thickness}px solid white`,
},
'top-right': {
top: -thickness / 2,
right: -thickness / 2,
borderTop: `${thickness}px solid white`,
borderRight: `${thickness}px solid white`,
},
'bottom-left': {
bottom: -thickness / 2,
left: -thickness / 2,
borderBottom: `${thickness}px solid white`,
borderLeft: `${thickness}px solid white`,
},
'bottom-right': {
bottom: -thickness / 2,
right: -thickness / 2,
borderBottom: `${thickness}px solid white`,
borderRight: `${thickness}px solid white`,
},
};
return (
<Box
sx={{
position: 'absolute',
width: size,
height: size,
...positionStyles[position],
}}
/>
);
};
export default GuidanceOverlay;