vg-x07df 0.1.0

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 (140) hide show
  1. package/.azure-pipelines/publish-public.yml +37 -0
  2. package/.azure-pipelines/publish.yml +39 -0
  3. package/.changeset/README.md +8 -0
  4. package/.changeset/config.json +11 -0
  5. package/AUTO_JOIN_GUIDE.md +411 -0
  6. package/README.md +215 -0
  7. package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
  8. package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
  9. package/biome.json +48 -0
  10. package/examples/demo/.env.example +19 -0
  11. package/examples/demo/CHANGELOG.md +22 -0
  12. package/examples/demo/README.md +72 -0
  13. package/examples/demo/eslint.config.js +23 -0
  14. package/examples/demo/index.html +13 -0
  15. package/examples/demo/package.json +34 -0
  16. package/examples/demo/pnpm-lock.yaml +2098 -0
  17. package/examples/demo/pnpm-workspace.yaml +1 -0
  18. package/examples/demo/public/vite.svg +1 -0
  19. package/examples/demo/src/App.css +52 -0
  20. package/examples/demo/src/App.tsx +176 -0
  21. package/examples/demo/src/assets/react.svg +1 -0
  22. package/examples/demo/src/components/auth/LoginForm.css +144 -0
  23. package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
  24. package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
  25. package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
  26. package/examples/demo/src/components/calling/CallInitiator.css +258 -0
  27. package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
  28. package/examples/demo/src/components/calling/CallNotifications.css +119 -0
  29. package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
  30. package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
  31. package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
  32. package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
  33. package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
  34. package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
  35. package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
  36. package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
  37. package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
  38. package/examples/demo/src/components/conference/PaginationControls.css +67 -0
  39. package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
  40. package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
  41. package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
  42. package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
  43. package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
  44. package/examples/demo/src/components/conference/VideoConference.css +214 -0
  45. package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
  46. package/examples/demo/src/contexts/AuthContext.tsx +105 -0
  47. package/examples/demo/src/hooks/useAuth.ts +5 -0
  48. package/examples/demo/src/hooks/useCallTimer.ts +42 -0
  49. package/examples/demo/src/index.css +68 -0
  50. package/examples/demo/src/main.tsx +10 -0
  51. package/examples/demo/src/services/auth.service.ts +153 -0
  52. package/examples/demo/src/types/auth.types.ts +31 -0
  53. package/examples/demo/tsconfig.app.json +28 -0
  54. package/examples/demo/tsconfig.json +7 -0
  55. package/examples/demo/tsconfig.node.json +26 -0
  56. package/examples/demo/vite.config.ts +15 -0
  57. package/images/callpad-without-ai.png +0 -0
  58. package/package.json +28 -0
  59. package/packages/sdk/CHANGELOG.md +33 -0
  60. package/packages/sdk/LICENSE +21 -0
  61. package/packages/sdk/README.md +97 -0
  62. package/packages/sdk/documentation.md +1132 -0
  63. package/packages/sdk/openapi-ts.config.ts +7 -0
  64. package/packages/sdk/package.json +88 -0
  65. package/packages/sdk/src/core/auth.manager.ts +52 -0
  66. package/packages/sdk/src/core/events/event-bus.ts +301 -0
  67. package/packages/sdk/src/core/events/index.ts +8 -0
  68. package/packages/sdk/src/core/events/types.ts +165 -0
  69. package/packages/sdk/src/core/index.ts +3 -0
  70. package/packages/sdk/src/core/signal/api.config.ts +49 -0
  71. package/packages/sdk/src/core/signal/index.ts +16 -0
  72. package/packages/sdk/src/core/signal/signal.client.ts +101 -0
  73. package/packages/sdk/src/core/signal/types.ts +110 -0
  74. package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
  75. package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
  76. package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
  77. package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
  78. package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
  79. package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
  80. package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
  81. package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
  82. package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
  83. package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
  84. package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
  85. package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
  86. package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
  87. package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
  88. package/packages/sdk/src/core/socketio/index.ts +5 -0
  89. package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
  90. package/packages/sdk/src/core/socketio/types.ts +14 -0
  91. package/packages/sdk/src/core/types.ts +23 -0
  92. package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
  93. package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
  94. package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
  95. package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
  96. package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
  97. package/packages/sdk/src/generated/api/core/request.ts +339 -0
  98. package/packages/sdk/src/generated/api/index.ts +5 -0
  99. package/packages/sdk/src/generated/api/models.ts +219 -0
  100. package/packages/sdk/src/generated/api/services.ts +225 -0
  101. package/packages/sdk/src/hooks/index.ts +21 -0
  102. package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
  103. package/packages/sdk/src/hooks/useCallActions.ts +28 -0
  104. package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
  105. package/packages/sdk/src/hooks/useCallState.ts +23 -0
  106. package/packages/sdk/src/hooks/useConnection.ts +15 -0
  107. package/packages/sdk/src/hooks/useDevices.ts +296 -0
  108. package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
  109. package/packages/sdk/src/hooks/useErrors.ts +84 -0
  110. package/packages/sdk/src/hooks/useEvent.ts +188 -0
  111. package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
  112. package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
  113. package/packages/sdk/src/hooks/useParticipants.ts +111 -0
  114. package/packages/sdk/src/index.ts +66 -0
  115. package/packages/sdk/src/livekit/constants.ts +76 -0
  116. package/packages/sdk/src/livekit/device.manager.ts +172 -0
  117. package/packages/sdk/src/livekit/error-classifier.ts +155 -0
  118. package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
  119. package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
  120. package/packages/sdk/src/livekit/index.ts +49 -0
  121. package/packages/sdk/src/livekit/livekit.service.ts +110 -0
  122. package/packages/sdk/src/livekit/media.controls.ts +315 -0
  123. package/packages/sdk/src/livekit/room.manager.ts +79 -0
  124. package/packages/sdk/src/livekit/track.utils.ts +230 -0
  125. package/packages/sdk/src/livekit/types.ts +135 -0
  126. package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
  127. package/packages/sdk/src/services/call-actions.ts +260 -0
  128. package/packages/sdk/src/services/error-recovery.ts +461 -0
  129. package/packages/sdk/src/services/index.ts +2 -0
  130. package/packages/sdk/src/services/sdk-builder.ts +104 -0
  131. package/packages/sdk/src/state/errors.ts +163 -0
  132. package/packages/sdk/src/state/selectors.ts +28 -0
  133. package/packages/sdk/src/state/store.ts +36 -0
  134. package/packages/sdk/src/state/types.ts +151 -0
  135. package/packages/sdk/src/utils/logger.ts +183 -0
  136. package/packages/sdk/tsconfig.json +49 -0
  137. package/packages/sdk/tsup.config.ts +51 -0
  138. package/pnpm-workspace.yaml +4 -0
  139. package/tsconfig.base.json +19 -0
  140. package/turbo.json +34 -0
@@ -0,0 +1,192 @@
1
+ .incoming-call-modal-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.8);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ animation: fadeIn 0.3s ease-out;
13
+ }
14
+
15
+ .incoming-call-modal {
16
+ background: white;
17
+ border-radius: 20px;
18
+ padding: 2rem;
19
+ min-width: 350px;
20
+ max-width: 400px;
21
+ text-align: center;
22
+ position: relative;
23
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
24
+ animation: slideUp 0.3s ease-out;
25
+ }
26
+
27
+ .incoming-call-content {
28
+ position: relative;
29
+ z-index: 2;
30
+ }
31
+
32
+ .caller-avatar {
33
+ margin: 0 auto 1.5rem;
34
+ width: 100px;
35
+ height: 100px;
36
+ border-radius: 50%;
37
+ overflow: hidden;
38
+ border: 4px solid #007bff;
39
+ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
40
+ }
41
+
42
+ .caller-avatar img {
43
+ width: 100%;
44
+ height: 100%;
45
+ object-fit: cover;
46
+ }
47
+
48
+ .avatar-placeholder {
49
+ width: 100%;
50
+ height: 100%;
51
+ background: linear-gradient(135deg, #007bff, #0056b3);
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ font-size: 2rem;
56
+ font-weight: bold;
57
+ color: white;
58
+ }
59
+
60
+ .call-details h2 {
61
+ margin: 0 0 0.5rem;
62
+ font-size: 1.5rem;
63
+ color: #333;
64
+ font-weight: 600;
65
+ }
66
+
67
+ .call-details p {
68
+ margin: 0 0 2rem;
69
+ color: #666;
70
+ font-size: 1rem;
71
+ }
72
+
73
+ .call-actions {
74
+ display: flex;
75
+ gap: 1rem;
76
+ justify-content: center;
77
+ }
78
+
79
+ .call-actions button {
80
+ padding: 1rem 2rem;
81
+ border: none;
82
+ border-radius: 50px;
83
+ font-size: 1rem;
84
+ font-weight: 600;
85
+ cursor: pointer;
86
+ transition: all 0.2s ease;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 0.5rem;
90
+ min-width: 120px;
91
+ justify-content: center;
92
+ }
93
+
94
+ .accept-button {
95
+ background: linear-gradient(135deg, #28a745, #20c997);
96
+ color: white;
97
+ }
98
+
99
+ .accept-button:hover {
100
+ transform: translateY(-2px);
101
+ box-shadow: 0 8px 20px rgba(40, 167, 69, 0.4);
102
+ }
103
+
104
+ .decline-button {
105
+ background: linear-gradient(135deg, #dc3545, #c82333);
106
+ color: white;
107
+ }
108
+
109
+ .decline-button:hover {
110
+ transform: translateY(-2px);
111
+ box-shadow: 0 8px 20px rgba(220, 53, 69, 0.4);
112
+ }
113
+
114
+ .call-actions button .icon {
115
+ font-size: 1.2rem;
116
+ }
117
+
118
+ .incoming-call-animation {
119
+ position: absolute;
120
+ top: 50%;
121
+ left: 50%;
122
+ transform: translate(-50%, -50%);
123
+ pointer-events: none;
124
+ }
125
+
126
+ .pulse-ring {
127
+ position: absolute;
128
+ border: 2px solid rgba(0, 123, 255, 0.3);
129
+ border-radius: 50%;
130
+ width: 200px;
131
+ height: 200px;
132
+ top: -100px;
133
+ left: -100px;
134
+ animation: pulse 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
135
+ }
136
+
137
+ .pulse-ring.delay-1 {
138
+ animation-delay: 0.5s;
139
+ }
140
+
141
+ .pulse-ring.delay-2 {
142
+ animation-delay: 1s;
143
+ }
144
+
145
+ @keyframes pulse {
146
+ 0% {
147
+ transform: scale(0.33);
148
+ opacity: 1;
149
+ }
150
+ 100% {
151
+ transform: scale(1);
152
+ opacity: 0;
153
+ }
154
+ }
155
+
156
+ @keyframes fadeIn {
157
+ from {
158
+ opacity: 0;
159
+ }
160
+ to {
161
+ opacity: 1;
162
+ }
163
+ }
164
+
165
+ @keyframes slideUp {
166
+ from {
167
+ opacity: 0;
168
+ transform: translateY(50px);
169
+ }
170
+ to {
171
+ opacity: 1;
172
+ transform: translateY(0);
173
+ }
174
+ }
175
+
176
+ /* Responsive design */
177
+ @media (max-width: 480px) {
178
+ .incoming-call-modal {
179
+ margin: 1rem;
180
+ padding: 1.5rem;
181
+ min-width: auto;
182
+ width: calc(100% - 2rem);
183
+ }
184
+
185
+ .call-actions {
186
+ flex-direction: column;
187
+ }
188
+
189
+ .call-actions button {
190
+ width: 100%;
191
+ }
192
+ }
@@ -0,0 +1,78 @@
1
+ import { useCallState, useCallActions } from 'vg-x07df';
2
+ import './IncomingCallModal.css';
3
+
4
+ export function IncomingCallModal() {
5
+ const { incomingCall } = useCallState();
6
+ const { accept, decline } = useCallActions();
7
+
8
+ const handleAccept = async () => {
9
+ if (!incomingCall) return;
10
+
11
+ try {
12
+ await accept(incomingCall.callId);
13
+ } catch (error) {
14
+ console.error('Failed to accept call:', error);
15
+ }
16
+ };
17
+
18
+ const handleDecline = async () => {
19
+ if (!incomingCall) return;
20
+
21
+ try {
22
+ await decline(incomingCall.callId);
23
+ } catch (error) {
24
+ console.error('Failed to decline call:', error);
25
+ }
26
+ };
27
+
28
+ if (!incomingCall) return null;
29
+
30
+ return (
31
+ <div className="incoming-call-modal-overlay">
32
+ <div className="incoming-call-modal">
33
+ <div className="incoming-call-content">
34
+ <div className="caller-avatar">
35
+ {incomingCall.caller.avatarUrl ? (
36
+ <img src={incomingCall.caller.avatarUrl} alt={incomingCall.caller.name} />
37
+ ) : (
38
+ <div className="avatar-placeholder">
39
+ {incomingCall.caller.name.charAt(0).toUpperCase()}
40
+ </div>
41
+ )}
42
+ </div>
43
+
44
+ <div className="call-details">
45
+ <h2>{incomingCall.caller.name}</h2>
46
+ <p>Incoming {incomingCall.type.toLowerCase()} call</p>
47
+ </div>
48
+
49
+ <div className="call-actions">
50
+ <button
51
+ className="decline-button"
52
+ onClick={handleDecline}
53
+ title="Decline call"
54
+ >
55
+ <span className="icon">📞</span>
56
+ Decline
57
+ </button>
58
+
59
+ <button
60
+ className="accept-button"
61
+ onClick={handleAccept}
62
+ title="Accept call"
63
+ >
64
+ <span className="icon">📞</span>
65
+ Accept
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ <div className="incoming-call-animation">
71
+ <div className="pulse-ring"></div>
72
+ <div className="pulse-ring delay-1"></div>
73
+ <div className="pulse-ring delay-2"></div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,156 @@
1
+ .minimized-call {
2
+ position: fixed;
3
+ bottom: 20px;
4
+ right: 20px;
5
+ background: #1f2937;
6
+ border-radius: 12px;
7
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 12px;
11
+ padding: 12px;
12
+ min-width: 280px;
13
+ max-width: 400px;
14
+ z-index: 1000;
15
+ border: 1px solid #374151;
16
+ animation: slideInUp 0.3s ease-out;
17
+ }
18
+
19
+ @keyframes slideInUp {
20
+ from {
21
+ transform: translateY(100%);
22
+ opacity: 0;
23
+ }
24
+ to {
25
+ transform: translateY(0);
26
+ opacity: 1;
27
+ }
28
+ }
29
+
30
+ .minimized-call-content {
31
+ flex: 1;
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 12px;
35
+ cursor: pointer;
36
+ padding: 4px;
37
+ border-radius: 8px;
38
+ transition: background-color 0.2s;
39
+ }
40
+
41
+ .minimized-call-content:hover {
42
+ background: rgba(55, 65, 81, 0.5);
43
+ }
44
+
45
+ .call-info {
46
+ flex: 1;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 4px;
50
+ }
51
+
52
+ .call-status {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 8px;
56
+ }
57
+
58
+ .status-indicator {
59
+ width: 8px;
60
+ height: 8px;
61
+ border-radius: 50%;
62
+ animation: pulse 2s infinite;
63
+ }
64
+
65
+ .status-indicator.active {
66
+ background: #10b981;
67
+ }
68
+
69
+ .status-indicator.connecting {
70
+ background: #f59e0b;
71
+ }
72
+
73
+ @keyframes pulse {
74
+ 0%, 100% {
75
+ opacity: 1;
76
+ }
77
+ 50% {
78
+ opacity: 0.5;
79
+ }
80
+ }
81
+
82
+ .status-text {
83
+ color: #e5e7eb;
84
+ font-size: 14px;
85
+ font-weight: 500;
86
+ }
87
+
88
+ .participant-info {
89
+ display: flex;
90
+ align-items: center;
91
+ }
92
+
93
+ .participant-text {
94
+ color: #9ca3af;
95
+ font-size: 12px;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .call-icon {
102
+ font-size: 20px;
103
+ opacity: 0.8;
104
+ }
105
+
106
+ .minimized-call-controls {
107
+ display: flex;
108
+ gap: 8px;
109
+ }
110
+
111
+ .control-button {
112
+ width: 32px;
113
+ height: 32px;
114
+ border: none;
115
+ border-radius: 6px;
116
+ background: #374151;
117
+ color: #e5e7eb;
118
+ cursor: pointer;
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ font-size: 14px;
123
+ transition: all 0.2s;
124
+ }
125
+
126
+ .control-button:hover {
127
+ background: #4b5563;
128
+ transform: translateY(-1px);
129
+ }
130
+
131
+ .control-button.restore {
132
+ background: #059669;
133
+ }
134
+
135
+ .control-button.restore:hover {
136
+ background: #10b981;
137
+ }
138
+
139
+ .control-button.end-call {
140
+ background: #dc2626;
141
+ }
142
+
143
+ .control-button.end-call:hover {
144
+ background: #ef4444;
145
+ }
146
+
147
+ /* Responsive adjustments */
148
+ @media (max-width: 640px) {
149
+ .minimized-call {
150
+ bottom: 10px;
151
+ right: 10px;
152
+ left: 10px;
153
+ min-width: auto;
154
+ max-width: none;
155
+ }
156
+ }
@@ -0,0 +1,78 @@
1
+ import { useCallState, useParticipants } from 'vg-x07df';
2
+ import { useCallTimer } from '../../hooks/useCallTimer';
3
+ import './MinimizedCall.css';
4
+
5
+ interface MinimizedCallProps {
6
+ onRestore: () => void;
7
+ onLeaveCall: () => void;
8
+ }
9
+
10
+ export function MinimizedCall({ onRestore, onLeaveCall }: MinimizedCallProps) {
11
+ const { status } = useCallState();
12
+ const participants = useParticipants();
13
+ const { formattedDuration, isActive } = useCallTimer();
14
+
15
+ const participantCount = participants.length;
16
+ const participantNames = participants
17
+ .slice(0, 2)
18
+ .map(p => p.firstName || 'Unknown')
19
+ .join(', ');
20
+
21
+ const getStatusText = () => {
22
+ switch (status) {
23
+ case 'RINGING':
24
+ return 'Ringing...';
25
+ case 'ACCEPTED':
26
+ case 'AWAITING_JOIN_INFO':
27
+ return 'Connecting...';
28
+ case 'ACTIVE':
29
+ return formattedDuration;
30
+ default:
31
+ return 'In call';
32
+ }
33
+ };
34
+
35
+ const getParticipantText = () => {
36
+ if (participantCount === 0) return 'No participants';
37
+ if (participantCount === 1) return participantNames;
38
+ if (participantCount === 2) return participantNames;
39
+ return `${participantNames} +${participantCount - 2} more`;
40
+ };
41
+
42
+ return (
43
+ <div className="minimized-call">
44
+ <div className="minimized-call-content" onClick={onRestore}>
45
+ <div className="call-info">
46
+ <div className="call-status">
47
+ <div className={`status-indicator ${isActive ? 'active' : 'connecting'}`}></div>
48
+ <span className="status-text">{getStatusText()}</span>
49
+ </div>
50
+ <div className="participant-info">
51
+ <span className="participant-text">{getParticipantText()}</span>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="call-icon">
56
+ <span>📞</span>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="minimized-call-controls">
61
+ <button
62
+ className="control-button restore"
63
+ onClick={onRestore}
64
+ title="Restore call"
65
+ >
66
+ ⬆
67
+ </button>
68
+ <button
69
+ className="control-button end-call"
70
+ onClick={onLeaveCall}
71
+ title="End call"
72
+ >
73
+ 📞
74
+ </button>
75
+ </div>
76
+ </div>
77
+ );
78
+ }