Files
maternal-app/maternal-web/components/common/PhotoUpload.tsx
Andrei f6c1483a36
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
fix: Switch to base64 photo upload for compatibility
The MinIO/Sharp approach doesn't work on the current server CPU architecture.
Switched to simple base64 encoding for photo uploads.

Changes:
- PhotoUpload component converts images to base64 data URLs
- 5MB file size limit
- Works on all platforms without external dependencies
- Stores photos directly in database (photoUrl field)

This is a temporary solution. For production scalability, we can:
- Upgrade server CPU to support Sharp
- Build Sharp from source
- Use Docker with prebuilt Sharp binaries
- Migrate to a proper CDN/object storage later

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 08:40:02 +00:00

186 lines
4.6 KiB
TypeScript

'use client';
import { useState, useRef } from 'react';
import {
Box,
Avatar,
IconButton,
TextField,
Typography,
Paper,
Alert,
CircularProgress,
} from '@mui/material';
import { PhotoCamera, Person } from '@mui/icons-material';
interface PhotoUploadProps {
value: string;
onChange: (url: string) => void;
label: string;
disabled?: boolean;
size?: number;
}
export function PhotoUpload({
value,
onChange,
label,
disabled = false,
size = 100
}: PhotoUploadProps) {
const [imageError, setImageError] = useState(false);
const [uploadError, setUploadError] = useState<string>('');
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImageError = () => {
setImageError(true);
};
const handleImageLoad = () => {
setImageError(false);
};
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
setUploadError('Please select an image file');
return;
}
// Validate file size (max 5MB for base64)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
setUploadError('Image size must be less than 5MB');
return;
}
try {
setUploading(true);
setUploadError('');
// Convert to base64 (simple, works everywhere)
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target?.result as string;
onChange(base64String);
setUploading(false);
setImageError(false);
};
reader.onerror = () => {
setUploadError('Failed to read image file');
setUploading(false);
};
reader.readAsDataURL(file);
} catch (error: any) {
console.error('Photo processing failed:', error);
setUploadError('Failed to process photo');
setUploading(false);
} finally {
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleCameraClick = () => {
fileInputRef.current?.click();
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }}>
<Paper
elevation={0}
sx={{
p: 2,
border: 1,
borderColor: 'divider',
borderRadius: 2,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2
}}
>
<Typography variant="body2" color="text.secondary">
{label}
</Typography>
{uploadError && (
<Alert severity="error" onClose={() => setUploadError('')} sx={{ width: '100%' }}>
{uploadError}
</Alert>
)}
<Box sx={{ position: 'relative' }}>
<Avatar
src={!imageError && value ? value : undefined}
sx={{
width: size,
height: size,
bgcolor: 'primary.light',
fontSize: size / 3,
}}
onError={handleImageError}
onLoad={handleImageLoad}
>
{!value || imageError ? <Person sx={{ fontSize: size / 2 }} /> : null}
</Avatar>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileSelect}
disabled={disabled || uploading}
/>
<IconButton
sx={{
position: 'absolute',
bottom: -4,
right: -4,
bgcolor: 'background.paper',
border: 2,
borderColor: 'divider',
'&:hover': {
bgcolor: 'action.hover',
},
}}
size="small"
disabled={disabled || uploading}
onClick={handleCameraClick}
>
{uploading ? (
<CircularProgress size={20} />
) : (
<PhotoCamera fontSize="small" />
)}
</IconButton>
</Box>
<TextField
label="Photo URL"
value={value}
onChange={(e) => onChange(e.target.value)}
fullWidth
size="small"
placeholder="https://example.com/photo.jpg"
disabled={disabled || uploading}
helperText={
uploading
? 'Processing image...'
: 'Click camera to upload or paste an image URL'
}
/>
</Paper>
</Box>
);
}