Socratic-Lens / src /components /CameraModal.tsx
Jainish1808
Initial commit - Socratic Lens
4000a4c
'use client';
import { useState, useRef, useEffect } from 'react';
import { X, Camera, RotateCcw, Check } from 'lucide-react';
import clsx from 'clsx';
interface CameraModalProps {
isOpen: boolean;
onClose: () => void;
onCapture: (file: File) => void;
}
export default function CameraModal({ isOpen, onClose, onCapture }: CameraModalProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('environment');
useEffect(() => {
if (isOpen) {
startCamera();
} else {
stopCamera();
setCapturedImage(null);
}
return () => stopCamera();
}, [isOpen, facingMode]);
const startCamera = async () => {
try {
setError(null);
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: facingMode,
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
});
setStream(mediaStream);
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
}
} catch (err) {
console.error('Camera error:', err);
setError('Unable to access camera. Please check permissions.');
}
};
const stopCamera = () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
setStream(null);
}
};
const switchCamera = () => {
setFacingMode(prev => prev === 'user' ? 'environment' : 'user');
};
const capturePhoto = () => {
if (videoRef.current && canvasRef.current) {
const video = videoRef.current;
const canvas = canvasRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(video, 0, 0);
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.9);
setCapturedImage(imageDataUrl);
stopCamera();
}
}
};
const retake = () => {
setCapturedImage(null);
startCamera();
};
const confirmCapture = () => {
if (capturedImage && canvasRef.current) {
canvasRef.current.toBlob((blob) => {
if (blob) {
const file = new File([blob], `photo_${Date.now()}.jpg`, { type: 'image/jpeg' });
onCapture(file);
onClose();
}
}, 'image/jpeg', 0.9);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm animate-fade-in-up">
<div className="w-full max-w-xl bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-800 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-neutral-800">
<div className="flex items-center gap-2">
<Camera size={20} className="text-sky-500" />
<h2 className="text-lg font-semibold text-white">Take Photo</h2>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-neutral-800 rounded-full"
>
<X size={20} className="text-gray-400" />
</button>
</div>
{/* Camera View */}
<div className="relative aspect-[4/3] bg-black">
{error ? (
<div className="absolute inset-0 flex items-center justify-center text-center p-6">
<div>
<Camera size={48} className="mx-auto mb-4 text-gray-600" />
<p className="text-gray-400">{error}</p>
</div>
</div>
) : capturedImage ? (
<img
src={capturedImage}
alt="Captured"
className="w-full h-full object-contain"
/>
) : (
<video
ref={videoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover"
/>
)}
<canvas ref={canvasRef} className="hidden" />
</div>
{/* Controls */}
<div className="p-4 flex items-center justify-center gap-4">
{!capturedImage ? (
<>
{/* Switch Camera */}
<button
onClick={switchCamera}
className="p-3 bg-neutral-800 hover:bg-neutral-700 rounded-full transition-colors"
title="Switch Camera"
>
<RotateCcw size={20} className="text-white" />
</button>
{/* Capture Button */}
<button
onClick={capturePhoto}
disabled={!stream}
className={clsx(
"w-16 h-16 rounded-full border-4 border-white flex items-center justify-center transition-all",
stream
? "bg-white hover:bg-gray-200 active:scale-95"
: "bg-gray-600 border-gray-600 cursor-not-allowed"
)}
>
<div className="w-12 h-12 rounded-full bg-sky-500"></div>
</button>
{/* Placeholder for symmetry */}
<div className="w-11"></div>
</>
) : (
<>
{/* Retake */}
<button
onClick={retake}
className="flex-1 py-3 bg-neutral-800 text-white rounded-xl font-medium hover:bg-neutral-700 flex items-center justify-center gap-2"
>
<RotateCcw size={18} />
Retake
</button>
{/* Confirm */}
<button
onClick={confirmCapture}
className="flex-1 py-3 bg-sky-500 text-white rounded-xl font-medium hover:bg-sky-600 flex items-center justify-center gap-2"
>
<Check size={18} />
Use Photo
</button>
</>
)}
</div>
</div>
</div>
);
}