feat: add broadcast-panel package and migrate broadcast components

This commit is contained in:
Cesar Mendivil 2025-11-05 00:10:44 -07:00
parent f7ede05001
commit 408e3b24b9
28 changed files with 396 additions and 0 deletions

View File

@ -0,0 +1,6 @@
{
"name": "broadcast-panel",
"version": "0.1.0",
"main": "src/index.ts",
"type": "module"
}

View File

@ -0,0 +1,29 @@
import React from 'react'
const Header: React.FC = () => {
const handleLogout = () => {
localStorage.removeItem('mock_user')
window.location.href = '/auth/login'
}
return (
<header className="h-16 bg-white border-b flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<img src="/assets/logo-dark.png" alt="logo" className="w-28 h-auto object-contain" />
</div>
<div className="flex items-center gap-4">
<button className="px-3 py-2 border rounded flex items-center gap-2"><i className="mdi mdi-help-circle-outline"></i> Ayuda</button>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
</div>
<div className="text-sm">Demo User</div>
</div>
<button onClick={handleLogout} className="px-3 py-2 bg-red-50 text-red-600 border rounded">Cerrar sesión</button>
</div>
</header>
)
}
export default Header

View File

@ -0,0 +1,28 @@
import React from 'react'
import Sidebar from './Sidebar'
import Header from './Header'
import TransmissionsTable from './TransmissionsTable'
const PageContainer: React.FC = () => {
return (
<div className="min-h-screen flex bg-[#f7f8fa]">
<Sidebar />
<div className="flex-1 flex flex-col">
<Header />
<main className="p-8">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-semibold">Transmisiones</h1>
<div className="flex items-center gap-3">
<button className="px-4 py-2 bg-indigo-600 text-white rounded">Nueva transmisión</button>
<button className="px-3 py-2 border rounded">Importar</button>
</div>
</div>
<TransmissionsTable />
</main>
</div>
</div>
)
}
export default PageContainer

View File

@ -0,0 +1,52 @@
import React from 'react'
const Sidebar: React.FC = () => {
const navItems = [
{ id: 'dashboard', label: 'Inicio' },
{ id: 'create', label: 'Crear' },
{ id: 'transmissions', label: 'Transmisiones' },
{ id: 'recordings', label: 'Grabaciones' },
{ id: 'settings', label: 'Ajustes' },
]
return (
<aside className="w-64 bg-white border-r shadow-sm flex flex-col">
<div className="p-4 border-b flex items-center gap-3">
<img src="/assets/logo-light.png" alt="logo" className="w-10 h-10 object-contain" />
<div>
<div className="text-lg font-semibold">AvanzaCast</div>
<div className="text-sm text-slate-500">Cuenta Demo</div>
</div>
</div>
<nav className="p-4 flex-1 overflow-y-auto">
<ul className="space-y-1">
{navItems.map(item => (
<li key={item.id}>
<a href="#" className="flex items-center gap-3 px-3 py-2 rounded hover:bg-indigo-50 text-sm">
<i className="mdi mdi-view-dashboard text-lg text-slate-400"></i>
<span>{item.label}</span>
</a>
</li>
))}
</ul>
</nav>
<div className="mt-auto p-4 border-t">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
</div>
<div>
<div className="text-sm font-medium">Demo User</div>
<div className="text-xs text-slate-500">demo@avanzacast.test</div>
</div>
</div>
<div className="text-sm text-slate-600 mb-3">Almacenamiento: <strong>0</strong> de 5 GB</div>
<button className="mt-3 w-full py-2 bg-indigo-600 text-white rounded">Comprar más</button>
</div>
</aside>
)
}
export default Sidebar

View File

@ -0,0 +1,51 @@
import React, { useState } from 'react'
interface Transmission { id: string, title: string, platform: string, scheduled: string }
const mockTransmissions: Transmission[] = [
{ id: 't1', title: 'Demo Stream - Producto A', platform: 'YouTube', scheduled: '2025-11-10 18:00' },
{ id: 't2', title: 'Webinar - Marketing', platform: 'Facebook', scheduled: '2025-11-12 16:00' },
]
const TransmissionsTable: React.FC = () => {
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
const filtered = mockTransmissions
return (
<div>
<div className="mb-4 flex items-center gap-3">
<button onClick={() => setActiveTab('upcoming')} className={`px-3 py-1 rounded ${activeTab==='upcoming' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Próximamente</button>
<button onClick={() => setActiveTab('past')} className={`px-3 py-1 rounded ${activeTab==='past' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Anteriores</button>
</div>
<div className="bg-white border rounded">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="text-left p-3">Título</th>
<th className="text-left p-3">Plataforma</th>
<th className="text-left p-3">Fecha</th>
<th className="text-right p-3">Acciones</th>
</tr>
</thead>
<tbody>
{filtered.map(t => (
<tr key={t.id} className="border-t">
<td className="p-3">{t.title}</td>
<td className="p-3">{t.platform}</td>
<td className="p-3">{t.scheduled}</td>
<td className="p-3 text-right">
<button className="px-3 py-1 bg-indigo-600 text-white rounded mr-2">Entrar</button>
<button className="px-3 py-1 border rounded">Opciones</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export default TransmissionsTable

View File

@ -0,0 +1,4 @@
export { default as PageContainer } from './components/PageContainer'
export { default as Sidebar } from './components/Sidebar'
export { default as Header } from './components/Header'
export { default as TransmissionsTable } from './components/TransmissionsTable'

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,29 @@
import React from 'react'
const Header: React.FC = () => {
const handleLogout = () => {
localStorage.removeItem('mock_user')
window.location.href = '/auth/login'
}
return (
<header className="h-16 bg-white border-b flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<img src="/assets/logo-dark.png" alt="logo" className="w-28 h-auto object-contain" />
</div>
<div className="flex items-center gap-4">
<button className="px-3 py-2 border rounded flex items-center gap-2"><i className="mdi mdi-help-circle-outline"></i> Ayuda</button>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
</div>
<div className="text-sm">Demo User</div>
</div>
<button onClick={handleLogout} className="px-3 py-2 bg-red-50 text-red-600 border rounded">Cerrar sesión</button>
</div>
</header>
)
}
export default Header

View File

@ -0,0 +1,28 @@
import React from 'react'
import Sidebar from './Sidebar'
import Header from './Header'
import TransmissionsTable from './TransmissionsTable'
const PageContainer: React.FC = () => {
return (
<div className="min-h-screen flex bg-[#f7f8fa]">
<Sidebar />
<div className="flex-1 flex flex-col">
<Header />
<main className="p-8">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-semibold">Transmisiones</h1>
<div className="flex items-center gap-3">
<button className="px-4 py-2 bg-indigo-600 text-white rounded">Nueva transmisión</button>
<button className="px-3 py-2 border rounded">Importar</button>
</div>
</div>
<TransmissionsTable />
</main>
</div>
</div>
)
}
export default PageContainer

View File

@ -0,0 +1,12 @@
Panel Broadcast - Instrucciones rápidas
Cómo probar el panel broadcast (mock):
1. Abre la app (npm run dev en el workspace `packages/landing-page`).
2. Ve a `/auth/login` y usa cualquier correo/contraseña para "loguearte". Esto guardará `mock_user` en localStorage.
3. Serás redirigido a `/broadcast`, que carga el panel con los assets del template.
4. Para cerrar sesión, usa el botón "Cerrar sesión" en la cabecera.
Notas:
- Este es un mock inicial para la UX; la autenticación real y llamadas al backend deben implementarse posteriormente.
- Los assets (logos, icon font) se copiaron desde el template Techwind adjunto y están en `public/assets` y `public/fonts`.

View File

@ -0,0 +1,52 @@
import React from 'react'
const Sidebar: React.FC = () => {
const navItems = [
{ id: 'dashboard', label: 'Inicio' },
{ id: 'create', label: 'Crear' },
{ id: 'transmissions', label: 'Transmisiones' },
{ id: 'recordings', label: 'Grabaciones' },
{ id: 'settings', label: 'Ajustes' },
]
return (
<aside className="w-64 bg-white border-r shadow-sm flex flex-col">
<div className="p-4 border-b flex items-center gap-3">
<img src="/assets/logo-light.png" alt="logo" className="w-10 h-10 object-contain" />
<div>
<div className="text-lg font-semibold">AvanzaCast</div>
<div className="text-sm text-slate-500">Cuenta Demo</div>
</div>
</div>
<nav className="p-4 flex-1 overflow-y-auto">
<ul className="space-y-1">
{navItems.map(item => (
<li key={item.id}>
<a href="#" className="flex items-center gap-3 px-3 py-2 rounded hover:bg-indigo-50 text-sm">
<i className="mdi mdi-view-dashboard text-lg text-slate-400"></i>
<span>{item.label}</span>
</a>
</li>
))}
</ul>
</nav>
<div className="mt-auto p-4 border-t">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
</div>
<div>
<div className="text-sm font-medium">Demo User</div>
<div className="text-xs text-slate-500">demo@avanzacast.test</div>
</div>
</div>
<div className="text-sm text-slate-600 mb-3">Almacenamiento: <strong>0</strong> de 5 GB</div>
<button className="mt-3 w-full py-2 bg-indigo-600 text-white rounded">Comprar más</button>
</div>
</aside>
)
}
export default Sidebar

View File

@ -0,0 +1,51 @@
import React, { useState } from 'react'
interface Transmission { id: string, title: string, platform: string, scheduled: string }
const mockTransmissions: Transmission[] = [
{ id: 't1', title: 'Demo Stream - Producto A', platform: 'YouTube', scheduled: '2025-11-10 18:00' },
{ id: 't2', title: 'Webinar - Marketing', platform: 'Facebook', scheduled: '2025-11-12 16:00' },
]
const TransmissionsTable: React.FC = () => {
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
const filtered = mockTransmissions
return (
<div>
<div className="mb-4 flex items-center gap-3">
<button onClick={() => setActiveTab('upcoming')} className={`px-3 py-1 rounded ${activeTab==='upcoming' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Próximamente</button>
<button onClick={() => setActiveTab('past')} className={`px-3 py-1 rounded ${activeTab==='past' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Anteriores</button>
</div>
<div className="bg-white border rounded">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="text-left p-3">Título</th>
<th className="text-left p-3">Plataforma</th>
<th className="text-left p-3">Fecha</th>
<th className="text-right p-3">Acciones</th>
</tr>
</thead>
<tbody>
{filtered.map(t => (
<tr key={t.id} className="border-t">
<td className="p-3">{t.title}</td>
<td className="p-3">{t.platform}</td>
<td className="p-3">{t.scheduled}</td>
<td className="p-3 text-right">
<button className="px-3 py-1 bg-indigo-600 text-white rounded mr-2">Entrar</button>
<button className="px-3 py-1 border rounded">Opciones</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export default TransmissionsTable

View File

@ -0,0 +1,2 @@
// Re-exports to migrate broadcast components to the new `broadcast-panel` package
export { PageContainer, Sidebar, Header, TransmissionsTable } from 'broadcast-panel'

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react'
const Login: React.FC = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Mock authentication: accept any email/password
const mockUser = { id: 'user-1', name: 'Demo User', email }
localStorage.setItem('mock_user', JSON.stringify(mockUser))
// Redirect to broadcast panel
window.location.href = '/broadcast'
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow">
<h2 className="text-2xl font-semibold mb-6">Iniciar sesión (Mock)</h2>
<form onSubmit={handleSubmit}>
<label className="block mb-2 text-sm font-medium">Correo</label>
<input className="w-full mb-4 p-2 border rounded" value={email} onChange={e => setEmail(e.target.value)} />
<label className="block mb-2 text-sm font-medium">Contraseña</label>
<input type="password" className="w-full mb-4 p-2 border rounded" value={password} onChange={e => setPassword(e.target.value)} />
<button className="w-full py-2 bg-indigo-600 text-white rounded">Entrar</button>
</form>
</div>
</div>
)
}
export default Login

View File

@ -0,0 +1,20 @@
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import PageContainer from '../components/broadcast/PageContainer'
const BroadcastPage: React.FC = () => {
const router = useRouter()
useEffect(() => {
const user = localStorage.getItem('mock_user')
if (!user) {
router.push('/auth/login')
}
}, [])
return (
<PageContainer />
)
}
export default BroadcastPage