Add team and stream counts to footer with improved layout
- Added team and stream counts displayed when OBS disconnected - Added counts display alongside live status when OBS connected - Moved OFFLINE/IDLE status indicators to left column for better balance - Fixed green dot positioning to be properly next to "Connected" text - Added custom CSS for status dots since Tailwind classes weren't applying - Enhanced footer layout with better visual hierarchy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7f475680ae
commit
ec6ff1b570
3 changed files with 127 additions and 26 deletions
25
app/api/counts/route.ts
Normal file
25
app/api/counts/route.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDatabase } from '../../../lib/database';
|
||||||
|
import { TABLE_NAMES } from '../../../lib/constants';
|
||||||
|
import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';
|
||||||
|
|
||||||
|
async function getCountsHandler() {
|
||||||
|
try {
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Get counts in parallel
|
||||||
|
const [streamsResult, teamsResult] = await Promise.all([
|
||||||
|
db.get(`SELECT COUNT(*) as count FROM ${TABLE_NAMES.STREAMS}`),
|
||||||
|
db.get(`SELECT COUNT(*) as count FROM ${TABLE_NAMES.TEAMS}`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return createSuccessResponse({
|
||||||
|
streams: streamsResult.count,
|
||||||
|
teams: teamsResult.count
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return createDatabaseError('fetch counts', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = withErrorHandling(getCountsHandler);
|
|
@ -40,6 +40,30 @@ body {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status Indicator Dots */
|
||||||
|
.status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connected {
|
||||||
|
background-color: #859900; /* Solarized green */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.disconnected {
|
||||||
|
background-color: #dc322f; /* Solarized red */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.streaming {
|
||||||
|
background-color: #dc322f; /* Solarized red */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.idle {
|
||||||
|
background-color: #586e75; /* Solarized base01 */
|
||||||
|
}
|
||||||
|
|
||||||
/* Glass Card Component */
|
/* Glass Card Component */
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(7, 54, 66, 0.4);
|
background: rgba(7, 54, 66, 0.4);
|
||||||
|
|
|
@ -19,8 +19,14 @@ type OBSStatus = {
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Counts = {
|
||||||
|
streams: number;
|
||||||
|
teams: number;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const [obsStatus, setObsStatus] = useState<OBSStatus | null>(null);
|
const [obsStatus, setObsStatus] = useState<OBSStatus | null>(null);
|
||||||
|
const [counts, setCounts] = useState<Counts | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// Smart polling with performance monitoring and visibility detection
|
// Smart polling with performance monitoring and visibility detection
|
||||||
|
@ -40,10 +46,25 @@ export default function Footer() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchCounts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/counts');
|
||||||
|
const data = await response.json();
|
||||||
|
// Handle both old and new API response formats
|
||||||
|
const countsData = data.success ? data.data : data;
|
||||||
|
setCounts(countsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch counts:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Use smart polling that respects page visibility and adapts interval based on connection status
|
// Use smart polling that respects page visibility and adapts interval based on connection status
|
||||||
const pollingInterval = obsStatus?.connected ? 15000 : 30000; // Poll faster when connected
|
const pollingInterval = obsStatus?.connected ? 15000 : 30000; // Poll faster when connected
|
||||||
useSmartPolling(fetchOBSStatus, pollingInterval, [obsStatus?.connected]);
|
useSmartPolling(fetchOBSStatus, pollingInterval, [obsStatus?.connected]);
|
||||||
|
|
||||||
|
// Poll counts less frequently (every 60 seconds) since they don't change as often
|
||||||
|
useSmartPolling(fetchCounts, 60000, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<footer className="glass p-6 mt-8">
|
<footer className="glass p-6 mt-8">
|
||||||
|
@ -60,29 +81,58 @@ export default function Footer() {
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
{/* Connection Status */}
|
{/* Connection Status */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h3 className="font-semibold mb-4">OBS Studio</h3>
|
||||||
<div className={`w-3 h-3 rounded-full ${obsStatus?.connected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<div>
|
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`}></div>
|
||||||
<h3 className="font-semibold">OBS Studio</h3>
|
<span className="text-sm">
|
||||||
<p className="text-sm opacity-60">
|
|
||||||
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
|
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{obsStatus && (
|
{obsStatus && (
|
||||||
<div className="text-sm opacity-80">
|
<div className="text-sm opacity-80">
|
||||||
<div>{obsStatus.host}:{obsStatus.port}</div>
|
<div>{obsStatus.host}:{obsStatus.port}</div>
|
||||||
{obsStatus.hasPassword && <div>🔒 Authenticated</div>}
|
{obsStatus.hasPassword && <div>🔒 Authenticated</div>}
|
||||||
|
|
||||||
|
{/* Streaming/Recording Status */}
|
||||||
|
{obsStatus.connected && (
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
<div className={`flex items-center gap-2 ${obsStatus.streaming ? 'text-red-400' : 'opacity-60'}`}>
|
||||||
|
<div className={`status-dot ${obsStatus.streaming ? 'streaming' : 'idle'}`} style={{width: '8px', height: '8px'}}></div>
|
||||||
|
<span className="text-sm">{obsStatus.streaming ? 'LIVE' : 'OFFLINE'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`flex items-center gap-2 ${obsStatus.recording ? 'text-red-400' : 'opacity-60'}`}>
|
||||||
|
<div className={`status-dot ${obsStatus.recording ? 'streaming' : 'idle'}`} style={{width: '8px', height: '8px'}}></div>
|
||||||
|
<span className="text-sm">{obsStatus.recording ? 'REC' : 'IDLE'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live Status */}
|
{/* Live Status or Database Stats */}
|
||||||
{obsStatus?.connected && (
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold mb-4">Live Status</h3>
|
<h3 className="font-semibold mb-4">
|
||||||
|
{obsStatus?.connected ? 'Live Status' : 'Database Stats'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Show counts when OBS is disconnected */}
|
||||||
|
{!obsStatus?.connected && counts && (
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Teams:</span>
|
||||||
|
<span className="font-medium">{counts.teams}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Streams:</span>
|
||||||
|
<span className="font-medium">{counts.streams}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{obsStatus?.connected && (
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
{obsStatus.currentScene && (
|
{obsStatus.currentScene && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
@ -98,21 +148,23 @@ export default function Footer() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4 mt-4">
|
{/* Database counts */}
|
||||||
<div className={`flex items-center gap-2 ${obsStatus.streaming ? 'text-red-400' : 'opacity-60'}`}>
|
{counts && (
|
||||||
<div className={`w-2 h-2 rounded-full ${obsStatus.streaming ? 'bg-red-500' : 'bg-gray-500'}`}></div>
|
<>
|
||||||
<span className="text-sm">{obsStatus.streaming ? 'LIVE' : 'OFFLINE'}</span>
|
<div className="flex justify-between">
|
||||||
</div>
|
<span>Teams:</span>
|
||||||
|
<span className="font-medium">{counts.teams}</span>
|
||||||
<div className={`flex items-center gap-2 ${obsStatus.recording ? 'text-red-400' : 'opacity-60'}`}>
|
|
||||||
<div className={`w-2 h-2 rounded-full ${obsStatus.recording ? 'bg-red-500' : 'bg-gray-500'}`}></div>
|
|
||||||
<span className="text-sm">{obsStatus.recording ? 'REC' : 'IDLE'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Streams:</span>
|
||||||
|
<span className="font-medium">{counts.streams}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
{obsStatus?.error && (
|
{obsStatus?.error && (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue