refactor(studio): GoLiveView module, re-export, stories
- Module go-live-view: types, useGoLiveView, Header, Preview, StreamInfo, EncoderSetup, QuickInstructions, MicLevel, Skeleton, orchestrator - Re-export from GoLiveView.tsx - Stories: Default, Loading (Skeleton); decorator min-h-layout-main - Fix: text-[10px] -> text-xs, w-[60%] -> w-3/5 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d5f2ec7178
commit
bdcbf277e8
13 changed files with 456 additions and 270 deletions
|
|
@ -1,23 +1,25 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { GoLiveView } from './GoLiveView';
|
||||
import { GoLiveView, GoLiveViewSkeleton } from './go-live-view';
|
||||
|
||||
const meta: Meta<typeof GoLiveView> = {
|
||||
title: 'Components/Features/Studio/GoLiveView',
|
||||
component: GoLiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
title: 'Components/Features/Studio/GoLiveView',
|
||||
component: GoLiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-layout-main p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Setup: Story = { name: 'Configuration' };
|
||||
export const Live: Story = { name: 'En direct' };
|
||||
export const Ended: Story = { name: 'Terminé' };
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => <GoLiveViewSkeleton />,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,255 +1 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import {
|
||||
Radio,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Play,
|
||||
Square,
|
||||
Settings,
|
||||
Monitor,
|
||||
Mic,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../components/feedback/ToastProvider';
|
||||
|
||||
export const GoLiveView: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const [streamKeyVisible, setStreamKeyVisible] = useState(false);
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
const [title, setTitle] = useState('My Awesome Stream');
|
||||
const [category, setCategory] = useState('Production');
|
||||
|
||||
const streamKey = 'live_83921_abc123xyz789_secret_key';
|
||||
const serverUrl = 'rtmp://live.veza.io/app';
|
||||
|
||||
const copyToClipboard = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
addToast(`${label} copied to clipboard`, 'success');
|
||||
};
|
||||
|
||||
const toggleStream = () => {
|
||||
if (!isLive) {
|
||||
addToast('Starting stream... Waiting for signal...', 'info');
|
||||
setTimeout(() => {
|
||||
setIsLive(true);
|
||||
addToast('You are LIVE!', 'success');
|
||||
}, 2000);
|
||||
} else {
|
||||
setIsLive(false);
|
||||
addToast('Stream ended', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fadeIn pb-20 max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-8 border-b border-kodo-steel/50 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-display font-bold text-white mb-2 flex items-center gap-4">
|
||||
<Radio
|
||||
className={`w-8 h-8 ${isLive ? 'text-kodo-red animate-pulse' : 'text-kodo-content-dim'}`}
|
||||
/>
|
||||
BROADCAST STUDIO
|
||||
</h1>
|
||||
<p className="text-kodo-content-dim font-mono text-sm">
|
||||
Configure your stream and go live.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
className={`px-4 py-2 rounded-lg border flex items-center gap-2 font-bold ${isLive ? 'bg-kodo-red text-white border-kodo-red' : 'bg-kodo-ink text-kodo-content-dim border-kodo-steel'}`}
|
||||
>
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${isLive ? 'bg-white animate-pulse' : 'bg-kodo-steel'}`}
|
||||
></div>
|
||||
{isLive ? 'LIVE' : 'OFFLINE'}
|
||||
</div>
|
||||
<Button
|
||||
variant={isLive ? 'secondary' : 'primary'}
|
||||
className={
|
||||
isLive
|
||||
? 'border-kodo-red text-kodo-red hover:bg-kodo-red/10'
|
||||
: 'bg-kodo-lime text-black hover:bg-white'
|
||||
}
|
||||
icon={
|
||||
isLive ? (
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
) : (
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
)
|
||||
}
|
||||
onClick={toggleStream}
|
||||
>
|
||||
{isLive ? 'END STREAM' : 'START STREAM'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left: Preview & Info */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Preview Player */}
|
||||
<div className="aspect-video bg-black rounded-xl overflow-hidden border border-kodo-steel relative group">
|
||||
{isLive ? (
|
||||
<div className="w-full h-full flex items-center justify-center bg-kodo-ink">
|
||||
{/* Mock live feed */}
|
||||
<div className="text-center animate-pulse">
|
||||
<Radio className="w-16 h-16 text-kodo-red mx-auto mb-4" />
|
||||
<p className="text-kodo-content-dim">Receiving Stream Data...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-col items-center justify-center bg-kodo-ink/50 text-kodo-content-dim">
|
||||
<div className="text-center">
|
||||
<Monitor className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>Stream Offline</p>
|
||||
<p className="text-xs mt-2">Connect OBS to start preview</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-4 right-4 bg-black/50 px-2 py-1 rounded text-xs text-white font-mono">
|
||||
1080p 60fps
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stream Info Form */}
|
||||
<Card variant="default">
|
||||
<h3 className="font-bold text-white mb-4">Stream Information</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-kodo-content-dim mb-2">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full bg-kodo-graphite border border-kodo-steel rounded-lg px-4 py-4 text-white focus:border-kodo-steel outline-none"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
>
|
||||
<option>Production</option>
|
||||
<option>DJ Set</option>
|
||||
<option>Listening Party</option>
|
||||
<option>Q&A / Talk</option>
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
label="Notification Text"
|
||||
placeholder="Going live with some new beats!"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => addToast('Info updated', 'success')}
|
||||
>
|
||||
Update Info
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right: Setup Keys */}
|
||||
<div className="space-y-6">
|
||||
<Card variant="glass">
|
||||
<h3 className="font-bold text-white mb-4 text-sm uppercase tracking-wider flex items-center gap-2">
|
||||
<Settings className="w-4 h-4 text-kodo-gold" /> Encoder Setup
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Server URL
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono"
|
||||
value={serverUrl}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={() => copyToClipboard(serverUrl, 'Server URL')}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Stream Key
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono"
|
||||
value={
|
||||
streamKeyVisible ? streamKey : '•••••••••••••••••••••••••'
|
||||
}
|
||||
readOnly
|
||||
type={streamKeyVisible ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={() => setStreamKeyVisible(!streamKeyVisible)}
|
||||
>
|
||||
{streamKeyVisible ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={() => copyToClipboard(streamKey, 'Stream Key')}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-kodo-red mt-2 flex items-center gap-1">
|
||||
<EyeOff className="w-3 h-3" /> Never share your stream key!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="default">
|
||||
<h3 className="font-bold text-white mb-4 text-sm uppercase tracking-wider">
|
||||
Quick Instructions
|
||||
</h3>
|
||||
<ol className="text-sm text-kodo-content-dim space-y-2 list-decimal pl-4">
|
||||
<li>Open OBS or Streamlabs.</li>
|
||||
<li>Go to Settings {'>'} Stream.</li>
|
||||
<li>Select "Custom" service.</li>
|
||||
<li>Paste Server URL and Stream Key.</li>
|
||||
<li>Start Streaming in OBS.</li>
|
||||
</ol>
|
||||
</Card>
|
||||
|
||||
<div className="bg-kodo-ink p-4 rounded-xl border border-kodo-steel">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-kodo-text-main">Microphone</span>
|
||||
<Mic className="w-4 h-4 text-kodo-lime" />
|
||||
</div>
|
||||
<div className="w-full bg-kodo-graphite h-2 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-kodo-lime w-[60%] animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { GoLiveView, GoLiveViewSkeleton } from './go-live-view';
|
||||
|
|
|
|||
56
apps/web/src/components/studio/go-live-view/GoLiveView.tsx
Normal file
56
apps/web/src/components/studio/go-live-view/GoLiveView.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { useGoLiveView } from './useGoLiveView';
|
||||
import { GoLiveViewHeader } from './GoLiveViewHeader';
|
||||
import { GoLiveViewPreview } from './GoLiveViewPreview';
|
||||
import { GoLiveViewStreamInfo } from './GoLiveViewStreamInfo';
|
||||
import { GoLiveViewEncoderSetup } from './GoLiveViewEncoderSetup';
|
||||
import { GoLiveViewQuickInstructions } from './GoLiveViewQuickInstructions';
|
||||
import { GoLiveViewMicLevel } from './GoLiveViewMicLevel';
|
||||
|
||||
export function GoLiveView() {
|
||||
const {
|
||||
streamKeyVisible,
|
||||
setStreamKeyVisible,
|
||||
isLive,
|
||||
title,
|
||||
setTitle,
|
||||
category,
|
||||
setCategory,
|
||||
streamKey,
|
||||
serverUrl,
|
||||
copyToClipboard,
|
||||
toggleStream,
|
||||
onUpdateInfo,
|
||||
} = useGoLiveView();
|
||||
|
||||
return (
|
||||
<div className="animate-fadeIn pb-20 max-w-6xl mx-auto">
|
||||
<GoLiveViewHeader isLive={isLive} onToggleStream={toggleStream} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<GoLiveViewPreview isLive={isLive} />
|
||||
<GoLiveViewStreamInfo
|
||||
title={title}
|
||||
onTitleChange={setTitle}
|
||||
category={category}
|
||||
onCategoryChange={setCategory}
|
||||
onUpdateInfo={onUpdateInfo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<GoLiveViewEncoderSetup
|
||||
serverUrl={serverUrl}
|
||||
streamKey={streamKey}
|
||||
streamKeyVisible={streamKeyVisible}
|
||||
onToggleStreamKeyVisible={() => setStreamKeyVisible(!streamKeyVisible)}
|
||||
onCopy={copyToClipboard}
|
||||
/>
|
||||
<GoLiveViewQuickInstructions />
|
||||
<GoLiveViewMicLevel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Settings, Copy, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
interface GoLiveViewEncoderSetupProps {
|
||||
serverUrl: string;
|
||||
streamKey: string;
|
||||
streamKeyVisible: boolean;
|
||||
onToggleStreamKeyVisible: () => void;
|
||||
onCopy: (text: string, label: string) => void;
|
||||
}
|
||||
|
||||
export function GoLiveViewEncoderSetup({
|
||||
serverUrl,
|
||||
streamKey,
|
||||
streamKeyVisible,
|
||||
onToggleStreamKeyVisible,
|
||||
onCopy,
|
||||
}: GoLiveViewEncoderSetupProps) {
|
||||
return (
|
||||
<Card variant="glass">
|
||||
<h3 className="font-bold text-white mb-4 text-sm uppercase tracking-wider flex items-center gap-2">
|
||||
<Settings className="w-4 h-4 text-kodo-gold" /> Encoder Setup
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Server URL
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono"
|
||||
value={serverUrl}
|
||||
readOnly
|
||||
aria-label="Server URL"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={() => onCopy(serverUrl, 'Server URL')}
|
||||
aria-label="Copy Server URL"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
|
||||
Stream Key
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono"
|
||||
value={streamKeyVisible ? streamKey : '•••••••••••••••••••••••••'}
|
||||
readOnly
|
||||
type={streamKeyVisible ? 'text' : 'password'}
|
||||
aria-label="Stream Key"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={onToggleStreamKeyVisible}
|
||||
aria-label={streamKeyVisible ? 'Hide stream key' : 'Show stream key'}
|
||||
>
|
||||
{streamKeyVisible ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="border border-kodo-steel"
|
||||
onClick={() => onCopy(streamKey, 'Stream Key')}
|
||||
aria-label="Copy Stream Key"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-kodo-red mt-2 flex items-center gap-1">
|
||||
<EyeOff className="w-3 h-3" /> Never share your stream key!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Radio, Play, Square } from 'lucide-react';
|
||||
|
||||
interface GoLiveViewHeaderProps {
|
||||
isLive: boolean;
|
||||
onToggleStream: () => void;
|
||||
}
|
||||
|
||||
export function GoLiveViewHeader({ isLive, onToggleStream }: GoLiveViewHeaderProps) {
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-8 border-b border-kodo-steel/50 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-display font-bold text-white mb-2 flex items-center gap-4">
|
||||
<Radio
|
||||
className={`w-8 h-8 ${isLive ? 'text-kodo-red animate-pulse' : 'text-kodo-content-dim'}`}
|
||||
/>
|
||||
BROADCAST STUDIO
|
||||
</h1>
|
||||
<p className="text-kodo-content-dim font-mono text-sm">
|
||||
Configure your stream and go live.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
className={`px-4 py-2 rounded-lg border flex items-center gap-2 font-bold ${isLive ? 'bg-kodo-red text-white border-kodo-red' : 'bg-kodo-ink text-kodo-content-dim border-kodo-steel'}`}
|
||||
>
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${isLive ? 'bg-white animate-pulse' : 'bg-kodo-steel'}`}
|
||||
/>
|
||||
{isLive ? 'LIVE' : 'OFFLINE'}
|
||||
</div>
|
||||
<Button
|
||||
variant={isLive ? 'secondary' : 'primary'}
|
||||
className={
|
||||
isLive
|
||||
? 'border-kodo-red text-kodo-red hover:bg-kodo-red/10'
|
||||
: 'bg-kodo-lime text-black hover:bg-white'
|
||||
}
|
||||
icon={
|
||||
isLive ? (
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
) : (
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
)
|
||||
}
|
||||
onClick={onToggleStream}
|
||||
>
|
||||
{isLive ? 'END STREAM' : 'START STREAM'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Mic } from 'lucide-react';
|
||||
|
||||
export function GoLiveViewMicLevel() {
|
||||
return (
|
||||
<div className="bg-kodo-ink p-4 rounded-xl border border-kodo-steel">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-kodo-text-main">Microphone</span>
|
||||
<Mic className="w-4 h-4 text-kodo-lime" />
|
||||
</div>
|
||||
<div className="w-full bg-kodo-graphite h-2 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-kodo-lime w-3/5 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Radio, Monitor } from 'lucide-react';
|
||||
|
||||
interface GoLiveViewPreviewProps {
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
export function GoLiveViewPreview({ isLive }: GoLiveViewPreviewProps) {
|
||||
return (
|
||||
<div className="aspect-video bg-black rounded-xl overflow-hidden border border-kodo-steel relative group">
|
||||
{isLive ? (
|
||||
<div className="w-full h-full flex items-center justify-center bg-kodo-ink">
|
||||
<div className="text-center animate-pulse">
|
||||
<Radio className="w-16 h-16 text-kodo-red mx-auto mb-4" />
|
||||
<p className="text-kodo-content-dim">Receiving Stream Data...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center bg-kodo-ink/50 text-kodo-content-dim">
|
||||
<div className="text-center">
|
||||
<Monitor className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>Stream Offline</p>
|
||||
<p className="text-xs mt-2">Connect OBS to start preview</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-4 right-4 bg-black/50 px-2 py-1 rounded text-xs text-white font-mono">
|
||||
1080p 60fps
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function GoLiveViewQuickInstructions() {
|
||||
return (
|
||||
<Card variant="default">
|
||||
<h3 className="font-bold text-white mb-4 text-sm uppercase tracking-wider">
|
||||
Quick Instructions
|
||||
</h3>
|
||||
<ol className="text-sm text-kodo-content-dim space-y-2 list-decimal pl-4">
|
||||
<li>Open OBS or Streamlabs.</li>
|
||||
<li>Go to Settings {'>'} Stream.</li>
|
||||
<li>Select "Custom" service.</li>
|
||||
<li>Paste Server URL and Stream Key.</li>
|
||||
<li>Start Streaming in OBS.</li>
|
||||
</ol>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Skeleton for GoLiveView — layout primitives, no arbitrary values.
|
||||
*/
|
||||
export function GoLiveViewSkeleton() {
|
||||
return (
|
||||
<div className="animate-fadeIn pb-20 max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8 border-b border-kodo-steel/50 pb-6">
|
||||
<div className="space-y-2">
|
||||
<div className="h-9 w-64 rounded bg-muted animate-pulse" />
|
||||
<div className="h-4 w-56 rounded bg-muted animate-pulse" />
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="h-10 w-24 rounded-lg bg-muted animate-pulse" />
|
||||
<div className="h-10 w-32 rounded-lg bg-muted animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="aspect-video rounded-xl bg-muted animate-pulse" />
|
||||
<div className="rounded-xl border border-kodo-steel p-6 space-y-4">
|
||||
<div className="h-5 w-40 rounded bg-muted animate-pulse" />
|
||||
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
|
||||
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
|
||||
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
|
||||
<div className="flex justify-end">
|
||||
<div className="h-9 w-24 rounded-lg bg-muted animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border p-6 space-y-4">
|
||||
<div className="h-5 w-32 rounded bg-muted animate-pulse" />
|
||||
<div className="h-11 w-full rounded bg-muted animate-pulse" />
|
||||
<div className="h-11 w-full rounded bg-muted animate-pulse" />
|
||||
</div>
|
||||
<div className="rounded-xl border p-6 space-y-2">
|
||||
<div className="h-5 w-36 rounded bg-muted animate-pulse" />
|
||||
<div className="h-4 w-full rounded bg-muted animate-pulse" />
|
||||
<div className="h-4 w-full rounded bg-muted animate-pulse" />
|
||||
<div className="h-4 w-3/4 rounded bg-muted animate-pulse" />
|
||||
</div>
|
||||
<div className="p-4 rounded-xl border border-kodo-steel space-y-2">
|
||||
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
|
||||
<div className="h-2 w-full rounded-full bg-muted animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { STREAM_CATEGORIES } from './types';
|
||||
import type { StreamCategory } from './types';
|
||||
|
||||
interface GoLiveViewStreamInfoProps {
|
||||
title: string;
|
||||
onTitleChange: (value: string) => void;
|
||||
category: StreamCategory;
|
||||
onCategoryChange: (value: StreamCategory) => void;
|
||||
onUpdateInfo: () => void;
|
||||
}
|
||||
|
||||
export function GoLiveViewStreamInfo({
|
||||
title,
|
||||
onTitleChange,
|
||||
category,
|
||||
onCategoryChange,
|
||||
onUpdateInfo,
|
||||
}: GoLiveViewStreamInfoProps) {
|
||||
return (
|
||||
<Card variant="default">
|
||||
<h3 className="font-bold text-white mb-4">Stream Information</h3>
|
||||
<div className="space-y-4">
|
||||
<Input label="Title" value={title} onChange={(e) => onTitleChange(e.target.value)} />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-kodo-content-dim mb-2">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full bg-kodo-graphite border border-kodo-steel rounded-lg px-4 py-4 text-white focus:border-kodo-steel outline-none"
|
||||
value={category}
|
||||
onChange={(e) => onCategoryChange(e.target.value as StreamCategory)}
|
||||
>
|
||||
{STREAM_CATEGORIES.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Input label="Notification Text" placeholder="Going live with some new beats!" />
|
||||
<div className="flex justify-end">
|
||||
<Button variant="secondary" size="sm" onClick={onUpdateInfo}>
|
||||
Update Info
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
11
apps/web/src/components/studio/go-live-view/index.ts
Normal file
11
apps/web/src/components/studio/go-live-view/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export { GoLiveView } from './GoLiveView';
|
||||
export { GoLiveViewSkeleton } from './GoLiveViewSkeleton';
|
||||
export { GoLiveViewHeader } from './GoLiveViewHeader';
|
||||
export { GoLiveViewPreview } from './GoLiveViewPreview';
|
||||
export { GoLiveViewStreamInfo } from './GoLiveViewStreamInfo';
|
||||
export { GoLiveViewEncoderSetup } from './GoLiveViewEncoderSetup';
|
||||
export { GoLiveViewQuickInstructions } from './GoLiveViewQuickInstructions';
|
||||
export { GoLiveViewMicLevel } from './GoLiveViewMicLevel';
|
||||
export { useGoLiveView } from './useGoLiveView';
|
||||
export { STREAM_CATEGORIES } from './types';
|
||||
export type { StreamCategory } from './types';
|
||||
8
apps/web/src/components/studio/go-live-view/types.ts
Normal file
8
apps/web/src/components/studio/go-live-view/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const STREAM_CATEGORIES = [
|
||||
'Production',
|
||||
'DJ Set',
|
||||
'Listening Party',
|
||||
'Q&A / Talk',
|
||||
] as const;
|
||||
|
||||
export type StreamCategory = (typeof STREAM_CATEGORIES)[number];
|
||||
50
apps/web/src/components/studio/go-live-view/useGoLiveView.ts
Normal file
50
apps/web/src/components/studio/go-live-view/useGoLiveView.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useToast } from '@/components/feedback/ToastProvider';
|
||||
import type { StreamCategory } from './types';
|
||||
|
||||
const DEFAULT_STREAM_KEY = 'live_83921_abc123xyz789_secret_key';
|
||||
const DEFAULT_SERVER_URL = 'rtmp://live.veza.io/app';
|
||||
|
||||
export function useGoLiveView() {
|
||||
const { addToast } = useToast();
|
||||
const [streamKeyVisible, setStreamKeyVisible] = useState(false);
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
const [title, setTitle] = useState('My Awesome Stream');
|
||||
const [category, setCategory] = useState<StreamCategory>('Production');
|
||||
|
||||
const copyToClipboard = useCallback(
|
||||
(text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
addToast(`${label} copied to clipboard`, 'success');
|
||||
},
|
||||
[addToast],
|
||||
);
|
||||
|
||||
const toggleStream = useCallback(() => {
|
||||
if (!isLive) {
|
||||
addToast('Starting stream... Waiting for signal...', 'info');
|
||||
setTimeout(() => {
|
||||
setIsLive(true);
|
||||
addToast('You are LIVE!', 'success');
|
||||
}, 2000);
|
||||
} else {
|
||||
setIsLive(false);
|
||||
addToast('Stream ended', 'info');
|
||||
}
|
||||
}, [isLive, addToast]);
|
||||
|
||||
return {
|
||||
streamKeyVisible,
|
||||
setStreamKeyVisible,
|
||||
isLive,
|
||||
title,
|
||||
setTitle,
|
||||
category,
|
||||
setCategory,
|
||||
streamKey: DEFAULT_STREAM_KEY,
|
||||
serverUrl: DEFAULT_SERVER_URL,
|
||||
copyToClipboard,
|
||||
toggleStream,
|
||||
onUpdateInfo,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue