feat: agregar funcionalidad de edición en el modal de transmisión y mejorar la tabla de transmisiones
This commit is contained in:
parent
e43686e36d
commit
3b524d1c23
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalRadioGroup,
|
ModalRadioGroup,
|
||||||
@ -25,6 +25,8 @@ interface Props {
|
|||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onCreate: (t: Transmission) => void
|
onCreate: (t: Transmission) => void
|
||||||
|
onUpdate?: (t: Transmission) => void
|
||||||
|
transmission?: Transmission // Transmisión a editar
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DestinationData {
|
interface DestinationData {
|
||||||
@ -34,9 +36,13 @@ interface DestinationData {
|
|||||||
badge?: React.ReactNode
|
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 [view, setView] = useState<'main' | 'add-destination'>('main')
|
||||||
const [source, setSource] = useState('studio')
|
const [source, setSource] = useState('studio')
|
||||||
|
|
||||||
|
// Modo edición: si hay transmission, inicializar con sus datos
|
||||||
|
const isEditMode = !!transmission
|
||||||
|
|
||||||
const [destinations, setDestinations] = useState<DestinationData[]>([
|
const [destinations, setDestinations] = useState<DestinationData[]>([
|
||||||
{
|
{
|
||||||
id: 'yt_1',
|
id: 'yt_1',
|
||||||
@ -63,6 +69,42 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|||||||
const [scheduledHour, setScheduledHour] = useState('01')
|
const [scheduledHour, setScheduledHour] = useState('01')
|
||||||
const [scheduledMinute, setScheduledMinute] = useState('10')
|
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 generateId = () => `t_${Date.now()}_${Math.floor(Math.random()*1000)}`
|
||||||
|
|
||||||
const handleAddDestination = () => {
|
const handleAddDestination = () => {
|
||||||
@ -115,39 +157,57 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|||||||
return
|
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') {
|
if (selectedDestination === 'blank') {
|
||||||
// Por el momento solo cierra el modal
|
const blankTransmission: Transmission = {
|
||||||
// TODO: navegar a studio con título blankTitle
|
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()
|
onClose()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transmisión con destino específico
|
||||||
const t: Transmission = {
|
const t: Transmission = {
|
||||||
id: generateId(),
|
id: isEditMode && transmission ? transmission.id : generateId(),
|
||||||
title: title || 'Nueva transmisión',
|
title: title || 'Nueva transmisión',
|
||||||
platform: destinations.find(d => d.id === selectedDestination)?.platform || 'YouTube',
|
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
|
// Si estamos editando, llamar a onUpdate, sino onCreate
|
||||||
setView('main')
|
if (isEditMode && onUpdate) {
|
||||||
setSource('studio')
|
onUpdate(t)
|
||||||
setSelectedDestination(null)
|
} else {
|
||||||
setTitle('')
|
onCreate(t)
|
||||||
setDescription('')
|
}
|
||||||
setPrivacy('Pública')
|
|
||||||
setCategory('')
|
resetForm()
|
||||||
setAddReferral(true)
|
onClose()
|
||||||
setScheduleForLater(false)
|
|
||||||
setScheduledDate('')
|
|
||||||
setScheduledHour('01')
|
|
||||||
setScheduledMinute('10')
|
|
||||||
setBlankTitle('')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
const showBackButton = view === 'add-destination'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -200,7 +260,7 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|||||||
onClick={handleAddDestination}
|
onClick={handleAddDestination}
|
||||||
title="Agregar destino"
|
title="Agregar destino"
|
||||||
/>
|
/>
|
||||||
{!selectedDestination && (
|
{!selectedDestination && (
|
||||||
<div className={styles.skipNowContainer}>
|
<div className={styles.skipNowContainer}>
|
||||||
<ModalLink onClick={handleSkipForNow}>
|
<ModalLink onClick={handleSkipForNow}>
|
||||||
Omitir por ahora
|
Omitir por ahora
|
||||||
@ -353,7 +413,10 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|||||||
className={styles.createButton}
|
className={styles.createButton}
|
||||||
disabled={!selectedDestination}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa'
|
|||||||
import { SkeletonTable } from './Skeleton'
|
import { SkeletonTable } from './Skeleton'
|
||||||
import styles from './TransmissionsTable.module.css'
|
import styles from './TransmissionsTable.module.css'
|
||||||
import InviteGuestsModal from './InviteGuestsModal'
|
import InviteGuestsModal from './InviteGuestsModal'
|
||||||
|
import NewTransmissionModal from './NewTransmissionModal'
|
||||||
import type { Transmission } from '../types'
|
import type { Transmission } from '../types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -19,16 +20,25 @@ const platformIcons: Record<string, React.ReactNode> = {
|
|||||||
'Facebook': <FaFacebook size={16} color="#1877F2" />,
|
'Facebook': <FaFacebook size={16} color="#1877F2" />,
|
||||||
'Twitch': <FaTwitch size={16} color="#9146FF" />,
|
'Twitch': <FaTwitch size={16} color="#9146FF" />,
|
||||||
'LinkedIn': <FaLinkedin size={16} color="#0A66C2" />,
|
'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 TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate, isLoading }) => {
|
||||||
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
|
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
|
||||||
const [inviteOpen, setInviteOpen] = useState(false)
|
const [inviteOpen, setInviteOpen] = useState(false)
|
||||||
const [inviteLink, setInviteLink] = useState<string | undefined>(undefined)
|
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
|
// Filtrado por fechas
|
||||||
const filtered = transmissions.filter(t => {
|
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 scheduledDate = new Date(t.scheduled)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -96,40 +106,47 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
|||||||
<tbody>
|
<tbody>
|
||||||
{filtered.map(t => (
|
{filtered.map(t => (
|
||||||
<tr key={t.id} className={styles.tableRow}>
|
<tr key={t.id} className={styles.tableRow}>
|
||||||
<td className={styles.tableCell} colSpan={4}>
|
<td className={styles.tableCell}>
|
||||||
<div className={styles.tableCard}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
<div className={styles.platformAvatar}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div className={styles.platformIcon}>{platformIcons[t.platform] || platformIcons['YouTube']}</div>
|
||||||
<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>
|
|
||||||
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -138,6 +155,14 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<NewTransmissionModal
|
||||||
|
open={editOpen}
|
||||||
|
onClose={() => { setEditOpen(false); setEditTransmission(undefined) }}
|
||||||
|
onCreate={() => {}}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
transmission={editTransmission}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,4 +3,5 @@ export interface Transmission {
|
|||||||
title: string
|
title: string
|
||||||
platform: string
|
platform: string
|
||||||
scheduled: string
|
scheduled: string
|
||||||
|
createdAt?: string // Fecha de creación
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user