Merge branch 'main' into add_booking_confirmed_webhook_event
commit
6b4e04b4fd
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" : ""}
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
​
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"jest-playwright-preset",
|
||||
"expect-playwright"
|
||||
],
|
||||
"allowJs": false,
|
||||
"allowJs": true,
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
|
|
Loading…
Reference in New Issue