refactor(web): split LiveView into live-view module
- types.ts: LiveViewProps, LiveViewChatMessage; mockData: FEATURED_STREAM, CHAT_MESSAGES - useLiveView: stream, chatMessages, msgInput, handleSend, addToast - LiveViewPlayer, LiveViewStreamInfo, LiveViewRecommended, LiveViewChat, LiveViewSkeleton - Layout h-[calc(100vh-120px)] -> min-h-layout-main; text-[10px] -> text-xs - Stories: Default, Loading (Skeleton); decorator min-h-layout-page - Re-export from LiveView.tsx Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
11611749df
commit
30df8c99ea
12 changed files with 482 additions and 282 deletions
|
|
@ -1,21 +1,26 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LiveView } from './LiveView';
|
||||
import { LiveView, LiveViewSkeleton } from './live-view';
|
||||
|
||||
const meta: Meta<typeof LiveView> = {
|
||||
title: 'Components/Features/Views/LiveView',
|
||||
component: LiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
title: 'Components/Features/Views/LiveView',
|
||||
component: LiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-layout-page">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Loading: Story = {
|
||||
name: 'Loading',
|
||||
render: () => <LiveViewSkeleton />,
|
||||
};
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
|
|
|
|||
|
|
@ -1,270 +1 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Badge } from '../ui/badge';
|
||||
import {
|
||||
Users,
|
||||
Heart,
|
||||
Share2,
|
||||
DollarSign,
|
||||
MessageSquare,
|
||||
Send,
|
||||
Radio,
|
||||
Settings,
|
||||
Maximize2,
|
||||
} from 'lucide-react';
|
||||
import { LiveStream } from '../../types';
|
||||
import { useToast } from '../../components/feedback/ToastProvider';
|
||||
|
||||
const featuredStream: LiveStream = {
|
||||
id: '1',
|
||||
title: 'Late Night DnB Production 🎧 | Feedback Session',
|
||||
streamer: 'Neuro_Glitch',
|
||||
viewers: 1240,
|
||||
thumbnailUrl: 'https://picsum.photos/id/140/800/450',
|
||||
tags: ['Production', 'Ableton', 'DnB'],
|
||||
isLive: true,
|
||||
category: 'Production',
|
||||
};
|
||||
|
||||
const chatMessages = [
|
||||
{
|
||||
user: 'BassHead99',
|
||||
text: 'That Reese bass is filthy! 🤮🔥',
|
||||
color: 'text-kodo-steel',
|
||||
},
|
||||
{ user: 'Studio_Rat', text: 'What VST is that?', color: 'text-kodo-content-dim' },
|
||||
{
|
||||
user: 'Neuro_Glitch',
|
||||
text: "It's Phase Plant, just initializing now.",
|
||||
color: 'text-kodo-gold font-bold',
|
||||
},
|
||||
{
|
||||
user: 'VocalChops',
|
||||
text: 'Sent a $5 dono! Check my track?',
|
||||
color: 'text-kodo-lime',
|
||||
},
|
||||
];
|
||||
|
||||
export const LiveView: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const [msgInput, setMsgInput] = useState('');
|
||||
|
||||
const handleSend = () => {
|
||||
if (!msgInput) return;
|
||||
addToast('Message sent to chat', 'success');
|
||||
setMsgInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 h-[calc(100vh-120px)] animate-fadeIn">
|
||||
{/* Main Stream Area */}
|
||||
<div className="lg:col-span-9 flex flex-col gap-4">
|
||||
<div className="relative aspect-video bg-black rounded-xl overflow-hidden shadow-2xl border border-kodo-steel group">
|
||||
{/* Mock Video Feed */}
|
||||
<img
|
||||
src={featuredStream.thumbnailUrl}
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
||||
|
||||
{/* Live Indicator */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<span className="bg-kodo-red text-white px-2 py-1 text-xs font-bold rounded flex items-center gap-1 animate-pulse">
|
||||
<Radio className="w-3 h-3" /> LIVE
|
||||
</span>
|
||||
<span className="bg-black/50 backdrop-blur text-white px-2 py-1 text-xs font-mono rounded flex items-center gap-1">
|
||||
<Users className="w-3 h-3" /> {featuredStream.viewers}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stream Controls Overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 flex justify-between items-end opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={() => addToast('Chat hidden')}
|
||||
>
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={() => addToast('Stream Settings')}
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={() => addToast('Entering Fullscreen')}
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stream Info */}
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-neon p-0.5">
|
||||
<img
|
||||
src="https://picsum.photos/100/100"
|
||||
className="w-full h-full rounded-full object-cover border-2 border-kodo-void"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
{featuredStream.title}
|
||||
</h1>
|
||||
<p
|
||||
className="text-kodo-cyan font-medium cursor-pointer hover:underline"
|
||||
onClick={() => addToast('Opening Streamer Profile')}
|
||||
>
|
||||
{featuredStream.streamer}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{featuredStream.tags.map((tag) => (
|
||||
<Badge key={tag} label={tag} variant="terminal" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<Heart className="w-4 h-4" />}
|
||||
onClick={() => addToast('Followed Streamer', 'success')}
|
||||
>
|
||||
FOLLOW
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={<DollarSign className="w-4 h-4" />}
|
||||
onClick={() => addToast('Donation modal opening...', 'info')}
|
||||
>
|
||||
DONATE
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={<Share2 className="w-4 h-4" />}
|
||||
onClick={() => addToast('Stream link copied!')}
|
||||
>
|
||||
SHARE
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggested Streams */}
|
||||
<div className="mt-4">
|
||||
<h3 className="font-bold text-kodo-content-dim mb-4 uppercase text-sm tracking-wider">
|
||||
Recommended Channels
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card
|
||||
key={i}
|
||||
variant="default"
|
||||
className="p-0 overflow-hidden group cursor-pointer"
|
||||
onClick={() => addToast('Switching stream...')}
|
||||
>
|
||||
<div className="aspect-video relative">
|
||||
<img
|
||||
src={`https://picsum.photos/300/200?random=${i}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-2 left-2 bg-kodo-void/80 px-2 py-0.5 rounded text-[10px] text-white">
|
||||
DJ Set
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-kodo-steel"></div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-white truncate">
|
||||
Techno Bunker 24/7
|
||||
</div>
|
||||
<div className="text-xs text-kodo-content-dim">
|
||||
Underground_Radio
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Chat */}
|
||||
<Card
|
||||
variant="glass"
|
||||
className="lg:col-span-3 flex flex-col p-0 overflow-hidden h-full max-h-[calc(100vh-120px)]"
|
||||
>
|
||||
<div className="p-4 border-b border-kodo-steel/50 flex justify-between items-center bg-kodo-ink">
|
||||
<span className="font-mono text-sm font-bold text-white">
|
||||
STREAM CHAT
|
||||
</span>
|
||||
<div className="w-2 h-2 bg-kodo-lime rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 font-mono text-sm">
|
||||
{chatMessages.map((msg, i) => (
|
||||
<div key={i} className="break-words">
|
||||
<span
|
||||
className={`font-bold ${msg.color} mr-2 cursor-pointer hover:underline`}
|
||||
>
|
||||
{msg.user}:
|
||||
</span>
|
||||
<span className="text-kodo-text-main">{msg.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-center py-2">
|
||||
<span className="text-[10px] text-kodo-content-dim bg-kodo-slate px-2 py-1 rounded-full">
|
||||
Welcome to the chat room!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-kodo-slate border-t border-kodo-steel">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
value={msgInput}
|
||||
onChange={(e) => setMsgInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
className="w-full bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-white focus:border-kodo-steel outline-none"
|
||||
placeholder="Say something..."
|
||||
/>
|
||||
<DollarSign className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-kodo-gold cursor-pointer hover:text-kodo-gold/80 transition-colors" />
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="px-4"
|
||||
onClick={handleSend}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 px-1">
|
||||
<span className="text-[10px] text-kodo-content-dim">
|
||||
Balance: 420 $VEZA
|
||||
</span>
|
||||
<span
|
||||
className="text-[10px] text-kodo-steel cursor-pointer"
|
||||
onClick={() => addToast('Opening Wallet...')}
|
||||
>
|
||||
Get Coins
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { LiveView } from './live-view';
|
||||
|
|
|
|||
50
apps/web/src/components/views/live-view/LiveView.tsx
Normal file
50
apps/web/src/components/views/live-view/LiveView.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { useLiveView } from './useLiveView';
|
||||
import { LiveViewPlayer } from './LiveViewPlayer';
|
||||
import { LiveViewStreamInfo } from './LiveViewStreamInfo';
|
||||
import { LiveViewRecommended } from './LiveViewRecommended';
|
||||
import { LiveViewChat } from './LiveViewChat';
|
||||
import type { LiveViewProps } from './types';
|
||||
|
||||
export function LiveView({ stream: streamOverride, chatMessages: chatOverride }: LiveViewProps = {}) {
|
||||
const {
|
||||
stream,
|
||||
chatMessages,
|
||||
msgInput,
|
||||
setMsgInput,
|
||||
handleSend,
|
||||
addToast,
|
||||
} = useLiveView({
|
||||
stream: streamOverride ?? undefined,
|
||||
chatMessages: chatOverride,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 min-h-layout-main animate-fadeIn">
|
||||
<div className="lg:col-span-9 flex flex-col gap-4">
|
||||
<LiveViewPlayer
|
||||
stream={stream}
|
||||
onToggleChat={() => addToast('Chat hidden')}
|
||||
onSettings={() => addToast('Stream Settings')}
|
||||
onFullscreen={() => addToast('Entering Fullscreen')}
|
||||
/>
|
||||
<LiveViewStreamInfo
|
||||
stream={stream}
|
||||
onStreamerClick={() => addToast('Opening Streamer Profile')}
|
||||
onFollow={() => addToast('Followed Streamer', 'success')}
|
||||
onDonate={() => addToast('Donation modal opening...', 'info')}
|
||||
onShare={() => addToast('Stream link copied!')}
|
||||
/>
|
||||
<LiveViewRecommended onChannelClick={() => addToast('Switching stream...')} />
|
||||
</div>
|
||||
|
||||
<LiveViewChat
|
||||
messages={chatMessages}
|
||||
msgInput={msgInput}
|
||||
onMsgInputChange={setMsgInput}
|
||||
onSend={handleSend}
|
||||
onWalletClick={() => addToast('Opening Wallet...')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
apps/web/src/components/views/live-view/LiveViewChat.tsx
Normal file
82
apps/web/src/components/views/live-view/LiveViewChat.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Send, DollarSign } from 'lucide-react';
|
||||
import type { LiveViewChatMessage } from './types';
|
||||
|
||||
interface LiveViewChatProps {
|
||||
messages: LiveViewChatMessage[];
|
||||
msgInput: string;
|
||||
onMsgInputChange: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onWalletClick?: () => void;
|
||||
}
|
||||
|
||||
export function LiveViewChat({
|
||||
messages,
|
||||
msgInput,
|
||||
onMsgInputChange,
|
||||
onSend,
|
||||
onWalletClick,
|
||||
}: LiveViewChatProps) {
|
||||
return (
|
||||
<Card
|
||||
variant="glass"
|
||||
className="lg:col-span-3 flex flex-col p-0 overflow-hidden h-full min-h-0"
|
||||
>
|
||||
<div className="p-4 border-b border-kodo-steel/50 flex justify-between items-center bg-kodo-ink">
|
||||
<span className="font-mono text-sm font-bold text-white">
|
||||
STREAM CHAT
|
||||
</span>
|
||||
<div className="w-2 h-2 bg-kodo-lime rounded-full animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 font-mono text-sm">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className="break-words">
|
||||
<span
|
||||
className={`font-bold ${msg.color} mr-2 cursor-pointer hover:underline`}
|
||||
>
|
||||
{msg.user}:
|
||||
</span>
|
||||
<span className="text-kodo-text-main">{msg.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-center py-2">
|
||||
<span className="text-xs text-kodo-content-dim bg-kodo-slate px-2 py-1 rounded-full">
|
||||
Welcome to the chat room!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-kodo-slate border-t border-kodo-steel">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
value={msgInput}
|
||||
onChange={(e) => onMsgInputChange(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onSend()}
|
||||
className="w-full bg-kodo-ink border border-kodo-steel rounded px-4 py-2 text-sm text-white focus:border-kodo-steel outline-none"
|
||||
placeholder="Say something..."
|
||||
/>
|
||||
<DollarSign className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-kodo-gold cursor-pointer hover:text-kodo-gold/80 transition-colors" />
|
||||
</div>
|
||||
<Button variant="primary" size="sm" className="px-4" onClick={onSend}>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 px-1">
|
||||
<span className="text-xs text-kodo-content-dim">
|
||||
Balance: 420 $VEZA
|
||||
</span>
|
||||
<span
|
||||
className="text-xs text-kodo-steel cursor-pointer"
|
||||
onClick={onWalletClick}
|
||||
>
|
||||
Get Coins
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
69
apps/web/src/components/views/live-view/LiveViewPlayer.tsx
Normal file
69
apps/web/src/components/views/live-view/LiveViewPlayer.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Users, Radio, MessageSquare, Settings, Maximize2 } from 'lucide-react';
|
||||
import type { LiveStream } from '@/types';
|
||||
|
||||
interface LiveViewPlayerProps {
|
||||
stream: LiveStream;
|
||||
onToggleChat?: () => void;
|
||||
onSettings?: () => void;
|
||||
onFullscreen?: () => void;
|
||||
}
|
||||
|
||||
export function LiveViewPlayer({
|
||||
stream,
|
||||
onToggleChat,
|
||||
onSettings,
|
||||
onFullscreen,
|
||||
}: LiveViewPlayerProps) {
|
||||
return (
|
||||
<div className="relative aspect-video bg-black rounded-xl overflow-hidden shadow-2xl border border-kodo-steel group">
|
||||
<img
|
||||
src={stream.thumbnailUrl}
|
||||
alt=""
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<span className="bg-kodo-red text-white px-2 py-1 text-xs font-bold rounded flex items-center gap-1 animate-pulse">
|
||||
<Radio className="w-3 h-3" /> LIVE
|
||||
</span>
|
||||
<span className="bg-black/50 backdrop-blur text-white px-2 py-1 text-xs font-mono rounded flex items-center gap-1">
|
||||
<Users className="w-3 h-3" /> {stream.viewers}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 flex justify-between items-end opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={onToggleChat}
|
||||
>
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={onSettings}
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={onFullscreen}
|
||||
>
|
||||
<Maximize2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
interface LiveViewRecommendedProps {
|
||||
onChannelClick?: (index: number) => void;
|
||||
}
|
||||
|
||||
export function LiveViewRecommended({ onChannelClick }: LiveViewRecommendedProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-bold text-kodo-content-dim mb-4 uppercase text-sm tracking-wider">
|
||||
Recommended Channels
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card
|
||||
key={i}
|
||||
variant="default"
|
||||
className="p-0 overflow-hidden group cursor-pointer"
|
||||
onClick={() => onChannelClick?.(i)}
|
||||
>
|
||||
<div className="aspect-video relative">
|
||||
<img
|
||||
src={`https://picsum.photos/300/200?random=${i}`}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-2 left-2 bg-kodo-void/80 px-2 py-0.5 rounded text-xs text-white">
|
||||
DJ Set
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-kodo-steel" />
|
||||
<div>
|
||||
<div className="font-bold text-sm text-white truncate">
|
||||
Techno Bunker 24/7
|
||||
</div>
|
||||
<div className="text-xs text-kodo-content-dim">
|
||||
Underground_Radio
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
apps/web/src/components/views/live-view/LiveViewSkeleton.tsx
Normal file
48
apps/web/src/components/views/live-view/LiveViewSkeleton.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export function LiveViewSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 min-h-layout-main animate-fadeIn">
|
||||
<div className="lg:col-span-9 flex flex-col gap-4">
|
||||
<Skeleton className="aspect-video w-full rounded-xl" />
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex gap-4">
|
||||
<Skeleton className="w-12 h-12 rounded-full shrink-0" />
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Skeleton className="h-8 w-3/4" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
<Skeleton className="h-5 w-20" />
|
||||
<Skeleton className="h-5 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Skeleton className="h-4 w-48 mb-4" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="aspect-video w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-3 flex flex-col min-h-0">
|
||||
<Skeleton className="h-12 rounded-t-xl mb-4" />
|
||||
<div className="flex-1 space-y-4">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<Skeleton key={i} className="h-8 w-full" />
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-24 rounded-b-xl mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Heart, Share2, DollarSign } from 'lucide-react';
|
||||
import type { LiveStream } from '@/types';
|
||||
|
||||
interface LiveViewStreamInfoProps {
|
||||
stream: LiveStream;
|
||||
onStreamerClick?: () => void;
|
||||
onFollow?: () => void;
|
||||
onDonate?: () => void;
|
||||
onShare?: () => void;
|
||||
}
|
||||
|
||||
export function LiveViewStreamInfo({
|
||||
stream,
|
||||
onStreamerClick,
|
||||
onFollow,
|
||||
onDonate,
|
||||
onShare,
|
||||
}: LiveViewStreamInfoProps) {
|
||||
return (
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-neon p-0.5">
|
||||
<img
|
||||
src="https://picsum.photos/100/100"
|
||||
alt=""
|
||||
className="w-full h-full rounded-full object-cover border-2 border-kodo-void"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">{stream.title}</h1>
|
||||
<p
|
||||
className="text-kodo-cyan font-medium cursor-pointer hover:underline"
|
||||
onClick={onStreamerClick}
|
||||
>
|
||||
{stream.streamer}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{stream.tags.map((tag) => (
|
||||
<Badge key={tag} label={tag} variant="terminal" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<Heart className="w-4 h-4" />}
|
||||
onClick={onFollow}
|
||||
>
|
||||
FOLLOW
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={<DollarSign className="w-4 h-4" />}
|
||||
onClick={onDonate}
|
||||
>
|
||||
DONATE
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={<Share2 className="w-4 h-4" />}
|
||||
onClick={onShare}
|
||||
>
|
||||
SHARE
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
apps/web/src/components/views/live-view/index.ts
Normal file
6
apps/web/src/components/views/live-view/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export type { LiveViewProps, LiveViewChatMessage } from './types';
|
||||
export type { LiveStream } from '@/types';
|
||||
export { LiveView } from './LiveView';
|
||||
export { LiveViewSkeleton } from './LiveViewSkeleton';
|
||||
export { useLiveView } from './useLiveView';
|
||||
export { FEATURED_STREAM, CHAT_MESSAGES } from './mockData';
|
||||
36
apps/web/src/components/views/live-view/mockData.ts
Normal file
36
apps/web/src/components/views/live-view/mockData.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { LiveStream } from '@/types';
|
||||
import type { LiveViewChatMessage } from './types';
|
||||
|
||||
export const FEATURED_STREAM: LiveStream = {
|
||||
id: '1',
|
||||
title: 'Late Night DnB Production 🎧 | Feedback Session',
|
||||
streamer: 'Neuro_Glitch',
|
||||
viewers: 1240,
|
||||
thumbnailUrl: 'https://picsum.photos/id/140/800/450',
|
||||
tags: ['Production', 'Ableton', 'DnB'],
|
||||
isLive: true,
|
||||
category: 'Production',
|
||||
};
|
||||
|
||||
export const CHAT_MESSAGES: LiveViewChatMessage[] = [
|
||||
{
|
||||
user: 'BassHead99',
|
||||
text: 'That Reese bass is filthy! 🤮🔥',
|
||||
color: 'text-kodo-steel',
|
||||
},
|
||||
{
|
||||
user: 'Studio_Rat',
|
||||
text: 'What VST is that?',
|
||||
color: 'text-kodo-content-dim',
|
||||
},
|
||||
{
|
||||
user: 'Neuro_Glitch',
|
||||
text: "It's Phase Plant, just initializing now.",
|
||||
color: 'text-kodo-gold font-bold',
|
||||
},
|
||||
{
|
||||
user: 'VocalChops',
|
||||
text: 'Sent a $5 dono! Check my track?',
|
||||
color: 'text-kodo-lime',
|
||||
},
|
||||
];
|
||||
16
apps/web/src/components/views/live-view/types.ts
Normal file
16
apps/web/src/components/views/live-view/types.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { LiveStream } from '@/types';
|
||||
|
||||
export type { LiveStream };
|
||||
|
||||
export interface LiveViewChatMessage {
|
||||
user: string;
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface LiveViewProps {
|
||||
/** Optional stream override for stories */
|
||||
stream?: LiveStream | null;
|
||||
/** Optional chat messages override */
|
||||
chatMessages?: LiveViewChatMessage[];
|
||||
}
|
||||
37
apps/web/src/components/views/live-view/useLiveView.ts
Normal file
37
apps/web/src/components/views/live-view/useLiveView.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useToast } from '@/components/feedback/ToastProvider';
|
||||
import { FEATURED_STREAM, CHAT_MESSAGES } from './mockData';
|
||||
import type { LiveStream } from '@/types';
|
||||
import type { LiveViewChatMessage } from './types';
|
||||
|
||||
export interface UseLiveViewOptions {
|
||||
stream?: LiveStream | null;
|
||||
chatMessages?: LiveViewChatMessage[];
|
||||
onSendMessage?: (text: string) => void;
|
||||
}
|
||||
|
||||
export function useLiveView(options: UseLiveViewOptions = {}) {
|
||||
const { addToast } = useToast();
|
||||
const stream = options.stream ?? FEATURED_STREAM;
|
||||
const chatMessages = options.chatMessages ?? CHAT_MESSAGES;
|
||||
const [msgInput, setMsgInput] = useState('');
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
if (!msgInput.trim()) return;
|
||||
if (options.onSendMessage) {
|
||||
options.onSendMessage(msgInput);
|
||||
} else {
|
||||
addToast('Message sent to chat', 'success');
|
||||
}
|
||||
setMsgInput('');
|
||||
}, [msgInput, options.onSendMessage, addToast]);
|
||||
|
||||
return {
|
||||
stream,
|
||||
chatMessages,
|
||||
msgInput,
|
||||
setMsgInput,
|
||||
handleSend,
|
||||
addToast,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue