From 5206fb4f88ef0b87759ccf28336a9bc3fee0892f Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 9 Jul 2021 22:59:21 +0000 Subject: [PATCH 01/13] Implemented theme through user preferences --- components/Theme.tsx | 19 ++++ pages/[user].tsx | 28 +++--- pages/[user]/[type].tsx | 6 +- pages/[user]/book.tsx | 32 +++--- pages/api/user/profile.ts | 24 +++-- pages/settings/profile.tsx | 97 +++++++++++++------ pages/success.tsx | 48 +++++---- .../migration.sql | 2 + prisma/schema.prisma | 1 + tailwind.config.js | 2 +- 10 files changed, 161 insertions(+), 98 deletions(-) create mode 100644 components/Theme.tsx create mode 100644 prisma/migrations/20210709231256_add_user_theme/migration.sql diff --git a/components/Theme.tsx b/components/Theme.tsx new file mode 100644 index 00000000..78350999 --- /dev/null +++ b/components/Theme.tsx @@ -0,0 +1,19 @@ +import {useEffect, useState} from "react"; + +const Theme = (theme?: string) => { + const [isReady, setIsReady] = useState(false); + useEffect( () => { + if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.add(theme); + } + setIsReady(true); + }, []); + + return { + isReady + } +}; + +export default Theme; diff --git a/pages/[user].tsx b/pages/[user].tsx index bdb78725..960e90a0 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,10 +1,14 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; -import prisma from "../lib/prisma"; +import prisma, {whereAndSelect} from "@lib/prisma"; import Avatar from "../components/Avatar"; +import Theme from "@components/Theme"; export default function User(props): User { + + const { isReady } = Theme(props.user.theme); + const eventTypes = props.eventTypes.map((type) => (
  • )); - return ( + return isReady && (
    {props.user.name || props.user.username} | Calendso @@ -50,21 +54,13 @@ export default function User(props): User { } export const getServerSideProps: GetServerSideProps = async (context) => { - const user = await prisma.user.findFirst({ - where: { - username: context.query.user.toLowerCase(), - }, - select: { - id: true, - username: true, - email: true, - name: true, - bio: true, - avatar: true, - eventTypes: true, - }, - }); + const user = await whereAndSelect(prisma.user.findFirst, { + username: context.query.user.toLowerCase(), + }, [ + "id", "username", "email", "name", "bio", "avatar", "eventTypes", "theme" + ] + ); if (!user) { return { notFound: true, diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 52e0773b..d8ba07bf 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -13,12 +13,15 @@ import Avatar from "../../components/Avatar"; import { timeZone } from "../../lib/clock"; import DatePicker from "../../components/booking/DatePicker"; import PoweredByCalendso from "../../components/ui/PoweredByCalendso"; +import Theme from "@components/Theme"; export default function Type(props): Type { // Get router variables const router = useRouter(); const { rescheduleUid } = router.query; + const { isReady } = Theme(props.user.theme); + const [selectedDate, setSelectedDate] = useState(); const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [timeFormat, setTimeFormat] = useState("h:mma"); @@ -44,7 +47,7 @@ export default function Type(props): Type { setTimeFormat(is24hClock ? "HH:mm" : "h:mma"); }; - return ( + return isReady && (
    @@ -174,6 +177,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { "weekStart", "availability", "hideBranding", + "theme", ] ); diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index ec348b69..fe305e2c 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid"; -import prisma from "../../lib/prisma"; +import prisma, {whereAndSelect} from "../../lib/prisma"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; import { useEffect, useState } from "react"; import dayjs from "dayjs"; @@ -14,6 +14,7 @@ import { LocationType } from "../../lib/location"; import Avatar from "../../components/Avatar"; import Button from "../../components/ui/Button"; import { EventTypeCustomInputType } from "../../lib/eventTypeInput"; +import Theme from "@components/Theme"; dayjs.extend(utc); dayjs.extend(timezone); @@ -32,7 +33,10 @@ export default function Book(props: any): JSX.Element { const [selectedLocation, setSelectedLocation] = useState<LocationType>( locations.length === 1 ? locations[0].type : "" ); + + const { isReady } = Theme(props.user.theme); const telemetry = useTelemetry(); + useEffect(() => { setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()); setIs24h(!!localStorage.getItem("timeOption.is24hClock")); @@ -137,7 +141,7 @@ export default function Book(props: any): JSX.Element { book(); }; - return ( + return isReady && ( <div> <Head> <title> @@ -366,19 +370,19 @@ export default function Book(props: any): JSX.Element { } export async function getServerSideProps(context) { - const user = await prisma.user.findFirst({ - where: { + + const user = await whereAndSelect(prisma.user.findFirst, { username: context.query.user, - }, - select: { - username: true, - name: true, - email: true, - bio: true, - avatar: true, - eventTypes: true, - }, - }); + }, [ + "username", + "name", + "email", + "bio", + "avatar", + "eventTypes", + "theme", + ] + ); const eventType = await prisma.eventType.findUnique({ where: { diff --git a/pages/api/user/profile.ts b/pages/api/user/profile.ts index dfb13315..0cfcab43 100644 --- a/pages/api/user/profile.ts +++ b/pages/api/user/profile.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from 'next-auth/client'; -import prisma from '../../../lib/prisma'; +import prisma, {whereAndSelect} from '@lib/prisma'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({req: req}); @@ -11,15 +11,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // Get user - const user = await prisma.user.findUnique({ - where: { - email: session.user.email, + const user = await whereAndSelect(prisma.user.findUnique, { + id: session.user.id, }, - select: { - id: true, - password: true - } - }); + [ "id", "password" ] + ); if (!user) { res.status(404).json({message: 'User not found'}); return; } @@ -42,6 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const timeZone = req.body.timeZone; const weekStart = req.body.weekStart; const hideBranding = req.body.hideBranding; + const theme = req.body.theme; const updateUser = await prisma.user.update({ where: { @@ -52,11 +49,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) name, avatar, bio: description, - timeZone: timeZone, - weekStart: weekStart, - hideBranding: hideBranding, + timeZone, + weekStart, + hideBranding, + theme, }, }); return res.status(200).json({message: 'Profile updated successfully'}); -} \ No newline at end of file +} diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 8278a55c..e103b61c 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -1,12 +1,13 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; -import { useRef, useState } from "react"; -import prisma from "../../lib/prisma"; +import {useEffect, useRef, useState} from "react"; +import prisma, {whereAndSelect} from "@lib/prisma"; import Modal from "../../components/Modal"; import Shell from "../../components/Shell"; import SettingsShell from "../../components/Settings"; import Avatar from "../../components/Avatar"; import { getSession } from "next-auth/client"; +import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; import { UsernameInput } from "../../components/ui/UsernameInput"; import ErrorAlert from "../../components/ui/alerts/Error"; @@ -18,12 +19,23 @@ export default function Settings(props) { const descriptionRef = useRef<HTMLTextAreaElement>(); const avatarRef = useRef<HTMLInputElement>(); const hideBrandingRef = useRef<HTMLInputElement>(); + const [selectedTheme, setSelectedTheme] = useState({ value: "" }); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); - const [selectedWeekStartDay, setSelectedWeekStartDay] = useState(props.user.weekStart || "Sunday"); + const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: "" }); const [hasErrors, setHasErrors] = useState(false); const [errorMessage, setErrorMessage] = useState(""); + const themeOptions = [ + {value: 'light', label: 'Light'}, + {value: 'dark', label: 'Dark'} + ]; + + useEffect( () => { + setSelectedTheme(props.user.theme ? themeOptions.find( (theme) => theme.value === props.user.theme ) : null); + setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart }); + }, []); + const closeSuccessModal = () => { setSuccessModalOpen(false); }; @@ -43,7 +55,7 @@ export default function Settings(props) { const enteredDescription = descriptionRef.current.value; const enteredAvatar = avatarRef.current.value; const enteredTimeZone = selectedTimeZone.value; - const enteredWeekStartDay = selectedWeekStartDay; + const enteredWeekStartDay = selectedWeekStartDay.value; const enteredHideBranding = hideBrandingRef.current.checked; // TODO: Add validation @@ -58,6 +70,7 @@ export default function Settings(props) { timeZone: enteredTimeZone, weekStart: enteredWeekStartDay, hideBranding: enteredHideBranding, + theme: selectedTheme ? selectedTheme.value : null, }), headers: { "Content-Type": "application/json", @@ -124,8 +137,8 @@ export default function Settings(props) { name="about" placeholder="A little something about yourself." rows={3} + defaultValue={props.user.bio} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"> - {props.user.bio} </textarea> </div> </div> @@ -147,14 +160,46 @@ export default function Settings(props) { First Day of Week </label> <div className="mt-1"> - <select + <Select id="weekStart" value={selectedWeekStartDay} - onChange={(e) => setSelectedWeekStartDay(e.target.value)} - className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"> - <option value="Sunday">Sunday</option> - <option value="Monday">Monday</option> - </select> + onChange={setSelectedWeekStartDay} + className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" + options={[ + { value:'Sunday', label:'Sunday' }, + { value:'Monday', label:'Monday' } + ]} /> + </div> + </div> + <div> + <label htmlFor="theme" className="block text-sm font-medium text-gray-700"> + Single Theme + </label> + <div className="my-1"> + <Select + id="theme" + isDisabled={!selectedTheme} + defaultValue={selectedTheme || themeOptions[0]} + onChange={setSelectedTheme} + className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" + options={themeOptions} /> + </div> + <div className="relative flex items-start"> + <div className="flex items-center h-5"> + <input + id="theme-adjust-os" + name="theme-adjust-os" + type="checkbox" + onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])} + defaultChecked={!selectedTheme} + className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" + /> + </div> + <div className="ml-3 text-sm"> + <label htmlFor="theme-adjust-os" className="font-medium text-gray-700"> + Automatically adjust theme based on invitee preferences + </label> + </div> </div> </div> <div> @@ -257,22 +302,20 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, destination: "/auth/login" } }; } - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - username: true, - name: true, - email: true, - bio: true, - avatar: true, - timeZone: true, - weekStart: true, - hideBranding: true, - }, - }); + const user = await whereAndSelect(prisma.user.findFirst, { + id: session.user.id, + }, [ + "id", + "username", + "name", + "email", + "bio", + "avatar", + "timeZone", + "weekStart", + "hideBranding", + "theme" + ]); return { props: { user }, // will be passed to the page component as props diff --git a/pages/success.tsx b/pages/success.tsx index 286897b9..20d9e7c3 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -1,6 +1,6 @@ import Head from "next/head"; import Link from "next/link"; -import prisma from "../lib/prisma"; +import prisma, {whereAndSelect} from "../lib/prisma"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { CheckIcon } from "@heroicons/react/outline"; @@ -11,6 +11,7 @@ import toArray from "dayjs/plugin/toArray"; import timezone from "dayjs/plugin/timezone"; import { createEvent } from "ics"; import { getEventName } from "../lib/event"; +import Theme from "@components/Theme"; dayjs.extend(utc); dayjs.extend(toArray); @@ -22,6 +23,7 @@ export default function Success(props) { const [is24h, setIs24h] = useState(false); const [date, setDate] = useState(dayjs.utc(router.query.date)); + const { isReady } = Theme(props.user.theme); useEffect(() => { setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess())); @@ -56,7 +58,7 @@ export default function Success(props) { return encodeURIComponent(event.value); } - return ( + return isReady && ( <div> <Head> <title>Booking Confirmed | {eventName} | Calendso @@ -212,32 +214,26 @@ export default function Success(props) { } export async function getServerSideProps(context) { - const user = await prisma.user.findFirst({ - where: { - username: context.query.user, - }, - select: { - username: true, - name: true, - bio: true, - avatar: true, - eventTypes: true, - hideBranding: true, - }, - }); - const eventType = await prisma.eventType.findUnique({ - where: { + const user = (context.query.user) ? await whereAndSelect(prisma.user.findFirst, { + username: context.query.user, + }, [ + "username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme" + ] + ) : null; + + if (!user) { + return { + notFound: true, + }; + } + + const eventType = await whereAndSelect(prisma.eventType.findUnique, { id: parseInt(context.query.type), - }, - select: { - id: true, - title: true, - description: true, - length: true, - eventName: true, - }, - }); + }, [ + "id", "title", "description", "length", "eventName" + ] + ); return { props: { diff --git a/prisma/migrations/20210709231256_add_user_theme/migration.sql b/prisma/migrations/20210709231256_add_user_theme/migration.sql new file mode 100644 index 00000000..75875267 --- /dev/null +++ b/prisma/migrations/20210709231256_add_user_theme/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "theme" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ddaf7d81..f0838096 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -50,6 +50,7 @@ model User { endTime Int @default(1440) bufferTime Int @default(0) hideBranding Boolean @default(false) + theme String? createdDate DateTime @default(now()) @map(name: "created") eventTypes EventType[] credentials Credential[] diff --git a/tailwind.config.js b/tailwind.config.js index 24bfaf0f..c14305a9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,7 +1,7 @@ module.exports = { mode: "jit", purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], - darkMode: "media", + darkMode: "class", theme: { extend: { colors: { From 3283eb422c001fcc5b3ab8c0393b7e0a419d27e4 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Sun, 11 Jul 2021 18:05:49 +0200 Subject: [PATCH 02/13] Fixed a bug that selected the following day on the booking page, when the selected time is smaller than the utc offset of the current timezone. Also fixed the reloading of the last 24h/12h selection --- lib/slots.ts | 2 +- pages/[user]/[type].tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/slots.ts b/lib/slots.ts index 862e8b14..3c0d45a1 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -131,7 +131,7 @@ const getSlots = ({ ) .reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], []) .map((slot) => - slot.month(inviteeDate.month()).date(inviteeDate.date()).utcOffset(inviteeDate.utcOffset()) + slot.utcOffset(inviteeDate.utcOffset()).month(inviteeDate.month()).date(inviteeDate.date()) ); }; diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 52e0773b..a2e9c82b 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -25,6 +25,7 @@ export default function Type(props): Type { const telemetry = useTelemetry(); useEffect(() => { + handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true"); telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters())); }, [telemetry]); From 4d7427ad914fd780fa64367484f58e020f6d99c0 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Sun, 11 Jul 2021 19:35:56 +0000 Subject: [PATCH 03/13] Fixes some linting + codacy issues --- components/Theme.tsx | 14 +- pages/[user].tsx | 65 +++--- pages/[user]/[type].tsx | 204 ++++++++--------- pages/[user]/book.tsx | 442 ++++++++++++++++++------------------- pages/api/user/profile.ts | 31 +-- pages/settings/profile.tsx | 46 ++-- pages/success.tsx | 317 +++++++++++++------------- 7 files changed, 563 insertions(+), 556 deletions(-) diff --git a/components/Theme.tsx b/components/Theme.tsx index 78350999..2c0dc1ca 100644 --- a/components/Theme.tsx +++ b/components/Theme.tsx @@ -1,8 +1,8 @@ -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; -const Theme = (theme?: string) => { +export default function Theme(theme?: string) { const [isReady, setIsReady] = useState(false); - useEffect( () => { + useEffect(() => { if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } else { @@ -12,8 +12,6 @@ const Theme = (theme?: string) => { }, []); return { - isReady - } -}; - -export default Theme; + isReady, + }; +} diff --git a/pages/[user].tsx b/pages/[user].tsx index 960e90a0..71c311ce 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,12 +1,11 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; -import prisma, {whereAndSelect} from "@lib/prisma"; +import prisma, { whereAndSelect } from "@lib/prisma"; import Avatar from "../components/Avatar"; import Theme from "@components/Theme"; export default function User(props): User { - const { isReady } = Theme(props.user.theme); const eventTypes = props.eventTypes.map((type) => ( @@ -24,42 +23,44 @@ export default function User(props): User { )); - return isReady && ( -
    - - {props.user.name || props.user.username} | Calendso - - + return ( + isReady && ( +
    + + {props.user.name || props.user.username} | Calendso + + -
    -
    - -

    - {props.user.name || props.user.username} -

    -

    {props.user.bio}

    -
    -
    -
      {eventTypes}
    - {eventTypes.length == 0 && ( -
    -

    Uh oh!

    -

    This user hasn't set up any event types yet.

    -
    - )} -
    -
    -
    +
    +
    + +

    + {props.user.name || props.user.username} +

    +

    {props.user.bio}

    +
    +
    +
      {eventTypes}
    + {eventTypes.length == 0 && ( +
    +

    Uh oh!

    +

    This user hasn't set up any event types yet.

    +
    + )} +
    +
    +
    + ) ); } export const getServerSideProps: GetServerSideProps = async (context) => { - - const user = await whereAndSelect(prisma.user.findFirst, { + const user = await whereAndSelect( + prisma.user.findFirst, + { username: context.query.user.toLowerCase(), - }, [ - "id", "username", "email", "name", "bio", "avatar", "eventTypes", "theme" - ] + }, + ["id", "username", "email", "name", "bio", "avatar", "eventTypes", "theme"] ); if (!user) { return { diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index d8ba07bf..ed5262aa 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -47,113 +47,115 @@ export default function Type(props): Type { setTimeFormat(is24hClock ? "HH:mm" : "h:mma"); }; - return isReady && ( -
    - - - {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} | - Calendso - - - + return ( + isReady && ( +
    + + + {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username}{" "} + | Calendso + + + - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> + + + + + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + /> - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - -
    -
    -
    -
    - -

    {props.user.name}

    -

    - {props.eventType.title} -

    -

    - - {props.eventType.length} minutes -

    - - {isTimeOptionsOpen && ( - + + + + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + /> + +
    +
    +
    +
    + +

    {props.user.name}

    +

    + {props.eventType.title} +

    +

    + + {props.eventType.length} minutes +

    + + {isTimeOptionsOpen && ( + + )} +

    {props.eventType.description}

    +
    + + {selectedDate && ( + )} -

    {props.eventType.description}

    - - {selectedDate && ( - - )}
    -
    - {!props.user.hideBranding && } -
    -
    + {!props.user.hideBranding && } + +
    + ) ); } diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index fe305e2c..c03b86da 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid"; -import prisma, {whereAndSelect} from "../../lib/prisma"; +import prisma, { whereAndSelect } from "../../lib/prisma"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; import { useEffect, useState } from "react"; import dayjs from "dayjs"; @@ -141,247 +141,247 @@ export default function Book(props: any): JSX.Element { book(); }; - return isReady && ( -
    - - - {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} - {props.user.name || props.user.username} | Calendso - - - + return ( + isReady && ( +
    + + + {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} + {props.user.name || props.user.username} | Calendso + + + -
    -
    -
    -
    - -

    {props.user.name}

    -

    - {props.eventType.title} -

    -

    - - {props.eventType.length} minutes -

    - {selectedLocation === LocationType.InPerson && ( +
    +
    +
    +
    + +

    {props.user.name}

    +

    + {props.eventType.title} +

    - - {locationInfo(selectedLocation).address} + + {props.eventType.length} minutes

    - )} -

    - - {preferredTimeZone && - dayjs(date) - .tz(preferredTimeZone) - .format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} -

    -

    {props.eventType.description}

    -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    - {locations.length > 1 && ( -
    - Location - {locations.map((location) => ( - - ))} -
    + {selectedLocation === LocationType.InPerson && ( +

    + + {locationInfo(selectedLocation).address} +

    )} - {selectedLocation === LocationType.Phone && ( +

    + + {preferredTimeZone && + dayjs(date) + .tz(preferredTimeZone) + .format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} +

    +

    {props.eventType.description}

    +
    +
    +
    -
    - )} - {props.eventType.customInputs && - props.eventType.customInputs - .sort((a, b) => a.id - b.id) - .map((input) => ( -
    - {input.type !== EventTypeCustomInputType.Bool && ( - - )} - {input.type === EventTypeCustomInputType.TextLong && ( - + className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
    @@ -166,9 +167,10 @@ export default function Settings(props) { onChange={setSelectedWeekStartDay} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" options={[ - { value:'Sunday', label:'Sunday' }, - { value:'Monday', label:'Monday' } - ]} /> + { value: "Sunday", label: "Sunday" }, + { value: "Monday", label: "Monday" }, + ]} + />
    @@ -182,7 +184,8 @@ export default function Settings(props) { defaultValue={selectedTheme || themeOptions[0]} onChange={setSelectedTheme} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" - options={themeOptions} /> + options={themeOptions} + />
    @@ -302,20 +305,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, destination: "/auth/login" } }; } - const user = await whereAndSelect(prisma.user.findFirst, { + const user = await whereAndSelect( + prisma.user.findFirst, + { id: session.user.id, - }, [ - "id", - "username", - "name", - "email", - "bio", - "avatar", - "timeZone", - "weekStart", - "hideBranding", - "theme" - ]); + }, + ["id", "username", "name", "email", "bio", "avatar", "timeZone", "weekStart", "hideBranding", "theme"] + ); return { props: { user }, // will be passed to the page component as props diff --git a/pages/success.tsx b/pages/success.tsx index 20d9e7c3..9b08377e 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -1,6 +1,6 @@ import Head from "next/head"; import Link from "next/link"; -import prisma, {whereAndSelect} from "../lib/prisma"; +import prisma, { whereAndSelect } from "../lib/prisma"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { CheckIcon } from "@heroicons/react/outline"; @@ -33,7 +33,7 @@ export default function Success(props) { const eventName = getEventName(name, props.eventType.title, props.eventType.eventName); function eventLink(): string { - let optional = {}; + const optional = {}; if (location) { optional["location"] = location; } @@ -58,169 +58,173 @@ export default function Success(props) { return encodeURIComponent(event.value); } - return isReady && ( -
    - - Booking Confirmed | {eventName} | Calendso - - -
    -
    -
    -
    +
    + ) ); } export async function getServerSideProps(context) { - - const user = (context.query.user) ? await whereAndSelect(prisma.user.findFirst, { - username: context.query.user, - }, [ - "username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme" - ] - ) : null; + const user = context.query.user + ? await whereAndSelect( + prisma.user.findFirst, + { + username: context.query.user, + }, + ["username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme"] + ) + : null; if (!user) { return { @@ -228,11 +232,12 @@ export async function getServerSideProps(context) { }; } - const eventType = await whereAndSelect(prisma.eventType.findUnique, { + const eventType = await whereAndSelect( + prisma.eventType.findUnique, + { id: parseInt(context.query.type), - }, [ - "id", "title", "description", "length", "eventName" - ] + }, + ["id", "title", "description", "length", "eventName"] ); return { From 949fcf88846cf567b83c5b23e3e1c4639467b8e9 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 13 Jul 2021 14:32:25 +0200 Subject: [PATCH 04/13] fixed amoount of line breaks (
    ) in new organizer email --- lib/emails/EventOrganizerMail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index 48b5f078..9c60383c 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -66,13 +66,13 @@ export default class EventOrganizerMail extends EventMail { ${this.calEvent.attendees[0].email}

    ` + this.getAdditionalBody() + - "
    " + `Invitee Time Zone:
    ${this.calEvent.attendees[0].timeZone}

    Additional notes:
    ${this.calEvent.description} ` + + "
    " + this.getAdditionalFooter() + `
    From f2c5ecbb9960b734310230d2fa7b5e94b45df09b Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 13 Jul 2021 14:37:42 +0200 Subject: [PATCH 05/13] darkmode fix for is all booked today text --- components/booking/AvailableTimes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index 0f074293..68ba0241 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -43,7 +43,7 @@ const AvailableTimes = ({ ))} {isFullyBooked && (
    -

    {user.name} is all booked today.

    +

    {user.name} is all booked today.

    )} From de14b2de33ca26ceacecb37eabfb616ff22e7759 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 13 Jul 2021 14:38:54 +0200 Subject: [PATCH 06/13] wip --- lib/emails/EventOrganizerMail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index 9c60383c..48b5f078 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -66,13 +66,13 @@ export default class EventOrganizerMail extends EventMail { ${this.calEvent.attendees[0].email}

    ` + this.getAdditionalBody() + + "
    " + `Invitee Time Zone:
    ${this.calEvent.attendees[0].timeZone}

    Additional notes:
    ${this.calEvent.description} ` + - "
    " + this.getAdditionalFooter() + `
    From 61fd300bc8283472411f04aa5d6677daaddbad30 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 13 Jul 2021 15:11:01 +0200 Subject: [PATCH 07/13] re-added last two commits (
    fix and dark mode for all booked today text --- components/booking/AvailableTimes.tsx | 2 +- lib/emails/EventOrganizerMail.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index 68ba0241..64cb4dd0 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -43,7 +43,7 @@ const AvailableTimes = ({ ))} {isFullyBooked && (
    -

    {user.name} is all booked today.

    +

    {user.name} is all booked today.

    )} diff --git a/lib/emails/EventOrganizerMail.ts b/lib/emails/EventOrganizerMail.ts index 48b5f078..9c60383c 100644 --- a/lib/emails/EventOrganizerMail.ts +++ b/lib/emails/EventOrganizerMail.ts @@ -66,13 +66,13 @@ export default class EventOrganizerMail extends EventMail { ${this.calEvent.attendees[0].email}

    ` + this.getAdditionalBody() + - "
    " + `Invitee Time Zone:
    ${this.calEvent.attendees[0].timeZone}

    Additional notes:
    ${this.calEvent.description} ` + + "
    " + this.getAdditionalFooter() + `
    From 793351fe96f8bef29bae04ed24cadd9c0b7bcfa9 Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Tue, 13 Jul 2021 11:10:22 -0500 Subject: [PATCH 08/13] [cal-184] add and handle date query params for event types (#359) --- components/booking/DatePicker.tsx | 11 ++++++++-- pages/[user]/[type].tsx | 35 +++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index 62907e16..ff28095c 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -15,12 +15,19 @@ const DatePicker = ({ organizerTimeZone, inviteeTimeZone, eventLength, + date, }) => { const [calendar, setCalendar] = useState([]); - const [selectedMonth, setSelectedMonth]: number = useState(); - const [selectedDate, setSelectedDate]: Dayjs = useState(); + const [selectedMonth, setSelectedMonth] = useState(); + const [selectedDate, setSelectedDate] = useState(); useEffect(() => { + if (date) { + setSelectedDate(dayjs(date).tz(inviteeTimeZone)); + setSelectedMonth(dayjs(date).tz(inviteeTimeZone).month()); + return; + } + setSelectedMonth(dayjs().tz(inviteeTimeZone).month()); }, []); diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 9716a3d5..8c9c97b1 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; -import { GetServerSideProps } from "next"; +import { GetServerSideProps, GetServerSidePropsContext } from "next"; import Head from "next/head"; import { ChevronDownIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid"; import { useRouter } from "next/router"; -import { Dayjs } from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import prisma, { whereAndSelect } from "@lib/prisma"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; @@ -22,7 +22,9 @@ export default function Type(props): Type { const { isReady } = Theme(props.user.theme); - const [selectedDate, setSelectedDate] = useState(); + const [selectedDate, setSelectedDate] = useState(() => { + return props.date && dayjs(props.date).isValid() ? dayjs(props.date) : null; + }); const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [timeFormat, setTimeFormat] = useState("h:mma"); const telemetry = useTelemetry(); @@ -37,6 +39,26 @@ export default function Type(props): Type { setSelectedDate(date); }; + useEffect(() => { + if (!selectedDate) { + return; + } + + const formattedDate = selectedDate.utc().format("YYYY-MM-DD"); + + router.replace( + { + query: { + date: formattedDate, + }, + }, + undefined, + { + shallow: true, + } + ); + }, [selectedDate]); + const handleSelectTimeZone = (selectedTimeZone: string): void => { if (selectedDate) { setSelectedDate(selectedDate.tz(selectedTimeZone)); @@ -133,6 +155,7 @@ export default function Type(props): Type {

    {props.eventType.description}

    { +export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => { + const dateQuery = context.query?.date ?? null; + const date = Array.isArray(dateQuery) && dateQuery.length > 0 ? dateQuery.pop() : dateQuery; + const user = await whereAndSelect( prisma.user.findFirst, { @@ -226,6 +252,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { user, + date, eventType, workingHours, }, From cb3c23e9b0336054e8f18b067d5caad32a15caaf Mon Sep 17 00:00:00 2001 From: Egor Zaitsev Date: Wed, 14 Jul 2021 12:58:08 +0300 Subject: [PATCH 09/13] fix: truncate event type description instead of title --- pages/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/index.tsx b/pages/index.tsx index 4cf03b68..b19fbef8 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -103,8 +103,8 @@ export default function Home(props) {
    -

    {type.title}

    -

    +

    {type.title}

    +

    in {type.description}

    From 1dadf9f9a6ecf70b15481e8acefb9f84993b363d Mon Sep 17 00:00:00 2001 From: Egor Zaitsev Date: Wed, 14 Jul 2021 14:19:55 +0300 Subject: [PATCH 10/13] fix: same problem on the availability page --- pages/availability/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pages/availability/index.tsx b/pages/availability/index.tsx index ad18b232..0a8ad471 100644 --- a/pages/availability/index.tsx +++ b/pages/availability/index.tsx @@ -121,7 +121,7 @@ export default function Availability(props) {
    -
    +
    @@ -143,7 +143,7 @@ export default function Availability(props) { {props.types.map((eventType) => - - - - From 45689059c7773a78329eebe003c1ad9434d2dc03 Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Thu, 15 Jul 2021 09:10:26 -0500 Subject: [PATCH 11/13] Feat/cal 95/date range on event types (#353) * add edit links to events on dashboard * fit elements on screen for mobile * initialize components for consistent text styles * add more fine grained width/height settings * add higher level setting for when an event is available - db: add supporting values to period allow setting an amount of days, a range, or unlimited number days an event is available * fix issue where periodDates are null * return minimal required data, handle date parsing * [ui] limit booking days based on user period settings * api: validate user period settings * [db] migration for event type period settings --- components/booking/DatePicker.tsx | 68 +- components/ui/Scheduler.tsx | 16 +- components/ui/Text/Body/Body.tsx | 12 + components/ui/Text/Body/index.ts | 2 + components/ui/Text/Caption/Caption.tsx | 12 + components/ui/Text/Caption/index.ts | 2 + components/ui/Text/Caption2/Caption2.tsx | 12 + components/ui/Text/Caption2/index.ts | 2 + components/ui/Text/Footnote/Footnote.tsx | 11 + components/ui/Text/Footnote/index.ts | 2 + components/ui/Text/Headline/Headline.tsx | 11 + components/ui/Text/Headline/index.ts | 2 + components/ui/Text/Largetitle/Largetitle.tsx | 12 + components/ui/Text/Largetitle/index.ts | 2 + components/ui/Text/Overline/Overline.tsx | 12 + components/ui/Text/Overline/index.ts | 2 + .../ui/Text/Subheadline/Subheadline.tsx | 12 + components/ui/Text/Subheadline/index.ts | 2 + components/ui/Text/Subtitle/Subtitle.tsx | 12 + components/ui/Text/Subtitle/index.ts | 2 + components/ui/Text/Text.module.css | 55 ++ components/ui/Text/Text.tsx | 166 +++++ components/ui/Text/Title/Title.tsx | 12 + components/ui/Text/Title/index.ts | 2 + components/ui/Text/Title2/Title2.tsx | 12 + components/ui/Text/Title2/index.ts | 2 + components/ui/Text/Title3/Title3.tsx | 12 + components/ui/Text/Title3/index.ts | 2 + components/ui/Text/index.ts | 39 ++ package.json | 5 + pages/[user].tsx | 7 +- pages/[user]/[type].tsx | 27 +- pages/[user]/book.tsx | 14 +- pages/api/availability/eventtype.ts | 5 + pages/api/book/[user].ts | 66 ++ pages/availability/event/[type].tsx | 307 +++++++-- pages/index.tsx | 590 +++++++++--------- pages/success.tsx | 2 +- .../migration.sql | 6 + prisma/schema.prisma | 5 + tailwind.config.js | 27 +- yarn.lock | 264 +++++++- 42 files changed, 1455 insertions(+), 380 deletions(-) create mode 100644 components/ui/Text/Body/Body.tsx create mode 100644 components/ui/Text/Body/index.ts create mode 100644 components/ui/Text/Caption/Caption.tsx create mode 100644 components/ui/Text/Caption/index.ts create mode 100644 components/ui/Text/Caption2/Caption2.tsx create mode 100644 components/ui/Text/Caption2/index.ts create mode 100644 components/ui/Text/Footnote/Footnote.tsx create mode 100644 components/ui/Text/Footnote/index.ts create mode 100644 components/ui/Text/Headline/Headline.tsx create mode 100644 components/ui/Text/Headline/index.ts create mode 100644 components/ui/Text/Largetitle/Largetitle.tsx create mode 100644 components/ui/Text/Largetitle/index.ts create mode 100644 components/ui/Text/Overline/Overline.tsx create mode 100644 components/ui/Text/Overline/index.ts create mode 100644 components/ui/Text/Subheadline/Subheadline.tsx create mode 100644 components/ui/Text/Subheadline/index.ts create mode 100644 components/ui/Text/Subtitle/Subtitle.tsx create mode 100644 components/ui/Text/Subtitle/index.ts create mode 100644 components/ui/Text/Text.module.css create mode 100644 components/ui/Text/Text.tsx create mode 100644 components/ui/Text/Title/Title.tsx create mode 100644 components/ui/Text/Title/index.ts create mode 100644 components/ui/Text/Title2/Title2.tsx create mode 100644 components/ui/Text/Title2/index.ts create mode 100644 components/ui/Text/Title3/Title3.tsx create mode 100644 components/ui/Text/Title3/index.ts create mode 100644 components/ui/Text/index.ts create mode 100644 prisma/migrations/20210714151216_event_type_period_settings/migration.sql diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index ff28095c..b49b887e 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -4,7 +4,9 @@ import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import getSlots from "@lib/slots"; +import dayjsBusinessDays from "dayjs-business-days"; +dayjs.extend(dayjsBusinessDays); dayjs.extend(utc); dayjs.extend(timezone); @@ -16,6 +18,11 @@ const DatePicker = ({ inviteeTimeZone, eventLength, date, + periodType = "unlimited", + periodStartDate, + periodEndDate, + periodDays, + periodCountCalendarDays, }) => { const [calendar, setCalendar] = useState([]); const [selectedMonth, setSelectedMonth] = useState(); @@ -28,7 +35,11 @@ const DatePicker = ({ return; } - setSelectedMonth(dayjs().tz(inviteeTimeZone).month()); + if (periodType === "range") { + setSelectedMonth(dayjs(periodStartDate).tz(inviteeTimeZone).month()); + } else { + setSelectedMonth(dayjs().tz(inviteeTimeZone).month()); + } }, []); useEffect(() => { @@ -54,15 +65,52 @@ const DatePicker = ({ const isDisabled = (day: number) => { const date: Dayjs = inviteeDate.date(day); - return ( - date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || - !getSlots({ - inviteeDate: date, - frequency: eventLength, - workingHours, - organizerTimeZone, - }).length - ); + + switch (periodType) { + case "rolling": { + const periodRollingEndDay = periodCountCalendarDays + ? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day") + : dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day"); + return ( + date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || + date.endOf("day").isAfter(periodRollingEndDay) || + !getSlots({ + inviteeDate: date, + frequency: eventLength, + workingHours, + organizerTimeZone, + }).length + ); + } + + case "range": { + const periodRangeStartDay = dayjs(periodStartDate).tz(organizerTimeZone).endOf("day"); + const periodRangeEndDay = dayjs(periodEndDate).tz(organizerTimeZone).endOf("day"); + return ( + date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || + date.endOf("day").isBefore(periodRangeStartDay) || + date.endOf("day").isAfter(periodRangeEndDay) || + !getSlots({ + inviteeDate: date, + frequency: eventLength, + workingHours, + organizerTimeZone, + }).length + ); + } + + case "unlimited": + default: + return ( + date.endOf("day").isBefore(dayjs().tz(inviteeTimeZone)) || + !getSlots({ + inviteeDate: date, + frequency: eventLength, + workingHours, + organizerTimeZone, + }).length + ); + } }; // Set up calendar diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx index edec1319..fcb4688c 100644 --- a/components/ui/Scheduler.tsx +++ b/components/ui/Scheduler.tsx @@ -70,7 +70,7 @@ export const Scheduler = ({ const OpeningHours = ({ idx, item }) => (
  • -
    +
    (item.days = selected)} />
    -
    - {/*

    Add date overrides

    -

    - Add dates when your availability changes from your weekly hours -

    - */} -
    {editSchedule >= 0 && ( setEditSchedule(-1)} /> )} - {/*{showDateOverrideModal && - - }*/} ); }; diff --git a/components/ui/Text/Body/Body.tsx b/components/ui/Text/Body/Body.tsx new file mode 100644 index 00000000..f8332092 --- /dev/null +++ b/components/ui/Text/Body/Body.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Body: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--body"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Body; diff --git a/components/ui/Text/Body/index.ts b/components/ui/Text/Body/index.ts new file mode 100644 index 00000000..e77f776a --- /dev/null +++ b/components/ui/Text/Body/index.ts @@ -0,0 +1,2 @@ +import Body from "./Body"; +export default Body; diff --git a/components/ui/Text/Caption/Caption.tsx b/components/ui/Text/Caption/Caption.tsx new file mode 100644 index 00000000..4dadc57c --- /dev/null +++ b/components/ui/Text/Caption/Caption.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Caption: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--caption"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Caption; diff --git a/components/ui/Text/Caption/index.ts b/components/ui/Text/Caption/index.ts new file mode 100644 index 00000000..a3c3d984 --- /dev/null +++ b/components/ui/Text/Caption/index.ts @@ -0,0 +1,2 @@ +import Caption from "./Caption"; +export default Caption; diff --git a/components/ui/Text/Caption2/Caption2.tsx b/components/ui/Text/Caption2/Caption2.tsx new file mode 100644 index 00000000..ca02e615 --- /dev/null +++ b/components/ui/Text/Caption2/Caption2.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Caption2: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--caption2"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Caption2; diff --git a/components/ui/Text/Caption2/index.ts b/components/ui/Text/Caption2/index.ts new file mode 100644 index 00000000..b93a9079 --- /dev/null +++ b/components/ui/Text/Caption2/index.ts @@ -0,0 +1,2 @@ +import Caption2 from "./Caption2"; +export default Caption2; diff --git a/components/ui/Text/Footnote/Footnote.tsx b/components/ui/Text/Footnote/Footnote.tsx new file mode 100644 index 00000000..3beda4fa --- /dev/null +++ b/components/ui/Text/Footnote/Footnote.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; + +const Footnote: React.FunctionComponent = (props: TextProps) => { + const classes = classnames("text--footnote", props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Footnote; diff --git a/components/ui/Text/Footnote/index.ts b/components/ui/Text/Footnote/index.ts new file mode 100644 index 00000000..4fa2ec93 --- /dev/null +++ b/components/ui/Text/Footnote/index.ts @@ -0,0 +1,2 @@ +import Footnote from "./Footnote"; +export default Footnote; diff --git a/components/ui/Text/Headline/Headline.tsx b/components/ui/Text/Headline/Headline.tsx new file mode 100644 index 00000000..cec7956b --- /dev/null +++ b/components/ui/Text/Headline/Headline.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; + +const Headline: React.FunctionComponent = (props: TextProps) => { + const classes = classnames("text--headline", props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Headline; diff --git a/components/ui/Text/Headline/index.ts b/components/ui/Text/Headline/index.ts new file mode 100644 index 00000000..42ad4d66 --- /dev/null +++ b/components/ui/Text/Headline/index.ts @@ -0,0 +1,2 @@ +import Headline from "./Headline"; +export default Headline; diff --git a/components/ui/Text/Largetitle/Largetitle.tsx b/components/ui/Text/Largetitle/Largetitle.tsx new file mode 100644 index 00000000..8b0c3271 --- /dev/null +++ b/components/ui/Text/Largetitle/Largetitle.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Largetitle: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--largetitle"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Largetitle; diff --git a/components/ui/Text/Largetitle/index.ts b/components/ui/Text/Largetitle/index.ts new file mode 100644 index 00000000..4c16d0f1 --- /dev/null +++ b/components/ui/Text/Largetitle/index.ts @@ -0,0 +1,2 @@ +import Largetitle from "./Largetitle"; +export default Largetitle; diff --git a/components/ui/Text/Overline/Overline.tsx b/components/ui/Text/Overline/Overline.tsx new file mode 100644 index 00000000..82003351 --- /dev/null +++ b/components/ui/Text/Overline/Overline.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Overline: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--overline"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Overline; diff --git a/components/ui/Text/Overline/index.ts b/components/ui/Text/Overline/index.ts new file mode 100644 index 00000000..8d40c8c9 --- /dev/null +++ b/components/ui/Text/Overline/index.ts @@ -0,0 +1,2 @@ +import Overline from "./Overline"; +export default Overline; diff --git a/components/ui/Text/Subheadline/Subheadline.tsx b/components/ui/Text/Subheadline/Subheadline.tsx new file mode 100644 index 00000000..550a955f --- /dev/null +++ b/components/ui/Text/Subheadline/Subheadline.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Subheadline: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--subheadline"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Subheadline; diff --git a/components/ui/Text/Subheadline/index.ts b/components/ui/Text/Subheadline/index.ts new file mode 100644 index 00000000..86c9def1 --- /dev/null +++ b/components/ui/Text/Subheadline/index.ts @@ -0,0 +1,2 @@ +import Subheadline from "./Subheadline"; +export default Subheadline; diff --git a/components/ui/Text/Subtitle/Subtitle.tsx b/components/ui/Text/Subtitle/Subtitle.tsx new file mode 100644 index 00000000..11065651 --- /dev/null +++ b/components/ui/Text/Subtitle/Subtitle.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Subtitle: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--subtitle"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Subtitle; diff --git a/components/ui/Text/Subtitle/index.ts b/components/ui/Text/Subtitle/index.ts new file mode 100644 index 00000000..5091f41b --- /dev/null +++ b/components/ui/Text/Subtitle/index.ts @@ -0,0 +1,2 @@ +import Subtitle from "./Subtitle"; +export default Subtitle; diff --git a/components/ui/Text/Text.module.css b/components/ui/Text/Text.module.css new file mode 100644 index 00000000..0afe4759 --- /dev/null +++ b/components/ui/Text/Text.module.css @@ -0,0 +1,55 @@ +/* strong { + @apply font-medium; +} */ + +.text { +} + +.text--body { + @apply text-lg leading-relaxed; +} + +.text--overline { + @apply text-sm uppercase font-semibold leading-snug tracking-wide; +} + +.text--caption { + @apply text-sm text-gray-500 leading-tight; +} + +.text--caption2 { + @apply text-xs italic text-gray-500 leading-tight; +} + +.text--footnote { + @apply text-base font-normal; +} + +.text--headline { + /* @apply text-base font-normal; */ + @apply text-3xl leading-8 font-semibold tracking-tight text-gray-900 sm:text-4xl; +} + +.text--subheadline { + @apply text-xl text-gray-500 leading-relaxed; +} + +.text--largetitle { + @apply text-2xl font-normal; +} + +.text--subtitle { + @apply text-base font-normal; +} + +.text--title { + @apply text-base font-normal; +} + +.text--title2 { + @apply text-base font-normal; +} + +.text--title3 { + @apply text-xs font-semibold leading-tight; +} diff --git a/components/ui/Text/Text.tsx b/components/ui/Text/Text.tsx new file mode 100644 index 00000000..eb3d1897 --- /dev/null +++ b/components/ui/Text/Text.tsx @@ -0,0 +1,166 @@ +import React from "react"; +import Body from "./Body"; +import Caption from "./Caption"; +import Caption2 from "./Caption2"; +import Footnote from "./Footnote"; +import Headline from "./Headline"; +import Largetitle from "./Largetitle"; +import Overline from "./Overline"; +import Subheadline from "./Subheadline"; +import Subtitle from "./Subtitle"; +import Title from "./Title"; +import Title2 from "./Title2"; +import Title3 from "./Title3"; + +import classnames from "classnames"; + +type Props = { + variant?: + | "overline" + | "caption" + | "body" + | "caption2" + | "footnote" + | "headline" + | "largetitle" + | "subheadline" + | "subtitle" + | "title" + | "title2" + | "title3"; + children: any; + text?: string; + tx?: string; + className?: string; + color?: string; +}; + +export type TextProps = { + children: any; + text?: string; + tx?: string; + color?: string; + className?: string; +}; + +/** + * static let largeTitle: Font + * A font with the large title text style. + * + * static let title: Font + * A font with the title text style. + * + * static let title2: Font + * Create a font for second level hierarchical headings. + * + * static let title3: Font + * Create a font for third level hierarchical headings. + * + * static let headline: Font + * A font with the headline text style. + * + * static let subheadline: Font + * A font with the subheadline text style. + * + * static let body: Font + * A font with the body text style. + * + * static let callout: Font + * A font with the callout text style. + * + * static let caption: Font + * A font with the caption text style. + * + * static let caption2: Font + * Create a font with the alternate caption text style. + * + * static let footnote: Font + * A font with the footnote text style. + */ + +const Text: React.FunctionComponent = (props: Props) => { + const classes = classnames(props?.className, props?.color); + + switch (props?.variant) { + case "overline": + return ( + + {props.children} + + ); + case "body": + return ( + + {props.children} + + ); + case "caption": + return ( +
  • + ); + case "caption2": + return ( + + {props.children} + + ); + case "footnote": + return ( + + {props.children} + + ); + case "headline": + return ( + + {props.children} + + ); + case "largetitle": + return ( + + {props.children} + + ); + case "subheadline": + return ( + + {props.children} + + ); + case "subtitle": + return ( + + {props.children} + + ); + case "title": + return ( + + {props.children} + + ); + case "title2": + return ( + + {props.children} + + ); + case "title3": + return ( + + {props.children} + + ); + default: + return ( + + {props.children} + + ); + } +}; + +export default Text; diff --git a/components/ui/Text/Title/Title.tsx b/components/ui/Text/Title/Title.tsx new file mode 100644 index 00000000..7ebb409b --- /dev/null +++ b/components/ui/Text/Title/Title.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Title: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--title"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Title; diff --git a/components/ui/Text/Title/index.ts b/components/ui/Text/Title/index.ts new file mode 100644 index 00000000..6036b4be --- /dev/null +++ b/components/ui/Text/Title/index.ts @@ -0,0 +1,2 @@ +import Title from "./Title"; +export default Title; diff --git a/components/ui/Text/Title2/Title2.tsx b/components/ui/Text/Title2/Title2.tsx new file mode 100644 index 00000000..3534c02e --- /dev/null +++ b/components/ui/Text/Title2/Title2.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Title2: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--title2"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Title2; diff --git a/components/ui/Text/Title2/index.ts b/components/ui/Text/Title2/index.ts new file mode 100644 index 00000000..73c63d9e --- /dev/null +++ b/components/ui/Text/Title2/index.ts @@ -0,0 +1,2 @@ +import Title2 from "./Title2"; +export default Title2; diff --git a/components/ui/Text/Title3/Title3.tsx b/components/ui/Text/Title3/Title3.tsx new file mode 100644 index 00000000..6f60baed --- /dev/null +++ b/components/ui/Text/Title3/Title3.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import classnames from "classnames"; +import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; + +const Title3: React.FunctionComponent = (props: TextProps) => { + const classes = classnames(Styles["text--title3"], props?.className, props?.color); + + return

    {props.children}

    ; +}; + +export default Title3; diff --git a/components/ui/Text/Title3/index.ts b/components/ui/Text/Title3/index.ts new file mode 100644 index 00000000..0c673c02 --- /dev/null +++ b/components/ui/Text/Title3/index.ts @@ -0,0 +1,2 @@ +import Title3 from "./Title3"; +export default Title3; diff --git a/components/ui/Text/index.ts b/components/ui/Text/index.ts new file mode 100644 index 00000000..b426e062 --- /dev/null +++ b/components/ui/Text/index.ts @@ -0,0 +1,39 @@ +import Text from "./Text"; +export { Text }; +export default Text; + +import Title from "./Title"; +export { Title }; + +import Title2 from "./Title2"; +export { Title2 }; + +import Title3 from "./Title3"; +export { Title3 }; + +import Largetitle from "./Largetitle"; +export { Largetitle }; + +import Subtitle from "./Subtitle"; +export { Subtitle }; + +import Headline from "./Headline"; +export { Headline }; + +import Subheadline from "./Subheadline"; +export { Subheadline }; + +import Caption from "./Caption"; +export { Caption }; + +import Caption2 from "./Caption2"; +export { Caption2 }; + +import Footnote from "./Footnote"; +export { Footnote }; + +import Overline from "./Overline"; +export { Overline }; + +import Body from "./Body"; +export { Body }; diff --git a/package.json b/package.json index 99e3462e..2875cf53 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,21 @@ "@tailwindcss/forms": "^0.2.1", "async": "^3.2.0", "bcryptjs": "^2.4.3", + "classnames": "^2.3.1", "dayjs": "^1.10.4", + "dayjs-business-days": "^1.0.4", "googleapis": "^67.1.1", "handlebars": "^4.7.7", "ics": "^2.27.0", "lodash.debounce": "^4.0.8", "lodash.merge": "^4.6.2", + "lodash.throttle": "^4.1.1", "next": "^10.2.0", "next-auth": "^3.13.2", "next-transpile-modules": "^7.0.0", "nodemailer": "^6.6.1", "react": "17.0.1", + "react-dates": "^21.8.0", "react-dom": "17.0.1", "react-phone-number-input": "^3.1.21", "react-select": "^4.3.0", @@ -44,6 +48,7 @@ "@types/node": "^14.14.33", "@types/nodemailer": "^6.4.2", "@types/react": "^17.0.3", + "@types/react-dates": "^21.8.3", "@typescript-eslint/eslint-plugin": "^4.27.0", "@typescript-eslint/parser": "^4.27.0", "autoprefixer": "^10.2.5", diff --git a/pages/[user].tsx b/pages/[user].tsx index 71c311ce..58e28c30 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -60,7 +60,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { { username: context.query.user.toLowerCase(), }, - ["id", "username", "email", "name", "bio", "avatar", "eventTypes", "theme"] + ["id", "username", "email", "name", "bio", "avatar", "theme"] ); if (!user) { return { @@ -73,6 +73,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => { userId: user.id, hidden: false, }, + select: { + slug: true, + title: true, + description: true, + }, }); return { diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 8c9c97b1..04cdbd76 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -156,6 +156,11 @@ export default function Type(props): Type { a.startTime - b.startTime); + const eventTypeObject = Object.assign({}, eventType, { + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }); + return { props: { user, date, - eventType, + eventType: eventTypeObject, workingHours, }, }; diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index c03b86da..34430e30 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -381,7 +381,7 @@ export async function getServerSideProps(context) { { username: context.query.user, }, - ["username", "name", "email", "bio", "avatar", "eventTypes", "theme"] + ["username", "name", "email", "bio", "avatar", "theme"] ); const eventType = await prisma.eventType.findUnique({ @@ -396,9 +396,19 @@ export async function getServerSideProps(context) { length: true, locations: true, customInputs: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, }, }); + const eventTypeObject = Object.assign({}, eventType, { + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }); + let booking = null; if (context.query.rescheduleUid) { @@ -421,7 +431,7 @@ export async function getServerSideProps(context) { return { props: { user, - eventType, + eventType: eventTypeObject, booking, }, }; diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts index dc5709e6..10d3a2f6 100644 --- a/pages/api/availability/eventtype.ts +++ b/pages/api/availability/eventtype.ts @@ -49,6 +49,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, })), }, + periodType: req.body.periodType, + periodDays: req.body.periodDays, + periodStartDate: req.body.periodStartDate, + periodEndDate: req.body.periodEndDate, + periodCountCalendarDays: req.body.periodCountCalendarDays, }; if (req.method == "POST") { diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 89eb8ca8..6aa4bb5c 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -12,6 +12,14 @@ import merge from "lodash.merge"; import dayjs from "dayjs"; import logger from "../../../lib/logger"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import dayjsBusinessDays from "dayjs-business-days"; + +dayjs.extend(dayjsBusinessDays); +dayjs.extend(utc); +dayjs.extend(timezone); + const translator = short(); const log = logger.getChildLogger({ prefix: ["[api] book:user"] }); @@ -49,6 +57,32 @@ function isAvailable(busyTimes, time, length) { return t; } +function isOutOfBounds( + time: dayjs.ConfigType, + { periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone } +): boolean { + const date = dayjs(time); + + switch (periodType) { + case "rolling": { + const periodRollingEndDay = periodCountCalendarDays + ? dayjs().tz(timeZone).add(periodDays, "days").endOf("day") + : dayjs().tz(timeZone).businessDaysAdd(periodDays, "days").endOf("day"); + return date.endOf("day").isAfter(periodRollingEndDay); + } + + case "range": { + const periodRangeStartDay = dayjs(periodStartDate).tz(timeZone).endOf("day"); + const periodRangeEndDay = dayjs(periodEndDate).tz(timeZone).endOf("day"); + return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay); + } + + case "unlimited": + default: + return false; + } +} + interface GetLocationRequestFromIntegrationRequest { location: string; } @@ -166,6 +200,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) eventName: true, title: true, length: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, }, }); @@ -228,6 +267,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json(error); } + let timeOutOfBounds = false; + + try { + timeOutOfBounds = isOutOfBounds(req.body.start, { + periodType: selectedEventType.periodType, + periodDays: selectedEventType.periodDays, + periodEndDate: selectedEventType.periodEndDate, + periodStartDate: selectedEventType.periodStartDate, + periodCountCalendarDays: selectedEventType.periodCountCalendarDays, + timeZone: currentUser.timeZone, + }); + } catch { + log.debug({ + message: "Unable set timeOutOfBounds. Using false. ", + }); + } + + if (timeOutOfBounds) { + const error = { + errorCode: "BookingUserUnAvailable", + message: `${currentUser.name} is unavailable at this time.`, + }; + + log.debug(`Booking ${user} failed`, error); + return res.status(400).json(error); + } + let results = []; let referencesToCreate = []; diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index 6302ede0..87e57ac2 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -19,6 +19,13 @@ import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import { Availability, EventType, User } from "@prisma/client"; import { validJson } from "@lib/jsonUtils"; +import Text from "@components/ui/Text"; +import { RadioGroup } from "@headlessui/react"; +import classnames from "classnames"; +import throttle from "lodash.throttle"; +import "react-dates/initialize"; +import "react-dates/lib/css/_datepicker.css"; +import { DateRangePicker, OrientationShape, toMomentObject } from "react-dates"; dayjs.extend(utc); dayjs.extend(timezone); @@ -54,8 +61,28 @@ type EventTypeInput = { customInputs: EventTypeCustomInput[]; timeZone: string; availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] }; + periodType?: string; + periodDays?: number; + periodStartDate?: Date | string; + periodEndDate?: Date | string; + periodCountCalendarDays?: boolean; }; +const PERIOD_TYPES = [ + { + type: "rolling", + suffix: "into the future", + }, + { + type: "range", + prefix: "Within a date range", + }, + { + type: "unlimited", + prefix: "Indefinitely into the future", + }, +]; + export default function EventTypePage({ user, eventType, @@ -64,6 +91,7 @@ export default function EventTypePage({ }: Props): JSX.Element { const router = useRouter(); + console.log(eventType); const inputOptions: OptionBase[] = [ { value: EventTypeCustomInputType.Text, label: "Text" }, { value: EventTypeCustomInputType.TextLong, label: "Multiline Text" }, @@ -71,6 +99,39 @@ export default function EventTypePage({ { value: EventTypeCustomInputType.Bool, label: "Checkbox" }, ]; + const [DATE_PICKER_ORIENTATION, setDatePickerOrientation] = useState("horizontal"); + const [contentSize, setContentSize] = useState({ width: 0, height: 0 }); + + const handleResizeEvent = () => { + const elementWidth = parseFloat(getComputedStyle(document.body).width); + const elementHeight = parseFloat(getComputedStyle(document.body).height); + + setContentSize({ + width: elementWidth, + height: elementHeight, + }); + }; + + const throttledHandleResizeEvent = throttle(handleResizeEvent, 100); + + useEffect(() => { + handleResizeEvent(); + + window.addEventListener("resize", throttledHandleResizeEvent); + + return () => { + window.removeEventListener("resize", throttledHandleResizeEvent); + }; + }, []); + + useEffect(() => { + if (contentSize.width < 500) { + setDatePickerOrientation("vertical"); + } else { + setDatePickerOrientation("horizontal"); + } + }, [contentSize]); + const [enteredAvailability, setEnteredAvailability] = useState(); const [showLocationModal, setShowLocationModal] = useState(false); const [showAddCustomModal, setShowAddCustomModal] = useState(false); @@ -83,12 +144,37 @@ export default function EventTypePage({ eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); + const [periodStartDate, setPeriodStartDate] = useState(() => { + if (eventType.periodType === "range" && eventType?.periodStartDate) { + return toMomentObject(new Date(eventType.periodStartDate)); + } + + return null; + }); + + const [periodEndDate, setPeriodEndDate] = useState(() => { + if (eventType.periodType === "range" && eventType.periodEndDate) { + return toMomentObject(new Date(eventType?.periodEndDate)); + } + + return null; + }); + const [focusedInput, setFocusedInput] = useState(null); + const [periodType, setPeriodType] = useState(() => { + return ( + PERIOD_TYPES.find((s) => s.type === eventType.periodType) || + PERIOD_TYPES.find((s) => s.type === "unlimited") + ); + }); + const titleRef = useRef(); const slugRef = useRef(); const descriptionRef = useRef(); const lengthRef = useRef(); const isHiddenRef = useRef(); const eventNameRef = useRef(); + const periodDaysRef = useRef(); + const periodDaysTypeRef = useRef(); useEffect(() => { setSelectedTimeZone(eventType.timeZone || user.timeZone); @@ -103,6 +189,22 @@ export default function EventTypePage({ const enteredLength: number = parseInt(lengthRef.current.value); const enteredIsHidden: boolean = isHiddenRef.current.checked; const enteredEventName: string = eventNameRef.current.value; + + const type = periodType.type; + const enteredPeriodDays = parseInt(periodDaysRef?.current?.value); + const enteredPeriodDaysType = Boolean(parseInt(periodDaysTypeRef?.current.value)); + + const enteredPeriodStartDate = periodStartDate ? periodStartDate.toDate() : null; + const enteredPeriodEndDate = periodEndDate ? periodEndDate.toDate() : null; + + console.log("values", { + type, + periodDaysTypeRef, + enteredPeriodDays, + enteredPeriodDaysType, + enteredPeriodStartDate, + enteredPeriodEndDate, + }); // TODO: Add validation const payload: EventTypeInput = { @@ -116,6 +218,11 @@ export default function EventTypePage({ eventName: enteredEventName, customInputs, timeZone: selectedTimeZone, + periodType: type, + periodDays: enteredPeriodDays, + periodStartDate: enteredPeriodStartDate, + periodEndDate: enteredPeriodEndDate, + periodCountCalendarDays: enteredPeriodDaysType, }; if (enteredAvailability) { @@ -268,8 +375,8 @@ export default function EventTypePage({ -
    -
    +
    +
    @@ -330,7 +437,7 @@ export default function EventTypePage({
    )} {locations.length > 0 && ( -
      +
        {locations.map((location) => (
      • @@ -450,26 +557,6 @@ export default function EventTypePage({ defaultValue={eventType.description}>
    -
    - -
    - -
    - minutes -
    -
    -
    -
    -
    -

    How do you want to offer your availability for this event type?

    - -
    - - Cancel - - -
    -
    + +
    + When can people book this event? +
    +
    +
    + {/* */} + Invitees can schedule... +
    + + Date Range +
    + {PERIOD_TYPES.map((period) => ( + + classnames( + checked ? "bg-indigo-50 border-indigo-200 z-10" : "border-gray-200", + "relative py-4 px-2 lg:p-4 min-h-20 lg:flex items-center cursor-pointer focus:outline-none" + ) + }> + {({ active, checked }) => ( + <> + + ))} +
    +
    +
    +
    +
    +
    + +
    + +
    + minutes +
    +
    +
    +
    +
    +

    + How do you want to offer your availability for this event type? +

    + +
    + + Cancel + + +
    +
    +
    +
    - -

    Delete this event type

    @@ -777,6 +986,11 @@ export const getServerSideProps: GetServerSideProps = async ({ req, query availability: true, customInputs: true, timeZone: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, }, }); @@ -853,10 +1067,15 @@ export const getServerSideProps: GetServerSideProps = async ({ req, query availability.sort((a, b) => a.startTime - b.startTime); + const eventTypeObject = Object.assign({}, eventType, { + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }); + return { props: { user, - eventType, + eventType: eventTypeObject, locationOptions, availability, }, diff --git a/pages/index.tsx b/pages/index.tsx index b19fbef8..54ddf6e6 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,94 +1,84 @@ -import Head from 'next/head'; -import Link from 'next/link'; -import prisma from '../lib/prisma'; -import Shell from '../components/Shell'; -import {getSession, useSession} from 'next-auth/client'; -import {CheckIcon, ClockIcon, InformationCircleIcon} from '@heroicons/react/outline'; -import DonateBanner from '../components/DonateBanner'; +import Head from "next/head"; +import Link from "next/link"; +import prisma from "../lib/prisma"; +import Shell from "../components/Shell"; +import { getSession, useSession } from "next-auth/client"; +import { CheckIcon, ClockIcon, InformationCircleIcon } from "@heroicons/react/outline"; +import DonateBanner from "../components/DonateBanner"; function classNames(...classes) { - return classes.filter(Boolean).join(' ') + return classes.filter(Boolean).join(" "); } export default function Home(props) { - const [session, loading] = useSession(); - if (loading) { - return
    ; - } + const [session, loading] = useSession(); + if (loading) { + return
    ; + } - function convertMinsToHrsMins(mins) { - let h = Math.floor(mins / 60); - let m = mins % 60; - h = h < 10 ? '0' + h : h; - m = m < 10 ? '0' + m : m; - return `${h}:${m}`; - } + function convertMinsToHrsMins(mins) { + let h = Math.floor(mins / 60); + let m = mins % 60; + h = h < 10 ? "0" + h : h; + m = m < 10 ? "0" + m : m; + return `${h}:${m}`; + } - const stats = [ - { name: 'Event Types', stat: props.eventTypeCount }, - { name: 'Integrations', stat: props.integrationCount }, - { name: 'Available Hours', stat: Math.round(((props.user.endTime - props.user.startTime) / 60) * 100) / 100 + ' hours' }, + const stats = [ + { name: "Event Types", stat: props.eventTypeCount }, + { name: "Integrations", stat: props.integrationCount }, + { + name: "Available Hours", + stat: Math.round(((props.user.endTime - props.user.startTime) / 60) * 100) / 100 + " hours", + }, + ]; + + let timeline = []; + + if (session) { + timeline = [ + { + id: 1, + content: "Add your first", + target: "integration", + href: "/integrations", + icon: props.integrationCount != 0 ? CheckIcon : InformationCircleIcon, + iconBackground: props.integrationCount != 0 ? "bg-green-400" : "bg-gray-400", + }, + { + id: 2, + content: "Add one or more", + target: "event types", + href: "/availability", + icon: props.eventTypeCount != 0 ? CheckIcon : InformationCircleIcon, + iconBackground: props.eventTypeCount != 0 ? "bg-green-400" : "bg-gray-400", + }, + { + id: 3, + content: "Complete your", + target: "profile", + href: "/settings/profile", + icon: session.user.image ? CheckIcon : InformationCircleIcon, + iconBackground: session.user.image ? "bg-green-400" : "bg-gray-400", + }, ]; + } else { + timeline = []; + } - let timeline = []; + return ( +
    + + Calendso + + - if (session) { - timeline = [ - { - id: 1, - content: 'Add your first', - target: 'integration', - href: '/integrations', - icon: props.integrationCount != 0 ? CheckIcon : InformationCircleIcon, - iconBackground: props.integrationCount != 0 ? 'bg-green-400' : 'bg-gray-400', - }, - { - id: 2, - content: 'Add one or more', - target: 'event types', - href: '/availability', - icon: props.eventTypeCount != 0 ? CheckIcon : InformationCircleIcon, - iconBackground: props.eventTypeCount != 0 ? 'bg-green-400' : 'bg-gray-400', - }, - { - id: 3, - content: 'Complete your', - target: 'profile', - href: '/settings/profile', - icon: session.user.image ? CheckIcon : InformationCircleIcon, - iconBackground: session.user.image ? 'bg-green-400' : 'bg-gray-400', - }, - ]; - } else { - timeline = []; - } - - return ( -
    - - Calendso - - - - -
    -
    -
    -
    -

    Your stats

    -
    -
    - {stats.map((item) => ( -
    -
    {item.name}
    -
    -
    - {item.stat} -
    -
    -
    - ))} -
    + +
    +
    +
    +
    +

    Your stats

    @@ -119,235 +109,251 @@ export default function Home(props) {
    -
    - - - View - - -
    - - ))} - {props.eventTypes.length == 0 && ( -
    -

    You haven't created any event types.

    +
    + + Edit + + + + View + + +
    - )} - -
    -
    -
    -
    -

    Getting started

    -

    - Steps you should take to get started with Calendso. -

    + + ))} + {props.eventTypes.length == 0 && ( +
    +

    You haven't created any event types.

    -
    -
    -
      - {timeline.map((event, eventIdx) => ( -
    • -
      - {eventIdx !== timeline.length - 1 ? ( + )} +
    +
    +
    +
    +
    +

    Getting started

    +

    + Steps you should take to get started with Calendso. +

    +
    +
    +
    +
      + {timeline.map((event, eventIdx) => ( +
    • +
      + {eventIdx !== timeline.length - 1 ? ( +
    • - ))} -
    -
    +
    + + ))} +
    -
    -
    -
    -

    Your day

    -
    - - Configure - -
    -
    -
    -

    - Offering time slots between{" "} - {convertMinsToHrsMins(props.user.startTime)} and{" "} - {convertMinsToHrsMins(props.user.endTime)} -

    -
    -
    -
    -
    -

    - Your integrations -

    -
    - - View more - -
    -
    -
      - {props.credentials.map((integration) => ( -
    • - {integration.type == "google_calendar" && ( - Google Calendar - )} - {integration.type == "office365_calendar" && ( - Office 365 / Outlook.com Calendar - )} - {integration.type == "zoom_video" && ( - Zoom - )} -
      - {integration.type == "office365_calendar" && ( -

      - Office 365 / Outlook.com Calendar -

      - )} - {integration.type == "google_calendar" && ( -

      Google Calendar

      - )} - {integration.type == "zoom_video" && ( -

      Zoom

      - )} - {integration.type.endsWith("_calendar") && ( -

      Calendar Integration

      - )} - {integration.type.endsWith("_video") && ( -

      Video Conferencing

      - )} -
      -
    • - ))} - {props.credentials.length == 0 && ( -
      -

      You haven't added any integrations.

      -
      - )} -
    -
    -
    -
    -

    - Your event types -

    -
    - - View more - -
    -
    - -
    -
    +
    +
    +
    +

    Your day

    +
    + + Configure + +
    +
    +
    +

    + Offering time slots between{" "} + {convertMinsToHrsMins(props.user.startTime)} and{" "} + {convertMinsToHrsMins(props.user.endTime)} +

    +
    +
    +
    +
    +

    + Your integrations +

    +
    + + View more + +
    +
    +
      + {props.credentials.map((integration) => ( +
    • + {integration.type == "google_calendar" && ( + Google Calendar + )} + {integration.type == "office365_calendar" && ( + Office 365 / Outlook.com Calendar + )} + {integration.type == "zoom_video" && ( + Zoom + )} +
      + {integration.type == "office365_calendar" && ( +

      Office 365 / Outlook.com Calendar

      + )} + {integration.type == "google_calendar" && ( +

      Google Calendar

      + )} + {integration.type == "zoom_video" && ( +

      Zoom

      + )} + {integration.type.endsWith("_calendar") && ( +

      Calendar Integration

      + )} + {integration.type.endsWith("_video") && ( +

      Video Conferencing

      + )} +
      +
    • + ))} + {props.credentials.length == 0 && ( +
      +

      You haven't added any integrations.

      +
      + )} +
    +
    +
    +
    +

    + Your event types +

    +
    + + View more + +
    +
    + +
    +
    +
    - -
    -
    - ); + + +
    + ); } export async function getServerSideProps(context) { - const session = await getSession(context); + const session = await getSession(context); - let user = []; - let credentials = []; - let eventTypes = []; + let user = []; + let credentials = []; + let eventTypes = []; - if (session) { - user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - startTime: true, - endTime: true - } - }); + if (session) { + user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + startTime: true, + endTime: true, + }, + }); - credentials = await prisma.credential.findMany({ - where: { - userId: session.user.id, - }, - select: { - type: true - } - }); + credentials = await prisma.credential.findMany({ + where: { + userId: session.user.id, + }, + select: { + type: true, + }, + }); - eventTypes = await prisma.eventType.findMany({ - where: { - userId: session.user.id, - } - }); - } - return { - props: { user, credentials, eventTypes, eventTypeCount: eventTypes.length, integrationCount: credentials.length }, // will be passed to the page component as props - } -} \ No newline at end of file + eventTypes = ( + await prisma.eventType.findMany({ + where: { + userId: session.user.id, + }, + }) + ).map((eventType) => { + return { + ...eventType, + periodStartDate: eventType.periodStartDate?.toString() ?? null, + periodEndDate: eventType.periodEndDate?.toString() ?? null, + }; + }); + } + return { + props: { + user, + credentials, + eventTypes, + eventTypeCount: eventTypes.length, + integrationCount: credentials.length, + }, // will be passed to the page component as props + }; +} diff --git a/pages/success.tsx b/pages/success.tsx index 9b08377e..d36ea8d9 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -222,7 +222,7 @@ export async function getServerSideProps(context) { { username: context.query.user, }, - ["username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme"] + ["username", "name", "bio", "avatar", "hideBranding", "theme"] ) : null; diff --git a/prisma/migrations/20210714151216_event_type_period_settings/migration.sql b/prisma/migrations/20210714151216_event_type_period_settings/migration.sql new file mode 100644 index 00000000..0493981b --- /dev/null +++ b/prisma/migrations/20210714151216_event_type_period_settings/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "periodCountCalendarDays" BOOLEAN, +ADD COLUMN "periodDays" INTEGER, +ADD COLUMN "periodEndDate" TIMESTAMP(3), +ADD COLUMN "periodStartDate" TIMESTAMP(3), +ADD COLUMN "periodType" TEXT DEFAULT E'unlimited'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f0838096..7ea3cafe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,11 @@ model EventType { eventName String? customInputs EventTypeCustomInput[] timeZone String? + periodType String? @default("unlimited") // unlimited | rolling | range + periodStartDate DateTime? + periodEndDate DateTime? + periodDays Int? + periodCountCalendarDays Boolean? } model Credential { diff --git a/tailwind.config.js b/tailwind.config.js index c14305a9..a6063b5d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -28,9 +28,32 @@ module.exports = { 900: "#01579b", }, }, - maxHeight: { + maxHeight: (theme) => ({ + 0: "0", 97: "25rem", - }, + ...theme("spacing"), + full: "100%", + screen: "100vh", + }), + minHeight: (theme) => ({ + 0: "0", + ...theme("spacing"), + full: "100%", + screen: "100vh", + }), + minWidth: (theme) => ({ + 0: "0", + ...theme("spacing"), + full: "100%", + screen: "100vw", + }), + maxWidth: (theme, { breakpoints }) => ({ + 0: "0", + ...theme("spacing"), + ...breakpoints(theme("screens")), + full: "100%", + screen: "100vw", + }), }, }, variants: { diff --git a/yarn.lock b/yarn.lock index 478c781d..0fea0ba0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -893,6 +893,31 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/react-dates@^21.8.3": + version "21.8.3" + resolved "https://registry.yarnpkg.com/@types/react-dates/-/react-dates-21.8.3.tgz#dc4e71f83d09979b1c4f355c267e52a850d0fe2c" + integrity sha512-MSG/A5UCXepPw5a9BtdOXfCCSMcQ5+oQIkm0K2u39sf4EJbsgngUg1zcoY3amxa6Hz0EWZkZOiExK/92J6hxUw== + dependencies: + "@types/react" "*" + "@types/react-outside-click-handler" "*" + moment "^2.26.0" + +"@types/react-outside-click-handler@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#ccf0014032fc6ec286210f8a05d26a5c1f94cc96" + integrity sha512-BxQpd5GsbA9rjqLcM4lYp70VnvahgjMUeJ4OKi0A7QOsDLD2yUPswOVixtDpmvCu0PkRTfvMoStIR3gKC/L3XQ== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "17.0.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f" + integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@^17.0.3": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" @@ -1069,6 +1094,21 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +airbnb-prop-types@^2.10.0, airbnb-prop-types@^2.14.0, airbnb-prop-types@^2.15.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" + integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== + dependencies: + array.prototype.find "^2.1.1" + function.prototype.name "^1.1.2" + is-regex "^1.1.0" + object-is "^1.1.2" + object.assign "^4.1.0" + object.entries "^1.1.2" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.13.1" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1196,6 +1236,23 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.find@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" + integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.4" + +array.prototype.flat@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" + integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + array.prototype.flatmap@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" @@ -1397,6 +1454,11 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brcast@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef" + integrity sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg== + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -1663,7 +1725,7 @@ classnames@2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -classnames@^2.2.5: +classnames@^2.2.5, classnames@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== @@ -1796,6 +1858,11 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +"consolidated-events@^1.1.1 || ^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" + integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ== + constants-browserify@1.0.0, constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -1958,6 +2025,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dayjs-business-days@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dayjs-business-days/-/dayjs-business-days-1.0.4.tgz#36e93e7566149e175c1541d92ce16e12145412bf" + integrity sha512-kmb8Hvlhmv7INc2YXWew4SaEBJprqJ9C48CdlO1NTsqQa31ZEcO48ziFa3YFr5W9rdwB1nUmd5iIkLwgCkJ8nA== + dayjs@^1.10.4: version "1.10.5" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986" @@ -1992,12 +2064,17 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" + integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -define-properties@^1.1.3: +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -2072,6 +2149,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +direction@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/direction/-/direction-1.0.4.tgz#2b86fb686967e987088caf8b89059370d4837442" + integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ== + dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" @@ -2091,6 +2173,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +document.contains@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0" + integrity sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q== + dependencies: + define-properties "^1.1.3" + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -2188,6 +2277,14 @@ enquirer@^2.3.5, enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" +enzyme-shallow-equal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" + integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== + dependencies: + has "^1.0.3" + object-is "^1.1.2" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2195,7 +2292,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: +es-abstract@^1.17.4, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: version "1.18.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== @@ -2620,11 +2717,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83" + integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" + integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== + futoin-hkdf@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.3.3.tgz#6ee1c9c105dfa0995ba4f80633cf1c0c32defcb2" @@ -2721,6 +2833,14 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +global-cache@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/global-cache/-/global-cache-1.2.1.tgz#39ca020d3dd7b3f0934c52b75363f8d53312c16d" + integrity sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA== + dependencies: + define-properties "^1.1.2" + is-symbol "^1.0.1" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2883,7 +3003,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -3186,7 +3306,7 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.3: +is-regex@^1.1.0, is-regex@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== @@ -3209,13 +3329,18 @@ is-string@^1.0.5, is-string@^1.0.6: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== -is-symbol@^1.0.2, is-symbol@^1.0.3: +is-symbol@^1.0.1, is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: has-symbols "^1.0.2" +is-touch-device@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-touch-device/-/is-touch-device-1.0.1.tgz#9a2fd59f689e9a9bf6ae9a86924c4ba805a42eab" + integrity sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw== + is-typed-array@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e" @@ -4045,6 +4170,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" @@ -4060,7 +4190,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.1.1, lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4210,6 +4340,11 @@ modern-normalize@^1.1.0: resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7" integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA== +moment@>=1.6.0, moment@^2.26.0: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4458,7 +4593,7 @@ object-inspect@^1.10.3, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -4471,7 +4606,7 @@ object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.2: +object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -4481,7 +4616,7 @@ object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.4: +object.entries@^1.1.2, object.entries@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd" integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== @@ -4500,7 +4635,7 @@ object.fromentries@^2.0.4: es-abstract "^1.18.0-next.2" has "^1.0.3" -object.values@^1.1.4: +object.values@^1.0.4, object.values@^1.1.0, object.values@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== @@ -4691,6 +4826,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -4888,6 +5028,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -4976,6 +5125,13 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5001,6 +5157,27 @@ raw-body@2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +react-dates@^21.8.0: + version "21.8.0" + resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.8.0.tgz#355c3c7a243a7c29568fe00aca96231e171a5e94" + integrity sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw== + dependencies: + airbnb-prop-types "^2.15.0" + consolidated-events "^1.1.1 || ^2.0.0" + enzyme-shallow-equal "^1.0.0" + is-touch-device "^1.0.1" + lodash "^4.1.1" + object.assign "^4.1.0" + object.values "^1.1.0" + prop-types "^15.7.2" + raf "^3.4.1" + react-moment-proptypes "^1.6.0" + react-outside-click-handler "^1.2.4" + react-portal "^4.2.0" + react-with-direction "^1.3.1" + react-with-styles "^4.1.0" + react-with-styles-interface-css "^6.0.0" + react-dom@17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" @@ -5017,7 +5194,7 @@ react-input-autosize@^3.0.0: dependencies: prop-types "^15.5.8" -react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1: +react-is@16.13.1, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -5027,6 +5204,24 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-moment-proptypes@^1.6.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz#7ba4076147f6b5998f0d4f51d302d6d8c62049fd" + integrity sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ== + dependencies: + moment ">=1.6.0" + +react-outside-click-handler@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115" + integrity sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ== + dependencies: + airbnb-prop-types "^2.15.0" + consolidated-events "^1.1.1 || ^2.0.0" + document.contains "^1.0.1" + object.values "^1.1.0" + prop-types "^15.7.2" + react-phone-number-input@^3.1.21: version "3.1.23" resolved "https://registry.yarnpkg.com/react-phone-number-input/-/react-phone-number-input-3.1.23.tgz#8e8d2b7fb98d94514721d05ca5c58bd31f8d06e1" @@ -5038,6 +5233,13 @@ react-phone-number-input@^3.1.21: libphonenumber-js "^1.9.19" prop-types "^15.7.2" +react-portal@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6" + integrity sha512-fE9kOBagwmTXZ3YGRYb4gcMy+kSA+yLO0xnPankjRlfBv4uCpFXqKPfkpsGQQR15wkZ9EssnvTOl1yMzbkxhPQ== + dependencies: + prop-types "^15.5.8" + react-refresh@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -5075,6 +5277,39 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-with-direction@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-with-direction/-/react-with-direction-1.3.1.tgz#9fd414564f0ffe6947e5ff176f6132dd83f8b8df" + integrity sha512-aGcM21ZzhqeXFvDCfPj0rVNYuaVXfTz5D3Rbn0QMz/unZe+CCiLHthrjQWO7s6qdfXORgYFtmS7OVsRgSk5LXQ== + dependencies: + airbnb-prop-types "^2.10.0" + brcast "^2.0.2" + deepmerge "^1.5.2" + direction "^1.0.2" + hoist-non-react-statics "^3.3.0" + object.assign "^4.1.0" + object.values "^1.0.4" + prop-types "^15.6.2" + +react-with-styles-interface-css@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-with-styles-interface-css/-/react-with-styles-interface-css-6.0.0.tgz#b53da7fa8359d452cb934cface8738acaef7b5fe" + integrity sha512-6khSG1Trf4L/uXOge/ZAlBnq2O2PEXlQEqAhCRbvzaQU4sksIkdwpCPEl6d+DtP3+IdhyffTWuHDO9lhe1iYvA== + dependencies: + array.prototype.flat "^1.2.1" + global-cache "^1.2.1" + +react-with-styles@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-4.2.0.tgz#0b8a8e5d94d082518b9f564f6fcf6103e28096c5" + integrity sha512-tZCTY27KriRNhwHIbg1NkSdTTOSfXDg6Z7s+Q37mtz0Ym7Sc7IOr3PzVt4qJhJMW6Nkvfi3g34FuhtiGAJCBQA== + dependencies: + airbnb-prop-types "^2.14.0" + hoist-non-react-statics "^3.2.1" + object.assign "^4.1.0" + prop-types "^15.7.2" + react-with-direction "^1.3.1" + react@17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" @@ -5132,6 +5367,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= + regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" From f7a9c077b0c64a3ac2afe6e2b01355e995276a23 Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Thu, 15 Jul 2021 09:19:17 -0500 Subject: [PATCH 12/13] fix: styles missing on headline component (#366) --- components/ui/Text/Headline/Headline.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ui/Text/Headline/Headline.tsx b/components/ui/Text/Headline/Headline.tsx index cec7956b..a7b531a9 100644 --- a/components/ui/Text/Headline/Headline.tsx +++ b/components/ui/Text/Headline/Headline.tsx @@ -1,9 +1,10 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; +import Styles from "../Text.module.css"; const Headline: React.FunctionComponent = (props: TextProps) => { - const classes = classnames("text--headline", props?.className, props?.color); + const classes = classnames(Styles["text--headline"], props?.className, props?.color); return

    {props.children}

    ; }; From b27ba5123c081d37eedd6ddf35314fcc0c9a97ab Mon Sep 17 00:00:00 2001 From: Femi Odugbesan Date: Thu, 15 Jul 2021 09:48:02 -0500 Subject: [PATCH 13/13] fix: stats, layout defect from merge (#367) --- pages/index.tsx | 91 +++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/pages/index.tsx b/pages/index.tsx index 54ddf6e6..765842f7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -80,52 +80,62 @@ export default function Home(props) {

    Your stats

    -
    -
    -

    - Your event types -

    -
    -
      - {props.eventTypes.map((type) => ( -
    • -
      -
      -
      -
      -

      {type.title}

      -

      - in {type.description} -

      -
      -
      -
      -
      +
      + {stats.map((item) => ( +
      +
      {item.name}
      +
      +
      + {item.stat} +
      +
      +
      + ))} +
      +
      +
      +
      +

      + Your event types +

      +
      +
        + {props.eventTypes.map((type) => ( +
      • +
        +
        +
        +
        +

        {type.title}

        +

        in {type.description}

        +
        +
        +
        +
        -
        - - Edit - - - - View - - -
        +
        +
        + + Edit + + + + View + +
      • ))} {props.eventTypes.length == 0 && (
        -

        You haven't created any event types.

        +

        You haven't created any event types.

        )}
      @@ -213,7 +223,7 @@ export default function Home(props) {
        {props.credentials.map((integration) => ( -
      • +
      • {integration.type == "google_calendar" && ( -

        You haven't added any integrations.

        +

        You haven't added any integrations.

      )}
    @@ -291,14 +301,13 @@ export default function Home(props) { ))} {props.eventTypes.length == 0 && (
    -

    You haven't created any event types.

    +

    You haven't created any event types.

    )}
    -
    + {eventType.title} {eventType.hidden && @@ -151,13 +151,13 @@ export default function Availability(props) { } + {eventType.description} + {eventType.length} minutes + View Edit
    + {props.children} +