Placeholder skeleton added and permission mgnmt
This commit is contained in:
204
app/page.tsx
204
app/page.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user