- Changed VITE_SUPABASE_URL in .env.example for deployment consistency. - Added new user management functionality to reset user passwords in UserManagement component. - Updated supabase.ts to include first and last name fields in user profiles and added password reset functionality. - Enhanced DashboardLayout to include a user profile view and improved user display in TopNavbar. - Updated seed.sql to create additional users with roles for testing purposes.
145 lines
4.7 KiB
TypeScript
Executable File
145 lines
4.7 KiB
TypeScript
Executable File
import { useState, useEffect, useRef } from 'react'
|
|
|
|
interface LiveCameraViewProps {
|
|
cameraName: string
|
|
}
|
|
|
|
export function LiveCameraView({ cameraName }: LiveCameraViewProps) {
|
|
const [isStreaming, setIsStreaming] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const imgRef = useRef<HTMLImageElement>(null)
|
|
|
|
const API_BASE = import.meta.env.VITE_VISION_API_URL || 'http://localhost:8000'
|
|
|
|
useEffect(() => {
|
|
startStreaming()
|
|
return () => stopStreaming()
|
|
}, [cameraName])
|
|
|
|
const startStreaming = async () => {
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
// Start the stream
|
|
const response = await fetch(`${API_BASE}/cameras/${cameraName}/start-stream`, {
|
|
method: 'POST'
|
|
})
|
|
|
|
if (response.ok) {
|
|
setIsStreaming(true)
|
|
// Set the stream source with timestamp to prevent caching
|
|
if (imgRef.current) {
|
|
imgRef.current.src = `${API_BASE}/cameras/${cameraName}/stream?t=${Date.now()}`
|
|
}
|
|
} else {
|
|
throw new Error(`Failed to start stream: ${response.statusText}`)
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Failed to start stream'
|
|
setError(errorMessage)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const stopStreaming = async () => {
|
|
try {
|
|
if (isStreaming) {
|
|
await fetch(`${API_BASE}/cameras/${cameraName}/stop-stream`, {
|
|
method: 'POST'
|
|
})
|
|
setIsStreaming(false)
|
|
}
|
|
} catch (err) {
|
|
console.error('Error stopping stream:', err)
|
|
}
|
|
}
|
|
|
|
const handleImageError = () => {
|
|
setError('Failed to load camera stream')
|
|
}
|
|
|
|
const handleImageLoad = () => {
|
|
setError(null)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
|
<div className="text-center text-white">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto"></div>
|
|
<p className="mt-4">Starting camera stream...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
|
<div className="text-center text-white">
|
|
<div className="bg-red-600 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-semibold mb-2">Stream Error</h2>
|
|
<p className="text-gray-300 mb-4">{error}</p>
|
|
<button
|
|
onClick={startStreaming}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
|
<div className="relative">
|
|
{/* Camera Label */}
|
|
<div className="absolute top-4 left-4 z-10">
|
|
<div className="bg-black bg-opacity-75 text-white px-3 py-1 rounded-md text-sm font-medium">
|
|
{cameraName} - Live View
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live Stream */}
|
|
<img
|
|
ref={imgRef}
|
|
alt={`Live stream from ${cameraName}`}
|
|
className="max-w-full max-h-screen object-contain"
|
|
onError={handleImageError}
|
|
onLoad={handleImageLoad}
|
|
/>
|
|
|
|
{/* Status Indicator */}
|
|
<div className="absolute bottom-4 right-4 z-10">
|
|
<div className="flex items-center space-x-2 bg-black bg-opacity-75 text-white px-3 py-1 rounded-md">
|
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
<span className="text-sm">LIVE</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|