CUB-194: scaffold Vite + React + TypeScript + Tailwind frontend
otto/review PR approved by Otto - frontend scaffold verified clean
ci/build Build + type-check + lint verified clean

- Initialize Vite project with React + TypeScript + Tailwind CSS
- Dark theme dashboard design (rig-dark palette, rig-accent colors)
- Minimal App with RemoteRig header + 'Dashboard coming soon' placeholder
- Directory structure: components, hooks, services, types, utils
- TypeScript types for Camera, CameraFeed, SystemHealth, StreamConfig
- Custom hooks: useCameraStatus, useSystemHealth (with mock data)
- API service layer with proxy config for /api → localhost:8080
- eslint, postcss, autoprefixer configured
- lucide-react icons integrated (Camera icon)
- .env.example, .gitignore, tsconfig basepaths configured
- Build + type-check + lint verified clean
This commit is contained in:
2026-05-19 07:31:23 -04:00
commit 5793617be3
23 changed files with 4335 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { Camera } from 'lucide-react'
function App() {
return (
<div className="min-h-screen bg-rig-dark-900">
{/* Header */}
<header className="border-b border-rig-dark-700 bg-rig-dark-800/50 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-3">
<Camera className="h-7 w-7 text-rig-accent" />
<h1 className="text-xl font-bold tracking-tight text-rig-dark-50">
RemoteRig
</h1>
<span className="ml-2 rounded-full bg-rig-accent/10 px-2.5 py-0.5 text-xs font-medium text-rig-accent">
Dashboard
</span>
</div>
</div>
</header>
{/* Main Content */}
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-rig-dark-600 bg-rig-dark-800/30 py-24 text-center">
<Camera className="mb-4 h-12 w-12 text-rig-dark-500" />
<h2 className="text-lg font-semibold text-rig-dark-200">
Dashboard Coming Soon
</h2>
<p className="mt-2 max-w-sm text-sm text-rig-dark-400">
Camera monitoring and remote control interface under construction.
</p>
</div>
</main>
{/* Footer */}
<footer className="border-t border-rig-dark-700 bg-rig-dark-800/30">
<div className="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
<p className="text-center text-xs text-rig-dark-500">
RemoteRig v0.1.0 &mdash; Multi-Camera Remote Monitoring System
</p>
</div>
</footer>
</div>
)
}
export default App
+2
View File
@@ -0,0 +1,2 @@
export { useCameraStatus } from './useCameraStatus'
export { useSystemHealth } from './useSystemHealth'
+23
View File
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react'
import type { Camera } from '../types'
const MOCK_CAMERAS: Camera[] = [
{ id: 'cam-1', name: 'Front View', status: 'online', position: 'North', fps: 30, resolution: '1920x1080', streamUrl: '/api/cameras/cam-1/stream', recording: true },
{ id: 'cam-2', name: 'Side View', status: 'online', position: 'East', fps: 30, resolution: '1920x1080', streamUrl: '/api/cameras/cam-2/stream', recording: false },
{ id: 'cam-3', name: 'Top View', status: 'connecting', position: 'Center', fps: 15, streamUrl: '/api/cameras/cam-3/stream', recording: false },
]
export function useCameraStatus() {
const [cameras, setCameras] = useState<Camera[]>(MOCK_CAMERAS)
useEffect(() => {
const interval = setInterval(() => {
setCameras((prev) =>
prev.map((c) => ({ ...c })),
)
}, 5000)
return () => clearInterval(interval)
}, [])
return { cameras }
}
+28
View File
@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react'
import type { SystemHealth } from '../types'
const MOCK_HEALTH: SystemHealth = {
cpuUsage: 23,
memoryUsage: 45,
gpuUsage: 12,
temperature: 58,
uptime: '2d 14h 32m',
}
export function useSystemHealth() {
const [health, setHealth] = useState<SystemHealth>(MOCK_HEALTH)
useEffect(() => {
const interval = setInterval(() => {
setHealth((prev) => ({
...prev,
cpuUsage: Math.min(100, Math.max(0, prev.cpuUsage + Math.round((Math.random() - 0.5) * 10))),
memoryUsage: Math.min(100, Math.max(0, prev.memoryUsage + Math.round((Math.random() - 0.5) * 4))),
temperature: Math.min(95, Math.max(30, prev.temperature + Math.round((Math.random() - 0.5) * 3))),
}))
}, 3000)
return () => clearInterval(interval)
}, [])
return { health }
}
+30
View File
@@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Dashboard-specific resets */
html {
@apply antialiased;
}
body {
@apply bg-rig-dark-900 text-rig-dark-100;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@apply bg-rig-dark-800;
}
::-webkit-scrollbar-thumb {
@apply bg-rig-dark-600 rounded;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-rig-dark-500;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+20
View File
@@ -0,0 +1,20 @@
const API_BASE = import.meta.env.VITE_API_URL || '/api'
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options,
})
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`)
}
return response.json()
}
export const api = {
getCameras: () => request<[]>('/cameras'),
getCameraStatus: (id: string) => request<[]>(`/cameras/${id}/status`),
getSystemHealth: () => request<[]>('/system/health'),
toggleRecording: (cameraId: string) =>
request<[]>(`/cameras/${cameraId}/recording`, { method: 'POST' }),
}
+35
View File
@@ -0,0 +1,35 @@
// RemoteRig TypeScript types
export interface Camera {
id: string
name: string
streamUrl: string
status: 'online' | 'offline' | 'error' | 'connecting'
position?: string
fps: number
resolution?: string
recording: boolean
}
export interface CameraFeed {
cameraId: string
thumbnailUrl?: string
frameRate: number
bitrate?: string
}
export interface SystemHealth {
cpuUsage: number
memoryUsage: number
gpuUsage: number
temperature: number
uptime: string
}
export interface StreamConfig {
width: number
height: number
fps: number
codec: string
bitrate: string
}
+25
View File
@@ -0,0 +1,25 @@
export function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const parts: string[] = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
parts.push(`${minutes}m`)
return parts.join(' ')
}
export function statusColor(status: string): string {
switch (status) {
case 'online':
return 'bg-rig-success'
case 'offline':
return 'bg-rig-danger'
case 'error':
return 'bg-rig-danger'
case 'connecting':
return 'bg-rig-warning'
default:
return 'bg-rig-dark-500'
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />