Files
temp_SSA_SCAN/app/page.tsx
2025-08-27 14:12:38 -06:00

165 lines
6.5 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import { Camera, History, VideoOff } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; // Assuming shadcn/ui has Select
import { SHOE_DATABASE, type Shoe } from '@/lib/shoe-database';
import { detectShoe } from '@/lib/ml-classification';
import { addToHistory, getHistory } from '@/lib/history-storage';
import ShoeResultsPopup from '@/components/shoe-results-popup';
import HistorySidebar from '@/components/history-sidebar';
type CameraStatus = 'idle' | 'active' | 'denied' | 'no_devices';
export default function HomePage() {
const videoRef = useRef<HTMLVideoElement>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const [cameraStatus, setCameraStatus] = useState<CameraStatus>('idle');
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
const [activeShoe, setActiveShoe] = useState<Shoe | null>(null);
const [isPopupOpen, setPopupOpen] = useState(false);
const [isHistoryOpen, setHistoryOpen] = useState(false);
const [history, setHistory] = useState<Shoe[]>([]);
// Effect to clean up the stream when component unmounts or stream changes
useEffect(() => {
return () => {
stream?.getTracks().forEach((track) => track.stop());
};
}, [stream]);
const startStream = async (deviceId: string) => {
// Stop previous stream if it exists
stream?.getTracks().forEach((track) => track.stop());
try {
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } },
});
if (videoRef.current) {
videoRef.current.srcObject = newStream;
}
setStream(newStream);
setCameraStatus('active');
} catch (err) {
console.error("Error starting stream: ", err);
setCameraStatus('denied');
}
};
const handleOpenCamera = async () => {
setHistory(getHistory());
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoInputs = devices.filter((d) => d.kind === 'videoinput');
if (videoInputs.length === 0) {
setCameraStatus('no_devices');
return;
}
setVideoDevices(videoInputs);
const firstDeviceId = videoInputs[0].deviceId;
setSelectedDeviceId(firstDeviceId);
await startStream(firstDeviceId);
} catch (err) {
console.error("Error enumerating devices: ", err);
setCameraStatus('denied');
}
};
const handleCameraChange = (deviceId: string) => {
setSelectedDeviceId(deviceId);
startStream(deviceId);
};
const handleScan = () => {
const detected = detectShoe(SHOE_DATABASE);
if (detected) {
setActiveShoe(detected);
const updatedHistory = addToHistory(detected);
setHistory(updatedHistory);
setPopupOpen(true);
}
};
const handleHistoryItemClick = (shoe: Shoe) => {
setActiveShoe(shoe);
setHistoryOpen(false);
setPopupOpen(true);
};
const renderContent = () => {
switch (cameraStatus) {
case 'active':
return (
<>
<video ref={videoRef} autoPlay playsInline muted onCanPlay={() => videoRef.current?.play()} className="h-full w-full object-cover" />
<div className="absolute inset-0 flex flex-col items-center justify-between p-6">
<div className="flex w-full items-start justify-between gap-2">
<div className="w-64">
<Select value={selectedDeviceId} onValueChange={handleCameraChange}>
<SelectTrigger className="w-full bg-black/50 text-white border-white/30">
<SelectValue placeholder="Seleccionar cámara..." />
</SelectTrigger>
<SelectContent>
{videoDevices.map((device) => (
<SelectItem key={device.deviceId} value={device.deviceId}>
{device.label || `Cámara ${videoDevices.indexOf(device) + 1}`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button variant="ghost" size="icon" className="text-white hover:bg-white/20 hover:text-white" onClick={() => setHistoryOpen(true)}>
<History size={24} />
</Button>
</div>
<div className="flex flex-col items-center">
<Button size="lg" className="h-16 w-16 rounded-full border-4 border-white bg-transparent text-white shadow-lg hover:bg-white/20" onClick={handleScan}>
<Camera size={32} />
</Button>
</div>
</div>
</>
);
case 'no_devices':
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
<VideoOff size={48} className="mb-4 text-red-500" />
<h1 className="text-xl font-semibold">No se encontraron cámaras</h1>
<p className="text-gray-400">Asegúrate de que tu cámara esté conectada.</p>
</div>
);
case 'denied':
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
<Camera size={48} className="mb-4 text-red-500" />
<h1 className="text-xl font-semibold">Acceso a la cámara denegado</h1>
<p className="text-gray-400">Por favor, habilita el permiso en tu navegador.</p>
</div>
);
case 'idle':
default:
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
<h1 className="mb-4 text-3xl font-bold">Detector de Zapatos</h1>
<p className="mb-8 text-gray-400">Haz clic para iniciar la cámara y escanear</p>
<Button size="lg" onClick={handleOpenCamera}>
<Camera className="mr-2 h-4 w-4" /> Abrir Cámara
</Button>
</div>
);
}
};
return (
<main className="relative h-screen w-screen bg-black overflow-hidden">
{renderContent()}
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} shoe={activeShoe} />
<HistorySidebar isOpen={isHistoryOpen} onOpenChange={setHistoryOpen} history={history} onItemClick={handleHistoryItemClick} />
</main>
);
}