Merge branch 'main' into add_booking_confirmed_webhook_event

pull/1296/head
Bill Gale 2021-12-15 13:29:07 +00:00 committed by GitHub
commit 6b4e04b4fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 262 additions and 125 deletions

View File

@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v2
- name: crowdin action
uses: crowdin/github-action@1.4.0
uses: crowdin/github-action@1.4.2
with:
upload_translations: true
download_translations: true

View File

@ -13,14 +13,14 @@ export default function AddToHomescreen() {
}
}
return !closeBanner ? (
<div className="fixed sm:hidden bottom-0 inset-x-0 pb-2 sm:pb-5">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="fixed inset-x-0 bottom-0 pb-2 sm:hidden sm:pb-5">
<div className="px-2 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="p-2 rounded-lg shadow-lg sm:p-3" style={{ background: "#2F333D" }}>
<div className="flex items-center justify-between flex-wrap">
<div className="w-0 flex-1 flex items-center">
<span className="flex p-2 rounded-lg bg-opacity-30 bg-brand">
<div className="flex flex-wrap items-center justify-between">
<div className="flex items-center flex-1 w-0">
<span className="flex p-2 rounded-lg bg-opacity-30 bg-brand text-brandcontrast">
<svg
className="h-7 w-7 text-indigo-500 fill-current"
className="text-indigo-500 fill-current h-7 w-7"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
enableBackground="new 0 0 50 50">
@ -34,13 +34,13 @@ export default function AddToHomescreen() {
</p>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<div className="flex-shrink-0 order-2 sm:order-3 sm:ml-2">
<button
onClick={() => setCloseBanner(true)}
type="button"
className="-mr-1 flex p-2 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
className="flex p-2 -mr-1 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
<XIcon className="w-6 h-6 text-white" aria-hidden="true" />
</button>
</div>
</div>

View File

@ -1,8 +1,38 @@
import { useEffect } from "react";
function computeContrastRatio(a: number[], b: number[]) {
const lum1 = computeLuminance(a[0], a[1], a[2]);
const lum2 = computeLuminance(b[0], b[1], b[2]);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
function computeLuminance(r: number, g: number, b: number) {
const a = [r, g, b].map((v) => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function hexToRGB(hex: string) {
const color = hex.replace("#", "");
return [parseInt(color.slice(0, 2), 16), parseInt(color.slice(2, 4), 16), parseInt(color.slice(4, 6), 16)];
}
function getContrastingTextColor(bgColor: string | null): string {
bgColor = bgColor == "" || bgColor == null ? "#292929" : bgColor;
const rgb = hexToRGB(bgColor);
const whiteContrastRatio = computeContrastRatio(rgb, [255, 255, 255]);
const blackContrastRatio = computeContrastRatio(rgb, [41, 41, 41]); //#292929
return whiteContrastRatio > blackContrastRatio ? "#ffffff" : "#292929";
}
const BrandColor = ({ val = "#292929" }: { val: string | undefined | null }) => {
useEffect(() => {
document.documentElement.style.setProperty("--brand-color", val);
document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(val));
}, [val]);
return null;
};

View File

@ -23,7 +23,7 @@ export function Tooltip({
onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
className="bg-brand text-xs -mt-2 text-white px-1 py-0.5 shadow-lg rounded-sm"
className="bg-black text-xs -mt-2 text-white px-1 py-0.5 shadow-lg rounded-sm"
side="top"
align="center"
{...props}>

View File

@ -3,8 +3,9 @@ import { SchedulingType } from "@prisma/client";
import { Dayjs } from "dayjs";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { FC } from "react";
import React, { FC, useEffect, useState } from "react";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { useSlots } from "@lib/hooks/useSlots";
@ -44,6 +45,12 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
eventTypeId,
});
const [brand, setBrand] = useState("#292929");
useEffect(() => {
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
}, []);
return (
<div className="flex flex-col mt-8 text-center sm:pl-4 sm:mt-0 sm:w-1/3 md:-mb-5">
<div className="mb-4 text-lg font-light text-left text-gray-600">
@ -84,7 +91,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
<div key={slot.time.format()}>
<Link href={bookingUrl}>
<a
className="block py-4 mb-2 font-medium bg-white border rounded-sm dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border-brand dark:border-transparent hover:text-white hover:bg-brand dark:hover:border-black dark:hover:bg-black"
className={classNames(
"block py-4 mb-2 font-medium bg-white border rounded-sm dark:bg-gray-600 text-primary-500 dark:text-neutral-200 dark:border-transparent hover:text-white hover:bg-brand hover:text-brandcontrast dark:hover:border-black dark:hover:bg-brand dark:hover:text-brandcontrast",
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
)}
data-testid="time">
{slot.time.format(timeFormat)}
</a>

View File

@ -200,13 +200,11 @@ function DatePicker({
className={classNames(
"absolute w-full top-0 left-0 right-0 bottom-0 rounded-sm text-center mx-auto",
"hover:border hover:border-brand dark:hover:border-white",
day.disabled
? "text-gray-400 font-light hover:border-0 cursor-default"
: "dark:text-white text-primary-500 font-medium",
day.disabled ? "text-gray-400 font-light hover:border-0 cursor-default" : "font-medium",
date && date.isSame(inviteeDate().date(day.date), "day")
? "bg-brand text-white-important"
? "bg-brand text-brandcontrast"
: !day.disabled
? " bg-gray-100 dark:bg-gray-600"
? " bg-gray-100 dark:bg-gray-600 dark:text-white"
: ""
)}
data-testid="day"

View File

@ -35,19 +35,19 @@ const TimeOptions: FC<Props> = (props) => {
};
return selectedTimeZone !== "" ? (
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
<div className="absolute z-10 w-full px-4 py-2 bg-white border border-gray-200 rounded-sm max-w-80 dark:bg-gray-700 dark:border-0">
<div className="flex mb-4">
<div className="w-1/2 dark:text-white text-gray-600 font-medium">{t("time_options")}</div>
<div className="w-1/2 font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
<div className="w-1/2">
<Switch.Group as="div" className="flex items-center justify-end">
<Switch.Label as="span" className="mr-3">
<span className="text-sm dark:text-white text-gray-500">{t("am_pm")}</span>
<span className="text-sm text-gray-500 dark:text-white">{t("am_pm")}</span>
</Switch.Label>
<Switch
checked={is24hClock}
onChange={handle24hClockToggle}
className={classNames(
is24hClock ? "bg-brand" : "dark:bg-gray-600 bg-gray-200",
is24hClock ? "bg-brand text-brandcontrast" : "dark:bg-gray-600 bg-gray-200",
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
)}>
<span className="sr-only">{t("use_setting")}</span>
@ -60,7 +60,7 @@ const TimeOptions: FC<Props> = (props) => {
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm dark:text-white text-gray-500">{t("24_h")}</span>
<span className="text-sm text-gray-500 dark:text-white">{t("24_h")}</span>
</Switch.Label>
</Switch.Group>
</div>
@ -69,7 +69,7 @@ const TimeOptions: FC<Props> = (props) => {
id="timeZone"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="mb-2 shadow-sm focus:ring-black focus:border-brand mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full mt-1 mb-2 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
) : null;

View File

@ -4,11 +4,11 @@ import React from "react";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-brand rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-brand text-brandcontrast bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<ShieldCheckIcon className="w-6 h-6 text-black" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="text-lg font-medium leading-6 text-gray-900 font-cal" id="modal-title">
{title}
</h3>
<p className="text-sm text-gray-400">{description}</p>

View File

@ -62,8 +62,8 @@ export default function MemberInvitationModal(props: { team: TeamWithMembers | n
<div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="mb-4 sm:flex sm:items-start">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-brand bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UserIcon className="w-6 h-6 text-white" />
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto rounded-full bg-brand text-brandcontrast bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UserIcon className="w-6 h-6 text-brandcontrast" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">

View File

@ -36,7 +36,7 @@ export default function Avatar(props: AvatarProps) {
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="p-2 rounded-sm text-sm bg-brand text-white shadow-sm">
<Tooltip.Content className="p-2 text-sm rounded-sm shadow-sm bg-brand text-brandcontrast">
<Tooltip.Arrow />
{title}
</Tooltip.Content>

View File

@ -64,7 +64,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
color === "primary" &&
(disabled
? "border border-transparent bg-gray-400 text-white"
: "border border-transparent dark:text-black text-white bg-brand dark:bg-white hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900"),
: "border border-transparent dark:text-brandcontrast text-brandcontrast bg-brand dark:bg-brand hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900"),
color === "secondary" &&
(disabled
? "border border-gray-200 text-gray-400 bg-white"

View File

@ -34,7 +34,7 @@ export const WeekdaySelect = (props: WeekdaySelectProps) => {
}}
className={`
w-10 h-10
bg-brand text-white focus:outline-none px-3 py-1 rounded
bg-brand text-brandcontrast focus:outline-none px-3 py-1 rounded
${activeDays[idx + 1] ? "rounded-r-none" : ""}
${activeDays[idx - 1] ? "rounded-l-none" : ""}
${idx === 0 ? "rounded-l" : ""}

View File

@ -8,7 +8,7 @@ export const PhoneInput = (props: PhoneInputProps) => (
<BasePhoneInput
{...props}
className={classNames(
"shadow-sm rounded-md block w-full py-px px-3 border border-1 border-gray-300 ring-black focus-within:ring-1 focus-within:border-brand dark:border-gray-900 dark:text-white dark:bg-brand",
"shadow-sm rounded-sm block w-full py-px px-3 border border-1 border-gray-300 ring-black focus-within:ring-1 focus-within:border-brand dark:border-black dark:text-white dark:bg-black",
props.className
)}
onChange={() => {

View File

@ -15,7 +15,7 @@ import Button from "@components/ui/Button";
const CARD_OPTIONS = {
iconStyle: "solid" as const,
classes: {
base: "block p-2 w-full border-solid border-2 border-gray-300 rounded-md shadow-sm dark:bg-brand dark:text-white dark:border-gray-900 focus-within:ring-black focus-within:border-brand sm:text-sm",
base: "block p-2 w-full border-solid border-2 border-gray-300 rounded-md shadow-sm dark:bg-brand dark:text-brandcontrast dark:border-gray-900 focus-within:ring-black focus-within:border-brand sm:text-sm",
},
style: {
base: {

View File

@ -59,7 +59,7 @@ export default function TeamAvailabilityTimes(props: Props) {
{times.map((time) => (
<div key={time.format()} className="flex flex-row items-center">
<a
className="flex-grow block py-2 mb-2 mr-3 font-medium text-center bg-white border rounded-sm min-w-48 dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border-brand dark:border-transparent hover:text-white hover:bg-brand dark:hover:border-black dark:hover:bg-black"
className="flex-grow block py-2 mb-2 mr-3 font-medium text-center bg-white border rounded-sm min-w-48 dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border-brand dark:border-transparent hover:bg-brand hover:text-brandcontrast dark:hover:border-black dark:hover:text-white dark:hover:bg-black"
data-testid="time">
{time.format("HH:mm")}
</a>

View File

@ -1,5 +1,6 @@
import { BookOpenIcon, CheckIcon, CodeIcon, DocumentTextIcon } from "@heroicons/react/outline";
import { ChevronRightIcon } from "@heroicons/react/solid";
import { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
@ -8,6 +9,8 @@ import { useLocale } from "@lib/hooks/useLocale";
import { HeadSeo } from "@components/seo/head-seo";
import { ssgInit } from "@server/lib/ssg";
export default function Custom404() {
const { t } = useLocale();
const router = useRouter();
@ -170,3 +173,13 @@ export default function Custom404() {
</>
);
}
export const getStaticProps = async (context: GetStaticPropsContext) => {
const ssr = await ssgInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};

View File

@ -69,7 +69,7 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const Success = () => {
return (
<div className="space-y-6">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">{t("done")}</h2>
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900">{t("done")}</h2>
<p>{t("check_email_reset_password")}</p>
{error && <p className="text-red-600">{error.message}</p>}
</div>
@ -77,15 +77,15 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8">
<HeadSeo title={t("forgot_password")} description={t("forgot_password")} />
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10 space-y-6">
<div className="px-4 py-8 mx-2 space-y-6 bg-white rounded-lg shadow sm:px-10">
{success && <Success />}
{!success && (
<>
<div className="space-y-6">
<h2 className="font-cal mt-6 text-center text-3xl font-extrabold text-gray-900">
<h2 className="mt-6 text-3xl font-extrabold text-center text-gray-900 font-cal">
{t("forgot_password")}
</h2>
<p>{t("reset_instructions")}</p>
@ -107,7 +107,7 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
autoComplete="email"
placeholder="john.doe@example.com"
required
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-black focus:border-brand sm:text-sm"
className="block w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
</div>
@ -116,12 +116,12 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
<button
type="submit"
disabled={loading}
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-brand hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black ${
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-brandcontrast bg-brand hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black ${
loading ? "cursor-not-allowed" : ""
}`}>
{loading && (
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
className="w-5 h-5 mr-3 -ml-1 text-white animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
@ -145,7 +145,7 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
<Link href="/auth/login">
<button
type="button"
className="w-full flex justify-center py-2 px-4 text-sm font-medium text-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
{t("login")}
</button>
</Link>

View File

@ -13,22 +13,22 @@ export default function Logout() {
return (
<div
className="fixed z-50 inset-0 overflow-y-auto"
className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<HeadSeo title={t("logged_out")} description={t("logged_out")} />
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<div className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<div>
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<CheckIcon className="h-6 w-6 text-green-600" />
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
<CheckIcon className="w-6 h-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("youve_been_logged_out")}
</h3>
<div className="mt-2">
@ -38,7 +38,7 @@ export default function Logout() {
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login">
<a className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-brand text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black sm:text-sm">
<a className="inline-flex justify-center w-full px-4 py-2 text-base font-medium border border-transparent rounded-md shadow-sm bg-brand text-brandcontrast focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black sm:text-sm">
{t("go_back_login")}
</a>
</Link>

View File

@ -51,12 +51,12 @@ const AvailabilityView = ({ user }: { user: User }) => {
}, [selectedDate]);
return (
<div className="bg-white max-w-xl overflow-hidden shadow rounded-sm">
<div className="max-w-xl overflow-hidden bg-white rounded-sm shadow">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline border-none h-8 p-0"
className="inline h-8 p-0 border-none"
defaultValue={selectedDate.format("YYYY-MM-DD")}
onChange={(e) => {
setSelectedDate(dayjs(e.target.value));
@ -64,8 +64,8 @@ const AvailabilityView = ({ user }: { user: User }) => {
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-brand overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
<div className="overflow-hidden rounded-sm bg-brand">
<div className="px-4 py-2 sm:px-6 text-brandcontrast">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
@ -73,8 +73,8 @@ const AvailabilityView = ({ user }: { user: User }) => {
<Loader />
) : availability.length > 0 ? (
availability.map((slot) => (
<div key={slot.start} className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-black">
<div key={slot.start} className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
@ -89,13 +89,13 @@ const AvailabilityView = ({ user }: { user: User }) => {
</div>
))
) : (
<div className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-black">{t("calendar_no_busy_slots")}</div>
<div className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
)}
<div className="bg-brand overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
<div className="overflow-hidden rounded-sm bg-brand">
<div className="px-4 py-2 sm:px-6 text-brandcontrast">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
</div>
</div>

View File

@ -1,71 +1,39 @@
import { CalendarIcon, XIcon } from "@heroicons/react/solid";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { getSession } from "next-auth/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import { HeadSeo } from "@components/seo/head-seo";
import { Button } from "@components/ui/Button";
dayjs.extend(utc);
import { ssrInit } from "@server/lib/ssr";
export default function Type(props) {
export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
// Get router variables
const router = useRouter();
const { uid } = router.query;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [is24h, setIs24h] = useState(false);
const [is24h] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(props.booking ? null : t("booking_already_cancelled"));
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
const telemetry = useTelemetry();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const cancellationHandler = async (event) => {
setLoading(true);
const payload = {
uid: uid,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
if (res.status >= 200 && res.status < 300) {
await router.push(
`/cancel/success?name=${props.profile.name}&title=${props.booking.title}&eventPage=${
props.profile.slug
}&team=${props.booking.eventType.team ? 1 : 0}`
);
} else {
setLoading(false);
setError(`${t("error_with_status_code_occured", { status: res.status })} ${t("please_try_again")}`);
}
};
return (
<div>
<HeadSeo
title={`${t("cancel")} ${props.booking && props.booking.title} | ${props.profile.name}`}
description={`${t("cancel")} ${props.booking && props.booking.title} | ${props.profile.name}`}
title={`${t("cancel")} ${props.booking && props.booking.title} | ${props.profile?.name}`}
description={`${t("cancel")} ${props.booking && props.booking.title} | ${props.profile?.name}`}
/>
<CustomBranding val={props.profile.brandColor} />
<CustomBranding val={props.profile?.brandColor} />
<main className="max-w-3xl mx-auto my-24">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
@ -109,11 +77,11 @@ export default function Type(props) {
</div>
<div className="py-4 mt-4 border-t border-b">
<h2 className="mb-2 text-lg font-medium text-gray-600 font-cal">
{props.booking.title}
{props.booking?.title}
</h2>
<p className="text-gray-500">
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{dayjs(props.booking.startTime).format(
{dayjs(props.booking?.startTime).format(
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
)}
</p>
@ -125,7 +93,42 @@ export default function Type(props) {
<Button
color="secondary"
data-testid="cancel"
onClick={cancellationHandler}
onClick={async () => {
setLoading(true);
const payload = {
uid: uid,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
if (res.status >= 200 && res.status < 300) {
await router.push(
`/cancel/success?name=${props.profile.name}&title=${
props.booking.title
}&eventPage=${props.profile.slug}&team=${
props.booking.eventType?.team ? 1 : 0
}`
);
} else {
setLoading(false);
setError(
`${t("error_with_status_code_occured", { status: res.status })} ${t(
"please_try_again"
)}`
);
}
}}
loading={loading}>
{t("cancel")}
</Button>
@ -143,11 +146,12 @@ export default function Type(props) {
);
}
export async function getServerSideProps(context) {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getSession(context);
const booking = await prisma.booking.findUnique({
where: {
uid: context.query.uid,
uid: asStringOrUndefined(context.query.uid),
},
select: {
id: true,
@ -189,19 +193,19 @@ export async function getServerSideProps(context) {
endTime: booking.endTime.toString(),
});
const profile = booking.eventType.team
? {
name: booking.eventType.team.name,
slug: booking.eventType.team.slug,
}
: booking.user;
const profile = {
name: booking.eventType?.team?.name || booking.user?.name || null,
slug: booking.eventType?.team?.slug || booking.user?.username || null,
brandColor: booking.user?.brandColor || null,
};
return {
props: {
profile,
booking: bookingObj,
cancellationAllowed:
(!!session?.user && session.user.id == booking.user?.id) || booking.startTime >= new Date(),
(!!session?.user && session.user?.id === booking.user?.id) || booking.startTime >= new Date(),
trpcState: ssr.dehydrate(),
},
};
}
};

View File

@ -59,6 +59,19 @@ import * as RadioArea from "@components/ui/form/radio-area";
dayjs.extend(utc);
dayjs.extend(timezone);
const addDefaultLocationOptions = (
defaultLocations: OptionTypeBase[],
locationOptions: OptionTypeBase[]
): void => {
const existingLocationOptions = locationOptions.flatMap((locationOptionItem) => [locationOptionItem.value]);
defaultLocations.map((item) => {
if (!existingLocationOptions.includes(item.value)) {
locationOptions.push(item);
}
});
};
const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const { t } = useLocale();
const PERIOD_TYPES = [
@ -77,10 +90,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
];
const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } =
props;
locationOptions.push(
/** Appending default locations */
const defaultLocations = [
{ value: LocationType.InPerson, label: t("in_person_meeting") },
{ value: LocationType.Phone, label: t("phone_call") }
);
{ value: LocationType.Phone, label: t("phone_call") },
];
addDefaultLocationOptions(defaultLocations, locationOptions);
const router = useRouter();

View File

@ -22,6 +22,8 @@ import CustomBranding from "@components/CustomBranding";
import { HeadSeo } from "@components/seo/head-seo";
import Button from "@components/ui/Button";
import { ssrInit } from "@server/lib/ssr";
dayjs.extend(utc);
dayjs.extend(toArray);
dayjs.extend(timezone);
@ -145,7 +147,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
</div>
</div>
{!needsConfirmation && (
<div className="flex pt-2 pb-4 mt-5 text-center border-b sm:mt-0 sm:pt-4">
<div className="flex pt-2 pb-4 mt-5 text-center border-b dark:border-gray-900 sm:mt-0 sm:pt-4">
<span className="flex self-center mr-2 font-medium text-gray-700 dark:text-gray-50">
{t("add_to_calendar")}
</span>
@ -258,7 +260,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
id="email"
inputMode="email"
defaultValue={router.query.email}
className="block w-full text-gray-600 border-gray-300 shadow-sm dark:bg-brand dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
className="block w-full text-gray-600 border-gray-300 shadow-sm dark:bg-brand dark:text-brandcontrast dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder="rick.astley@cal.com"
/>
<Button type="submit" className="min-w-max" color="primary">
@ -279,6 +281,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const typeId = parseInt(asStringOrNull(context.query.type) ?? "");
if (isNaN(typeId)) {
@ -358,6 +361,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
hideBranding: eventType.team ? eventType.team.hideBranding : isBrandingHidden(eventType.users[0]),
profile,
eventType,
trpcState: ssr.dehydrate(),
},
};
}

View File

@ -77,7 +77,7 @@ function TeamPage({ team }: TeamPageProps) {
<div className="w-full border-t border-gray-200 dark:border-gray-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 text-sm text-gray-500 bg-gray-100 dark:bg-brand dark:text-gray-500">
<span className="px-2 text-sm text-gray-500 bg-gray-100 dark:bg-brand dark:text-brandcontrast">
{t("or")}
</span>
</div>

View File

@ -18,7 +18,9 @@ describe("free user", () => {
await expect(page).not.toHaveSelector(`[href="/free/60min"]`);
});
// TODO: make sure `/free/30min` is bookable and that `/free/60min` is not
test.todo("`/free/30min` is bookable");
test.todo("`/free/60min` is not bookable");
});
describe("pro user", () => {
@ -55,4 +57,8 @@ describe("pro user", () => {
},
});
});
test.todo("Can reschedule the recently created booking");
test.todo("Can cancel the recently created booking");
});

43
server/lib/ssg.ts Normal file
View File

@ -0,0 +1,43 @@
import { GetStaticPropsContext } from "next";
import { i18n } from "next-i18next.config";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import superjson from "superjson";
import prisma from "@lib/prisma";
import { appRouter } from "@server/routers/_app";
import { createSSGHelpers } from "@trpc/react/ssg";
/**
* Initialize static site rendering tRPC helpers.
* Provides a method to prefetch tRPC-queries in a `getStaticProps`-function.
* Automatically prefetches i18n based on the passed in `context`-object to prevent i18n-flickering.
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
*/
export async function ssgInit<TParams extends { locale?: string }>(opts: GetStaticPropsContext<TParams>) {
const requestedLocale = opts.params?.locale || opts.locale || i18n.defaultLocale;
const isSupportedLocale = i18n.locales.includes(requestedLocale);
if (!isSupportedLocale) {
console.warn(`Requested unsupported locale "${requestedLocale}"`);
}
const locale = isSupportedLocale ? requestedLocale : i18n.defaultLocale;
const _i18n = await serverSideTranslations(locale, ["common"]);
const ssg = createSSGHelpers({
router: appRouter,
transformer: superjson,
ctx: {
prisma,
session: null,
user: null,
locale,
i18n: _i18n,
},
});
// always preload i18n
await ssg.fetchQuery("viewer.i18n");
return ssg;
}

View File

@ -6,6 +6,12 @@ import { createSSGHelpers } from "@trpc/react/ssg";
import { appRouter } from "../routers/_app";
/**
* Initialize server-side rendering tRPC helpers.
* Provides a method to prefetch tRPC-queries in a `getServerSideProps`-function.
* Automatically prefetches i18n based on the passed in `context`-object to prevent i18n-flickering.
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
*/
export async function ssrInit(context: GetServerSidePropsContext) {
const ctx = await createContext(context);

View File

@ -4,6 +4,7 @@
:root {
--brand-color: #292929;
--brand-text-color: #ffffff;
}
/* PhoneInput dark-mode overrides (it would add a lot of boilerplate to do this in JavaScript) */
@ -11,7 +12,7 @@
@apply text-sm border-0 focus:ring-0;
}
.dark .PhoneInputInput {
@apply bg-brand;
@apply bg-black;
}
.PhoneInputCountrySelect {
@apply text-black;
@ -355,6 +356,10 @@ body {
background-color: #f3f4f6;
}
hr {
@apply border-gray-200;
}
.text-white-important {
color: white !important;
}

View File

@ -13,7 +13,7 @@ module.exports = {
colors: {
/* your primary brand color */
brand: "var(--brand-color)",
brandcontrast: "var(--brand-text-color)",
black: "#111111",
gray: {
50: "#F8F8F8",

View File

@ -38,7 +38,7 @@
"jest-playwright-preset",
"expect-playwright"
],
"allowJs": false,
"allowJs": true,
"incremental": true
},
"include": [