Improves UI stability of availability page. No CLS now. (#3347)
parent
b1e4c23adc
commit
ebd4750f2d
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
|
||||
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
|
@ -238,54 +238,32 @@ const BrandColor = ({
|
|||
? darkVal
|
||||
: "#" + darkVal
|
||||
: fallBackHex(darkVal, true);
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-highlight-color",
|
||||
embedBrandingColors.highlightColor || "#10B981" // green--500
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-lightest-color",
|
||||
embedBrandingColors.lightestColor || "#E1E1E1" // gray--200
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-lighter-color",
|
||||
embedBrandingColors.lighterColor || "#ACACAC" // gray--400
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-light-color",
|
||||
embedBrandingColors.lightColor || "#888888" // gray--500
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-median-color",
|
||||
embedBrandingColors.medianColor || "#494949" // gray--600
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-dark-color",
|
||||
embedBrandingColors.darkColor || "#313131" // gray--800
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--booking-darker-color",
|
||||
embedBrandingColors.darkerColor || "#292929" // gray--900
|
||||
);
|
||||
document.documentElement.style.setProperty("--brand-color", lightVal);
|
||||
document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(lightVal, true));
|
||||
document.documentElement.style.setProperty("--brand-color-dark-mode", darkVal);
|
||||
document.documentElement.style.setProperty(
|
||||
"--brand-text-color-dark-mode",
|
||||
getContrastingTextColor(darkVal, true)
|
||||
);
|
||||
}, [
|
||||
embedBrandingColors.highlightColor,
|
||||
embedBrandingColors.lightestColor,
|
||||
embedBrandingColors.lighterColor,
|
||||
embedBrandingColors.lightColor,
|
||||
embedBrandingColors.medianColor,
|
||||
embedBrandingColors.darkColor,
|
||||
embedBrandingColors.darkerColor,
|
||||
lightVal,
|
||||
darkVal,
|
||||
]);
|
||||
return null;
|
||||
return (
|
||||
<Head>
|
||||
<style>
|
||||
{`body {
|
||||
/* green--500*/
|
||||
--booking-highlight-color: ${embedBrandingColors.highlightColor || "#10B981"};
|
||||
/* gray--200 */
|
||||
--booking-lightest-color: ${embedBrandingColors.lightestColor || "#E1E1E1"};
|
||||
/* gray--400 */
|
||||
--booking-lighter-color: ${embedBrandingColors.lighterColor || "#ACACAC"};
|
||||
/* gray--500 */
|
||||
--booking-light-color: ${embedBrandingColors.lightColor || "#888888"};
|
||||
/* gray--600 */
|
||||
--booking-median-color: ${embedBrandingColors.medianColor || "#494949"};
|
||||
/* gray--800 */
|
||||
--booking-dark-color: ${embedBrandingColors.darkColor || "#313131"};
|
||||
/* gray--900 */
|
||||
--booking-darker-color: ${embedBrandingColors.darkerColor || "#292929"};
|
||||
--brand-color: ${lightVal};
|
||||
--brand-text-color: ${getContrastingTextColor(lightVal, true)};
|
||||
--brand-color-dark-mode: ${darkVal};
|
||||
--brand-text-color-dark-mode: ${getContrastingTextColor(darkVal, true)};
|
||||
`}
|
||||
</style>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandColor;
|
||||
|
|
|
@ -113,7 +113,7 @@ const useSlots = ({
|
|||
eventTypeId: number;
|
||||
startTime?: Dayjs;
|
||||
endTime?: Dayjs;
|
||||
timeZone: string;
|
||||
timeZone?: string;
|
||||
}) => {
|
||||
const { data, isLoading, isIdle } = trpc.useQuery(
|
||||
[
|
||||
|
@ -150,7 +150,7 @@ const SlotPicker = ({
|
|||
}: {
|
||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
||||
timeFormat: string;
|
||||
timeZone: string;
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot?: number;
|
||||
recurringEventCount?: number;
|
||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
@ -198,12 +198,10 @@ const SlotPicker = ({
|
|||
<>
|
||||
<DatePicker
|
||||
isLoading={isLoading}
|
||||
className={
|
||||
"mt-8 w-full sm:mt-0 sm:min-w-[455px] " +
|
||||
(selectedDate
|
||||
? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
|
||||
: "sm:pl-4")
|
||||
}
|
||||
className={classNames(
|
||||
"mt-8 w-full sm:mt-0 sm:min-w-[455px]",
|
||||
selectedDate ? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 " : "sm:pl-4"
|
||||
)}
|
||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
selected={selectedDate}
|
||||
|
@ -668,28 +666,20 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||
</div>
|
||||
)*/}
|
||||
</div>
|
||||
{timeZone && (
|
||||
<SlotPicker
|
||||
weekStart={
|
||||
typeof profile.weekStart === "string"
|
||||
? ([
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
|
||||
: profile.weekStart /* Allows providing weekStart as number */
|
||||
}
|
||||
eventType={eventType}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
/>
|
||||
)}
|
||||
<SlotPicker
|
||||
weekStart={
|
||||
typeof profile.weekStart === "string"
|
||||
? (["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
profile.weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
|
||||
: profile.weekStart /* Allows providing weekStart as number */
|
||||
}
|
||||
eventType={eventType}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
||||
|
|
|
@ -492,10 +492,13 @@ const BookingPage = ({
|
|||
<AvatarGroup
|
||||
border="border-2 border-white dark:border-gray-800"
|
||||
size={14}
|
||||
items={[{ image: profile.image || "", alt: profile.name || "" }].concat(
|
||||
items={[
|
||||
{ image: profile.image || "", alt: profile.name || "", title: profile.name || "" },
|
||||
].concat(
|
||||
eventType.users
|
||||
.filter((user) => user.name !== profile.name)
|
||||
.map((user) => ({
|
||||
title: user.name || "",
|
||||
image: user.avatar || "",
|
||||
alt: user.name || "",
|
||||
}))
|
||||
|
|
|
@ -15,6 +15,9 @@ export type AvatarProps = {
|
|||
gravatarFallbackMd5?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use AvatarSSR instead. Once, there is no usage of Avatar, AvatarSSR can be renamed.
|
||||
*/
|
||||
export default function Avatar(props: AvatarProps) {
|
||||
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
|
||||
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from "react";
|
|||
import classNames from "@lib/classNames";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
||||
|
||||
export type AvatarGroupProps = {
|
||||
border?: string; // this needs to be the color of the parent container background, i.e.: border-white dark:border-gray-900
|
||||
|
@ -23,7 +24,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
|||
if (item.image != null) {
|
||||
return (
|
||||
<li key={idx} className="-mr-2 inline-block">
|
||||
<Avatar
|
||||
<AvatarSSR
|
||||
className={props.border}
|
||||
imageSrc={item.image}
|
||||
title={item.title}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { User } from "@prisma/client";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
export type AvatarProps = {
|
||||
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
|
||||
export type AvatarProps = (
|
||||
| {
|
||||
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
|
||||
}
|
||||
| {
|
||||
user?: null;
|
||||
imageSrc: string;
|
||||
}
|
||||
) & {
|
||||
className?: string;
|
||||
size?: number;
|
||||
title?: string;
|
||||
|
@ -16,18 +24,38 @@ function defaultAvatarSrc(md5: string) {
|
|||
}
|
||||
|
||||
// An SSR Supported version of Avatar component.
|
||||
// FIXME: title support is missing
|
||||
export function AvatarSSR(props: AvatarProps) {
|
||||
const { user, size } = props;
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
|
||||
let imgSrc;
|
||||
const alt = props.alt || nameOrUsername;
|
||||
if (user.avatar) {
|
||||
imgSrc = user.avatar;
|
||||
} else if (user.emailMd5) {
|
||||
imgSrc = defaultAvatarSrc(user.emailMd5);
|
||||
const { size, title } = props;
|
||||
|
||||
let imgSrc = "";
|
||||
let alt: string = props.alt;
|
||||
|
||||
if (props.user) {
|
||||
const user = props.user;
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
alt = alt || nameOrUsername;
|
||||
|
||||
if (user.avatar) {
|
||||
imgSrc = user.avatar;
|
||||
} else if (user.emailMd5) {
|
||||
imgSrc = defaultAvatarSrc(user.emailMd5);
|
||||
}
|
||||
} else {
|
||||
imgSrc = props.imageSrc;
|
||||
}
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
|
||||
|
||||
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
|
||||
|
||||
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
|
||||
return title ? (
|
||||
<Tooltip.Tooltip delayDuration={300}>
|
||||
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
|
||||
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white shadow-sm">
|
||||
<Tooltip.Arrow />
|
||||
{title}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Tooltip>
|
||||
) : (
|
||||
<>{avatar}</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
|
|||
export default function Book(props: BookPageProps) {
|
||||
const { t } = useLocale();
|
||||
const locationLabels = getLocationLabels(t);
|
||||
|
||||
return props.away ? (
|
||||
<div className="h-screen dark:bg-neutral-900">
|
||||
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||
|
|
|
@ -15,6 +15,7 @@ class MyDocument extends Document<Props> {
|
|||
return (
|
||||
<Html lang={locale} dir={dir}>
|
||||
<Head>
|
||||
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
|
|
|
@ -250,6 +250,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
items={type.users.map((organizer) => ({
|
||||
alt: organizer.name || "",
|
||||
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
||||
title: organizer.name || "",
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -66,6 +66,7 @@ function TeamPage({ team }: TeamPageProps) {
|
|||
size={10}
|
||||
items={type.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
title: user.name || "",
|
||||
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||
}))}
|
||||
/>
|
||||
|
|
|
@ -13,6 +13,8 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
|
|||
|
||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
export type AvailabilityTeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function TeamType(props: AvailabilityTeamPageProps) {
|
||||
|
@ -24,6 +26,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
const typeParam = asStringOrNull(context.query.type);
|
||||
const dateParam = asStringOrNull(context.query.date);
|
||||
const rescheduleUid = asStringOrNull(context.query.rescheduleUid);
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
if (!slugParam || !typeParam) {
|
||||
throw new Error(`File is not named [idOrSlug]/[user]`);
|
||||
|
@ -145,6 +148,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
workingHours,
|
||||
previousPage: context.req.headers.referer ?? null,
|
||||
booking,
|
||||
trpcState: ssg.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue