cal video: show meeting info in a hideable box (#7295)

pull/7736/head
Peer Richelsen 2023-03-14 20:43:45 +01:00 committed by GitHub
parent f027f018ff
commit 1ba6b08edf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 95 deletions

View File

@ -17,7 +17,7 @@ export function TimezoneDropdown({
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>

View File

@ -22,7 +22,7 @@ type Props = {
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");

View File

@ -1,21 +1,27 @@
import DailyIframe from "@daily-co/daily-js";
import MarkdownIt from "markdown-it";
import type { GetServerSidePropsContext } from "next";
import Head from "next/head";
import { useEffect } from "react";
import { useState, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { FiChevronRight } from "@calcom/ui/components/icon";
import { ssrInit } from "@server/lib/ssr";
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
export default function JoinCall(props: JoinCallPageProps) {
const { t } = useLocale();
const { meetingUrl, meetingPassword } = props;
const { meetingUrl, meetingPassword, booking } = props;
useEffect(() => {
const callFrame = DailyIframe.createFrame({
@ -53,7 +59,6 @@ export default function JoinCall(props: JoinCallPageProps) {
<>
<Head>
<title>{title}</title>
<meta name="title" content={APP_NAME + " Video"} />
<meta name="description" content={t("quick_video_meeting")} />
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
<meta property="og:type" content="website" />
@ -77,6 +82,112 @@ export default function JoinCall(props: JoinCallPageProps) {
}}
/>
</div>
<VideoMeetingInfo booking={booking} />
</>
);
}
interface ProgressBarProps {
startTime: string;
endTime: string;
}
function ProgressBar(props: ProgressBarProps) {
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] = useState(() => {
if (currentDifference >= 0 && isPast) {
return startDuration - currentDifference;
} else {
return startDuration;
}
});
const prev = startDuration - duration;
const percentage = prev * (100 / startDuration);
return (
<div>
<p>{duration} 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 VideoMeetingInfo {
booking: JoinCallPageProps["booking"];
}
export function VideoMeetingInfo(props: VideoMeetingInfo) {
const [open, setOpen] = useState(false);
const { booking } = props;
const endTime = new Date(booking.endTime);
const startTime = new Date(booking.startTime);
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 max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
<h3>What:</h3>
<p>{booking.title}</p>
<h3>Invitee Time Zone:</h3>
<p>{booking.user?.timeZone}</p>
<h3>When:</h3>
<p>
{formatToLocalizedDate(startTime)} <br />
{formatToLocalizedTime(startTime)}
</p>
<h3>Time left</h3>
<ProgressBar
key={String(open)}
endTime={endTime.toISOString()}
startTime={startTime.toISOString()}
/>
<h3>Who:</h3>
<p>
{booking?.user?.name} - Organizer{" "}
<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}
<h3>Description</h3>
<div
className="prose-sm prose prose-invert"
dangerouslySetInnerHTML={{ __html: md.render(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-l-0 border-gray-300/20 bg-black/60 text-white shadow-sm backdrop-blur-lg"
onClick={() => setOpen(!open)}>
<FiChevronRight
aria-hidden
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
/>
</button>
</div>
</aside>
</>
);
}
@ -93,10 +204,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: {
...bookingMinimalSelect,
uid: true,
description: true,
user: {
select: {
id: true,
credentials: true,
timeZone: true,
name: true,
email: true,
},
},
references: {
@ -157,6 +272,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
...(typeof bookingObj.references[0].meetingPassword === "string" && {
meetingPassword: bookingObj.references[0].meetingPassword,
}),
booking: bookingObj,
trpcState: ssr.dehydrate(),
},
};

View File

@ -5,63 +5,35 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, HeadSeo } from "@calcom/ui";
import { FiArrowRight, FiCalendar, FiX } from "@calcom/ui/components/icon";
import { Button, HeadSeo, EmptyScreen } from "@calcom/ui";
import { FiArrowRight, FiCalendar, FiClock } from "@calcom/ui/components/icon";
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
return (
<div>
<HeadSeo title="Meeting Unavailable" description="Meeting Unavailable" />
<>
<HeadSeo title={t("this_meeting_has_not_started_yet")} description={props.booking.title} />
<main className="mx-auto my-24 max-w-3xl">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<FiX className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
This meeting has not started yet
</h3>
</div>
<div className="mt-4 border-t border-b py-4">
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
{props.booking.title}
</h2>
<p className="text-center text-gray-500">
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
</p>
</div>
<div className="mt-3 text-center sm:mt-5">
<p className="text-sm text-gray-500">
This meeting will be accessible 60 minutes in advance.
</p>
</div>
</div>
<div className="mt-5 text-center sm:mt-6">
<div className="mt-5">
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
{t("go_back")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
<EmptyScreen
Icon={FiClock}
headline={t("this_meeting_has_not_started_yet")}
description={
<>
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
<p className="text-center text-gray-500">
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
</p>
</>
}
buttonRaw={
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
{t("go_back")}
</Button>
}
/>
</main>
</div>
</>
);
}

View File

@ -1,50 +1,25 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, HeadSeo } from "@calcom/ui";
import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
import { FiX, FiArrowRight } from "@calcom/ui/components/icon";
export default function NoMeetingFound() {
const { t } = useLocale();
return (
<div>
<>
<HeadSeo title={t("no_meeting_found")} description={t("no_meeting_found")} />
<main className="mx-auto my-24 max-w-3xl">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<FiX className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
{t("no_meeting_found")}
</h3>
</div>
<div className="mt-2">
<p className="text-center text-sm text-gray-500">{t("no_meeting_found_description")}</p>
</div>
</div>
<div className="mt-5 text-center sm:mt-6">
<div className="mt-5">
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
{t("go_back_home")}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
<EmptyScreen
Icon={FiX}
headline={t("no_meeting_found")}
description={t("no_meeting_found_description")}
buttonRaw={
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
{t("go_back_home")}
</Button>
}
/>
</main>
</div>
</>
);
}

View File

@ -1647,6 +1647,7 @@
"add_a_new_route": "Add a new Route",
"no_responses_yet": "No responses yet",
"this_will_be_the_placeholder": "This will be the placeholder",
"this_meeting_has_not_started_yet": "This meeting has not started yet",
"this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account",
"connect_app": "Connect {{dependencyName}}",
"app_is_connected": "{{dependencyName}} is connected",

View File

@ -208,7 +208,7 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
const textLabel = isPasswordVisible ? t("hide_password") : t("show_password");
return (
<div className="relative [&_.group:hover_.addon-wrapper]:border-gray-400 [&_.group:focus-within_.addon-wrapper]:border-neutral-300">
<div className="relative [&_.group:focus-within_.addon-wrapper]:border-neutral-300 [&_.group:hover_.addon-wrapper]:border-gray-400">
<InputField
type={isPasswordVisible ? "text" : "password"}
placeholder={props.placeholder || "•••••••••••••"}