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 ShoeResultsPopup from '@/components/shoe-results-popup';
|
||||||
import HistorySidebar from '@/components/history-sidebar';
|
import HistorySidebar from '@/components/history-sidebar';
|
||||||
|
|
||||||
type CameraStatus = 'idle' | 'active' | 'denied' | 'no_devices';
|
type CameraStatus = 'loading' | 'active' | 'denied' | 'no_devices';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [stream, setStream] = useState<MediaStream | null>(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 [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
|
||||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
|
||||||
@@ -40,9 +40,9 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [stream, cameraStatus]); // Runs when stream or camera status changes
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const loadDevicesAndCamera = async () => {
|
const initializeCamera = async () => {
|
||||||
try {
|
try {
|
||||||
// First request permissions to get stable deviceIds
|
// First request permissions to get stable deviceIds
|
||||||
const permissionStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
const permissionStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
@@ -52,28 +52,24 @@ export default function HomePage() {
|
|||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
const videoInputs = devices.filter(d => d.kind === 'videoinput');
|
const videoInputs = devices.filter(d => d.kind === 'videoinput');
|
||||||
setVideoDevices(videoInputs); // Always populate the dropdown
|
setVideoDevices(videoInputs); // Always populate the dropdown
|
||||||
console.log(videoInputs)
|
|
||||||
const storedDeviceId = localStorage.getItem('selectedCameraDeviceId');
|
if (videoInputs.length === 0) {
|
||||||
if (storedDeviceId && videoInputs.length > 0) {
|
setCameraStatus('no_devices');
|
||||||
// Try to find camera by stable deviceId
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (err) {
|
||||||
// Permission denied or no camera
|
// Permission denied or no camera
|
||||||
console.error("Error accessing camera or enumerating devices:", err);
|
console.error("Error accessing camera or enumerating devices:", err);
|
||||||
@@ -81,8 +77,8 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadDevicesAndCamera();
|
initializeCamera();
|
||||||
}, []); // Empty dependency array ensures this runs only once on mount
|
}, []); // Auto-run on mount
|
||||||
|
|
||||||
const startStream = async (deviceId: string) => {
|
const startStream = async (deviceId: string) => {
|
||||||
// Stop previous stream if it exists
|
// 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) => {
|
const handleCameraChange = (deviceId: string) => {
|
||||||
console.log("SAVING: ",deviceId)
|
console.log("SAVING: ",deviceId)
|
||||||
@@ -157,6 +134,75 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (cameraStatus) {
|
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':
|
case 'active':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -191,29 +237,67 @@ export default function HomePage() {
|
|||||||
);
|
);
|
||||||
case 'no_devices':
|
case 'no_devices':
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
<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">
|
||||||
<VideoOff size={48} className="mb-4 text-red-500" />
|
<div className="text-center p-8">
|
||||||
<h1 className="text-xl font-semibold">No se encontraron cámaras</h1>
|
<div className="relative mb-6">
|
||||||
<p className="text-gray-400">Asegúrate de que tu cámara esté conectada.</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
case 'denied':
|
case 'denied':
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
<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">
|
||||||
<Camera size={48} className="mb-4 text-red-500" />
|
<div className="text-center p-8">
|
||||||
<h1 className="text-xl font-semibold">Acceso a la cámara denegado</h1>
|
<div className="relative mb-6">
|
||||||
<p className="text-gray-400">Por favor, habilita el permiso en tu navegador.</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
case 'idle':
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
<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>
|
<div className="text-center">
|
||||||
<p className="mb-8 text-gray-400">Haz clic para iniciar la cámara y escanear</p>
|
<VideoOff size={48} className="mb-4 text-gray-500 mx-auto" />
|
||||||
<Button size="lg" onClick={handleOpenCamera}>
|
<h1 className="text-xl font-semibold mb-2">Estado desconocido</h1>
|
||||||
<Camera className="mr-2 h-4 w-4" /> Abrir Cámara
|
<p className="text-gray-400">Por favor, recarga la página</p>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user