Initial commit
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Icon } from '../../components/Icon'
|
||||
import { Modal } from '../../components/Modal'
|
||||
import { OfferTab } from './OfferTab'
|
||||
import type { useSpeakRequest } from '../../lib/useSpeakRequest'
|
||||
|
||||
/**
|
||||
* Pasek akcji uczestnika.
|
||||
*
|
||||
* DLACZEGO NAD PANELEM, A NIE W ZAKLADCE
|
||||
* ------------------------------------------------------------------------
|
||||
* Te trzy akcje musza byc osiagalne **niezaleznie od tego, co uczestnik
|
||||
* ogląda**. Prosba o glos schowana w zakladce "Pytania" jest bezuzyteczna
|
||||
* dokladnie wtedy, gdy jest potrzebna: prowadzacy wlasnie zaprosil do
|
||||
* zabrania glosu, a uczestnik czyta czat.
|
||||
*
|
||||
* Zapytanie ofertowe otwiera sie w oknie, a nie przelacza zakladke: wysylajac
|
||||
* zapytanie w trakcie prezentacji uczestnik nie chce stracic z oczu tego, co
|
||||
* dzieje sie na sali.
|
||||
*
|
||||
* STAN PRZYCISKU PO WYSLANIU
|
||||
* ------------------------------------------------------------------------
|
||||
* Po wyslaniu zapytania przycisk pokazuje potwierdzenie zamiast zachety.
|
||||
* Bez tego uczestnik, ktory nie widzi zadnej zmiany, klika drugi i trzeci raz,
|
||||
* a handlowiec dostaje trzy zgloszenia od tej samej osoby. Kolejne zapytanie
|
||||
* jest mozliwe, ale wymaga swiadomego klikniecia w "Wyslij kolejne".
|
||||
*/
|
||||
export function MeetingActionBar({
|
||||
token,
|
||||
speak,
|
||||
/** Prelegent nie prosi o glos: ma go z definicji. */
|
||||
isSpeaker,
|
||||
/** Tryb otwartego mikrofonu: kazdy mowi bez proszenia o glos. */
|
||||
openMic,
|
||||
/**
|
||||
* Temat wybrany klikiem w baner.
|
||||
*
|
||||
* Baner z tematem oferty otwiera formularz **tutaj**, a nie u siebie:
|
||||
* okno oferty juz istnieje w pasku akcji wraz z obsluga zgody i stanem
|
||||
* po wyslaniu, a drugie takie okno oznaczaloby dwie kopie tej samej logiki.
|
||||
*/
|
||||
offerTopicFromBanner,
|
||||
onOfferHandled,
|
||||
}: {
|
||||
token: string
|
||||
speak: ReturnType<typeof useSpeakRequest>
|
||||
isSpeaker: boolean
|
||||
openMic: boolean
|
||||
offerTopicFromBanner?: string | null
|
||||
onOfferHandled?: () => void
|
||||
}) {
|
||||
const [offerOpen, setOfferOpen] = useState<'full' | 'callback' | null>(null)
|
||||
const [offerSent, setOfferSent] = useState(false)
|
||||
/** Temat podstawiony z banera, przekazywany do formularza. */
|
||||
const [presetTopic, setPresetTopic] = useState<string | null>(null)
|
||||
|
||||
/*
|
||||
* Klikniecie w baner z tematem otwiera pelny formularz z tym tematem.
|
||||
* Stan "wyslane" cofamy: uczestnik swiadomie kliknal reklame, wiec chce
|
||||
* wyslac kolejne zapytanie, a nie zobaczyc potwierdzenie poprzedniego.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!offerTopicFromBanner) return
|
||||
setPresetTopic(offerTopicFromBanner)
|
||||
setOfferSent(false)
|
||||
setOfferOpen('full')
|
||||
onOfferHandled?.()
|
||||
}, [offerTopicFromBanner, onOfferHandled])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 bg-white px-3 py-2">
|
||||
{/*
|
||||
Prelegent nie zglasza sie o glos ani nie wysyla zapytan ofertowych.
|
||||
Zamiast pustego paska pokazujemy mu, gdzie moderowac: kolejka glosu,
|
||||
otwarty mikrofon i moderacja pytan sa w panelu prowadzenia, dostepnym
|
||||
dla zalogowanego prowadzacego. Sala sluzy do obrazu.
|
||||
*/}
|
||||
{isSpeaker && (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-slate-500">
|
||||
<Icon name="mic" size={16} className="text-ok-500" />
|
||||
Prowadzisz spotkanie. Zgłoszeniami do głosu i pytaniami zarządzasz w panelu
|
||||
prowadzenia (otwórz go w panelu, przy tym webinarze).
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/*
|
||||
W trybie otwartego mikrofonu przycisk zgloszenia znika: proszenie
|
||||
o cos, co juz sie ma, tylko myli. Zostaje informacja, ze mikrofon
|
||||
jest dostepny, bo uczestnik musi wiedziec, ze moze byc slyszany.
|
||||
*/}
|
||||
{!isSpeaker &&
|
||||
(openMic ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-ok-50 px-3 py-1.5 text-sm font-medium text-ok-600">
|
||||
<Icon name="mic" size={16} />
|
||||
Możesz mówić bez zgłaszania się
|
||||
</span>
|
||||
) : (
|
||||
<SpeakButton speak={speak} />
|
||||
))}
|
||||
|
||||
{/*
|
||||
Pasek oferty to akcje UCZESTNIKA (zapytanie do handlowca). Prelegent
|
||||
i prowadzacy ich nie wysylaja - kontakt handlowy zglasza osoba
|
||||
ogladajaca, nie ta, ktora prowadzi. Dlatego caly blok tylko dla
|
||||
nie-prelegenta. Moderacja glosu i pytan jest w panelu prowadzenia,
|
||||
nie tutaj.
|
||||
*/}
|
||||
{!isSpeaker && (
|
||||
<div className="ml-auto flex flex-wrap items-center gap-2">
|
||||
{offerSent ? (
|
||||
/*
|
||||
* Potwierdzenie zamiast przyciskow. Zachowujemy mozliwosc wyslania
|
||||
* kolejnego zapytania, ale dopiero po swiadomym kliknieciu:
|
||||
* uczestnik ma zobaczyc, ze pierwsze doszlo.
|
||||
*/
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-ok-50 px-3 py-1.5 text-sm font-medium text-ok-600">
|
||||
<Icon name="check-circle" size={16} />
|
||||
Zapytanie wysłane
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOfferSent(false)
|
||||
setOfferOpen('full')
|
||||
}}
|
||||
className="min-h-[44px] rounded-md px-2 text-sm text-slate-500 underline-offset-2 hover:text-slate-900 hover:underline"
|
||||
>
|
||||
Wyślij kolejne
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOfferOpen('callback')}
|
||||
className="inline-flex min-h-[44px] items-center gap-2 rounded-md border border-slate-200 px-3 text-sm font-medium text-slate-700 transition-colors duration-fast ease-standard hover:bg-slate-50"
|
||||
>
|
||||
<Icon name="mail" size={16} />
|
||||
Proszę o kontakt
|
||||
</button>
|
||||
|
||||
{/* wyrozniony: to jest akcja, na ktorej zalezy organizatorowi */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOfferOpen('full')}
|
||||
className="inline-flex min-h-[44px] items-center gap-2 rounded-md bg-blue-500 px-4 text-sm font-semibold text-white shadow-sm transition-colors duration-fast ease-standard hover:bg-blue-600"
|
||||
>
|
||||
<Icon name="send" size={16} />
|
||||
Interesuje mnie oferta
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{offerOpen && (
|
||||
<Modal
|
||||
title={offerOpen === 'callback' ? 'Proszę o kontakt' : 'Zapytanie ofertowe'}
|
||||
description={
|
||||
offerOpen === 'callback'
|
||||
? 'Zostaw numer, oddzwonimy po webinarze.'
|
||||
: 'Napisz, czego potrzebujesz. Nie tracisz kontaktu ze spotkaniem.'
|
||||
}
|
||||
onClose={() => {
|
||||
setOfferOpen(null)
|
||||
setPresetTopic(null)
|
||||
}}
|
||||
>
|
||||
<OfferTab
|
||||
token={token}
|
||||
callbackOnly={offerOpen === 'callback'}
|
||||
presetTopic={offerOpen === 'full' ? presetTopic : null}
|
||||
onSent={() => {
|
||||
setOfferSent(true)
|
||||
// okno zostaje otwarte na potwierdzeniu z tresc zwrotna
|
||||
// z backendu; uczestnik zamyka je sam, gdy je przeczyta
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Przycisk prosby o glos.
|
||||
*
|
||||
* Trzy stany, bo trzy rozne sytuacje: nie prosil, czeka, ma glos. Jeden
|
||||
* przycisk przelaczajacy sie miedzy "zglos" i "wycofaj" gubilby stan
|
||||
* oczekiwania, czyli dokladnie ten, w ktorym uczestnik zastanawia sie,
|
||||
* czy zgloszenie w ogole doszlo.
|
||||
*/
|
||||
function SpeakButton({ speak }: { speak: ReturnType<typeof useSpeakRequest> }) {
|
||||
if (speak.granted) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-ok-50 px-3 py-1.5 text-sm font-semibold text-ok-600">
|
||||
<Icon name="check-circle" size={16} />
|
||||
Masz głos, mikrofon jest włączony
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={speak.lowerHand}
|
||||
disabled={speak.busy}
|
||||
className="min-h-[44px] rounded-md px-2 text-sm text-slate-500 underline-offset-2 hover:text-slate-900 hover:underline disabled:opacity-50"
|
||||
>
|
||||
Oddaj głos
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (speak.pending) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-warn-50 px-3 py-1.5 text-sm font-medium text-warn-600">
|
||||
<Icon name="clock" size={16} />
|
||||
Czekasz na głos
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={speak.lowerHand}
|
||||
disabled={speak.busy}
|
||||
className="min-h-[44px] rounded-md px-2 text-sm text-slate-500 underline-offset-2 hover:text-slate-900 hover:underline disabled:opacity-50"
|
||||
>
|
||||
Wycofaj
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={speak.raiseHand}
|
||||
disabled={speak.busy}
|
||||
className="inline-flex min-h-[44px] items-center gap-2 rounded-md border border-slate-200 px-3 text-sm font-medium text-slate-700 transition-colors duration-fast ease-standard hover:bg-slate-50 disabled:opacity-50"
|
||||
>
|
||||
<Icon name="hand" size={16} />
|
||||
Chcę zabrać głos
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user