fix: design improvements for log in screen for cal video (#22359)
* fix: design improvements for log in screen for cal video Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com> * fix: type Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com> * Update videos-single-view.tsx * Update videos-single-view.tsx * Update videos-single-view.tsx * add: light mode * Update videos-single-view.tsx * add suggestions added padding change and removed hard coded values * Update videos-single-view.tsx * Update videos-single-view.tsx * Update apps/web/modules/videos/views/videos-single-view.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Update common.json * Update videos-single-view.tsx * remove isLoggedin from LogInOverlayProps * type fix * screen flash fixed * rabbit suggestions * rabbit suggestions * Update videos-single-view.tsx * Update videos-single-view.tsx * Update videos-single-view.tsx * flakey test * Update videos-single-view.tsx * Update videos-single-view.tsx * Update videos-single-view.tsx * add: deps * coderabbit --------- Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
Kartik Saini
parent
3c72d7aaa0
commit
a7889984a0
@@ -4,7 +4,7 @@ import type { DailyCall } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import { DailyProvider } from "@daily-co/daily-react";
|
||||
import { useDailyEvent } from "@daily-co/daily-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
@@ -17,6 +17,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
|
||||
import { Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
|
||||
@@ -26,6 +27,7 @@ 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,
|
||||
@@ -42,88 +44,144 @@ export default function JoinCall(props: PageProps) {
|
||||
rediectAttendeeToOnExit,
|
||||
} = props;
|
||||
const [daily, setDaily] = useState<DailyCall | null>(null);
|
||||
const [userNameForCall, setUserNameForCall] = useState<string | undefined>(
|
||||
overrideName ?? loggedInUserName ?? undefined
|
||||
);
|
||||
const [isUserNameConfirmed, setIsUserNameConfirmed] = useState<boolean>(!displayLogInOverlay);
|
||||
const [isCallFrameReady, setIsCallFrameReady] = useState<boolean>(false);
|
||||
|
||||
const createCallFrame = useCallback(
|
||||
(userName?: string) => {
|
||||
let callFrame: DailyCall | undefined;
|
||||
|
||||
try {
|
||||
callFrame = DailyIframe.createFrame({
|
||||
theme: {
|
||||
colors: {
|
||||
accent: "#FFF",
|
||||
accentText: "#111111",
|
||||
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: meetingUrl,
|
||||
userName: userName,
|
||||
...(typeof meetingPassword === "string" && { token: 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(() => {
|
||||
let callFrame: DailyCall | undefined;
|
||||
try {
|
||||
callFrame = DailyIframe.createFrame({
|
||||
theme: {
|
||||
colors: {
|
||||
accent: "#FFF",
|
||||
accentText: "#111111",
|
||||
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: meetingUrl,
|
||||
userName: overrideName ?? loggedInUserName ?? undefined,
|
||||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
...(hasTeamPlan && {
|
||||
customTrayButtons: {
|
||||
...(showRecordingButton
|
||||
? {
|
||||
recording: {
|
||||
label: "Record",
|
||||
tooltip: "Start or stop recording",
|
||||
iconPath: RECORDING_DEFAULT_ICON,
|
||||
iconPathDarkMode: RECORDING_DEFAULT_ICON,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(showTranscriptionButton
|
||||
? {
|
||||
transcription: {
|
||||
label: "Transcribe",
|
||||
tooltip: "Transcription powered by AI",
|
||||
iconPath: TRANSCRIPTION_STOPPED_ICON,
|
||||
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (displayLogInOverlay && !loggedInUserName && !overrideName && !isUserNameConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let callFrame: DailyCall | null = null;
|
||||
|
||||
try {
|
||||
callFrame = createCallFrame(userNameForCall) ?? null;
|
||||
setDaily(callFrame);
|
||||
setIsCallFrameReady(true);
|
||||
|
||||
if (overrideName) {
|
||||
callFrame.setUserName(overrideName);
|
||||
}
|
||||
} catch (err) {
|
||||
callFrame = DailyIframe.getCallInstance();
|
||||
} finally {
|
||||
setDaily(callFrame ?? null);
|
||||
callFrame?.join();
|
||||
} catch (error) {
|
||||
console.error("Failed to create or join call:", error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
callFrame?.destroy();
|
||||
if (callFrame) {
|
||||
try {
|
||||
callFrame.destroy();
|
||||
} catch (error) {
|
||||
console.error("Error destroying call frame:", error);
|
||||
}
|
||||
}
|
||||
setDaily(null);
|
||||
setIsCallFrameReady(false);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
displayLogInOverlay,
|
||||
isUserNameConfirmed,
|
||||
userNameForCall,
|
||||
createCallFrame,
|
||||
loggedInUserName,
|
||||
overrideName,
|
||||
]);
|
||||
|
||||
const handleJoinAsGuest = useCallback((guestName: string) => {
|
||||
const trimmedName = guestName.trim();
|
||||
if (!trimmedName) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUserNameForCall(trimmedName);
|
||||
setIsUserNameConfirmed(true);
|
||||
}, []);
|
||||
|
||||
const handleUserNameConfirmed = useCallback(() => {
|
||||
setIsUserNameConfirmed(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DailyProvider callObject={daily}>
|
||||
<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>
|
||||
{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
|
||||
@@ -147,7 +205,16 @@ export default function JoinCall(props: PageProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{displayLogInOverlay && <LogInOverlay isLoggedIn={!!loggedInUserName} bookingUid={booking.uid} />}
|
||||
{displayLogInOverlay && !isUserNameConfirmed && (
|
||||
<LogInOverlay
|
||||
isLoggedIn={!!loggedInUserName}
|
||||
bookingUid={booking.uid}
|
||||
loggedInUserName={loggedInUserName ?? undefined}
|
||||
overrideName={overrideName}
|
||||
onJoinAsGuest={handleJoinAsGuest}
|
||||
onUserNameConfirmed={handleUserNameConfirmed}
|
||||
/>
|
||||
)}
|
||||
|
||||
<VideoMeetingInfo booking={booking} rediectAttendeeToOnExit={rediectAttendeeToOnExit} />
|
||||
</DailyProvider>
|
||||
@@ -206,8 +273,7 @@ function ProgressBar(props: ProgressBarProps) {
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [startingTime]);
|
||||
|
||||
const prev = startDuration - duration;
|
||||
const percentage = prev * (100 / startDuration);
|
||||
@@ -227,49 +293,126 @@ function ProgressBar(props: ProgressBarProps) {
|
||||
interface LogInOverlayProps {
|
||||
isLoggedIn: boolean;
|
||||
bookingUid: string;
|
||||
loggedInUserName?: string;
|
||||
overrideName?: string;
|
||||
onJoinAsGuest: (guestName: string) => void;
|
||||
onUserNameConfirmed?: () => void;
|
||||
}
|
||||
|
||||
export function LogInOverlay(props: LogInOverlayProps) {
|
||||
const { t } = useLocale();
|
||||
const { isLoggedIn, bookingUid } = props;
|
||||
const [open, setOpen] = useState(!isLoggedIn);
|
||||
const { bookingUid, isLoggedIn, loggedInUserName, overrideName, onJoinAsGuest, onUserNameConfirmed } =
|
||||
props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(!isLoggedIn);
|
||||
const [userName, setUserName] = useState(overrideName ?? loggedInUserName ?? "");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleContinueAsGuest = useCallback(async () => {
|
||||
const trimmedName = userName.trim();
|
||||
if (!trimmedName) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
onJoinAsGuest(trimmedName);
|
||||
onUserNameConfirmed?.();
|
||||
setIsOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Error joining as guest:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : t("failed_to_join_call");
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userName, onJoinAsGuest, onUserNameConfirmed, t]);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
title={t("join_video_call")}
|
||||
description={t("choose_how_you_d_like_to_join_call")}
|
||||
className="bg-black text-white sm:max-w-[480px]">
|
||||
<div className="pb-8">
|
||||
<div className="space-y-8">
|
||||
<Button color="primary" className="mt-4 w-full justify-center " onClick={() => setOpen(false)}>
|
||||
{t("continue_as_guest")}
|
||||
</Button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative py-2">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-gray-600" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-black px-4 text-sm text-gray-400">{t("or")}</span>
|
||||
</div>
|
||||
description={t("choose_how_you_d_like_to_appear_on_the_call")}
|
||||
className="bg-white text-black dark:bg-black dark:text-white sm:max-w-[480px]">
|
||||
<div className="pb-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="font-semibold">{t("join_as_guest")}</div>
|
||||
<p className="text-subtle text-sm">{t("ideal_for_one_time_calls")}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-lg font-semibold text-white">{t("sign_in_to_cal_com")}</h4>
|
||||
<p className="text-sm text-gray-300">{t("track_your_meetings")}</p>
|
||||
<Button
|
||||
color="primary"
|
||||
className="mt-4 w-full justify-center"
|
||||
onClick={() =>
|
||||
(window.location.href = `${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL}/video/${bookingUid}`)
|
||||
}>
|
||||
<Icon name="external-link" className="mr-2 h-4 w-4" />
|
||||
{t("log_in_to_cal_com")}
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("your_name")}
|
||||
className="w-full flex-1"
|
||||
value={userName}
|
||||
onChange={handleUserNameChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
<Button color="secondary" onClick={handleContinueAsGuest} loading={isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-3 dark:bg-red-900/20">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="my-5 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
|
||||
|
||||
<div className="mt-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-base font-semibold text-black dark:text-white">
|
||||
{t("sign_in_to_cal_com")}
|
||||
</h4>
|
||||
<p className="text-sm text-[#6B7280] dark:text-gray-300">
|
||||
{t("track_meetings_and_manage_schedule")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
className="w-full justify-center"
|
||||
onClick={() =>
|
||||
(window.location.href = `${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL}/video/${bookingUid}`)
|
||||
}>
|
||||
{t("sign_in")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -125,10 +125,14 @@
|
||||
"price": "Price",
|
||||
"paid": "Paid",
|
||||
"refunded": "Refunded",
|
||||
"record": "Record",
|
||||
"transcribe": "Transcribe",
|
||||
"payment": "Payment",
|
||||
"missing_card_fields": "Missing card fields",
|
||||
"pay_now": "Pay now",
|
||||
"general_prompt": "General Prompt",
|
||||
"transcription_powered_by_ai": "Transcription powered by AI",
|
||||
"start_or_stop_recording": "Start or stop recording",
|
||||
"begin_message": "Begin Message",
|
||||
"codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not",
|
||||
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
|
||||
@@ -661,6 +665,8 @@
|
||||
"somewhere_else": "Somewhere Else",
|
||||
"custom_attendee_location": "Custom attendee location",
|
||||
"continue_as_guest": "Continue as Guest",
|
||||
"join_as_guest": "Join as Guest",
|
||||
"ideal_for_one_time_calls": "Ideal for one-time calls",
|
||||
"any_location": "Any location",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
@@ -1415,10 +1421,13 @@
|
||||
"after_hosts_cal_video_no_show": "After hosts didn't join cal video",
|
||||
"after_guests_cal_video_no_show": "After guests didn't join cal video",
|
||||
"join_video_call": "Join the video call",
|
||||
"sign_in_to_cal_com": "Sign in to your Cal.com account",
|
||||
"sign_in_to_cal_com": "Sign in to Cal.com",
|
||||
"log_in_to_cal_com": "Log in to Cal.com",
|
||||
"track_your_meetings": "Track your meetings, get notifications, and manage your schedule more effectively.",
|
||||
"track_meetings_and_manage_schedule": "Track your meetings and manage your schedule more effectively",
|
||||
"failed_to_join_call": "Failed to join the call. Please try again.",
|
||||
"choose_how_you_d_like_to_join_call": "Choose how you'd like to join this call",
|
||||
"choose_how_you_d_like_to_appear_on_the_call": "Choose how you'd like to appear on the call",
|
||||
"new_event_trigger": "when new event is booked",
|
||||
"email_host_action": "send email to host",
|
||||
"email_attendee_action": "send email to attendees",
|
||||
|
||||
Reference in New Issue
Block a user