Placeholder skeleton added and permission mgnmt

This commit is contained in:
2025-08-27 19:48:43 -06:00
parent 82785f24c7
commit 082036b1f9

View File

@@ -10,12 +10,12 @@ 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';
type CameraStatus = 'loading' | '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 [cameraStatus, setCameraStatus] = useState<CameraStatus>('loading');
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
@@ -40,9 +40,9 @@ export default function HomePage() {
}
}, [stream, cameraStatus]); // Runs when stream or camera status changes
// Effect to enumerate devices and auto-load camera on mount
// Effect to automatically initialize camera on app start
useEffect(() => {
const loadDevicesAndCamera = async () => {
const initializeCamera = async () => {
try {
// First request permissions to get stable deviceIds
const permissionStream = await navigator.mediaDevices.getUserMedia({ video: true });
@@ -52,28 +52,24 @@ export default function HomePage() {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoInputs = devices.filter(d => d.kind === 'videoinput');
setVideoDevices(videoInputs); // Always populate the dropdown
console.log(videoInputs)
const storedDeviceId = localStorage.getItem('selectedCameraDeviceId');
if (storedDeviceId && videoInputs.length > 0) {
// Try to find camera by stable deviceId
const matchedDevice = videoInputs.find(d => d.deviceId === storedDeviceId);
if (matchedDevice) {
setSelectedDeviceId(matchedDevice.deviceId);
startStream(matchedDevice.deviceId);
} else {
// If stored device not found, use first available
const firstDevice = videoInputs[0];
setSelectedDeviceId(firstDevice.deviceId);
localStorage.setItem('selectedCameraDeviceId', firstDevice.deviceId);
startStream(firstDevice.deviceId);
}
} else if (videoInputs.length > 0) {
// No stored device, start with first available
const firstDevice = videoInputs[0];
setSelectedDeviceId(firstDevice.deviceId);
localStorage.setItem('selectedCameraDeviceId', firstDevice.deviceId);
startStream(firstDevice.deviceId);
if (videoInputs.length === 0) {
setCameraStatus('no_devices');
return;
}
// Load history
setHistory(getHistory());
// Try to use stored device, otherwise use first available
const storedDeviceId = localStorage.getItem('selectedCameraDeviceId');
const targetDevice = (storedDeviceId && videoInputs.find(d => d.deviceId === storedDeviceId))
|| videoInputs[0];
setSelectedDeviceId(targetDevice.deviceId);
localStorage.setItem('selectedCameraDeviceId', targetDevice.deviceId);
await startStream(targetDevice.deviceId);
} catch (err) {
// Permission denied or no camera
console.error("Error accessing camera or enumerating devices:", err);
@@ -81,8 +77,8 @@ export default function HomePage() {
}
};
loadDevicesAndCamera();
}, []); // Empty dependency array ensures this runs only once on mount
initializeCamera();
}, []); // Auto-run on mount
const startStream = async (deviceId: string) => {
// Stop previous stream if it exists
@@ -112,25 +108,6 @@ export default function HomePage() {
}
};
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 firstDevice = videoInputs[0];
setSelectedDeviceId(firstDevice.deviceId);
localStorage.setItem('selectedCameraDeviceId', firstDevice.deviceId); // Save stable deviceId
await startStream(firstDevice.deviceId);
} catch (err) {
console.error("Error enumerating devices: ", err);
setCameraStatus('denied');
}
};
const handleCameraChange = (deviceId: string) => {
console.log("SAVING: ",deviceId)
@@ -157,6 +134,75 @@ export default function HomePage() {
const renderContent = () => {
switch (cameraStatus) {
case 'loading':
return (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-gray-900 via-black to-gray-800">
{/* Modern Loading Skeleton */}
<div className="relative w-full h-full">
{/* Animated Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/20 via-purple-900/20 to-gray-900/20"></div>
{/* Pulsing Camera Icon */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
{/* Outer Ring */}
<div className="w-32 h-32 rounded-full border-4 border-white/20 animate-pulse"></div>
{/* Middle Ring */}
<div className="absolute inset-4 w-24 h-24 rounded-full border-2 border-blue-500/40 animate-ping"></div>
{/* Inner Ring */}
<div className="absolute inset-8 w-16 h-16 rounded-full border-2 border-white/60 animate-pulse"></div>
{/* Camera Icon */}
<div className="absolute inset-0 flex items-center justify-center">
<Camera size={32} className="text-white animate-pulse" />
</div>
</div>
</div>
{/* Loading Text */}
<div className="absolute bottom-1/3 left-1/2 transform -translate-x-1/2 text-center">
<div className="mb-4">
<div className="flex space-x-1 justify-center">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
<h2 className="text-xl font-bold text-white mb-2 animate-pulse">Iniciando Smart Store Asistant: Scan</h2>
<p className="text-gray-400 animate-pulse">Accediendo a la cámara...</p>
</div>
{/* Skeleton UI Elements */}
<div className="absolute top-6 left-6 right-6">
{/* Top Bar Skeleton */}
<div className="flex justify-between items-center mb-4">
<div className="w-64 h-12 bg-white/10 rounded-lg animate-pulse"></div>
<div className="w-12 h-12 bg-white/10 rounded-lg animate-pulse"></div>
</div>
</div>
{/* Bottom Button Skeleton */}
<div className="absolute bottom-20 left-1/2 transform -translate-x-1/2">
<div className="w-16 h-16 bg-white/20 rounded-full animate-pulse"></div>
</div>
{/* Particle Effect */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(20)].map((_, i) => (
<div
key={i}
className="absolute w-1 h-1 bg-blue-500/30 rounded-full animate-ping"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 2}s`,
animationDuration: `${2 + Math.random() * 2}s`,
}}
></div>
))}
</div>
</div>
</div>
);
case 'active':
return (
<>
@@ -191,29 +237,67 @@ export default function HomePage() {
);
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 className="flex h-full w-full flex-col items-center justify-center bg-gradient-to-br from-gray-900 via-black to-gray-800">
<div className="text-center p-8">
<div className="relative mb-6">
<div className="w-24 h-24 mx-auto rounded-full bg-red-500/20 flex items-center justify-center mb-4">
<VideoOff size={48} className="text-red-400" />
</div>
<div className="absolute inset-0 w-24 h-24 mx-auto rounded-full border-2 border-red-400/30 animate-ping"></div>
</div>
<h1 className="text-2xl font-bold text-white mb-3">No se encontraron cámaras</h1>
<p className="text-gray-400 text-lg mb-6 max-w-md mx-auto">
Asegúrate de que tu cámara esté conectada y funcionando correctamente.
</p>
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 max-w-sm mx-auto">
<p className="text-red-300 text-sm">
💡 Verifica la conexión de tu cámara web o cámara integrada
</p>
</div>
</div>
</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 className="flex h-full w-full flex-col items-center justify-center bg-gradient-to-br from-gray-900 via-black to-gray-800">
<div className="text-center p-8">
<div className="relative mb-6">
<div className="w-24 h-24 mx-auto rounded-full bg-yellow-500/20 flex items-center justify-center mb-4">
<Camera size={48} className="text-yellow-400" />
</div>
<div className="absolute inset-0 w-24 h-24 mx-auto rounded-full border-2 border-yellow-400/30 animate-pulse"></div>
</div>
<h1 className="text-2xl font-bold text-white mb-3">Acceso a la cámara requerido</h1>
<p className="text-gray-400 text-lg mb-6 max-w-md mx-auto">
Necesitamos acceso a tu cámara para detectar zapatos.
</p>
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 max-w-sm mx-auto mb-4">
<p className="text-yellow-300 text-sm mb-2">
🔒 Habilita el permiso de cámara:
</p>
<ol className="text-yellow-300/80 text-xs text-left space-y-1">
<li>1. Haz clic en el ícono de la cámara en la barra de direcciones</li>
<li>2. Selecciona "Permitir"</li>
<li>3. Recarga la página</li>
</ol>
</div>
<button
onClick={() => window.location.reload()}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
>
Recargar página
</button>
</div>
</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 className="text-center">
<VideoOff size={48} className="mb-4 text-gray-500 mx-auto" />
<h1 className="text-xl font-semibold mb-2">Estado desconocido</h1>
<p className="text-gray-400">Por favor, recarga la página</p>
</div>
</div>
);
}