Refactor code structure for improved readability and maintainability

This commit is contained in:
Cesar Mendivil 2025-11-05 11:49:09 -07:00
parent ec88eeeadc
commit d26a18f702
84 changed files with 12031 additions and 118 deletions

1101
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,15 @@
"dev:landing": "npm run dev --workspace=packages/landing-page", "dev:landing": "npm run dev --workspace=packages/landing-page",
"dev:api": "npm run dev --workspace=packages/backend-api", "dev:api": "npm run dev --workspace=packages/backend-api",
"dev:studio": "npm run dev --workspace=packages/broadcast-studio", "dev:studio": "npm run dev --workspace=packages/broadcast-studio",
"dev:studio-panel": "npm run dev --workspace=packages/studio-panel",
"dev:broadcast-panel": "npm run dev --workspace=packages/broadcast-panel",
"dev:admin": "npm run dev --workspace=packages/admin-panel", "dev:admin": "npm run dev --workspace=packages/admin-panel",
"build": "npm run build --workspaces", "build": "npm run build --workspaces",
"build:landing": "npm run build --workspace=packages/landing-page", "build:landing": "npm run build --workspace=packages/landing-page",
"build:api": "npm run build --workspace=packages/backend-api", "build:api": "npm run build --workspace=packages/backend-api",
"build:studio": "npm run build --workspace=packages/broadcast-studio", "build:studio": "npm run build --workspace=packages/broadcast-studio",
"build:studio-panel": "npm run build --workspace=packages/studio-panel",
"build:broadcast-panel": "npm run build --workspace=packages/broadcast-panel",
"build:admin": "npm run build --workspace=packages/admin-panel", "build:admin": "npm run build --workspace=packages/admin-panel",
"clean": "rm -rf packages/*/node_modules packages/*/dist shared/*/node_modules node_modules", "clean": "rm -rf packages/*/node_modules packages/*/dist shared/*/node_modules node_modules",
"typecheck": "npm run typecheck --workspaces", "typecheck": "npm run typecheck --workspaces",

View File

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Broadcast Panel - Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1634
packages/broadcast-panel/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,18 @@
"name": "broadcast-panel", "name": "broadcast-panel",
"version": "0.1.0", "version": "0.1.0",
"main": "src/index.ts", "main": "src/index.ts",
"type": "module" "type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "^7.2.0",
"@vitejs/plugin-react": "^4.0.0"
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
{
"dependencies": {
"@vitejs/plugin-react": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"vite": "^7.2.0"
}
}

View File

@ -0,0 +1,6 @@
{
"name": "e2e",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -9,11 +9,13 @@ const Header: React.FC = () => {
return ( return (
<header className="h-16 bg-white border-b flex items-center justify-between px-6"> <header className="h-16 bg-white border-b flex items-center justify-between px-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<img src="/assets/logo-dark.png" alt="logo" className="w-28 h-auto object-contain" /> <img src="/assets/logo-dark.png" alt="logo" className="w-36 h-auto object-contain" />
</div> </div>
<div className="flex items-center gap-4"> <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="hidden sm:block">
<button className="px-3 py-2 border rounded flex items-center gap-2"><i className="mdi mdi-help-circle-outline"></i> Ayuda</button>
</div>
<div className="flex items-center gap-2"> <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"> <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" /> <img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />

View File

@ -0,0 +1,60 @@
import React, { useState } from 'react'
import type { Transmission } from '../types'
interface Props {
open: boolean
onClose: () => void
onCreate: (t: Transmission) => void
}
const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
const [title, setTitle] = useState('')
const [platform, setPlatform] = useState('YouTube')
const [scheduled, setScheduled] = useState('')
if (!open) return null
const generateId = () => `t_${Date.now()}_${Math.floor(Math.random()*1000)}`
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const t: Transmission = { id: generateId(), title, platform, scheduled }
onCreate(t)
setTitle('')
setPlatform('YouTube')
setScheduled('')
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg p-6 w-full max-w-lg">
<h3 className="text-lg font-semibold mb-4">Nueva transmisión</h3>
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="block text-sm">Título</label>
<input value={title} onChange={(e) => setTitle(e.target.value)} className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm">Plataforma</label>
<select value={platform} onChange={(e) => setPlatform(e.target.value)} className="w-full p-2 border rounded">
<option>YouTube</option>
<option>Facebook</option>
<option>Twitch</option>
<option>LinkedIn</option>
</select>
</div>
<div>
<label className="block text-sm">Fecha y hora</label>
<input value={scheduled} onChange={(e) => setScheduled(e.target.value)} placeholder="YYYY-MM-DD HH:mm" className="w-full p-2 border rounded" />
</div>
<div className="flex items-center justify-end gap-3">
<button type="button" onClick={onClose} className="px-4 py-2 border rounded">Cancelar</button>
<button type="submit" className="px-4 py-2 bg-indigo-600 text-white rounded">Crear</button>
</div>
</form>
</div>
</div>
)
}
export default NewTransmissionModal

View File

@ -1,24 +1,75 @@
import React from 'react' import React, { useEffect, useState } from 'react'
import Sidebar from './Sidebar' import Sidebar from './Sidebar'
import Header from './Header' import Header from './Header'
import TransmissionsTable from './TransmissionsTable' import TransmissionsTable from './TransmissionsTable'
import NewTransmissionModal from './NewTransmissionModal'
import type { Transmission } from '../types'
const STORAGE_KEY = 'broadcast_transmissions'
const PageContainer: React.FC = () => { const PageContainer: React.FC = () => {
const [transmissions, setTransmissions] = useState<Transmission[]>([])
const [isModalOpen, setIsModalOpen] = useState(false)
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) setTransmissions(JSON.parse(raw))
} catch (e) {
console.error('Failed to load transmissions', e)
}
}, [])
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(transmissions))
} catch (e) {
console.error('Failed to save transmissions', e)
}
}, [transmissions])
const handleCreate = (t: Transmission) => {
setTransmissions(prev => [t, ...prev])
setIsModalOpen(false)
}
const handleDelete = (id: string) => {
setTransmissions(prev => prev.filter(p => p.id !== id))
}
const handleUpdate = (updated: Transmission) => {
setTransmissions(prev => prev.map(p => p.id === updated.id ? updated : p))
}
return ( return (
<div className="min-h-screen flex bg-[#f7f8fa]"> <div className="min-h-screen flex bg-[#f7f8fa]">
<Sidebar /> <Sidebar />
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<Header /> <Header />
<main className="p-8"> <main className="p-8">
<div className="mb-6 flex items-center justify-between"> <div className="max-w-6xl mx-auto">
<h1 className="text-2xl font-semibold">Transmisiones</h1> <div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-3"> <h1 className="text-2xl font-semibold">Transmisiones</h1>
<button className="px-4 py-2 bg-indigo-600 text-white rounded">Nueva transmisión</button> <div className="flex items-center gap-3">
<button className="px-3 py-2 border rounded">Importar</button> <button onClick={() => setIsModalOpen(true)} 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>
<div className="bg-white border rounded p-4 shadow-sm">
<TransmissionsTable
transmissions={transmissions}
onDelete={handleDelete}
onUpdate={handleUpdate}
/>
</div> </div>
</div> </div>
<TransmissionsTable /> <NewTransmissionModal
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
onCreate={handleCreate}
/>
</main> </main>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ const Sidebar: React.FC = () => {
] ]
return ( return (
<aside className="w-64 bg-white border-r shadow-sm flex flex-col"> <aside className="w-72 bg-white border-r shadow-sm flex flex-col min-h-screen">
<div className="p-4 border-b flex items-center gap-3"> <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" /> <img src="/assets/logo-light.png" alt="logo" className="w-10 h-10 object-contain" />
<div> <div>
@ -23,16 +23,17 @@ const Sidebar: React.FC = () => {
<ul className="space-y-1"> <ul className="space-y-1">
{navItems.map(item => ( {navItems.map(item => (
<li key={item.id}> <li key={item.id}>
<a href="#" className="flex items-center gap-3 px-3 py-2 rounded hover:bg-indigo-50 text-sm"> <a href="#" className="flex items-center gap-3 px-3 py-2 rounded hover:bg-slate-50 text-sm text-slate-700">
<i className="mdi mdi-view-dashboard text-lg text-slate-400"></i> <span className="w-9 h-9 flex items-center justify-center rounded bg-slate-100 text-slate-500">{item.label.charAt(0)}</span>
<span>{item.label}</span> <span className="flex-1">{item.label}</span>
<i className="mdi mdi-chevron-right text-slate-300"></i>
</a> </a>
</li> </li>
))} ))}
</ul> </ul>
</nav> </nav>
<div className="mt-auto p-4 border-t"> <div className="mt-auto p-4 border-t bg-white">
<div className="flex items-center gap-3 mb-3"> <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"> <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" /> <img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />

View File

@ -1,16 +1,20 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import type { Transmission } from '../types'
interface Transmission { id: string, title: string, platform: string, scheduled: string } interface Props {
transmissions: Transmission[]
onDelete: (id: string) => void
onUpdate: (t: Transmission) => void
}
const mockTransmissions: Transmission[] = [ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate }) => {
{ 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 [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
const filtered = mockTransmissions const filtered = transmissions // filtering by tab can be implemented later
if (!filtered || filtered.length === 0) {
return <div className="p-4">No hay transmisiones todavía.</div>
}
return ( return (
<div> <div>
@ -34,10 +38,11 @@ const TransmissionsTable: React.FC = () => {
<tr key={t.id} className="border-t"> <tr key={t.id} className="border-t">
<td className="p-3">{t.title}</td> <td className="p-3">{t.title}</td>
<td className="p-3">{t.platform}</td> <td className="p-3">{t.platform}</td>
<td className="p-3">{t.scheduled}</td> <td className="p-3">{t.scheduled || '-'}</td>
<td className="p-3 text-right"> <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 bg-indigo-600 text-white rounded mr-2">Entrar</button>
<button className="px-3 py-1 border rounded">Opciones</button> <button onClick={() => onDelete(t.id)} className="px-3 py-1 bg-red-500 text-white rounded mr-2">Eliminar</button>
<button onClick={() => onUpdate(t)} className="px-3 py-1 border rounded">Editar</button>
</td> </td>
</tr> </tr>
))} ))}

View File

@ -0,0 +1,7 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import PageContainer from './components/PageContainer'
import './styles.css'
const root = createRoot(document.getElementById('root')!)
root.render(<PageContainer />)

View File

@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body { font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; }

View File

@ -0,0 +1,6 @@
export interface Transmission {
id: string
title: string
platform: string
scheduled: string
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "node_modules/.cache/broadcast-panel.tsbuildinfo"
},
"include": ["src"]
}

945
packages/e2e/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
packages/e2e/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@avanzacast/e2e",
"version": "0.1.0",
"private": true,
"description": "E2E tests for AvanzaCast (Playwright)",
"scripts": {
"test": "playwright test",
"install-browsers": "playwright install"
},
"devDependencies": {
"@playwright/test": "^1.51.0"
},
"dependencies": {
"puppeteer-core": "^20.9.0"
}
}

View File

@ -0,0 +1,71 @@
{
"name": "e2e",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@playwright/test": "^1.51.0"
}
},
"node_modules/@playwright/test": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.0.tgz",
"integrity": "sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==",
"dev": true,
"dependencies": {
"playwright": "1.51.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz",
"integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==",
"dev": true,
"dependencies": {
"playwright-core": "1.51.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz",
"integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"@playwright/test": "^1.51.0"
}
}

View File

@ -0,0 +1,26 @@
import { defineConfig } from '@playwright/test'
// Playwright config runs in Node.js — declare `process` for TypeScript here to avoid needing @types/node
declare const process: any;
export default defineConfig({
testDir: './tests',
timeout: 30_000,
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium',
// If PW_WS_ENDPOINT is provided, connect to the remote Playwright server instead of launching a local browser
...(process.env.PW_WS_ENDPOINT
? { connectOptions: { wsEndpoint: (process.env.PW_WS_ENDPOINT as string) } }
: {}),
},
},
],
use: {
headless: true,
// allow overriding the target base URL via PW_BASE_URL env var (useful when vite chooses a different port)
baseURL: (process.env.PW_BASE_URL as string) || 'http://localhost:5173',
},
})

View File

@ -0,0 +1,46 @@
// Smoke test to try connecting to browserless via Puppeteer
// Tries common WS endpoints and navigates to the local app to validate connectivity.
const puppeteer = require('puppeteer-core');
const TOKEN = 'e2e098863b912f6a178b68e71ec3c58d';
const HOST = process.env.HOST_IP || '192.168.1.19';
const PORT = process.env.HOST_PORT || '5173';
const BASE_URL = process.env.BASE_URL || `http://${HOST}:${PORT}`;
const endpoints = [
`wss://browserless.bfzqqk.easypanel.host/puppeteer?token=${TOKEN}`,
`wss://browserless.bfzqqk.easypanel.host/?token=${TOKEN}`,
`wss://browserless.bfzqqk.easypanel.host/devtools?token=${TOKEN}`,
// try without path
`wss://browserless.bfzqqk.easypanel.host/playwright?token=${TOKEN}`,
];
(async () => {
console.log('BASE_URL =', BASE_URL);
for (const ep of endpoints) {
console.log('\nTrying endpoint:', ep);
try {
const browser = await puppeteer.connect({
browserWSEndpoint: ep,
defaultViewport: null,
// some providers require a slowMo or ignoreHTTPSErrors
ignoreHTTPSErrors: true,
timeout: 15000,
});
const page = await browser.newPage();
console.log('Connected to browserless, navigating to', BASE_URL);
await page.goto(BASE_URL, { waitUntil: 'load', timeout: 20000 }).catch(e => { throw e; });
const title = await page.title().catch(() => 'no-title');
const contentLen = await page.content().then(c => c.length).catch(() => 0);
console.log('Navigation OK. title=', title, 'content length=', contentLen);
await browser.close();
console.log('SUCCESS endpoint:', ep);
process.exit(0);
} catch (err) {
console.error('Endpoint failed:', ep, '\n', err && err.message ? err.message : err);
// continue to next
}
}
console.error('\nAll endpoints failed. See errors above.');
process.exit(2);
})();

View File

@ -0,0 +1,6 @@
{
"status": "failed",
"failedTests": [
"eef615229cfa0ca986ca-e478202a50da98c5dd83"
]
}

View File

@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test'
test('crear transmisión y persistir', async ({ page }) => {
// Visitar la app
await page.goto('/')
// Forzar mock user en localStorage
await page.evaluate(() => localStorage.setItem('mock_user', JSON.stringify({ name: 'Demo' })))
// Navegar a /broadcast
await page.goto('/broadcast')
// Esperar el botón 'Nueva transmisión' y abrir modal
await page.waitForSelector('button:has-text("Nueva transmisión")')
await page.click('button:has-text("Nueva transmisión")')
// Rellenar formulario
await page.fill('input[placeholder]', 'Transmisión E2E')
await page.selectOption('select', 'YouTube')
await page.fill('input[placeholder="YYYY-MM-DD HH:mm"]', '2025-11-05 15:00')
// Crear
await page.click('button:has-text("Crear")')
// Esperar que la tabla tenga la nueva fila
await page.waitForSelector('td:has-text("Transmisión E2E")')
// Comprobar localStorage
const transmissions = await page.evaluate(() => JSON.parse(localStorage.getItem('broadcast_transmissions')||'[]'))
expect(transmissions.length).toBeGreaterThan(0)
expect(transmissions[0].title).toBe('Transmisión E2E')
})

6449
packages/landing-page/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
const colors = require('tailwindcss/colors') /** Minimal consolidated Tailwind shared config for the monorepo */
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: 'class', darkMode: 'class',
@ -14,10 +13,10 @@ module.exports = {
'2xl': '1536px', '2xl': '1536px',
}, },
fontFamily: { fontFamily: {
'sans': ['"Nunito", sans-serif'], sans: ['"Nunito", sans-serif'],
'nunito': ['"Nunito", sans-serif'], nunito: ['"Nunito", sans-serif'],
'cursive': ['"Alex Brush", cursive'], cursive: ['"Alex Brush", cursive'],
'serif': ['"EB Garamond", serif'], serif: ['"EB Garamond", serif'],
}, },
container: { container: {
center: true, center: true,
@ -31,10 +30,9 @@ module.exports = {
}, },
extend: { extend: {
colors: { colors: {
'dark': '#3c4858', dark: '#3c4858',
'black': '#161c2d', black: '#161c2d',
'dark-footer': '#192132', 'dark-footer': '#192132',
// Colores principales de AvanzaCast
primary: { primary: {
50: '#f5f3ff', 50: '#f5f3ff',
100: '#ede9fe', 100: '#ede9fe',
@ -60,70 +58,31 @@ module.exports = {
900: '#1e3a8a', 900: '#1e3a8a',
}, },
}, },
boxShadow: { boxShadow: {
sm: '0 2px 4px 0 rgb(60 72 88 / 0.15)', sm: '0 2px 4px 0 rgb(60 72 88 / 0.15)',
DEFAULT: '0 0 3px rgb(60 72 88 / 0.15)', DEFAULT: '0 0 3px rgb(60 72 88 / 0.15)',
md: '0 5px 13px rgb(60 72 88 / 0.20)', md: '0 5px 13px rgb(60 72 88 / 0.20)',
lg: '0 10px 25px -3px rgb(60 72 88 / 0.15)', lg: '0 10px 25px -3px rgb(60 72 88 / 0.15)',
xl: '0 20px 25px -5px rgb(60 72 88 / 0.1), 0 8px 10px -6px rgb(60 72 88 / 0.1)',
'2xl': '0 25px 50px -12px rgb(60 72 88 / 0.25)',
inner: 'inset 0 2px 4px 0 rgb(60 72 88 / 0.05)',
testi: '2px 2px 2px -1px rgb(60 72 88 / 0.15)',
}, },
spacing: { spacing: {
0.75: '0.1875rem', 0.75: '0.1875rem',
3.25: '0.8125rem' 3.25: '0.8125rem',
}, },
maxWidth: { maxWidth: {
'1200': '71.25rem', '1200': '71.25rem',
'992': '60rem', '992': '60rem',
'768': '45rem', '768': '45rem',
}, },
zIndex: {
1: '1',
2: '2',
3: '3',
999: '999',
},
animation: {
'float': 'float 6s ease-in-out infinite',
'float-delayed': 'float 6s ease-in-out infinite 3s',
'fadeInUp': 'fadeInUp 0.6s ease-out',
'marquee': 'marquee 30s linear infinite',
'spin-slow': 'spin 10s linear infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-20px)' },
},
fadeInUp: {
'0%': {
opacity: '0',
transform: 'translateY(30px)',
},
'100%': {
opacity: '1',
transform: 'translateY(0)',
},
},
marquee: {
'0%': { transform: 'translateX(0%)' },
'100%': { transform: 'translateX(-50%)' },
},
},
}, },
}, },
plugins: [ plugins: [
require('@tailwindcss/forms')({ (function () {
strategy: 'class', try {
}), return require('@tailwindcss/forms')({ strategy: 'class' })
} catch (e) {
// plugin not installed at repo root; swallow during dry require
return function () {}
}
})(),
], ],
} }

View File

@ -16,5 +16,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src"] "include": ["src", "packages/*/src"]
} }