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
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:
145
frontend/src/shared/components/CameraCapture/GuidanceOverlay.tsx
Normal file
145
frontend/src/shared/components/CameraCapture/GuidanceOverlay.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user