Add comprehensive studio mode support and stream organization
- Implement studio mode transition workflow with Go Live buttons - Add collapsible team grouping for better stream organization - Include source locking functionality for newly created streams - Enhance footer status indicators with improved visual styling - Create triggerTransition API endpoint for studio mode operations - Add CollapsibleGroup component for expandable content sections 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
07028b0792
commit
3bad71cb26
8 changed files with 603 additions and 116 deletions
64
components/CollapsibleGroup.tsx
Normal file
64
components/CollapsibleGroup.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
'use client';
|
||||
|
||||
import { useState, ReactNode } from 'react';
|
||||
|
||||
interface CollapsibleGroupProps {
|
||||
title: string;
|
||||
itemCount: number;
|
||||
children: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
export default function CollapsibleGroup({
|
||||
title,
|
||||
itemCount,
|
||||
children,
|
||||
defaultOpen = true,
|
||||
isOpen: controlledIsOpen,
|
||||
onToggle
|
||||
}: CollapsibleGroupProps) {
|
||||
const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
|
||||
|
||||
// Use controlled state if provided, otherwise use internal state
|
||||
const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
|
||||
|
||||
const handleToggle = () => {
|
||||
if (onToggle) {
|
||||
onToggle();
|
||||
} else {
|
||||
setInternalIsOpen(!internalIsOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="collapsible-group">
|
||||
<button
|
||||
className="collapsible-header"
|
||||
onClick={handleToggle}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<div className="collapsible-header-content">
|
||||
<svg
|
||||
className={`collapsible-icon ${isOpen ? 'open' : ''}`}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<h3 className="collapsible-title">{title}</h3>
|
||||
<span className="collapsible-count">{itemCount}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className={`collapsible-content ${isOpen ? 'open' : ''}`}>
|
||||
<div className="collapsible-content-inner">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -85,8 +85,8 @@ export default function Footer() {
|
|||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold mb-2">OBS Studio</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`}></div>
|
||||
<div className="flex items-center">
|
||||
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`} style={{marginRight: '4px'}}></div>
|
||||
<p className="text-sm opacity-60">
|
||||
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
|
||||
</p>
|
||||
|
@ -100,20 +100,20 @@ export default function Footer() {
|
|||
|
||||
{/* Streaming/Recording/Studio Mode 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 className="flex flex-wrap gap-6 mt-4">
|
||||
<div className={`flex items-center ${obsStatus.streaming ? 'text-red-400' : 'opacity-60'}`}>
|
||||
<div className={`status-dot ${obsStatus.streaming ? 'streaming' : 'idle'}`} style={{width: '10px', height: '10px', marginRight: '4px'}}></div>
|
||||
<span className="text-sm font-medium" style={{marginRight: '8px'}}>{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 className={`flex items-center ${obsStatus.recording ? 'text-red-400' : 'opacity-60'}`}>
|
||||
<div className={`status-dot ${obsStatus.recording ? 'streaming' : 'idle'}`} style={{width: '10px', height: '10px', marginRight: '4px'}}></div>
|
||||
<span className="text-sm font-medium" style={{marginRight: '8px'}}>{obsStatus.recording ? 'REC' : 'IDLE'}</span>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-2 ${obsStatus.studioModeEnabled ? 'text-yellow-400' : 'opacity-60'}`}>
|
||||
<div className={`status-dot ${obsStatus.studioModeEnabled ? 'connected' : 'idle'}`} style={{width: '8px', height: '8px'}}></div>
|
||||
<span className="text-sm">{obsStatus.studioModeEnabled ? 'STUDIO' : 'DIRECT'}</span>
|
||||
<div className={`flex items-center ${obsStatus.studioModeEnabled ? 'text-yellow-400' : 'opacity-60'}`}>
|
||||
<div className={`status-dot ${obsStatus.studioModeEnabled ? 'connected' : 'idle'}`} style={{width: '10px', height: '10px', marginRight: '4px'}}></div>
|
||||
<span className="text-sm font-medium">{obsStatus.studioModeEnabled ? 'STUDIO' : 'DIRECT'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue