@fraku/video 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +75 -0
- package/src/App.tsx +7 -0
- package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.stories.tsx +50 -0
- package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.tsx +253 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/ButtonsDock.tsx +353 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/DockButton.tsx +90 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/MenuItemTemplate.tsx +35 -0
- package/src/components/ZoomVideoPlugin/components/ButtonsDock/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/MobileIconButton/MobileIconButton.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/MobileIconButton/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/Overlay/Overlay.tsx +74 -0
- package/src/components/ZoomVideoPlugin/components/Overlay/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/ParticipantsList.tsx +52 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsContent.tsx +19 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsMenu.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsOverlay.tsx +52 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/AudioSettings.tsx +191 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/BackgroundSettings.tsx +47 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownItemTemplate.tsx +20 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownValueTemplate.tsx +12 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/VideoSettings.tsx +30 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/context.ts +20 -0
- package/src/components/ZoomVideoPlugin/components/SettingsOverlay/index.ts +1 -0
- package/src/components/ZoomVideoPlugin/components/Video.tsx +35 -0
- package/src/components/ZoomVideoPlugin/constants.ts +4 -0
- package/src/components/ZoomVideoPlugin/context.ts +86 -0
- package/src/components/ZoomVideoPlugin/hooks/useClientMessages.ts +142 -0
- package/src/components/ZoomVideoPlugin/hooks/useDeviceSize.ts +24 -0
- package/src/components/ZoomVideoPlugin/hooks/useStartVideoOptions.ts +14 -0
- package/src/components/ZoomVideoPlugin/hooks/useZoomVideoPlayer.tsx +142 -0
- package/src/components/ZoomVideoPlugin/index.ts +2 -0
- package/src/components/ZoomVideoPlugin/lib/platforms.ts +17 -0
- package/src/components/ZoomVideoPlugin/pages/AfterSession.tsx +14 -0
- package/src/components/ZoomVideoPlugin/pages/MainSession.tsx +53 -0
- package/src/components/ZoomVideoPlugin/pages/PanelistsSession.tsx +97 -0
- package/src/components/ZoomVideoPlugin/pages/PreSessionConfiguration.tsx +154 -0
- package/src/components/ZoomVideoPlugin/types.global.d.ts +15 -0
- package/src/components/ZoomVideoPlugin/types.ts +23 -0
- package/src/global.d.ts +46 -0
- package/src/index.css +4 -0
- package/src/index.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { MenuItem, MenuItemOptions } from 'primereact/menuitem'
|
|
3
|
+
import cn from 'classnames'
|
|
4
|
+
import { Menu } from 'primereact/menu'
|
|
5
|
+
import { Breakpoint } from './../../hooks/useDeviceSize'
|
|
6
|
+
import Overlay from '../Overlay'
|
|
7
|
+
import { SessionStep } from '../../types'
|
|
8
|
+
import DockButton from './DockButton'
|
|
9
|
+
import MenuItemTemplate from './MenuItemTemplate'
|
|
10
|
+
import MobileIconButton from '../MobileIconButton'
|
|
11
|
+
import SettingsOverlay from '../SettingsOverlay'
|
|
12
|
+
import { SettingsTab } from '../SettingsOverlay/context'
|
|
13
|
+
import { useZoomVideoContext } from '../../context'
|
|
14
|
+
|
|
15
|
+
type DockItem = {
|
|
16
|
+
component: JSX.Element
|
|
17
|
+
mobileComponent?: JSX.Element
|
|
18
|
+
group: number
|
|
19
|
+
key: string
|
|
20
|
+
show?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ButtonsDockProps = {
|
|
24
|
+
exit: () => void
|
|
25
|
+
setIsCamOn: () => void
|
|
26
|
+
setIsMicOn: () => void
|
|
27
|
+
sessionStep: SessionStep
|
|
28
|
+
setActiveMicrophone: (deviceId: string) => void
|
|
29
|
+
setActiveCamera: (deviceId: string) => void
|
|
30
|
+
setActiveAudioOutput: (deviceId: string) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ButtonsDock = ({
|
|
34
|
+
sessionStep,
|
|
35
|
+
exit,
|
|
36
|
+
setIsMicOn,
|
|
37
|
+
setIsCamOn,
|
|
38
|
+
setActiveMicrophone,
|
|
39
|
+
setActiveCamera,
|
|
40
|
+
setActiveAudioOutput
|
|
41
|
+
}: ButtonsDockProps) => {
|
|
42
|
+
const {
|
|
43
|
+
audioOutputList,
|
|
44
|
+
breakpoint,
|
|
45
|
+
cameraList,
|
|
46
|
+
isCamOn,
|
|
47
|
+
isMicOn,
|
|
48
|
+
micList,
|
|
49
|
+
activeAudioOutput,
|
|
50
|
+
activeMicrophone,
|
|
51
|
+
activeCamera
|
|
52
|
+
} = useZoomVideoContext()
|
|
53
|
+
|
|
54
|
+
const isTabletOrDesktop = breakpoint >= Breakpoint.Tablet
|
|
55
|
+
const [isSettingsOverlayVisible, setIsSettingsOverlayVisible] = useState({ visible: false, tab: SettingsTab.Audio })
|
|
56
|
+
const [isMobileActionsOverlayVisible, setIsMobileActionsOverlayVisible] = useState(false)
|
|
57
|
+
const isLocalSettingConfiguration = sessionStep === SessionStep.LocalSettingsConfiguration
|
|
58
|
+
const audioMenuOptionsRef = useRef<Menu>(null)
|
|
59
|
+
const videoMenuOptionsRef = useRef<Menu>(null)
|
|
60
|
+
const micMenuOptionsRef = useRef<Menu>(null)
|
|
61
|
+
|
|
62
|
+
const items: DockItem[] = [
|
|
63
|
+
// Microphone Button
|
|
64
|
+
{
|
|
65
|
+
component: (
|
|
66
|
+
<DockButton
|
|
67
|
+
mainIcon={`fa-regular ${isMicOn ? 'fa-microphone' : 'fa-microphone-slash'}`}
|
|
68
|
+
mainLabel={isLocalSettingConfiguration && isTabletOrDesktop ? 'Microphone' : undefined}
|
|
69
|
+
mainTitle={isMicOn ? 'Mute' : 'Unmute'}
|
|
70
|
+
onMainClick={setIsMicOn}
|
|
71
|
+
showSecondary={isTabletOrDesktop}
|
|
72
|
+
secondaryIcon="fa-regular fa-chevron-up"
|
|
73
|
+
secondaryTitle="Microphone Settings"
|
|
74
|
+
onSecondaryClick={(e) => micMenuOptionsRef.current?.toggle(e)}
|
|
75
|
+
/>
|
|
76
|
+
),
|
|
77
|
+
group: 1,
|
|
78
|
+
key: 'microphone',
|
|
79
|
+
show: true
|
|
80
|
+
},
|
|
81
|
+
// Camera Button
|
|
82
|
+
{
|
|
83
|
+
component: (
|
|
84
|
+
<DockButton
|
|
85
|
+
mainIcon={`fa-regular ${isCamOn ? 'fa-video' : 'fa-video-slash'}`}
|
|
86
|
+
mainLabel={isLocalSettingConfiguration && isTabletOrDesktop ? 'Camera' : undefined}
|
|
87
|
+
mainTitle={isCamOn ? 'Turn off Camera' : 'Turn on Camera'}
|
|
88
|
+
onMainClick={setIsCamOn}
|
|
89
|
+
showSecondary={isTabletOrDesktop}
|
|
90
|
+
secondaryIcon="fa-regular fa-chevron-up"
|
|
91
|
+
secondaryTitle="Camera Settings"
|
|
92
|
+
onSecondaryClick={(e) => videoMenuOptionsRef.current?.toggle(e)}
|
|
93
|
+
/>
|
|
94
|
+
),
|
|
95
|
+
group: 1,
|
|
96
|
+
key: 'camera',
|
|
97
|
+
show: true
|
|
98
|
+
},
|
|
99
|
+
// Speakers Button
|
|
100
|
+
{
|
|
101
|
+
component: (
|
|
102
|
+
<DockButton
|
|
103
|
+
mainIcon="fa-regular fa-volume"
|
|
104
|
+
mainLabel={isLocalSettingConfiguration && isTabletOrDesktop ? 'Speakers' : undefined}
|
|
105
|
+
mainTitle="Speakers"
|
|
106
|
+
onMainClick={(e) => audioMenuOptionsRef.current?.toggle(e)}
|
|
107
|
+
showSecondary={isTabletOrDesktop}
|
|
108
|
+
secondaryIcon="fa-regular fa-chevron-up"
|
|
109
|
+
secondaryTitle="Speaker Settings"
|
|
110
|
+
onSecondaryClick={(e) => audioMenuOptionsRef.current?.toggle(e)}
|
|
111
|
+
/>
|
|
112
|
+
),
|
|
113
|
+
group: 1,
|
|
114
|
+
key: 'speakers',
|
|
115
|
+
show: isTabletOrDesktop
|
|
116
|
+
},
|
|
117
|
+
// Share Screen Button
|
|
118
|
+
{
|
|
119
|
+
component: <DockButton mainIcon="fa-regular fa-screencast" mainTitle="Share Screen" onMainClick={() => {}} />,
|
|
120
|
+
mobileComponent: (
|
|
121
|
+
<MobileIconButton
|
|
122
|
+
icon="fa-regular fa-screencast"
|
|
123
|
+
title="Share Screen"
|
|
124
|
+
onClick={() => {
|
|
125
|
+
setIsMobileActionsOverlayVisible(false)
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
),
|
|
129
|
+
group: 2,
|
|
130
|
+
key: 'share-screen',
|
|
131
|
+
show: !isLocalSettingConfiguration
|
|
132
|
+
},
|
|
133
|
+
// Raise Hand Button
|
|
134
|
+
{
|
|
135
|
+
component: <DockButton mainIcon="fa-regular fa-hand" mainTitle="Raise/Lower Hand" onMainClick={() => {}} />,
|
|
136
|
+
mobileComponent: (
|
|
137
|
+
<MobileIconButton
|
|
138
|
+
icon="fa-regular fa-hand"
|
|
139
|
+
title="Raise/Lower Hand"
|
|
140
|
+
onClick={() => {
|
|
141
|
+
setIsMobileActionsOverlayVisible(false)
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
),
|
|
145
|
+
group: 2,
|
|
146
|
+
key: 'hand-raise',
|
|
147
|
+
show: !isLocalSettingConfiguration
|
|
148
|
+
},
|
|
149
|
+
// Participants List Button
|
|
150
|
+
{
|
|
151
|
+
key: 'participants-list',
|
|
152
|
+
component: <DockButton mainIcon="fa-regular fa-user-group" mainTitle="Participants" onMainClick={() => {}} />,
|
|
153
|
+
mobileComponent: (
|
|
154
|
+
<MobileIconButton
|
|
155
|
+
icon="fa-regular fa-user-group"
|
|
156
|
+
title="Participants"
|
|
157
|
+
onClick={() => {
|
|
158
|
+
setIsMobileActionsOverlayVisible(false)
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
group: 2,
|
|
163
|
+
show: !isLocalSettingConfiguration
|
|
164
|
+
},
|
|
165
|
+
// Chat Button
|
|
166
|
+
{
|
|
167
|
+
component: <DockButton mainIcon="fa-regular fa-comment-lines" mainTitle="Chat" onMainClick={() => {}} />,
|
|
168
|
+
mobileComponent: (
|
|
169
|
+
<MobileIconButton
|
|
170
|
+
icon="fa-regular fa-comment-lines"
|
|
171
|
+
title="Chat"
|
|
172
|
+
onClick={() => {
|
|
173
|
+
setIsMobileActionsOverlayVisible(false)
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
),
|
|
177
|
+
group: 2,
|
|
178
|
+
key: 'chat',
|
|
179
|
+
show: !isLocalSettingConfiguration
|
|
180
|
+
},
|
|
181
|
+
// Settings Button
|
|
182
|
+
{
|
|
183
|
+
component: (
|
|
184
|
+
<DockButton
|
|
185
|
+
mainIcon="fa-regular fa-gear"
|
|
186
|
+
mainLabel={isLocalSettingConfiguration && isTabletOrDesktop ? 'Settings' : undefined}
|
|
187
|
+
mainTitle="Settings"
|
|
188
|
+
onMainClick={() => setIsSettingsOverlayVisible({ visible: true, tab: SettingsTab.Audio })}
|
|
189
|
+
/>
|
|
190
|
+
),
|
|
191
|
+
mobileComponent: (
|
|
192
|
+
<MobileIconButton
|
|
193
|
+
icon="fa-regular fa-gear"
|
|
194
|
+
title="Settings"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
setIsMobileActionsOverlayVisible(false)
|
|
197
|
+
setIsSettingsOverlayVisible({ visible: true, tab: SettingsTab.Audio })
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
),
|
|
201
|
+
group: 2,
|
|
202
|
+
key: 'settings',
|
|
203
|
+
show: true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
component: (
|
|
207
|
+
<DockButton
|
|
208
|
+
mainIcon="fa-regular fa-phone-hangup !text-white"
|
|
209
|
+
mainTitle="Leave Meeting"
|
|
210
|
+
onMainClick={exit}
|
|
211
|
+
className="!bg-danger-50"
|
|
212
|
+
/>
|
|
213
|
+
),
|
|
214
|
+
group: 3,
|
|
215
|
+
key: 'leave',
|
|
216
|
+
show: !isLocalSettingConfiguration
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
const renderGroup = (group: number, justify: 'start' | 'center' | 'end') => {
|
|
221
|
+
const filteredItems = items.filter((item) => item.group === group && item.show)
|
|
222
|
+
if (filteredItems.length === 0) return null
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className={cn('flex items-center gap-16', `justify-${justify}`)}>
|
|
226
|
+
{filteredItems.map((item) => (
|
|
227
|
+
<span key={item.key}>{item.component}</span>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const renderElipsisButton = () => {
|
|
234
|
+
return (
|
|
235
|
+
<div className="relative">
|
|
236
|
+
<DockButton
|
|
237
|
+
mainIcon="fa-regular fa-ellipsis-v"
|
|
238
|
+
mainTitle="More Actions"
|
|
239
|
+
onMainClick={() => setIsMobileActionsOverlayVisible(true)}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const audioMenuItems: MenuItem[] = audioOutputList
|
|
246
|
+
.map((device) => ({
|
|
247
|
+
id: device.deviceId,
|
|
248
|
+
label: device.label,
|
|
249
|
+
command: () => setActiveAudioOutput(device.deviceId),
|
|
250
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
251
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeAudioOutput} />
|
|
252
|
+
)
|
|
253
|
+
}))
|
|
254
|
+
.concat([
|
|
255
|
+
{
|
|
256
|
+
label: 'Audio Settings',
|
|
257
|
+
id: 'audio-settings',
|
|
258
|
+
command: () => {
|
|
259
|
+
setIsSettingsOverlayVisible({ visible: true, tab: SettingsTab.Audio })
|
|
260
|
+
},
|
|
261
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
262
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeAudioOutput} />
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
])
|
|
266
|
+
|
|
267
|
+
const videoMenuItems: MenuItem[] = cameraList
|
|
268
|
+
.map((device) => ({
|
|
269
|
+
label: device.label,
|
|
270
|
+
id: device.deviceId,
|
|
271
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
272
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeCamera} />
|
|
273
|
+
),
|
|
274
|
+
command: () => setActiveCamera(device.deviceId)
|
|
275
|
+
}))
|
|
276
|
+
.concat([
|
|
277
|
+
{
|
|
278
|
+
label: 'Camera Settings',
|
|
279
|
+
id: 'camera-settings',
|
|
280
|
+
command: () => {
|
|
281
|
+
setIsSettingsOverlayVisible({ visible: true, tab: SettingsTab.Video })
|
|
282
|
+
},
|
|
283
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
284
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeCamera} />
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
const micMenuItems: MenuItem[] = micList
|
|
290
|
+
.map((device) => ({
|
|
291
|
+
id: device.deviceId,
|
|
292
|
+
label: device.label,
|
|
293
|
+
command: () => setActiveMicrophone(device.deviceId),
|
|
294
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
295
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeMicrophone} />
|
|
296
|
+
)
|
|
297
|
+
}))
|
|
298
|
+
.concat([
|
|
299
|
+
{
|
|
300
|
+
id: 'microphone-settings',
|
|
301
|
+
label: 'Microphone Settings',
|
|
302
|
+
command: () => {
|
|
303
|
+
setIsSettingsOverlayVisible({ visible: true, tab: SettingsTab.Audio })
|
|
304
|
+
},
|
|
305
|
+
template: (item: MenuItem, options: MenuItemOptions) => (
|
|
306
|
+
<MenuItemTemplate item={item} options={options} activeItem={activeMicrophone} />
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<>
|
|
313
|
+
<div
|
|
314
|
+
className={cn(
|
|
315
|
+
'flex items-center justify-between rounded-full px-16 py-12 w-full shadow-medium overflow-hidden',
|
|
316
|
+
{ 'gap-16 !w-fit': isLocalSettingConfiguration }
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{renderGroup(1, 'start')}
|
|
320
|
+
{isTabletOrDesktop ? renderGroup(2, 'center') : renderElipsisButton()}
|
|
321
|
+
{renderGroup(3, 'end')}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<SettingsOverlay
|
|
325
|
+
breakpoint={breakpoint}
|
|
326
|
+
onHide={() => setIsSettingsOverlayVisible({ ...isSettingsOverlayVisible, visible: false })}
|
|
327
|
+
visible={isSettingsOverlayVisible.visible}
|
|
328
|
+
defaultTab={isSettingsOverlayVisible.tab}
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
{/* Mobile Actions Overlay */}
|
|
332
|
+
<Overlay
|
|
333
|
+
visible={isMobileActionsOverlayVisible}
|
|
334
|
+
onHide={() => setIsMobileActionsOverlayVisible(false)}
|
|
335
|
+
header="Options"
|
|
336
|
+
breakpoint={breakpoint}
|
|
337
|
+
>
|
|
338
|
+
<div className="flex flex-col gap-16 px-16 py-8 font-bold">
|
|
339
|
+
{items
|
|
340
|
+
.filter((item) => item.key !== 'more-actions' && item.group === 2)
|
|
341
|
+
.map((item) => (
|
|
342
|
+
<span key={item.key}>{item.mobileComponent ?? item.component}</span>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
</Overlay>
|
|
346
|
+
<Menu model={audioMenuItems} popup ref={audioMenuOptionsRef} id="popup_menu" popupAlignment="left" />
|
|
347
|
+
<Menu model={videoMenuItems} popup ref={videoMenuOptionsRef} id="popup_menu" popupAlignment="left" />
|
|
348
|
+
<Menu model={micMenuItems} popup ref={micMenuOptionsRef} id="popup_menu" popupAlignment="left" />
|
|
349
|
+
</>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export default ButtonsDock
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
|
|
4
|
+
type DockButtonProps = {
|
|
5
|
+
className?: string
|
|
6
|
+
// Main action (always required)
|
|
7
|
+
mainIcon: string
|
|
8
|
+
mainLabel?: string
|
|
9
|
+
mainTitle?: string
|
|
10
|
+
onMainClick: (e: React.MouseEvent<HTMLButtonElement>) => void
|
|
11
|
+
// Optional secondary action
|
|
12
|
+
showSecondary?: boolean
|
|
13
|
+
secondaryIcon?: string
|
|
14
|
+
secondaryTitle?: string
|
|
15
|
+
onSecondaryClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders a customizable dock button component with optional main and secondary actions.
|
|
20
|
+
*
|
|
21
|
+
* @param className - Additional CSS class names to apply to the container.
|
|
22
|
+
* @param mainIcon - The CSS class for the main icon to display.
|
|
23
|
+
* @param mainLabel - Optional label text for the main button.
|
|
24
|
+
* @param mainTitle - The title attribute for the main button (for accessibility/tooltips).
|
|
25
|
+
* @param onMainClick - Click handler for the main button action.
|
|
26
|
+
* @param showSecondary - Flag to indicate if the secondary button should be displayed.
|
|
27
|
+
* @param secondaryIcon - Optional CSS class for the secondary icon to display.
|
|
28
|
+
* @param secondaryTitle - The title attribute for the secondary button (for accessibility/tooltips).
|
|
29
|
+
* @param onSecondaryClick - Optional click handler for the secondary button action.
|
|
30
|
+
*
|
|
31
|
+
* @returns A dock button component with main and optional secondary actions.
|
|
32
|
+
*/
|
|
33
|
+
const DockButton = ({
|
|
34
|
+
className,
|
|
35
|
+
mainIcon,
|
|
36
|
+
mainLabel,
|
|
37
|
+
mainTitle,
|
|
38
|
+
onMainClick,
|
|
39
|
+
secondaryIcon,
|
|
40
|
+
secondaryTitle,
|
|
41
|
+
onSecondaryClick,
|
|
42
|
+
showSecondary = false
|
|
43
|
+
}: DockButtonProps) => {
|
|
44
|
+
const hasLabel = Boolean(mainLabel)
|
|
45
|
+
const hasSecondary = showSecondary && secondaryIcon && onSecondaryClick
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex items-center bg-neutral-85 h-40 rounded-full',
|
|
51
|
+
{
|
|
52
|
+
'justify-center min-w-40': !hasLabel && !hasSecondary,
|
|
53
|
+
'justify-between gap-12 px-4': hasSecondary,
|
|
54
|
+
'px-12': hasLabel && !hasSecondary
|
|
55
|
+
},
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{/* Main action */}
|
|
60
|
+
<button
|
|
61
|
+
className={cn('dock-button-main flex items-center justify-center', {
|
|
62
|
+
'p-8 h-full': hasSecondary,
|
|
63
|
+
'w-full h-40': !hasSecondary
|
|
64
|
+
})}
|
|
65
|
+
onClick={onMainClick}
|
|
66
|
+
title={mainTitle}
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
<span className="flex items-center justify-center w-24 h-24">
|
|
70
|
+
<i className={mainIcon} aria-hidden="true" />
|
|
71
|
+
</span>
|
|
72
|
+
{hasLabel && <span className="ml-4">{mainLabel}</span>}
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
{/* Secondary action (optional) */}
|
|
76
|
+
{hasSecondary && (
|
|
77
|
+
<button
|
|
78
|
+
className="dock-button-secondary flex items-center justify-center w-32 h-full hover:bg-neutral-95 rounded-full"
|
|
79
|
+
onClick={onSecondaryClick}
|
|
80
|
+
title={secondaryTitle}
|
|
81
|
+
type="button"
|
|
82
|
+
>
|
|
83
|
+
<i className={secondaryIcon} aria-hidden="true" />
|
|
84
|
+
</button>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default DockButton
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MenuItem, MenuItemOptions } from 'primereact/menuitem'
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
item: MenuItem
|
|
5
|
+
options: MenuItemOptions
|
|
6
|
+
activeItem: string | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MenuItemTemplate = ({ item, options, activeItem }: Props) => {
|
|
10
|
+
const isActive = item.id === activeItem
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="p-menuitem-content" data-pc-section="content">
|
|
14
|
+
<a
|
|
15
|
+
aria-label={item.label}
|
|
16
|
+
className={options.className}
|
|
17
|
+
data-test={item?.data?.dataTestId}
|
|
18
|
+
data-pc-section="action"
|
|
19
|
+
href="#"
|
|
20
|
+
onClick={options.onClick}
|
|
21
|
+
>
|
|
22
|
+
{isActive && (
|
|
23
|
+
<span className={options.iconClassName} data-pc-section="icon">
|
|
24
|
+
<i className="fa-regular fa-check" aria-hidden="true" />
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
<span className={options.labelClassName} data-pc-section="label">
|
|
28
|
+
{item.label}
|
|
29
|
+
</span>
|
|
30
|
+
</a>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default MenuItemTemplate
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ButtonsDock'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import cn from 'classnames'
|
|
2
|
+
|
|
3
|
+
type MobileIconButtonProps = {
|
|
4
|
+
className?: string
|
|
5
|
+
icon: string
|
|
6
|
+
onClick: () => void
|
|
7
|
+
title?: string
|
|
8
|
+
id?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MobileIconButton = ({ className, icon, onClick, title, id }: MobileIconButtonProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
aria-label={title}
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex items-center justify-start gap-4 w-full h-40 px-8 rounded-l border border-neutral-80 hover:bg-neutral-85',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
id={id}
|
|
20
|
+
onClick={onClick}
|
|
21
|
+
>
|
|
22
|
+
<span className="flex items-center justify-center w-24 h-24">
|
|
23
|
+
<i className={icon} aria-hidden="true" />
|
|
24
|
+
</span>
|
|
25
|
+
<span className="flex-1 text-left">{title}</span>
|
|
26
|
+
</button>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default MobileIconButton
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MobileIconButton'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Dialog } from 'primereact/dialog'
|
|
2
|
+
import { Sidebar } from 'primereact/sidebar'
|
|
3
|
+
import { Breakpoint } from '../../hooks/useDeviceSize'
|
|
4
|
+
|
|
5
|
+
type OverlayProps = {
|
|
6
|
+
breakpoint: Breakpoint
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
header?: string | React.ReactNode
|
|
10
|
+
onHide: () => void
|
|
11
|
+
width?: string | number
|
|
12
|
+
visible: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Overlay = ({
|
|
16
|
+
breakpoint,
|
|
17
|
+
children,
|
|
18
|
+
onHide,
|
|
19
|
+
className,
|
|
20
|
+
visible = false,
|
|
21
|
+
width = '50vw',
|
|
22
|
+
header = ''
|
|
23
|
+
}: OverlayProps) => {
|
|
24
|
+
const isTablet = breakpoint < Breakpoint.Desktop
|
|
25
|
+
|
|
26
|
+
if (isTablet) {
|
|
27
|
+
return (
|
|
28
|
+
<Sidebar
|
|
29
|
+
blockScroll
|
|
30
|
+
className={className}
|
|
31
|
+
closeIcon={<i className="fa-regular fa-xmark fa-lg" aria-hidden="true" />}
|
|
32
|
+
header={header}
|
|
33
|
+
onHide={onHide}
|
|
34
|
+
position="bottom"
|
|
35
|
+
pt={{
|
|
36
|
+
header: { className: 'border-b border-neutral-80 font-bold text-l' },
|
|
37
|
+
root: { className: '[&_.p-sidebar-content]:px-0 h-fit' },
|
|
38
|
+
content: { className: 'p-0' }
|
|
39
|
+
}}
|
|
40
|
+
visible={visible}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Sidebar>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Dialog
|
|
49
|
+
blockScroll
|
|
50
|
+
className={className}
|
|
51
|
+
closable
|
|
52
|
+
closeIcon={<i className="fa-regular fa-xmark fa-lg" aria-hidden="true" />}
|
|
53
|
+
contentStyle={{ padding: '0' }}
|
|
54
|
+
dismissableMask
|
|
55
|
+
draggable
|
|
56
|
+
focusOnShow={false}
|
|
57
|
+
header={header}
|
|
58
|
+
headerStyle={{
|
|
59
|
+
borderBottom: '1px solid var(--neutral-80)',
|
|
60
|
+
padding: '16px'
|
|
61
|
+
}}
|
|
62
|
+
onHide={onHide}
|
|
63
|
+
pt={{
|
|
64
|
+
headerTitle: { className: '!font-bold !text-l' }
|
|
65
|
+
}}
|
|
66
|
+
style={{ width }}
|
|
67
|
+
visible={visible}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</Dialog>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default Overlay
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Overlay'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Participant, VideoClient } from '@zoom/videosdk'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
import { ReactSetter } from '../types'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
participants: Participant[]
|
|
7
|
+
setIsCamOn: ReactSetter<boolean>
|
|
8
|
+
setIsMicOn: ReactSetter<boolean>
|
|
9
|
+
zmClient: typeof VideoClient
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ParticipantsList = ({ zmClient, setIsCamOn, setIsMicOn, participants }: Props) => {
|
|
13
|
+
return (
|
|
14
|
+
<ul className="flex flex-col gap-8 w-full self-start">
|
|
15
|
+
{participants.map((user) => {
|
|
16
|
+
const isCurrentUser = user.userId === zmClient?.getCurrentUserInfo()?.userId
|
|
17
|
+
return (
|
|
18
|
+
<li key={user.userId} className="flex items-center justify-between gap-16 p-4 rounded shadow-medium w-full">
|
|
19
|
+
<span className="truncate flex gap-4">
|
|
20
|
+
<span className="truncate">{user.displayName}</span>
|
|
21
|
+
<span className="truncate">id: {user.userId}</span>
|
|
22
|
+
<span className="text-sm">{isCurrentUser ? '(You)' : ''}</span>
|
|
23
|
+
</span>
|
|
24
|
+
<div className="flex items-center gap-8">
|
|
25
|
+
<button
|
|
26
|
+
onClick={() => setIsCamOn((prev) => !prev)}
|
|
27
|
+
title={user.bVideoOn ? 'Stop Video' : 'Start Video'}
|
|
28
|
+
disabled={!isCurrentUser}
|
|
29
|
+
className={cn('w-24', { 'opacity-32 cursor-not-allowed': !isCurrentUser })}
|
|
30
|
+
>
|
|
31
|
+
<i className={`fa-regular ${user.bVideoOn ? 'fa-video' : 'fa-video-slash'}`} aria-hidden="true" />
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => setIsMicOn((prev) => !prev)}
|
|
35
|
+
title={user.muted || !user.audio ? 'Unmute' : 'Mute'}
|
|
36
|
+
disabled={!isCurrentUser}
|
|
37
|
+
className={cn('w-24', { 'opacity-32 cursor-not-allowed': !isCurrentUser })}
|
|
38
|
+
>
|
|
39
|
+
<i
|
|
40
|
+
className={`fa-regular ${user.muted || !user.audio ? 'fa-microphone-slash' : 'fa-microphone'}`}
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
/>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</li>
|
|
46
|
+
)
|
|
47
|
+
})}
|
|
48
|
+
</ul>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default ParticipantsList
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useSettingsOverlayContext } from './context'
|
|
2
|
+
import AudioSettings from './Tabs/AudioSettings'
|
|
3
|
+
import BackgroundSettings from './Tabs/BackgroundSettings'
|
|
4
|
+
import VideoSettings from './Tabs/VideoSettings'
|
|
5
|
+
|
|
6
|
+
const settingsRegistry = {
|
|
7
|
+
Audio: AudioSettings,
|
|
8
|
+
Video: VideoSettings,
|
|
9
|
+
Background: BackgroundSettings
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SettingsContent = () => {
|
|
13
|
+
const { selectedSettingsTab } = useSettingsOverlayContext()
|
|
14
|
+
|
|
15
|
+
const ActiveComponent = settingsRegistry[selectedSettingsTab]
|
|
16
|
+
return ActiveComponent ? <ActiveComponent /> : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default SettingsContent
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import cn from 'classnames'
|
|
2
|
+
import MobileIconButton from '../MobileIconButton'
|
|
3
|
+
import { SettingsTab, useSettingsOverlayContext } from './context'
|
|
4
|
+
|
|
5
|
+
const tabs: { id: SettingsTab; icon: string; title: string }[] = [
|
|
6
|
+
{ id: SettingsTab.Audio, icon: 'fa-regular fa-microphone', title: 'Audio' },
|
|
7
|
+
{ id: SettingsTab.Video, icon: 'fa-regular fa-video', title: 'Video' },
|
|
8
|
+
{ id: SettingsTab.Background, icon: 'fa-regular fa-image', title: 'Fondos' }
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const SettingsMenu = () => {
|
|
12
|
+
const { selectedSettingsTab, setSelectedSettingsTab } = useSettingsOverlayContext()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-col sm:flex-row md:flex-col w-full p-16 md:p-0 gap-16 md:gap-0 md:w-fit md:min-w-[200px] md:flex-shrink-0 overflow-visible">
|
|
16
|
+
{tabs.map(({ id, icon, title }) => (
|
|
17
|
+
<MobileIconButton
|
|
18
|
+
key={id}
|
|
19
|
+
className={cn('font-bold md:rounded-none md:border-0', { 'bg-neutral-95': selectedSettingsTab === id })}
|
|
20
|
+
icon={icon}
|
|
21
|
+
id={id}
|
|
22
|
+
onClick={() => setSelectedSettingsTab(id)}
|
|
23
|
+
title={title}
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default SettingsMenu
|