- Introduced ModalDestinationButton for destination selection with customizable icons and labels. - Added ModalInput for text input with optional character counter. - Implemented ModalLink for reusable links styled as underlined text. - Created ModalPlatformCard for platform selection with badges. - Developed ModalRadioGroup for radio button groups with custom styling. - Added ModalSection for grouping modal content with optional labels. - Implemented ModalSelect for dropdown selections with custom styling. - Created ModalShareButtons for sharing options via Gmail, Email, and Messenger. - Developed ModalTextarea for multi-line text input with character counter. - Introduced ModalToggle for toggle switches with optional help text and links. - Updated README.md with component descriptions, usage examples, and design guidelines. - Added index.ts for centralized exports of modal components.
418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
import React, { useState } from 'react'
|
|
import {
|
|
Modal,
|
|
ModalRadioGroup,
|
|
ModalSection,
|
|
ModalDestinationButton,
|
|
ModalInput,
|
|
ModalTextarea,
|
|
ModalSelect,
|
|
ModalCheckbox,
|
|
ModalLink,
|
|
ModalDateTimeGroup,
|
|
ModalButton,
|
|
ModalButtonGroup,
|
|
ModalPlatformCard
|
|
} from '@shared/components'
|
|
import { MdVideocam, MdVideoLibrary, MdAdd, MdImage, MdAutoAwesome, MdArrowBack } from 'react-icons/md'
|
|
import { FaYoutube, FaFacebook, FaLinkedin, FaTwitch, FaInstagram, FaKickstarterK } from 'react-icons/fa'
|
|
import { FaXTwitter } from 'react-icons/fa6'
|
|
import { BsInfoCircle } from 'react-icons/bs'
|
|
import styles from './NewTransmissionModal.module.css'
|
|
import type { Transmission } from '../types'
|
|
|
|
interface Props {
|
|
open: boolean
|
|
onClose: () => void
|
|
onCreate: (t: Transmission) => void
|
|
}
|
|
|
|
interface DestinationData {
|
|
id: string
|
|
platform: string
|
|
icon: React.ReactNode
|
|
badge?: React.ReactNode
|
|
}
|
|
|
|
const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|
const [view, setView] = useState<'main' | 'add-destination'>('main')
|
|
const [source, setSource] = useState('studio')
|
|
const [destinations, setDestinations] = useState<DestinationData[]>([
|
|
{
|
|
id: 'yt_1',
|
|
platform: 'YouTube',
|
|
icon: <FaYoutube color="#FF0000" />,
|
|
badge: <span style={{ color: '#FF0000', fontSize: '12px' }}>▶</span>
|
|
}
|
|
])
|
|
const [selectedDestination, setSelectedDestination] = useState<string | null>(null)
|
|
|
|
// Blank stream
|
|
const [blankTitle, setBlankTitle] = useState('')
|
|
|
|
// Form fields
|
|
const [title, setTitle] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [privacy, setPrivacy] = useState('Pública')
|
|
const [category, setCategory] = useState('')
|
|
const [addReferral, setAddReferral] = useState(true)
|
|
const [scheduleForLater, setScheduleForLater] = useState(false)
|
|
|
|
// Scheduled date/time
|
|
const [scheduledDate, setScheduledDate] = useState('')
|
|
const [scheduledHour, setScheduledHour] = useState('01')
|
|
const [scheduledMinute, setScheduledMinute] = useState('10')
|
|
|
|
const generateId = () => `t_${Date.now()}_${Math.floor(Math.random()*1000)}`
|
|
|
|
const handleAddDestination = () => {
|
|
setView('add-destination')
|
|
}
|
|
|
|
const handleBackToMain = () => {
|
|
setView('main')
|
|
}
|
|
|
|
const handleSkipForNow = () => {
|
|
setSelectedDestination('blank')
|
|
}
|
|
|
|
const handlePlatformSelect = (platform: string) => {
|
|
const platformData: Record<string, { icon: React.ReactNode; color: string }> = {
|
|
'YouTube': { icon: <FaYoutube color="#FF0000" />, color: '#FF0000' },
|
|
'Facebook': { icon: <FaFacebook color="#1877F2" />, color: '#1877F2' },
|
|
'LinkedIn': { icon: <FaLinkedin color="#0A66C2" />, color: '#0A66C2' },
|
|
'X (Twitter)': { icon: <FaXTwitter color="#000000" />, color: '#000000' },
|
|
'Twitch': { icon: <FaTwitch color="#9146FF" />, color: '#9146FF' },
|
|
'Instagram Live': { icon: <FaInstagram color="#E4405F" />, color: '#E4405F' },
|
|
'Kick': { icon: <FaKickstarterK color="#53FC18" />, color: '#53FC18' },
|
|
'Brightcove': { icon: <div style={{ fontSize: '24px', fontWeight: 'bold' }}>B</div>, color: '#000000' }
|
|
}
|
|
|
|
const newDest: DestinationData = {
|
|
id: `dest_${Date.now()}`,
|
|
platform,
|
|
icon: platformData[platform]?.icon || <MdAdd />,
|
|
badge: platform === 'YouTube' ? <span style={{ color: '#FF0000', fontSize: '12px' }}>▶</span> : undefined
|
|
}
|
|
|
|
setDestinations([...destinations, newDest])
|
|
setView('main')
|
|
}
|
|
|
|
const handleDestinationClick = (destId: string) => {
|
|
// Si el destino ya está seleccionado, deseleccionarlo
|
|
if (selectedDestination === destId) {
|
|
setSelectedDestination(null)
|
|
} else {
|
|
setSelectedDestination(destId)
|
|
}
|
|
}
|
|
|
|
const handleCreate = () => {
|
|
if (!selectedDestination) {
|
|
alert('Por favor selecciona un destino de transmisión')
|
|
return
|
|
}
|
|
|
|
// Si es transmisión en blanco, usar el título del formulario blanco
|
|
if (selectedDestination === 'blank') {
|
|
// Por el momento solo cierra el modal
|
|
// TODO: navegar a studio con título blankTitle
|
|
onClose()
|
|
return
|
|
}
|
|
|
|
const t: Transmission = {
|
|
id: generateId(),
|
|
title: title || 'Nueva transmisión',
|
|
platform: destinations.find(d => d.id === selectedDestination)?.platform || 'YouTube',
|
|
scheduled: ''
|
|
}
|
|
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('')
|
|
}
|
|
|
|
const modalTitle = view === 'add-destination' ? 'Agregar destino' : 'Crear transmisión en vivo'
|
|
const showBackButton = view === 'add-destination'
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
onClose={onClose}
|
|
title={modalTitle}
|
|
width="md"
|
|
>
|
|
{showBackButton && (
|
|
<button onClick={handleBackToMain} className={styles.backButton}>
|
|
<MdArrowBack size={20} />
|
|
</button>
|
|
)}
|
|
|
|
{view === 'main' && (
|
|
<>
|
|
<div className={styles.content}>
|
|
<ModalSection>
|
|
<ModalRadioGroup
|
|
label="Fuente"
|
|
helpIcon={<BsInfoCircle />}
|
|
name="source"
|
|
value={source}
|
|
onChange={setSource}
|
|
options={[
|
|
{ value: 'studio', label: 'Estudio', icon: <MdVideocam /> },
|
|
{ value: 'prerecorded', label: 'Video pregrabado', icon: <MdVideoLibrary /> }
|
|
]}
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection label="Selecciona los destinos">
|
|
<div className={styles.destinations}>
|
|
{destinations.map((dest) => (
|
|
<div key={dest.id} className={selectedDestination === dest.id ? styles.selectedDestination : ''}>
|
|
<ModalDestinationButton
|
|
icon={dest.icon}
|
|
label=""
|
|
badge={dest.badge}
|
|
onClick={() => handleDestinationClick(dest.id)}
|
|
title={dest.platform}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
<ModalDestinationButton
|
|
icon={<MdAdd />}
|
|
label=""
|
|
onClick={handleAddDestination}
|
|
title="Agregar destino"
|
|
/>
|
|
{!selectedDestination && (
|
|
<div className={styles.skipNowContainer}>
|
|
<ModalLink onClick={handleSkipForNow}>
|
|
Omitir por ahora
|
|
</ModalLink>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
|
|
</ModalSection>
|
|
|
|
{selectedDestination && selectedDestination !== 'blank' && (
|
|
<>
|
|
<ModalSection>
|
|
<ModalInput
|
|
label="Título"
|
|
value={title}
|
|
onChange={setTitle}
|
|
maxLength={100}
|
|
showCounter={true}
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalTextarea
|
|
label="Descripción"
|
|
value={description}
|
|
onChange={setDescription}
|
|
placeholder="Cuéntanos un poco sobre esta transmisión en vivo"
|
|
maxLength={5000}
|
|
showCounter={true}
|
|
rows={3}
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalCheckbox
|
|
checked={addReferral}
|
|
onChange={setAddReferral}
|
|
label="Agrega el mensaje de referencia a la descripción"
|
|
helpIcon={<BsInfoCircle />}
|
|
subtext="Gana $25 en crédito por cada referencia exitosa."
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalSelect
|
|
label="Privacidad"
|
|
value={privacy}
|
|
onChange={setPrivacy}
|
|
options={[
|
|
{ value: 'Pública', label: 'Pública' },
|
|
{ value: 'No listada', label: 'No listada' },
|
|
{ value: 'Privada', label: 'Privada' }
|
|
]}
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalSelect
|
|
label="Categoría"
|
|
value={category}
|
|
onChange={setCategory}
|
|
placeholder="Seleccionar categoría"
|
|
options={[
|
|
{ value: 'gaming', label: 'Juegos' },
|
|
{ value: 'education', label: 'Educación' },
|
|
{ value: 'entertainment', label: 'Entretenimiento' },
|
|
{ value: 'music', label: 'Música' },
|
|
{ value: 'sports', label: 'Deportes' },
|
|
{ value: 'news', label: 'Noticias' },
|
|
{ value: 'tech', label: 'Tecnología' }
|
|
]}
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalCheckbox
|
|
checked={scheduleForLater}
|
|
onChange={setScheduleForLater}
|
|
label="Programar para más tarde"
|
|
/>
|
|
</ModalSection>
|
|
|
|
{scheduleForLater && (
|
|
<>
|
|
<ModalSection>
|
|
<ModalDateTimeGroup
|
|
label="Hora de inicio programada"
|
|
helpIcon={<BsInfoCircle />}
|
|
dateValue={scheduledDate}
|
|
hourValue={scheduledHour}
|
|
minuteValue={scheduledMinute}
|
|
onDateChange={setScheduledDate}
|
|
onHourChange={setScheduledHour}
|
|
onMinuteChange={setScheduledMinute}
|
|
timezone="GMT-7"
|
|
/>
|
|
</ModalSection>
|
|
|
|
<ModalSection>
|
|
<ModalButtonGroup>
|
|
<ModalButton
|
|
variant="secondary"
|
|
icon={<MdImage />}
|
|
>
|
|
Subir imagen en miniatura
|
|
</ModalButton>
|
|
<ModalButton
|
|
variant="primary"
|
|
icon={<MdAutoAwesome />}
|
|
>
|
|
Crear con IA
|
|
</ModalButton>
|
|
</ModalButtonGroup>
|
|
</ModalSection>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{selectedDestination === 'blank' && (
|
|
<div className={styles.blankStreamForm}>
|
|
<p className={styles.blankStreamDescription}>
|
|
Empezarás una transmisión en el estudio sin configurar ningún destino.
|
|
Podrás agregar destinos más tarde desde el estudio.
|
|
</p>
|
|
|
|
<ModalInput
|
|
label="Título de la transmisión (opcional)"
|
|
value={blankTitle}
|
|
onChange={setBlankTitle}
|
|
placeholder="Ej: Mi transmisión en vivo"
|
|
maxLength={100}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.footer}>
|
|
{selectedDestination && selectedDestination !== 'blank' && (
|
|
<p className={styles.footerNote}>
|
|
Esta transmisión no se grabará en StreamYard. Para grabar, tendrás que{' '}
|
|
<ModalLink href="/pricing">pasarte a un plan superior.</ModalLink>
|
|
</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={handleCreate}
|
|
className={styles.createButton}
|
|
disabled={!selectedDestination}
|
|
>
|
|
{selectedDestination === 'blank' ? 'Empezar ahora' : 'Crear transmisión en vivo'}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{view === 'add-destination' && (
|
|
<div className={styles.platformGrid}>
|
|
<ModalPlatformCard
|
|
icon={<FaYoutube color="#FF0000" />}
|
|
label="YouTube"
|
|
onClick={() => handlePlatformSelect('YouTube')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaFacebook color="#1877F2" />}
|
|
label="Facebook"
|
|
onClick={() => handlePlatformSelect('Facebook')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaLinkedin color="#0A66C2" />}
|
|
label="LinkedIn"
|
|
onClick={() => handlePlatformSelect('LinkedIn')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaXTwitter color="#000000" />}
|
|
label="X (Twitter)"
|
|
onClick={() => handlePlatformSelect('X (Twitter)')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaTwitch color="#9146FF" />}
|
|
label="Twitch"
|
|
onClick={() => handlePlatformSelect('Twitch')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaInstagram color="#E4405F" />}
|
|
label="Instagram Live"
|
|
onClick={() => handlePlatformSelect('Instagram Live')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<FaKickstarterK color="#53FC18" />}
|
|
label="Kick"
|
|
onClick={() => handlePlatformSelect('Kick')}
|
|
badge="pro"
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<div style={{ fontSize: '24px', fontWeight: 'bold' }}>B</div>}
|
|
label="Brightcove"
|
|
onClick={() => handlePlatformSelect('Brightcove')}
|
|
/>
|
|
<ModalPlatformCard
|
|
icon={<div style={{ fontSize: '20px', fontWeight: 'bold' }}>RTMP</div>}
|
|
label="Otras plataformas"
|
|
onClick={() => handlePlatformSelect('RTMP')}
|
|
badge="pro"
|
|
/>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export default NewTransmissionModal
|