@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.
Files changed (44) hide show
  1. package/README.md +2 -0
  2. package/package.json +75 -0
  3. package/src/App.tsx +7 -0
  4. package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.stories.tsx +50 -0
  5. package/src/components/ZoomVideoPlugin/ZoomVideoPlugin.tsx +253 -0
  6. package/src/components/ZoomVideoPlugin/components/ButtonsDock/ButtonsDock.tsx +353 -0
  7. package/src/components/ZoomVideoPlugin/components/ButtonsDock/DockButton.tsx +90 -0
  8. package/src/components/ZoomVideoPlugin/components/ButtonsDock/MenuItemTemplate.tsx +35 -0
  9. package/src/components/ZoomVideoPlugin/components/ButtonsDock/index.ts +1 -0
  10. package/src/components/ZoomVideoPlugin/components/MobileIconButton/MobileIconButton.tsx +30 -0
  11. package/src/components/ZoomVideoPlugin/components/MobileIconButton/index.ts +1 -0
  12. package/src/components/ZoomVideoPlugin/components/Overlay/Overlay.tsx +74 -0
  13. package/src/components/ZoomVideoPlugin/components/Overlay/index.ts +1 -0
  14. package/src/components/ZoomVideoPlugin/components/ParticipantsList.tsx +52 -0
  15. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsContent.tsx +19 -0
  16. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsMenu.tsx +30 -0
  17. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/SettingsOverlay.tsx +52 -0
  18. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/AudioSettings.tsx +191 -0
  19. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/BackgroundSettings.tsx +47 -0
  20. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownItemTemplate.tsx +20 -0
  21. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/DropdownValueTemplate.tsx +12 -0
  22. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/Tabs/VideoSettings.tsx +30 -0
  23. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/context.ts +20 -0
  24. package/src/components/ZoomVideoPlugin/components/SettingsOverlay/index.ts +1 -0
  25. package/src/components/ZoomVideoPlugin/components/Video.tsx +35 -0
  26. package/src/components/ZoomVideoPlugin/constants.ts +4 -0
  27. package/src/components/ZoomVideoPlugin/context.ts +86 -0
  28. package/src/components/ZoomVideoPlugin/hooks/useClientMessages.ts +142 -0
  29. package/src/components/ZoomVideoPlugin/hooks/useDeviceSize.ts +24 -0
  30. package/src/components/ZoomVideoPlugin/hooks/useStartVideoOptions.ts +14 -0
  31. package/src/components/ZoomVideoPlugin/hooks/useZoomVideoPlayer.tsx +142 -0
  32. package/src/components/ZoomVideoPlugin/index.ts +2 -0
  33. package/src/components/ZoomVideoPlugin/lib/platforms.ts +17 -0
  34. package/src/components/ZoomVideoPlugin/pages/AfterSession.tsx +14 -0
  35. package/src/components/ZoomVideoPlugin/pages/MainSession.tsx +53 -0
  36. package/src/components/ZoomVideoPlugin/pages/PanelistsSession.tsx +97 -0
  37. package/src/components/ZoomVideoPlugin/pages/PreSessionConfiguration.tsx +154 -0
  38. package/src/components/ZoomVideoPlugin/types.global.d.ts +15 -0
  39. package/src/components/ZoomVideoPlugin/types.ts +23 -0
  40. package/src/global.d.ts +46 -0
  41. package/src/index.css +4 -0
  42. package/src/index.ts +4 -0
  43. package/src/main.tsx +10 -0
  44. 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