feat: agregar funcionalidad de edición en el modal de transmisión y mejorar la tabla de transmisiones

This commit is contained in:
Cesar Mendivil 2025-11-06 00:48:26 -07:00
parent e43686e36d
commit 3b524d1c23
3 changed files with 148 additions and 59 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import {
Modal,
ModalRadioGroup,
@ -25,6 +25,8 @@ interface Props {
open: boolean
onClose: () => void
onCreate: (t: Transmission) => void
onUpdate?: (t: Transmission) => void
transmission?: Transmission // Transmisión a editar
}
interface DestinationData {
@ -34,9 +36,13 @@ interface DestinationData {
badge?: React.ReactNode
}
const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate, onUpdate, transmission }) => {
const [view, setView] = useState<'main' | 'add-destination'>('main')
const [source, setSource] = useState('studio')
// Modo edición: si hay transmission, inicializar con sus datos
const isEditMode = !!transmission
const [destinations, setDestinations] = useState<DestinationData[]>([
{
id: 'yt_1',
@ -63,6 +69,42 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
const [scheduledHour, setScheduledHour] = useState('01')
const [scheduledMinute, setScheduledMinute] = useState('10')
// Inicializar campos en modo edición
useEffect(() => {
if (transmission && open) {
setTitle(transmission.title)
// Si es genérico, activar modo blank
if (transmission.platform === 'Genérico') {
setSelectedDestination('blank')
setBlankTitle(transmission.title)
} else {
// Si tiene una plataforma específica, crear el destino y seleccionarlo
// Por ahora, simplemente no seleccionamos nada hasta que se agregue el destino
setSelectedDestination(null)
}
} else if (!open) {
// Reset cuando se cierra el modal
resetForm()
}
}, [transmission, open])
const resetForm = () => {
setView('main')
setSource('studio')
setSelectedDestination(null)
setTitle('')
setDescription('')
setPrivacy('Pública')
setCategory('')
setAddReferral(true)
setScheduleForLater(false)
setScheduledDate('')
setScheduledHour('01')
setScheduledMinute('10')
setBlankTitle('')
}
const generateId = () => `t_${Date.now()}_${Math.floor(Math.random()*1000)}`
const handleAddDestination = () => {
@ -115,39 +157,57 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
return
}
// Si es transmisión en blanco, usar el título del formulario blanco
// Si es transmisión en blanco (genérica)
if (selectedDestination === 'blank') {
// Por el momento solo cierra el modal
// TODO: navegar a studio con título blankTitle
const blankTransmission: Transmission = {
id: isEditMode && transmission ? transmission.id : generateId(),
title: blankTitle || 'Transmisión en vivo',
platform: 'Genérico',
scheduled: 'Próximamente',
createdAt: isEditMode && transmission?.createdAt ? transmission.createdAt : new Date().toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
// Si estamos editando, llamar a onUpdate, sino onCreate
if (isEditMode && onUpdate) {
onUpdate(blankTransmission)
} else {
onCreate(blankTransmission)
}
resetForm()
onClose()
return
}
// Transmisión con destino específico
const t: Transmission = {
id: generateId(),
id: isEditMode && transmission ? transmission.id : generateId(),
title: title || 'Nueva transmisión',
platform: destinations.find(d => d.id === selectedDestination)?.platform || 'YouTube',
scheduled: ''
scheduled: '',
createdAt: isEditMode && transmission?.createdAt ? transmission.createdAt : new Date().toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
onCreate(t)
// Reset form
setView('main')
setSource('studio')
setSelectedDestination(null)
setTitle('')
setDescription('')
setPrivacy('Pública')
setCategory('')
setAddReferral(true)
setScheduleForLater(false)
setScheduledDate('')
setScheduledHour('01')
setScheduledMinute('10')
setBlankTitle('')
// Si estamos editando, llamar a onUpdate, sino onCreate
if (isEditMode && onUpdate) {
onUpdate(t)
} else {
onCreate(t)
}
resetForm()
onClose()
}
const modalTitle = view === 'add-destination' ? 'Agregar destino' : 'Crear transmisión en vivo'
const modalTitle = view === 'add-destination' ? 'Agregar destino' : (isEditMode ? 'Editar transmisión' : 'Crear transmisión en vivo')
const showBackButton = view === 'add-destination'
return (
@ -200,7 +260,7 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
onClick={handleAddDestination}
title="Agregar destino"
/>
{!selectedDestination && (
{!selectedDestination && (
<div className={styles.skipNowContainer}>
<ModalLink onClick={handleSkipForNow}>
Omitir por ahora
@ -353,7 +413,10 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
className={styles.createButton}
disabled={!selectedDestination}
>
{selectedDestination === 'blank' ? 'Empezar ahora' : 'Crear transmisión en vivo'}
{isEditMode
? (selectedDestination === 'blank' ? 'Guardar cambios' : 'Actualizar transmisión')
: (selectedDestination === 'blank' ? 'Empezar ahora' : 'Crear transmisión en vivo')
}
</button>
</div>
</>

View File

@ -5,6 +5,7 @@ import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa'
import { SkeletonTable } from './Skeleton'
import styles from './TransmissionsTable.module.css'
import InviteGuestsModal from './InviteGuestsModal'
import NewTransmissionModal from './NewTransmissionModal'
import type { Transmission } from '../types'
interface Props {
@ -19,16 +20,25 @@ const platformIcons: Record<string, React.ReactNode> = {
'Facebook': <FaFacebook size={16} color="#1877F2" />,
'Twitch': <FaTwitch size={16} color="#9146FF" />,
'LinkedIn': <FaLinkedin size={16} color="#0A66C2" />,
'Genérico': <MdVideocam size={16} color="#5f6368" />, // Logo genérico para transmisiones sin destino
}
const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate, isLoading }) => {
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
const [inviteOpen, setInviteOpen] = useState(false)
const [inviteLink, setInviteLink] = useState<string | undefined>(undefined)
const [editOpen, setEditOpen] = useState(false)
const [editTransmission, setEditTransmission] = useState<Transmission | undefined>(undefined)
const handleEdit = (t: Transmission) => {
setEditTransmission(t)
setEditOpen(true)
}
// Filtrado por fechas
const filtered = transmissions.filter(t => {
if (!t.scheduled) return activeTab === 'upcoming'
// Si es "Próximamente" o no tiene fecha programada, siempre va a "upcoming"
if (!t.scheduled || t.scheduled === 'Próximamente') return activeTab === 'upcoming'
const scheduledDate = new Date(t.scheduled)
const now = new Date()
@ -96,40 +106,47 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
<tbody>
{filtered.map(t => (
<tr key={t.id} className={styles.tableRow}>
<td className={styles.tableCell} colSpan={4}>
<div className={styles.tableCard}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div className={styles.platformAvatar}>
<div className={styles.platformIcon}>{platformIcons[t.platform] || platformIcons['YouTube']}</div>
</div>
<div>
<div className={styles.transmissionTitle}>{t.title}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{t.scheduled || '—'}</div>
</div>
</div>
<div className={styles.actionsCell}>
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
Entrar al estudio
</button>
<>
<Dropdown
trigger={<button className={styles.moreOptionsButton} aria-label={`Más opciones ${t.title}`}><MdMoreVert size={20} /></button>}
items={[
{ label: 'Agregar invitados', icon: <MdPersonAdd size={16} />, onClick: () => { setInviteLink(`https://streamyard.com/${t.id}`); setInviteOpen(true) } },
{ label: 'Editar', icon: <MdEdit size={16} />, onClick: () => {/* editar */} },
{ divider: true, label: '', disabled: false },
{ label: 'Ver en YouTube', icon: <MdOpenInNew size={16} />, onClick: () => {/* abrir */} },
{ divider: true, label: '', disabled: false },
{ label: 'Eliminar transmisión', icon: <MdDelete size={16} />, onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } }
]}
/>
<InviteGuestsModal open={inviteOpen} onClose={() => setInviteOpen(false)} link={inviteLink} />
</>
</div>
<td className={styles.tableCell}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div className={styles.platformAvatar}>
<div className={styles.platformIcon}>{platformIcons[t.platform] || platformIcons['YouTube']}</div>
</div>
<div>
<div className={styles.transmissionTitle}>{t.title}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
{t.platform === 'Genérico' ? 'Solo grabación' : (t.platform || 'YouTube')}
</div>
</div>
</div>
</td>
<td className={styles.tableCell}>
<span style={{ fontSize: 14, color: 'var(--text-primary)' }}>
{t.createdAt || '---'}
</span>
</td>
<td className={styles.tableCell}>
<span style={{ fontSize: 14, color: 'var(--text-primary)' }}>
{(t.scheduled && t.scheduled !== 'Próximamente') ? t.scheduled : '---'}
</span>
</td>
<td className={styles.tableCell} style={{ textAlign: 'right' }}>
<div className={styles.actionsCell}>
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
Entrar al estudio
</button>
<Dropdown
trigger={<button className={styles.moreOptionsButton} aria-label={`Más opciones ${t.title}`}><MdMoreVert size={20} /></button>}
items={[
{ label: 'Agregar invitados', icon: <MdPersonAdd size={16} />, onClick: () => { setInviteLink(`https://streamyard.com/${t.id}`); setInviteOpen(true) } },
{ label: 'Editar', icon: <MdEdit size={16} />, onClick: () => handleEdit(t) },
{ divider: true, label: '', disabled: false },
{ label: 'Ver en YouTube', icon: <MdOpenInNew size={16} />, onClick: () => {/* abrir */} },
{ divider: true, label: '', disabled: false },
{ label: 'Eliminar transmisión', icon: <MdDelete size={16} />, onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } }
]}
/>
<InviteGuestsModal open={inviteOpen} onClose={() => setInviteOpen(false)} link={inviteLink} />
</div>
</td>
</tr>
@ -138,6 +155,14 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
</table>
</div>
)}
<NewTransmissionModal
open={editOpen}
onClose={() => { setEditOpen(false); setEditTransmission(undefined) }}
onCreate={() => {}}
onUpdate={onUpdate}
transmission={editTransmission}
/>
</div>
)
}

View File

@ -3,4 +3,5 @@ export interface Transmission {
title: string
platform: string
scheduled: string
createdAt?: string // Fecha de creación
}