Files
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
bf348ee209 refactor: migrate Icon to component icons from @coss/ui/icons (#27458)
* feat: add @coss/ui/icons package and migrate static icon usages

Create a new icon system in @coss/ui/icons that wraps lucide-react icons
with controlled props (size and className only). Migrate 44 component
files from <Icon name="xxx" /> to <XxxIcon /> pattern.

This is Phase 1 of the icon migration, covering only static icon names.
Dynamic icon usages (e.g., name={variable}) are skipped for now.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate more static icons to @coss/ui/icons (batch 2)

Migrated 18 files from <Icon name="xxx" /> to <XxxIcon /> pattern:
- Event meta, insights, shell, user table bulk actions
- Webhooks, blocklist, form-builder, form components

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate more static icons to @coss/ui/icons (batch 3)

Migrated 7 files:
- CancelBooking, AddGuestsDialog, ChargeCardDialog
- ReassignDialog, RescheduleDialog
- Categories, Slider

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate more static icons to @coss/ui/icons (batch 4)

Migrated 6 files:
- Organizations: AboutOrganizationForm, PaymentStatusView, profile
- Workflows: CallDetailsSheet, SkeletonLoaderList, TimeTimeUnitInput

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate more static icons to @coss/ui/icons (batch 5)

Migrated 4 files:
- TwoFactorModalHeader
- App setups: alby, make, btcpayserver

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 6)

- confirmDialog.tsx: clock icon
- ics-feedcalendar/Setup.tsx: trash, plus icons
- hitpay/Setup.tsx: info icon
- paypal/Setup.tsx: circle-alert icons

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 7)

- webhook-edit-view.tsx: external-link icon
- webhook-new-view.tsx: external-link icon
- UserDropdown.tsx: chevron-down, user, settings, moon, map, circle-help, download, blocks, log-out icons
- TopNav.tsx: settings icon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 8)

- EmailInviteForm.tsx: x icon
- InviteOptions.tsx: mail, upload, link icons
- SkeletonLoader.tsx: search, clock, user icons
- Embed.tsx: sun, arrow-left icons
- CalendarSwitch.tsx: arrow-left, rotate-cw icons

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 9)

- apps-view.tsx: search icon
- categories-view.tsx: arrow-left, arrow-right icons
- DatePicker.tsx: calendar icon
- EditLocationDialog.tsx: map-pin icon
- AppPage.tsx: circle-alert, book-open, external-link, mail, file, shield, flag icons

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 10)

- LicenseSelection.tsx: loader, check icons
- AppConnectionItem.tsx: circle-alert, arrow-right icons
- oauth-client-form/index.tsx: info icons (x2)
- auth/error/page.tsx: x icon
- troubleshoot/layout.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 11)

- UpgradeTipWrapper.tsx: users, refresh-ccw, user-plus icons
- PbacOptInModal.tsx: shield-check, lock, users, shield icons
- AdvancedPermissionGroup.tsx: chevron-right, info icons

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 12)

- AdminAppsList.tsx: pencil icon
- AvailableTimes.tsx: calendar-x-2 icon
- BookingDetailsSheet.tsx: external-link (x2), repeat icons
- BookingCalendarContainer.tsx: chevron-left, chevron-right icons

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 13)

- videos-single-view.tsx: chevron-right icon
- TroubleshooterSidebar.tsx: arrow-left icon
- RuleBuilder.tsx: filter icon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 14)

- FieldMappingBuilder.tsx: arrow-right, link icons
- IntegrationAttributeSyncCard.tsx: key icon
- onboarding-view.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static icons to @coss/ui/icons (batch 15)

- team-profile-view.tsx: copy icon
- DateRangeFilter.tsx: check icon
- ColumnVisibilityButton.tsx: check icon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static Icon to @coss/ui/icons (batch 16)

- EventWorkflowsTab.tsx: lock
- event-types-listing-view.tsx: search
- onboarding-migrate-members-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, user-plus
- onboarding-migrate-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- EventAvailabilityTab.tsx: globe, user
- onboarding-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- FormBuilder.tsx: mail, phone, arrow-up, arrow-down
- AIEventController.tsx: info
- EventTeamAssignmentTab.tsx: x

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static Icon to @coss/ui/icons (batch 17)

- EventAdvancedTab.tsx: pencil, info
- HostLocations.tsx: triangle-alert, loader
- EventTypeLayout.tsx: loader
- Locations.tsx: x, corner-down-right, check
- EditWeightsForAllTeamMembers.tsx: upload, search, info, chevron-down
- WorkflowPage.tsx: pencil

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static Icon to @coss/ui/icons (batch 18)

- TestPhoneCallDialog.tsx: phone
- WorkflowListPage.tsx: link
- UsersTable.tsx: building, lock
- TeamList.tsx: building, paintbrush, chart-line, user-plus, users, pencil
- bookings-single-view.tsx: chevron-left, check, calendar, x, external-link

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate static Icon to @coss/ui/icons (batch 19)

- TeamsListing.tsx: users, refresh-ccw, user-plus, mail, video, eye-off, info
- ConnectionInfo.tsx: clipboard
- IntercomContactForm.tsx: message-circle, loader, send

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: migrate static Icon to @coss/ui/icons in teams components

- EventTypesList.tsx: check → CheckIcon
- MemberInvitationModal.tsx: user, users, building → UserIcon, UsersIcon, BuildingIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: migrate static Icon to @coss/ui/icons in workflow components

- ImportNumberDialog.tsx: info → InfoIcon
- VoiceSelector.tsx: user → UserIcon
- IncomingCallsTab.tsx: info, phone-incoming → InfoIcon, PhoneIncomingIcon
- PhoneNumberTab.tsx: loader, phone → LoaderIcon, PhoneIcon
- VoiceSelectionDialog.tsx: pause, play, user → PauseIcon, PlayIcon, UserIcon
- WebCallDialog.tsx: loader, phone, phone-off, triangle-alert, trash, message-circle → new icons (kept Icon import for dynamic mic icon)
- WorkflowDetailsPage.tsx: zap, arrow-right → ZapIcon, ArrowRightIcon
- AddActionDialog.tsx: info → InfoIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: migrate static Icon to @coss/ui/icons in WorkflowStepContainer

- info → InfoIcon (2 instances)
- phone → PhoneIcon (2 instances)
- circle-help → CircleHelpIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: migrate static Icon to @coss/ui/icons in data-table and org components

- attributes-list-view.tsx: tags → TagsIcon
- AddFilterButton.tsx: plus, eye-off → PlusIcon, EyeOffIcon
- BaseSelectFilterOptions.tsx: check → CheckIcon
- DataTable.tsx: arrow-up, arrow-down, check, chevrons-up-down, eye-off → new icons
- booking-dry-run-success-view.tsx: check → CheckIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: migrate static Icon to @coss/ui/icons in username and routing components

- UsernameTextfield.tsx: check → CheckIcon
- PremiumTextfield.tsx: check, external-link → CheckIcon, ExternalLinkIcon (kept Icon for star with fill prop)
- Header.tsx (routing-forms): pencil, menu, waypoints → PencilIcon, MenuIcon, WaypointsIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in routing-forms pages

Batch 25: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- Forms.tsx: file-text, shuffle, chart-bar, circle-check, mail, download → FileTextIcon, ShuffleIcon, ChartBarIcon, CircleCheckIcon, MailIcon, DownloadIcon
- FormEdit.tsx: chevron-down, menu → ChevronDownIcon, MenuIcon
- IncompleteBooking.tsx: globe → GlobeIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in UI package

Batch 26: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- ProgressToast.tsx: file-down, x → FileDownIcon, XIcon
- showToast.tsx: check, info, x → CheckIcon, InfoIcon, XIcon
- InfoBadge.tsx: info → InfoIcon
- MeetingTimeInTimezones.tsx: globe → GlobeIcon
- InputError.tsx: info → InfoIcon
- ConfirmationDialogContent.tsx: circle-alert, check → CircleAlertIcon, CheckIcon
- CalendarSwitch.tsx: arrow-left, rotate-cw → ArrowLeftIcon, RotateCwIcon
- ArrowButton.tsx: arrow-up, arrow-down → ArrowUpIcon, ArrowDownIcon
- FileUploader.tsx: upload → UploadIcon (kept Icon for dynamic)
- Checkbox.tsx: check → CheckIcon
- EditableHeading.tsx: pencil → PencilIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in UI and app-store packages

Batch 27: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- MultiEmail.tsx: x → XIcon (converted width to size)
- AppListCard.tsx: circle-alert → CircleAlertIcon
- SplitButton.tsx: chevron-down → ChevronDownIcon (kept Icon for dynamic)
- Calendar.tsx: chevron-left, chevron-right → ChevronLeftIcon, ChevronRightIcon
- AppDependencyComponent.tsx: check, circle-x, arrow-right → CheckIcon, CircleXIcon, ArrowRightIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in form input components

Batch 28: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- TextField.tsx: info → InfoIcon (kept Icon for x with onClick)
- Input.tsx: eye, eye-off, search → EyeIcon, EyeOffIcon, SearchIcon

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: use @coss/ui/src/icons in packages with legacy moduleResolution

Packages like packages/ui and packages/app-store use a legacy moduleResolution
setting that doesn't support subpath exports. This changes the import path from
@coss/ui/icons to @coss/ui/src/icons for these packages.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in UI form and editor components

Migrates static Icon usages:
- MultiOptionInput: x (2x), arrow-up, arrow-down
- components (select): check
- AddVariablesDropdown: chevron-down (3x)
- ToolbarPlugin: chevron-down

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: migrate Icon to component icons in settings and platform atoms

Migrates static Icon usages:
- SettingsLayoutAppDirClient: chevron-down (2x), chevron-right (2x), arrow-left
- platform/atoms dialog: x
- platform/atoms toast: x

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: resolve @coss/ui/icons module resolution for legacy moduleResolution

Add typesVersions to @coss/ui package.json to help TypeScript resolve
subpath exports even with legacy moduleResolution: "node" setting.

Change imports from @coss/ui/src/icons to @coss/ui/icons to avoid
double-src path issue when Next.js applies import maps.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: localize 'Select field type' fallback label in FormEdit

Address Cubic AI review feedback (confidence 9/10) by using t() for
the hardcoded 'Select field type' string to ensure proper i18n support.

Co-Authored-By: unknown <>

* fix: add typesVersions wildcards for @coss/ui subpath imports

Add lib/*, components/*, and hooks/* to typesVersions to support
legacy moduleResolution with all package subpath patterns.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: resolve missing Icon references in settings and workflows

- Add Icon import for dynamic icon usages in SettingsLayoutAppDirClient
- Replace static Icon usages with component icons (InfoIcon, PhoneIcon)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: resolve icon component type errors

- Add onClick prop support to IconProps
- Fix size prop type (use number instead of string)
- Rename local MailOpenIcon to MailOpenIconWrapper to avoid shadowing

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: add data-testid to icon components for test compatibility

- Add testId parameter to createIcon function
- Auto-generate data-testid for all icons (e.g., "check-icon", "search-icon")
- Remove outdated mock for @calcom/ui/components/icon in tests

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Update package.json

* Update package.json

* fix: migrate Icon to CalendarDaysIcon in videos-single-view.tsx

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 14:30:27 +01:00

607 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import dayjs from "@calcom/dayjs";
import {
RECORDING_DEFAULT_ICON,
TRANSCRIPTION_STOPPED_ICON,
WEBAPP_URL,
WEBSITE_URL,
} from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/dayjs";
import { emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
import { Input } from "@calcom/ui/components/form";
import { CalendarDaysIcon, ChevronRightIcon } from "@coss/ui/icons";
import type { DailyCall } from "@daily-co/daily-js";
import DailyIframe from "@daily-co/daily-js";
import { DailyProvider, useDailyEvent } from "@daily-co/daily-react";
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
import { useCallback, useEffect, useRef, useState } from "react";
import { CalVideoPremiumFeatures } from "../cal-video-premium-features";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function JoinCall(props: PageProps) {
const { t } = useLocale();
const {
meetingUrl,
meetingPassword,
booking,
hasTeamPlan,
calVideoLogo,
loggedInUserName,
overrideName,
showRecordingButton,
enableAutomaticTranscription,
enableAutomaticRecordingForOrganizer,
showTranscriptionButton,
rediectAttendeeToOnExit,
requireEmailForGuests,
isLoggedInUserPartOfMeeting,
} = props;
const [daily, setDaily] = useState<DailyCall | null>(null);
const [guestCredentials, setGuestCredentials] = useState<{
meetingPassword: string;
meetingUrl: string;
userName: string;
} | null>(null);
const userNameForCall = overrideName ?? loggedInUserName ?? undefined;
const hideLoginModal =
!!userNameForCall && (requireEmailForGuests ? !!loggedInUserName && isLoggedInUserPartOfMeeting : true);
const [isCallFrameReady, setIsCallFrameReady] = useState<boolean>(false);
const activeMeetingPassword = guestCredentials?.meetingPassword ?? meetingPassword;
const activeMeetingUrl = guestCredentials?.meetingUrl ?? meetingUrl;
const activeUserName = guestCredentials?.userName ?? userNameForCall;
const createCallFrame = useCallback(
(userName?: string, password?: string, url?: string) => {
let callFrame: DailyCall | undefined;
try {
callFrame = DailyIframe.createFrame({
theme: {
colors: {
accent: "#292929",
accentText: "#FFF",
background: "#111111",
backgroundAccent: "#111111",
baseText: "#FFF",
border: "#292929",
mainAreaBg: "#111111",
mainAreaBgAccent: "#1A1A1A",
mainAreaText: "#FFF",
supportiveText: "#FFF",
},
},
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
url: url ?? meetingUrl,
userName: userName,
...(typeof (password ?? meetingPassword) === "string" && { token: password ?? meetingPassword }),
...(hasTeamPlan && {
customTrayButtons: {
...(showRecordingButton
? {
recording: {
label: t("record"),
tooltip: t("start_or_stop_recording"),
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
}
: {}),
...(showTranscriptionButton
? {
transcription: {
label: t("transcribe"),
tooltip: t("transcription_powered_by_ai"),
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
}
: {}),
},
}),
});
if (userName) {
callFrame.setUserName(userName);
}
return callFrame;
} catch (_err) {
return DailyIframe.getCallInstance();
}
},
[meetingUrl, meetingPassword, hasTeamPlan, showRecordingButton, showTranscriptionButton, t]
);
useEffect(() => {
if (!hideLoginModal && !guestCredentials) {
return;
}
let callFrame: DailyCall | null = null;
try {
callFrame = createCallFrame(activeUserName, activeMeetingPassword, activeMeetingUrl) ?? null;
setDaily(callFrame);
setIsCallFrameReady(true);
callFrame?.join();
} catch (error) {
console.error("Failed to create or join call:", error);
}
return () => {
if (callFrame) {
try {
callFrame.destroy();
} catch (error) {
console.error("Error destroying call frame:", error);
}
}
setDaily(null);
setIsCallFrameReady(false);
};
}, [
hideLoginModal,
activeUserName,
activeMeetingPassword,
activeMeetingUrl,
createCallFrame,
guestCredentials,
]);
return (
<DailyProvider callObject={daily}>
{isCallFrameReady && (
<div
className="mx-auto hidden sm:block"
style={{ zIndex: 2, left: "30%", position: "absolute", bottom: 100, width: "auto" }}>
<CalVideoPremiumFeatures
showRecordingButton={showRecordingButton}
enableAutomaticRecordingForOrganizer={enableAutomaticRecordingForOrganizer}
enableAutomaticTranscription={enableAutomaticTranscription}
showTranscriptionButton={showTranscriptionButton}
/>
</div>
)}
<div style={{ zIndex: 2, position: "relative" }}>
{calVideoLogo ? (
<img
className="fixed z-10 hidden aspect-square h-16 min-h-16 w-16 min-w-16 rounded-full sm:inline-block"
src={calVideoLogo}
alt="My Org Logo"
style={{
top: 32,
left: 32,
}}
/>
) : (
<img
className="fixed z-10 hidden h-5 sm:inline-block"
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
alt="Logo"
style={{
top: 47,
left: 20,
}}
/>
)}
</div>
{!hideLoginModal && (
<LogInOverlay
isOpen={!hideLoginModal}
bookingUid={booking.uid}
bookingTitle={booking.title}
hostName={booking.user?.name ?? ""}
loggedInUserName={loggedInUserName ?? undefined}
overrideName={overrideName}
requireEmailForGuests={requireEmailForGuests}
onGuestCredentialsReceived={setGuestCredentials}
meetingPassword={meetingPassword}
meetingUrl={meetingUrl}
/>
)}
<VideoMeetingInfo booking={booking} rediectAttendeeToOnExit={rediectAttendeeToOnExit} />
</DailyProvider>
);
}
interface ProgressBarProps {
startTime: string;
endTime: string;
}
function ProgressBar(props: ProgressBarProps) {
const { t } = useLocale();
const { startTime, endTime } = props;
const currentTime = dayjs().second(0).millisecond(0);
const startingTime = dayjs(startTime).second(0).millisecond(0);
const isPast = currentTime.isAfter(startingTime);
const currentDifference = dayjs().diff(startingTime, "minutes");
const startDuration = dayjs(endTime).diff(startingTime, "minutes");
const [duration, setDuration] = useState(() => {
if (currentDifference >= 0 && isPast) {
return startDuration - currentDifference;
} else {
return startDuration;
}
});
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const now = dayjs();
const remainingMilliseconds = (60 - now.get("seconds")) * 1000 - now.get("milliseconds");
timeoutRef.current = setTimeout(() => {
const past = dayjs().isAfter(startingTime);
if (past) {
setDuration((prev) => prev - 1);
}
intervalRef.current = setInterval(() => {
if (dayjs().isAfter(startingTime)) {
setDuration((prev) => prev - 1);
}
}, 60000);
}, remainingMilliseconds);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [startingTime]);
const prev = startDuration - duration;
const percentage = prev * (100 / startDuration);
return (
<div>
<p>
{duration} {t("minutes")}
</p>
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
<div className="absolute h-full w-full bg-gray-500/10" />
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
</div>
</div>
);
}
interface LogInOverlayProps {
isOpen: boolean;
bookingUid: string;
bookingTitle: string;
hostName: string;
loggedInUserName?: string;
overrideName?: string;
requireEmailForGuests?: boolean;
onGuestCredentialsReceived: (credentials: {
meetingPassword: string;
meetingUrl: string;
userName: string;
}) => void;
meetingPassword?: string;
meetingUrl: string;
}
export function LogInOverlay(props: LogInOverlayProps) {
const { t } = useLocale();
const {
bookingUid,
bookingTitle,
hostName,
isOpen: _open,
loggedInUserName,
overrideName,
requireEmailForGuests = false,
onGuestCredentialsReceived,
meetingPassword,
meetingUrl,
} = props;
const [isOpen, setIsOpen] = useState(_open);
const [userName, setUserName] = useState(overrideName ?? loggedInUserName ?? "");
const [userEmail, setUserEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinueAsGuest = useCallback(async () => {
const trimmedName = userName.trim();
const trimmedEmail = userEmail.trim();
if (!trimmedName) {
setError(t("please_enter_name"));
return;
}
if (requireEmailForGuests) {
if (!trimmedEmail) {
setError(t("please_enter_name_and_email"));
return;
}
if (!emailRegex.test(trimmedEmail)) {
setError(t("invalid_email_address"));
return;
}
}
setIsLoading(true);
setError(null);
try {
// Only create guest session if email is required and provided
if (requireEmailForGuests && trimmedEmail) {
const csrfResponse = await fetch("/api/csrf", { cache: "no-store" });
const { csrfToken } = await csrfResponse.json();
const response = await fetch("/api/video/guest-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bookingUid,
email: trimmedEmail,
name: trimmedName,
csrfToken,
}),
});
if (!response.ok) {
const errorData = await response.json();
const errorKey = errorData.error;
throw new Error(errorKey || "Failed to create guest session");
}
const { meetingPassword, meetingUrl } = await response.json();
onGuestCredentialsReceived({
meetingPassword,
meetingUrl,
userName: trimmedName,
});
setIsOpen(false);
} else {
// If email not required, use existing credentials from SSR props
if (!meetingPassword) {
throw new Error("Meeting password not available");
}
onGuestCredentialsReceived({
meetingPassword,
meetingUrl,
userName: trimmedName,
});
setIsOpen(false);
}
} catch (error) {
const errorKey = error instanceof Error ? error.message : "failed_to_join_call";
const errorMessage = t(errorKey) || errorKey;
setError(errorMessage);
setIsLoading(false);
}
}, [
userName,
userEmail,
bookingUid,
requireEmailForGuests,
t,
onGuestCredentialsReceived,
meetingPassword,
meetingUrl,
]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && userName.trim() && !isLoading) {
handleContinueAsGuest();
}
},
[userName, isLoading, handleContinueAsGuest]
);
const handleUserNameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUserName(e.target.value);
if (error) {
setError(null);
}
},
[error]
);
const handleEmailChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUserEmail(e.target.value);
if (error) {
setError(null);
}
},
[error]
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="p-6 sm:max-w-md" onInteractOutside={(e) => e.preventDefault()}>
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between rounded-lg border border-subtle p-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-subtle">
<CalendarDaysIcon className="h-5 w-5 text-default" />
</div>
<div>
<p className="font-semibold text-emphasis text-sm">{bookingTitle}</p>
<p className="text-subtle text-xs">{t("hosted_by", { name: hostName })}</p>
</div>
</div>
<Badge variant="success" withDot>
{t("ready")}
</Badge>
</div>
<div>
<h3 className="font-bold text-emphasis text-lg">{t("ready_to_join")}</h3>
<p className="text-sm text-subtle">{t("enter_name_to_join_call")}</p>
</div>
<div className="flex flex-col gap-3">
<Input
type="text"
placeholder={t("your_name")}
className="w-full"
value={userName}
onChange={handleUserNameChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
{requireEmailForGuests && (
<Input
type="email"
placeholder={t("email_address")}
className="w-full"
value={userEmail}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
/>
)}
<Button
color="primary"
className="w-full justify-center"
onClick={handleContinueAsGuest}
loading={isLoading}>
{t("join_call_as_guest")}
</Button>
</div>
{error && (
<div className="rounded-md bg-error p-3">
<p className="font-medium text-error text-sm">{error}</p>
</div>
)}
<p className="text-center text-sm text-subtle">
<a
href={`${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL}/video/${bookingUid}`}
className="text-emphasis underline">
{t("sign_in")}
</a>{" "}
{t("to_track_no_shows")}
</p>
</div>
</DialogContent>
</Dialog>
);
}
interface VideoMeetingInfo {
booking: PageProps["booking"];
rediectAttendeeToOnExit?: string | null;
}
export function VideoMeetingInfo(props: VideoMeetingInfo) {
const [open, setOpen] = useState(false);
const { booking, rediectAttendeeToOnExit } = props;
const { t } = useLocale();
const endTime = new Date(booking.endTime);
const startTime = new Date(booking.startTime);
const timeZone = booking.user?.timeZone;
useDailyEvent("left-meeting", () => {
if (rediectAttendeeToOnExit) {
window.location.href = rediectAttendeeToOnExit;
}
});
return (
<aside
className={classNames(
"no-scrollbar fixed top-0 left-0 z-30 flex h-full w-64 transform justify-between overflow-x-hidden overflow-y-scroll transition-all duration-300 ease-in-out",
open ? "translate-x-0" : "-translate-x-[232px]"
)}>
<main className="prose-sm prose scroll-bar scrollbar-track-w-20 overflow-x-hidden! w-full max-w-64 overflow-scroll border-subtle border-r bg-default p-4 prose-h3:font-cal prose-a:text-emphasis prose-h3:text-emphasis text-emphasis shadow-sm backdrop-blur-lg">
<h3>{t("what")}:</h3>
<p>{booking.title}</p>
<h3>{t("invitee_timezone")}:</h3>
<p>{timeZone}</p>
<h3>{t("when")}:</h3>
<p suppressHydrationWarning={true}>
{formatToLocalizedDate(startTime)} <br />
{formatToLocalizedTime({ date: startTime, timeZone })}
</p>
<h3>{t("time_left")}</h3>
<ProgressBar key={String(open)} endTime={endTime.toISOString()} startTime={startTime.toISOString()} />
<h3>{t("who")}:</h3>
<p>
{booking?.user?.name} - {t("organizer")}
{!booking?.eventType?.hideOrganizerEmail && (
<>
: <a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
</>
)}
</p>
{booking.attendees.length
? booking.attendees.map((attendee) => (
<p key={attendee.id}>
{attendee.name} – <a href={`mailto:${attendee.email}`}>{attendee.email}</a>
</p>
))
: null}
{booking.description && (
<>
<h3>{t("description")}:</h3>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Content is sanitized via markdownToSafeHTML */}
<div
className="prose-sm prose prose-invert"
dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(booking.description) }}
/>
</>
)}
</main>
<div className="flex items-center justify-center">
<button
aria-label={`${open ? "close" : "open"} booking description sidebar`}
className="h-20 w-6 rounded-r-md border border-gray-300/20 border-l-0 bg-black/60 text-white shadow-sm backdrop-blur-lg"
onClick={() => setOpen(!open)}>
<ChevronRightIcon
aria-hidden
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
/>
</button>
</div>
</aside>
);
}