From a608f94590d71a389f5d95ffe8aeba724cba7cea Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Sat, 31 Jul 2021 00:05:38 +0100 Subject: [PATCH 01/28] Initial restyling --- components/Settings.tsx | 146 ++- components/Shell.tsx | 557 ++++++----- components/team/TeamList.tsx | 4 +- components/team/TeamListItem.tsx | 18 +- components/ui/Scheduler.tsx | 4 +- components/ui/UsernameInput.tsx | 4 +- pages/auth/error.tsx | 4 +- pages/auth/login.tsx | 42 +- pages/availability/index.tsx | 734 +++++++------- pages/availability/troubleshoot.tsx | 19 +- pages/bookings/index.tsx | 47 +- pages/event-types/[type].tsx | 1201 +++++++++++++++++++++++ pages/event-types/index.tsx | 662 +++++++++++++ pages/integrations/[integration].tsx | 233 ++--- pages/integrations/index.tsx | 808 ++++++++------- pages/settings/billing.tsx | 9 +- pages/settings/embed.tsx | 203 ++-- pages/settings/password.tsx | 205 ++-- pages/settings/profile.tsx | 44 +- pages/settings/teams.tsx | 23 +- public/calendso-logo-word.svg | 50 +- public/integrations/google-calendar.png | Bin 26889 -> 0 bytes styles/globals.css | 44 +- tailwind.config.js | 90 +- 24 files changed, 3566 insertions(+), 1585 deletions(-) create mode 100644 pages/event-types/[type].tsx create mode 100644 pages/event-types/index.tsx delete mode 100644 public/integrations/google-calendar.png diff --git a/components/Settings.tsx b/components/Settings.tsx index 1969c1ff..eed044b8 100644 --- a/components/Settings.tsx +++ b/components/Settings.tsx @@ -1,87 +1,71 @@ -import ActiveLink from '../components/ActiveLink'; -import {CodeIcon, CreditCardIcon, KeyIcon, UserCircleIcon, UserGroupIcon} from '@heroicons/react/outline'; +import Link from 'next/link'; +import { CreditCardIcon, UserIcon, CodeIcon, KeyIcon, UserGroupIcon } from "@heroicons/react/solid"; +import { useRouter } from "next/router"; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} export default function SettingsShell(props) { - return ( -
-
-
-
-
- - - {props.children} -
-
-
-
+ return ( +
+
+
+ +
- ); +
+
+ +
+
+
+
{props.children}
+
+ ); } diff --git a/components/Shell.tsx b/components/Shell.tsx index beafc8ed..f9b951e6 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -1,31 +1,68 @@ import Link from "next/link"; -import { useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { signOut, useSession } from "next-auth/client"; -import { MenuIcon, XIcon } from "@heroicons/react/outline"; +import { Dialog, Menu, Transition } from "@headlessui/react"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../lib/telemetry"; +import { MenuIcon, SelectorIcon, XIcon } from "@heroicons/react/outline"; +import { + CalendarIcon, + ClockIcon, + CogIcon, + PuzzleIcon, + SupportIcon, + ChatAltIcon, + LogoutIcon, + ExternalLinkIcon, + LinkIcon, +} from "@heroicons/react/solid"; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + export default function Shell(props) { const router = useRouter(); const [session, loading] = useSession(); - const [profileDropdownExpanded, setProfileDropdownExpanded] = useState(false); - const [mobileMenuExpanded, setMobileMenuExpanded] = useState(false); const telemetry = useTelemetry(); + const navigation = [ + { + name: "Event Types", + href: "/event-types", + icon: LinkIcon, + current: router.pathname.startsWith("/event-types"), + }, + { + name: "Bookings", + href: "/bookings", + icon: ClockIcon, + current: router.pathname.startsWith("/bookings"), + }, + { + name: "Availability", + href: "/availability", + icon: CalendarIcon, + current: router.pathname.startsWith("/availability"), + }, + { + name: "Integrations", + href: "/integrations", + icon: PuzzleIcon, + current: router.pathname.startsWith("/integrations"), + }, + { name: "Settings", href: "/settings", icon: CogIcon, current: router.pathname.startsWith("/settings") }, + ]; + + const [sidebarOpen, setSidebarOpen] = useState(false); + useEffect(() => { telemetry.withJitsu((jitsu) => { return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname)); }); }, [telemetry]); - const toggleProfileDropdown = () => { - setProfileDropdownExpanded(!profileDropdownExpanded); - }; - - const toggleMobileMenu = () => { - setMobileMenuExpanded(!mobileMenuExpanded); - }; - const logoutHandler = () => { signOut({ redirect: false }).then(() => router.push("/auth/logout")); }; @@ -35,243 +72,283 @@ export default function Shell(props) { } return session ? ( -
-
- +
+
+ +
+
+ +
+
+

+ Tom Cook +

+

+ View profile +

+
+
+
- )} - -
-
-

{props.heading}

-
-
-
+ +
{/* Force sidebar to shrink to fit close icon */}
+ + -
-
{props.children}
-
+ {/* Static sidebar for desktop */} +
+
+ {/* Sidebar component, swap this element with another sidebar if you like */} +
+
+
+ Calendso +
+ +
+
+ {/* User account dropdown */} + + {({ open }) => ( + <> +
+ + + + + + + {session.user.name} + + {session.user.username} + + + + +
+ + + +
+ + {({ active }) => ( + + + )} + + + {({ active }) => ( + + + )} + +
+
+ + {({ active }) => ( + + + )} + +
+
+
+ + )} +
+
+
+
+
+
+
+ +
+
+
+
+

{props.heading}

+
+
{props.children}
+
+
+
) : null; } diff --git a/components/team/TeamList.tsx b/components/team/TeamList.tsx index 7f04d322..2015bd79 100644 --- a/components/team/TeamList.tsx +++ b/components/team/TeamList.tsx @@ -22,7 +22,7 @@ export default function TeamList(props) { }; return (
-
); -} \ No newline at end of file +} diff --git a/components/team/TeamListItem.tsx b/components/team/TeamListItem.tsx index a7413d72..5aaf2bc8 100644 --- a/components/team/TeamListItem.tsx +++ b/components/team/TeamListItem.tsx @@ -26,25 +26,25 @@ export default function TeamListItem(props) {
- {props.team.name} + {props.team.name} {props.team.role.toLowerCase()}
{props.team.role === 'INVITEE' &&
- +
} {props.team.role === 'MEMBER' &&
- +
} {props.team.role === 'OWNER' &&
- -
} - - + + }*/} ); -} \ No newline at end of file +} diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx index fcb4688c..f60cb551 100644 --- a/components/ui/Scheduler.tsx +++ b/components/ui/Scheduler.tsx @@ -95,9 +95,9 @@ export const Scheduler = ({ return (
-
+
-
+
diff --git a/components/ui/UsernameInput.tsx b/components/ui/UsernameInput.tsx index bc611937..8bef983b 100644 --- a/components/ui/UsernameInput.tsx +++ b/components/ui/UsernameInput.tsx @@ -7,7 +7,7 @@ const UsernameInput = React.forwardRef((props, ref) => ( Username
- + {typeof window !== "undefined" && window.location.hostname}/ ( autoComplete="username" required {...props} - className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300 lowercase" + className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-sm sm:text-sm border-gray-300 lowercase" />
diff --git a/pages/auth/error.tsx b/pages/auth/error.tsx index edfef292..0c930650 100644 --- a/pages/auth/error.tsx +++ b/pages/auth/error.tsx @@ -33,7 +33,7 @@ export default function Error() {
- + Go back to the login page @@ -42,4 +42,4 @@ export default function Error() {
); -} \ No newline at end of file +} diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx index 76514aa4..b7a79aff 100644 --- a/pages/auth/login.tsx +++ b/pages/auth/login.tsx @@ -4,21 +4,22 @@ import { getCsrfToken } from "next-auth/client"; export default function Login({ csrfToken }) { return ( -
+
Login
-

Sign in to your account

+ Calendso Logo +

Sign in to your account

-
+
-
- +
+
+ +
+
+ + Forgot? + +
+
@@ -54,19 +62,15 @@ export default function Login({ csrfToken }) {
- - -
+
+ Don't have an account? Create an account +
); diff --git a/pages/availability/index.tsx b/pages/availability/index.tsx index 7a7824db..4070b161 100644 --- a/pages/availability/index.tsx +++ b/pages/availability/index.tsx @@ -1,424 +1,348 @@ -import Head from 'next/head'; -import Link from 'next/link'; -import prisma from '../../lib/prisma'; -import Modal from '../../components/Modal'; -import Shell from '../../components/Shell'; -import {useRouter} from 'next/router'; -import {useRef, useState} from 'react'; -import {getSession, useSession} from 'next-auth/client'; -import {ClockIcon, PlusIcon} from '@heroicons/react/outline'; +import Head from "next/head"; +import Link from "next/link"; +import prisma from "../../lib/prisma"; +import Modal from "../../components/Modal"; +import Shell from "../../components/Shell"; +import { useRouter } from "next/router"; +import { useRef, useState } from "react"; +import { getSession, useSession } from "next-auth/client"; +import { ClockIcon, PlusIcon } from "@heroicons/react/outline"; export default function Availability(props) { - const [ session, loading ] = useSession(); - const router = useRouter(); - const [showAddModal, setShowAddModal] = useState(false); - const [successModalOpen, setSuccessModalOpen] = useState(false); - const [showChangeTimesModal, setShowChangeTimesModal] = useState(false); - const titleRef = useRef(); - const slugRef = useRef(); - const descriptionRef = useRef(); - const lengthRef = useRef(); - const isHiddenRef = useRef(); + const [session, loading] = useSession(); + const router = useRouter(); + const [showAddModal, setShowAddModal] = useState(false); + const [successModalOpen, setSuccessModalOpen] = useState(false); + const [showChangeTimesModal, setShowChangeTimesModal] = useState(false); + const titleRef = useRef(); + const slugRef = useRef(); + const descriptionRef = useRef(); + const lengthRef = useRef(); + const isHiddenRef = useRef(); - const startHoursRef = useRef(); - const startMinsRef = useRef(); - const endHoursRef = useRef(); - const endMinsRef = useRef(); - const bufferHoursRef = useRef(); - const bufferMinsRef = useRef(); + const startHoursRef = useRef(); + const startMinsRef = useRef(); + const endHoursRef = useRef(); + const endMinsRef = useRef(); + const bufferHoursRef = useRef(); + const bufferMinsRef = useRef(); - if (loading) { - return
; + if (loading) { + return
; + } + + function toggleAddModal() { + setShowAddModal(!showAddModal); + } + + function toggleChangeTimesModal() { + setShowChangeTimesModal(!showChangeTimesModal); + } + + const closeSuccessModal = () => { + setSuccessModalOpen(false); + router.replace(router.asPath); + }; + + 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}`; + } + + async function createEventTypeHandler(event) { + event.preventDefault(); + + const enteredTitle = titleRef.current.value; + const enteredSlug = slugRef.current.value; + const enteredDescription = descriptionRef.current.value; + const enteredLength = lengthRef.current.value; + const enteredIsHidden = isHiddenRef.current.checked; + + // TODO: Add validation + + const response = await fetch("/api/availability/eventtype", { + method: "POST", + body: JSON.stringify({ + title: enteredTitle, + slug: enteredSlug, + description: enteredDescription, + length: enteredLength, + hidden: enteredIsHidden, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (enteredTitle && enteredLength) { + router.replace(router.asPath); + toggleAddModal(); } + } - function toggleAddModal() { - setShowAddModal(!showAddModal); - } + async function updateStartEndTimesHandler(event) { + event.preventDefault(); - function toggleChangeTimesModal() { - setShowChangeTimesModal(!showChangeTimesModal); - } + const enteredStartHours = parseInt(startHoursRef.current.value); + const enteredStartMins = parseInt(startMinsRef.current.value); + const enteredEndHours = parseInt(endHoursRef.current.value); + const enteredEndMins = parseInt(endMinsRef.current.value); + const enteredBufferHours = parseInt(bufferHoursRef.current.value); + const enteredBufferMins = parseInt(bufferMinsRef.current.value); - const closeSuccessModal = () => { setSuccessModalOpen(false); router.replace(router.asPath); } + const startMins = enteredStartHours * 60 + enteredStartMins; + const endMins = enteredEndHours * 60 + enteredEndMins; + const bufferMins = enteredBufferHours * 60 + enteredBufferMins; - 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}`; - } + // TODO: Add validation - async function createEventTypeHandler(event) { - event.preventDefault(); + const response = await fetch("/api/availability/day", { + method: "PATCH", + body: JSON.stringify({ start: startMins, end: endMins, buffer: bufferMins }), + headers: { + "Content-Type": "application/json", + }, + }); - const enteredTitle = titleRef.current.value; - const enteredSlug = slugRef.current.value; - const enteredDescription = descriptionRef.current.value; - const enteredLength = lengthRef.current.value; - const enteredIsHidden = isHiddenRef.current.checked; + setShowChangeTimesModal(false); + setSuccessModalOpen(true); + } - // TODO: Add validation - - const response = await fetch('/api/availability/eventtype', { - method: 'POST', - body: JSON.stringify({title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden}), - headers: { - 'Content-Type': 'application/json' - } - }); - - if (enteredTitle && enteredLength) { - router.replace(router.asPath); - toggleAddModal(); - } - } - - async function updateStartEndTimesHandler(event) { - event.preventDefault(); - - const enteredStartHours = parseInt(startHoursRef.current.value); - const enteredStartMins = parseInt(startMinsRef.current.value); - const enteredEndHours = parseInt(endHoursRef.current.value); - const enteredEndMins = parseInt(endMinsRef.current.value); - const enteredBufferHours = parseInt(bufferHoursRef.current.value); - const enteredBufferMins = parseInt(bufferMinsRef.current.value); - - const startMins = enteredStartHours * 60 + enteredStartMins; - const endMins = enteredEndHours * 60 + enteredEndMins; - const bufferMins = enteredBufferHours * 60 + enteredBufferMins; - - // TODO: Add validation - - const response = await fetch('/api/availability/day', { - method: 'PATCH', - body: JSON.stringify({start: startMins, end: endMins, buffer: bufferMins}), - headers: { - 'Content-Type': 'application/json' - } - }); - - setShowChangeTimesModal(false); - setSuccessModalOpen(true); - } - - return( -
- - Availability | Calendso - - - -
-

- Event Types -

-
- -
-
-
-
-
-
- - - - - - - - - - - {props.types.map((eventType) => - - - - - - - )} - -
- Name - - Description - - Length - - Edit -
- {eventType.title} - {eventType.hidden && - - Hidden - - } - - {eventType.description} - - {eventType.length} minutes - - View - Edit -
-
-
-
-
- -
-
-
-

- Change the start and end times of your day -

-
-

- Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end at {convertMinsToHrsMins(props.user.endTime)}. -

-
-
- -
-
-
- -
-
-

- Something doesn't look right? -

-
-

- Troubleshoot your availability to explore why your times are showing as they are. -

-
- -
-
-
- {showAddModal && -
-
- - - - -
-
-
- -
-
- -
-

- Create a new event type for people to book times with. -

-
-
-
-
-
-
- -
- -
-
-
- -
-
- - {location.hostname}/{props.user.username}/ - - -
-
-
-
- -
- -
-
-
- -
- -
- minutes -
-
-
-
-
-
-
- -
-
- -

Hide the event type from your page, so it can only be booked through it's URL.

-
-
-
- {/* TODO: Add an error message when required input fields empty*/} -
- - -
-
-
-
-
- } - {showChangeTimesModal && -
-
- - - - -
-
-
- -
-
- -
-

- Set the start and end time of your day and a minimum buffer between your meetings. -

-
-
-
-
-
- -
- - -
- : -
- - -
-
-
- -
- - -
- : -
- - -
-
-
- -
- - -
- : -
- - -
-
-
- - -
-
-
-
-
- } - -
+ return ( +
+ + Availability | Calendso + + + +
+

Configure times when you are available for bookings.

- ); + +
+
+
+

+ Change the start and end times of your day +

+
+

+ Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end + at {convertMinsToHrsMins(props.user.endTime)}. +

+
+
+ +
+
+
+ +
+
+

+ Something doesn't look right? +

+
+

Troubleshoot your availability to explore why your times are showing as they are.

+
+ +
+
+
+ {showChangeTimesModal && ( +
+
+ + + + +
+
+
+ +
+
+ +
+

+ Set the start and end time of your day and a minimum buffer between your meetings. +

+
+
+
+
+
+ +
+ + +
+ : +
+ + +
+
+
+ +
+ + +
+ : +
+ + +
+
+
+ +
+ + +
+ : +
+ + +
+
+
+ + +
+
+
+
+
+ )} + +
+
+ ); } export async function getServerSideProps(context) { - const session = await getSession(context); - if (!session) { - return { redirect: { permanent: false, destination: '/auth/login' } }; - } + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - username: true, - startTime: true, - endTime: true, - bufferTime: true - } - }); + const user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + username: true, + startTime: true, + endTime: true, + bufferTime: true, + }, + }); - const types = await prisma.eventType.findMany({ - where: { - userId: user.id, - }, - select: { - id: true, - title: true, - slug: true, - description: true, - length: true, - hidden: true - } - }); - return { - props: {user, types}, // will be passed to the page component as props - } + const types = await prisma.eventType.findMany({ + where: { + userId: user.id, + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + hidden: true, + }, + }); + return { + props: { user, types }, // will be passed to the page component as props + }; } diff --git a/pages/availability/troubleshoot.tsx b/pages/availability/troubleshoot.tsx index f6f16972..fe07c869 100644 --- a/pages/availability/troubleshoot.tsx +++ b/pages/availability/troubleshoot.tsx @@ -51,25 +51,30 @@ export default function Troubleshoot({ user }) { -
+
+

+ Understand why certain times are available and others are blocked. +

+
+
Here is an overview of your day on {selectedDate.format("D MMMM YYYY")}: - Tip: Hover over the bold times for a full timestamp + Tip: Hover over the bold times for a full timestamp
-
+
Your day starts at {convertMinsToHrsMins(user.startTime)}
{availability.map((slot) => ( -
-
- Your calendar shows you as busy between {dayjs(slot.start).format("HH:mm")} and {dayjs(slot.end).format("HH:mm")} on {dayjs(slot.start).format("D MMMM YYYY")} +
+
+ Your calendar shows you as busy between {dayjs(slot.start).format("HH:mm")} and {dayjs(slot.end).format("HH:mm")} on {dayjs(slot.start).format("D MMMM YYYY")}
))} {availability.length === 0 &&
} -
+
Your day ends at {convertMinsToHrsMins(user.endTime)}
diff --git a/pages/bookings/index.tsx b/pages/bookings/index.tsx index dbcaadf8..65c14bef 100644 --- a/pages/bookings/index.tsx +++ b/pages/bookings/index.tsx @@ -32,33 +32,16 @@ export default function Bookings({ bookings }) { +
+

+ See upcoming and past events booked through your event type links. +

+
-
+
- - - - - {/* */} - - - {bookings .filter((booking) => !booking.confirmed && !booking.rejected) @@ -70,7 +53,7 @@ export default function Bookings({ bookings }) { "px-6 py-4 whitespace-nowrap" + (booking.rejected ? " line-through" : "") }> {!booking.confirmed && !booking.rejected && ( - + Unconfirmed )} @@ -83,8 +66,8 @@ export default function Bookings({ bookings }) { className={ "px-6 py-4 max-w-20 w-full" + (booking.rejected ? " line-through" : "") }> -
{booking.title}
-
{booking.description}
+
{booking.title}
+
You and {booking.attendees[0].name}
{/*
{/*
- Person - - Event - - Date - - Actions -
@@ -109,14 +92,14 @@ export default function Bookings({ bookings }) { {booking.confirmed && !booking.rejected && ( <> - Reschedule + href={window.location.href + "/../cancel/" + booking.uid} + className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-neutral-700 bg-white hover:bg-neutral-100 border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mr-2"> + Cancel - Cancel + href={window.location.href + "/../reschedule/" + booking.uid} + className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-neutral-700 bg-white hover:bg-neutral-100 border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mr-2"> + Reschedule )} diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx new file mode 100644 index 00000000..8a2f1823 --- /dev/null +++ b/pages/event-types/[type].tsx @@ -0,0 +1,1201 @@ +import { GetServerSideProps } from "next"; +import Head from "next/head"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useRef, useState } from "react"; +import Select, { OptionBase } from "react-select"; +import prisma from "@lib/prisma"; +import { LocationType } from "@lib/location"; +import Shell from "@components/Shell"; +import { getSession, useSession } from "next-auth/client"; +import { Scheduler } from "@components/ui/Scheduler"; +import { Disclosure } from "@headlessui/react"; + +import { PhoneIcon, PlusCircleIcon, XIcon } from "@heroicons/react/outline"; +import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput"; +import { + LocationMarkerIcon, + LinkIcon, + PencilIcon, + PlusIcon, + DocumentIcon, + ChevronRightIcon, + ClockIcon, + TrashIcon, + ExternalLinkIcon, +} from "@heroicons/react/solid"; + +import dayjs from "dayjs"; +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); + +type Props = { + user: User; + eventType: EventType; + locationOptions: OptionBase[]; + availability: Availability[]; +}; + +type OpeningHours = { + days: number[]; + startTime: number; + endTime: number; +}; + +type DateOverride = { + date: string; + startTime: number; + endTime: number; +}; + +type EventTypeInput = { + id: number; + title: string; + slug: string; + description: string; + length: number; + hidden: boolean; + locations: unknown; + eventName: string; + customInputs: EventTypeCustomInput[]; + timeZone: string; + availability?: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] }; + periodType?: string; + periodDays?: number; + periodStartDate?: Date | string; + periodEndDate?: Date | string; + periodCountCalendarDays?: boolean; + enteredRequiresConfirmation: 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, + locationOptions, + availability, + }: Props): JSX.Element { + const router = useRouter(); + const [session, loading] = useSession(); + + console.log(eventType); + const inputOptions: OptionBase[] = [ + { value: EventTypeCustomInputType.Text, label: "Text" }, + { value: EventTypeCustomInputType.TextLong, label: "Multiline Text" }, + { value: EventTypeCustomInputType.Number, label: "Number" }, + { 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); + const [selectedTimeZone, setSelectedTimeZone] = useState(""); + const [selectedLocation, setSelectedLocation] = useState(undefined); + const [selectedInputOption, setSelectedInputOption] = useState(inputOptions[0]); + const [locations, setLocations] = useState(eventType.locations || []); + const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); + const [customInputs, setCustomInputs] = useState( + 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 requiresConfirmationRef = useRef(); + const eventNameRef = useRef(); + const periodDaysRef = useRef(); + const periodDaysTypeRef = useRef(); + + useEffect(() => { + setSelectedTimeZone(eventType.timeZone || user.timeZone); + }, []); + + async function updateEventTypeHandler(event) { + event.preventDefault(); + + const enteredTitle: string = titleRef.current.value; + const enteredSlug: string = slugRef.current.value; + const enteredDescription: string = descriptionRef.current.value; + const enteredLength: number = parseInt(lengthRef.current.value); + const enteredIsHidden: boolean = isHiddenRef.current.checked; + const enteredRequiresConfirmation: boolean = requiresConfirmationRef.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 = { + id: eventType.id, + title: enteredTitle, + slug: enteredSlug, + description: enteredDescription, + length: enteredLength, + hidden: enteredIsHidden, + locations, + eventName: enteredEventName, + customInputs, + timeZone: selectedTimeZone, + periodType: type, + periodDays: enteredPeriodDays, + periodStartDate: enteredPeriodStartDate, + periodEndDate: enteredPeriodEndDate, + periodCountCalendarDays: enteredPeriodDaysType, + requiresConfirmation: enteredRequiresConfirmation, + }; + + if (enteredAvailability) { + payload.availability = enteredAvailability; + } + + await fetch("/api/availability/eventtype", { + method: "PATCH", + body: JSON.stringify(payload), + headers: { + "Content-Type": "application/json", + }, + }); + + router.push("/availability"); + } + + async function deleteEventTypeHandler(event) { + event.preventDefault(); + + await fetch("/api/availability/eventtype", { + method: "DELETE", + body: JSON.stringify({ id: eventType.id }), + headers: { + "Content-Type": "application/json", + }, + }); + + router.push("/availability"); + } + + const openLocationModal = (type: LocationType) => { + setSelectedLocation(locationOptions.find((option) => option.value === type)); + setShowLocationModal(true); + }; + + const closeLocationModal = () => { + setSelectedLocation(undefined); + setShowLocationModal(false); + }; + + const closeAddCustomModal = () => { + setSelectedInputOption(inputOptions[0]); + setShowAddCustomModal(false); + setSelectedCustomInput(undefined); + }; + + const updateLocations = (e) => { + e.preventDefault(); + + let details = {}; + if (e.target.location.value === LocationType.InPerson) { + details = { address: e.target.address.value }; + } + + const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type); + if (existingIdx !== -1) { + const copy = locations; + copy[existingIdx] = { ...locations[existingIdx], ...details }; + setLocations(copy); + } else { + setLocations(locations.concat({ type: e.target.location.value, ...details })); + } + + setShowLocationModal(false); + }; + + const removeLocation = (selectedLocation) => { + setLocations(locations.filter((location) => location.type !== selectedLocation.type)); + }; + + const openEditCustomModel = (customInput: EventTypeCustomInput) => { + setSelectedCustomInput(customInput); + setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)); + setShowAddCustomModal(true); + }; + + const LocationOptions = () => { + if (!selectedLocation) { + return null; + } + switch (selectedLocation.value) { + case LocationType.InPerson: + return ( +
+ +
+ location.type === LocationType.InPerson)?.address} + /> +
+
+ ); + case LocationType.Phone: + return ( +

Calendso will ask your invitee to enter a phone number before scheduling.

+ ); + case LocationType.GoogleMeet: + return

Calendso will provide a Google Meet location.

; + case LocationType.Zoom: + return

Calendso will provide a Zoom meeting URL.

; + } + return null; + }; + + const updateCustom = (e) => { + e.preventDefault(); + + const customInput: EventTypeCustomInput = { + label: e.target.label.value, + required: e.target.required.checked, + type: e.target.type.value, + }; + + if (e.target.id?.value) { + const index = customInputs.findIndex((inp) => inp.id === +e.target.id?.value); + if (index >= 0) { + const input = customInputs[index]; + input.label = customInput.label; + input.required = customInput.required; + input.type = customInput.type; + setCustomInputs(customInputs); + } + } else { + setCustomInputs(customInputs.concat(customInput)); + } + closeAddCustomModal(); + }; + + const removeCustom = (customInput, e) => { + e.preventDefault(); + const index = customInputs.findIndex((inp) => inp.id === customInput.id); + if (index >= 0) { + customInputs.splice(index, 1); + setCustomInputs([...customInputs]); + } + }; + + return ( +
+ + {eventType.title} | Event Type | Calendso + + + +
+

+ {eventType.description} +

+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ + {typeof location !== "undefined" ? location.hostname : ""}/{user.username}/ + + +
+
+
+
+
+ +
+
+ {locations.length === 0 && ( +
+
+ +
+ + mins + +
+
+
+
+
+
+ +
+
+ +
+
+
+ + {({ open }) => ( + <> + + + Show advanced settings + + +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
    + {customInputs.map((customInput) => ( +
  • +
    +
    +
    + Label: {customInput.label} +
    +
    + Type: {customInput.type} +
    +
    + + {customInput.required ? "Required" : "Optional"} + +
    +
    +
    + + +
    +
    +
  • + ))} +
  • + +
  • +
+
+
+
+
+ +
+
+
+
+ +
+
+

+ Hide the event type from your page, so it can only be booked through its + URL. +

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+

+ The booking needs to be manually confirmed before it is pushed to the + integrations and a integrations and a confirmation mail is sent. +

+
+
+
+
+
+
+ +
+
+ + Date Range +
+ {PERIOD_TYPES.map((period) => ( + + classnames( + checked ? "border-secondary-200 z-10" : "border-gray-200", + "relative min-h-14 lg:flex items-center cursor-pointer focus:outline-none" + ) + }> + {({ active, checked }) => ( + <> + + ))} +
+
+
+
+
+
+ +
+
+ +
+
+
+ + )} +
+
+ + + Cancel + + + +
+ +
+
+
+
+ + + + +
+
+
+ {showLocationModal && ( +
+
+ + + + +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+
+
+ )} +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async ({ req, query }) => { + const session = await getSession({ req }); + if (!session) { + return { + redirect: { + permanent: false, + destination: "/auth/login", + }, + }; + } + + const user: User = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + username: true, + timeZone: true, + startTime: true, + endTime: true, + availability: true, + }, + }); + + const eventType: EventType | null = await prisma.eventType.findUnique({ + where: { + id: parseInt(query.type as string), + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + hidden: true, + locations: true, + eventName: true, + availability: true, + customInputs: true, + timeZone: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + periodCountCalendarDays: true, + requiresConfirmation: true, + }, + }); + + if (!eventType) { + return { + notFound: true, + }; + } + + const credentials = await prisma.credential.findMany({ + where: { + userId: user.id, + }, + select: { + id: true, + type: true, + key: true, + }, + }); + + const integrations = [ + { + installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), + enabled: credentials.find((integration) => integration.type === "google_calendar") != null, + type: "google_calendar", + title: "Google Calendar", + imageSrc: "integrations/google-calendar.png", + description: "For personal and business accounts", + }, + { + installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), + type: "office365_calendar", + enabled: credentials.find((integration) => integration.type === "office365_calendar") != null, + title: "Office 365 / Outlook.com Calendar", + imageSrc: "integrations/office-365.png", + description: "For personal and business accounts", + }, + ]; + + const locationOptions: OptionBase[] = [ + { value: LocationType.InPerson, label: "In-person meeting" }, + { value: LocationType.Phone, label: "Phone call" }, + { value: LocationType.Zoom, label: "Zoom Video" }, + ]; + + const hasGoogleCalendarIntegration = integrations.find( + (i) => i.type === "google_calendar" && i.installed === true && i.enabled + ); + if (hasGoogleCalendarIntegration) { + locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" }); + } + + const hasOfficeIntegration = integrations.find( + (i) => i.type === "office365_calendar" && i.installed === true && i.enabled + ); + if (hasOfficeIntegration) { + // TODO: Add default meeting option of the office integration. + // Assuming it's Microsoft Teams. + } + + const getAvailability = (providesAvailability) => + providesAvailability.availability && providesAvailability.availability.length + ? providesAvailability.availability + : null; + + const availability: Availability[] = getAvailability(eventType) || + getAvailability(user) || [ + { + days: [0, 1, 2, 3, 4, 5, 6], + startTime: user.startTime, + endTime: user.endTime, + }, + ]; + + 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: eventTypeObject, + locationOptions, + availability, + }, + }; +}; diff --git a/pages/event-types/index.tsx b/pages/event-types/index.tsx new file mode 100644 index 00000000..36eb5c27 --- /dev/null +++ b/pages/event-types/index.tsx @@ -0,0 +1,662 @@ +import Head from "next/head"; +import Link from "next/link"; +import prisma from "../../lib/prisma"; +import Shell from "../../components/Shell"; +import { useRouter } from "next/router"; +import { getSession, useSession } from "next-auth/client"; +import { Fragment, useRef, useState } from "react"; +import { Menu, Transition } from "@headlessui/react"; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +import { + ClockIcon, + DotsHorizontalIcon, + ExternalLinkIcon, + InformationCircleIcon, + LinkIcon, + PlusIcon, + UserIcon, +} from "@heroicons/react/solid"; + +export default function Availability({ user, types }) { + const [session, loading] = useSession(); + const router = useRouter(); + const [showAddModal, setShowAddModal] = useState(false); + + const titleRef = useRef(); + const slugRef = useRef(); + const descriptionRef = useRef(); + const lengthRef = useRef(); + + async function createEventTypeHandler(event) { + event.preventDefault(); + + const enteredTitle = titleRef.current.value; + const enteredSlug = slugRef.current.value; + const enteredDescription = descriptionRef.current.value; + const enteredLength = lengthRef.current.value; + + // TODO: Add validation + + const response = await fetch("/api/availability/eventtype", { + method: "POST", + body: JSON.stringify({ + title: enteredTitle, + slug: enteredSlug, + description: enteredDescription, + length: enteredLength, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (enteredTitle && enteredLength) { + router.replace(router.asPath); + toggleAddModal(); + } + } + + function toggleAddModal() { + setShowAddModal(!showAddModal); + } + + if (loading) { + return
; + } + + return ( +
+ + Event Types | Calendso + + + +
+

+ Create events to share for people to book on your calendar. +

+
+ +
+
    + {types.map((type) => ( +
  • + + +
    +
    +
    +
    +

    {type.title}

    + {type.hidden && ( + + Hidden + + )} +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    +
    +
    + + {({ open }) => ( + <> +
    + + Open options + +
    + + + +
    + + {({ active }) => ( + + + )} + + + {({ active }) => ( + + )} + + {/**/} + {/* {({ active }) => (*/} + {/* */} + {/* */} +
    + {/*
    */} + {/* */} + {/* {({ active }) => (*/} + {/* */} + {/* */} + {/*
    */} +
    +
    + + )} +
    +
    +
    + + +
  • + ))} +
+
+ {types.length === 0 && ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Create your first event type

+

+ Event types enable you to share links that show available times on your calendar and allow + people to make bookings with you. +

+
+ )} + {showAddModal && ( +
+
+ + + + +
+
+ +
+

+ Create a new event type for people to book times with. +

+
+
+
+
+
+ +
+ +
+
+
+ +
+
+ + {location.hostname}/{user.username}/ + + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+ minutes +
+
+
+
+ {/* TODO: Add an error message when required input fields empty*/} +
+ + +
+
+
+
+
+ )} +
+
+ ); +} + +export async function getServerSideProps(context) { + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } + + const user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + username: true, + startTime: true, + endTime: true, + bufferTime: true, + }, + }); + + const types = await prisma.eventType.findMany({ + where: { + userId: user.id, + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + hidden: true, + }, + }); + return { + props: { user, types }, // will be passed to the page component as props + }; +} diff --git a/pages/integrations/[integration].tsx b/pages/integrations/[integration].tsx index 62f21224..ba89ebbb 100644 --- a/pages/integrations/[integration].tsx +++ b/pages/integrations/[integration].tsx @@ -1,130 +1,131 @@ -import Head from 'next/head'; -import prisma from '../../lib/prisma'; -import { getIntegrationName, getIntegrationType } from '../../lib/integrations'; -import Shell from '../../components/Shell'; -import { useState } from 'react'; -import { useRouter } from 'next/router'; -import { useSession, getSession } from 'next-auth/client'; +import Head from "next/head"; +import prisma from "../../lib/prisma"; +import { getIntegrationName, getIntegrationType } from "../../lib/integrations"; +import Shell from "../../components/Shell"; +import { useState } from "react"; +import { useRouter } from "next/router"; +import { useSession, getSession } from "next-auth/client"; export default function integration(props) { - const router = useRouter(); - const [session, loading] = useSession(); - const [showAPIKey, setShowAPIKey] = useState(false); + const router = useRouter(); + const [session, loading] = useSession(); + const [showAPIKey, setShowAPIKey] = useState(false); - if (loading) { - return
; - } + if (loading) { + return
; + } - function toggleShowAPIKey() { - setShowAPIKey(!showAPIKey); - } + function toggleShowAPIKey() { + setShowAPIKey(!showAPIKey); + } - async function deleteIntegrationHandler(event) { - event.preventDefault(); + async function deleteIntegrationHandler(event) { + event.preventDefault(); - const response = await fetch('/api/integrations', { - method: 'DELETE', - body: JSON.stringify({id: props.integration.id}), - headers: { - 'Content-Type': 'application/json' - } - }); + const response = await fetch("/api/integrations", { + method: "DELETE", + body: JSON.stringify({ id: props.integration.id }), + headers: { + "Content-Type": "application/json", + }, + }); - router.push('/integrations'); - } + router.push("/integrations"); + } - return( -
- - {getIntegrationName(props.integration.type)} | Integrations | Calendso - - + return ( +
+ + {getIntegrationName(props.integration.type)} | Integrations | Calendso + + - -
-
-
-

- Integration Details -

-

- Information about your {getIntegrationName(props.integration.type)} integration. -

-
-
-
-
-
- Integration name -
-
- {getIntegrationName(props.integration.type)} -
-
-
-
- Integration type -
-
- {getIntegrationType(props.integration.type)} -
-
-
-
- API Key -
-
- {!showAPIKey ? - •••••••• - : -
- -
} - -
-
-
-
-
-
-
-
-

- Delete this integration -

-
-

- Once you delete this integration, it will be permanently removed. -

-
-
- -
-
-
-
-
-
+ +
+

Manage and delete integrations.

- ); +
+
+
+

Integration Details

+

+ Information about your {getIntegrationName(props.integration.type)} integration. +

+
+
+
+
+
Integration name
+
{getIntegrationName(props.integration.type)}
+
+
+
Integration type
+
{getIntegrationType(props.integration.type)}
+
+
+
API Key
+
+ {!showAPIKey ? ( + •••••••• + ) : ( +
+ +
+ )} + +
+
+
+
+
+
+
+
+

Delete this integration

+
+

Once you delete this integration, it will be permanently removed.

+
+
+ +
+
+
+
+
+
+
+ ); } export async function getServerSideProps(context) { - const session = await getSession(context); + const session = await getSession(context); - const integration = await prisma.credential.findFirst({ - where: { - id: parseInt(context.query.integration), - }, - select: { - id: true, - type: true, - key: true - } - }); - return { - props: {integration}, // will be passed to the page component as props - } -} \ No newline at end of file + const integration = await prisma.credential.findFirst({ + where: { + id: parseInt(context.query.integration), + }, + select: { + id: true, + type: true, + key: true, + }, + }); + return { + props: { integration }, // will be passed to the page component as props + }; +} diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index 25ff03e8..891fc4ef 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -1,387 +1,461 @@ -import Head from 'next/head'; -import Link from 'next/link'; -import prisma from '../../lib/prisma'; -import Shell from '../../components/Shell'; -import {useEffect, useState} from 'react'; -import {getSession, useSession} from 'next-auth/client'; -import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; -import {InformationCircleIcon} from '@heroicons/react/outline'; -import {Switch} from '@headlessui/react' +import Head from "next/head"; +import Link from "next/link"; +import prisma from "../../lib/prisma"; +import Shell from "../../components/Shell"; +import { useEffect, useState } from "react"; +import { getSession, useSession } from "next-auth/client"; +import { + CalendarIcon, + CheckCircleIcon, + ChevronRightIcon, + PlusIcon, + XCircleIcon, +} from "@heroicons/react/solid"; +import { InformationCircleIcon } from "@heroicons/react/outline"; +import { Switch } from "@headlessui/react"; export default function Home({ integrations }) { - const [session, loading] = useSession(); - const [showAddModal, setShowAddModal] = useState(false); - const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); - const [selectableCalendars, setSelectableCalendars] = useState([]); + const [session, loading] = useSession(); + const [showAddModal, setShowAddModal] = useState(false); + const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); + const [selectableCalendars, setSelectableCalendars] = useState([]); - function toggleAddModal() { - setShowAddModal(!showAddModal); + function toggleAddModal() { + setShowAddModal(!showAddModal); + } + + function toggleShowCalendarModal() { + setShowSelectCalendarModal(!showSelectCalendarModal); + } + + function loadCalendars() { + fetch("api/availability/calendar") + .then((response) => response.json()) + .then((data) => { + setSelectableCalendars(data); + }); + } + + function integrationHandler(type) { + fetch("/api/integrations/" + type.replace("_", "") + "/add") + .then((response) => response.json()) + .then((data) => (window.location.href = data.url)); + } + + function calendarSelectionHandler(calendar) { + return (selected) => { + const cals = [...selectableCalendars]; + const i = cals.findIndex((c) => c.externalId === calendar.externalId); + cals[i].selected = selected; + setSelectableCalendars(cals); + if (selected) { + fetch("api/availability/calendar", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(cals[i]), + }).then((response) => response.json()); + } else { + fetch("api/availability/calendar", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(cals[i]), + }).then((response) => response.json()); + } + }; + } + + function getCalendarIntegrationImage(integrationType: string) { + switch (integrationType) { + case "google_calendar": + return "integrations/google-calendar.png"; + case "office365_calendar": + return "integrations/office-365.png"; + default: + return ""; } + } - function toggleShowCalendarModal() { - setShowSelectCalendarModal(!showSelectCalendarModal); - } + function classNames(...classes) { + return classes.filter(Boolean).join(" "); + } - function loadCalendars() { - fetch('api/availability/calendar') - .then((response) => response.json()) - .then(data => { - setSelectableCalendars(data) - }); - } + useEffect(loadCalendars, [integrations]); - function integrationHandler(type) { - fetch('/api/integrations/' + type.replace('_', '') + '/add') - .then((response) => response.json()) - .then((data) => window.location.href = data.url); - } + if (loading) { + return
; + } - function calendarSelectionHandler(calendar) { - return (selected) => { - let cals = [...selectableCalendars]; - let i = cals.findIndex(c => c.externalId === calendar.externalId); - cals[i].selected = selected; - setSelectableCalendars(cals); - if (selected) { - fetch('api/availability/calendar', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(cals[i]) - }).then((response) => response.json()); - } else { - fetch('api/availability/calendar', { - method: 'DELETE', headers: { - 'Content-Type': 'application/json' - }, body: JSON.stringify(cals[i]) - }).then((response) => response.json()); - } - } - } + return ( +
+ + Integrations | Calendso + + - function getCalendarIntegrationImage(integrationType: string){ - switch (integrationType) { - case "google_calendar": return "integrations/google-calendar.png"; - case "office365_calendar": return "integrations/office-365.png"; - default: return ""; - } - } - - function classNames(...classes) { - return classes.filter(Boolean).join(' ') - } - - useEffect(loadCalendars, [integrations]); - - if (loading) { - return
; - } - - return ( -
- - Integrations | Calendso - - - - -
- -
-
- {integrations.filter( (ig) => ig.credential ).length !== 0 ? - : -
-
-
- -
-
-

- You don't have any integrations added. -

-
-

- You currently do not have any integrations set up. Add your first integration to get started. -

-
-
- -
-
-
-
- } -
- {showAddModal && -
-
- {/* */} - - - {/* */} -
-
-
- -
-
- -
-

- Link a new integration to your account. -

-
-
-
-
-
    - {integrations.filter( (integration) => integration.installed ).map( (integration) => (
  • -
    - {integration.title} -
    -
    -

    { integration.title }

    -

    { integration.description }

    -
    -
    - -
    -
  • ))} -
-
-
- -
-
-
-
- } -
-
-

- Select calendars -

-
-

- Select which calendars are checked for availability to prevent double bookings. -

-
-
- -
-
-
- {showSelectCalendarModal && -
-
- {/* */} - - - {/* */} -
-
-
- -
-
- -
-

- If no entry is selected, all calendars will be checked -

-
-
-
-
-
    - {selectableCalendars.map( (calendar) => (
  • -
    - {calendar.integration} -
    -
    -

    { calendar.name }

    -
    -
    - - Select calendar - -
    -
  • ))} -
-
-
- -
-
-
-
- } -
+ +
+

Connect your favourite apps.

- ); +
+ +
+
+ {integrations.filter((ig) => ig.credential).length !== 0 ? ( + + ) : ( +
+
+
+ +
+
+

+ You don't have any integrations added. +

+
+

+ You currently do not have any integrations set up. Add your first integration to get + started. +

+
+
+ +
+
+
+
+ )} +
+ {showAddModal && ( +
+
+ {/* */} + + + {/* */} +
+
+
+ +
+
+ +
+

Link a new integration to your account.

+
+
+
+
+
    + {integrations + .filter((integration) => integration.installed) + .map((integration) => ( +
  • +
    + {integration.title} +
    +
    +

    {integration.title}

    +

    {integration.description}

    +
    +
    + +
    +
  • + ))} +
+
+
+ +
+
+
+
+ )} +
+
+

Select calendars

+
+

Select which calendars are checked for availability to prevent double bookings.

+
+
+ +
+
+
+ {showSelectCalendarModal && ( +
+
+ {/* */} + + + {/* */} +
+
+
+ +
+
+ +
+

+ If no entry is selected, all calendars will be checked +

+
+
+
+
+
    + {selectableCalendars.map((calendar) => ( +
  • +
    + {calendar.integration} +
    +
    +

    {calendar.name}

    +
    +
    + + Select calendar + +
    +
  • + ))} +
+
+
+ +
+
+
+
+ )} +
+
+ ); } const validJson = (jsonString: string) => { - try { - const o = JSON.parse(jsonString); - if (o && typeof o === "object") { - return o; - } + try { + const o = JSON.parse(jsonString); + if (o && typeof o === "object") { + return o; } - catch (e) { console.error(e); } - return false; -} + } catch (e) { + console.error(e); + } + return false; +}; export async function getServerSideProps(context) { - const session = await getSession(context); - if (!session) { - return { redirect: { permanent: false, destination: '/auth/login' } }; - } - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true - } - }); + const session = await getSession(context); + if (!session) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } + const user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + }, + }); - const credentials = await prisma.credential.findMany({ - where: { - userId: user.id, - }, - select: { - id: true, - type: true, - key: true - } - }); + const credentials = await prisma.credential.findMany({ + where: { + userId: user.id, + }, + select: { + id: true, + type: true, + key: true, + }, + }); - const integrations = [ { - installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), - credential: credentials.find( (integration) => integration.type === "google_calendar" ) || null, - type: "google_calendar", - title: "Google Calendar", - imageSrc: "integrations/google-calendar.png", - description: "For personal and business calendars", - }, { - installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), - type: "office365_calendar", - credential: credentials.find( (integration) => integration.type === "office365_calendar" ) || null, - title: "Office 365 / Outlook.com Calendar", - imageSrc: "integrations/office-365.png", - description: "For personal and business calendars", - }, { - installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET), - type: "zoom_video", - credential: credentials.find( (integration) => integration.type === "zoom_video" ) || null, - title: "Zoom", - imageSrc: "integrations/zoom.png", - description: "Video Conferencing", - } ]; + const integrations = [ + { + installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), + credential: credentials.find((integration) => integration.type === "google_calendar") || null, + type: "google_calendar", + title: "Google Calendar", + imageSrc: "integrations/google-calendar.png", + description: "For personal and business calendars", + }, + { + installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), + type: "office365_calendar", + credential: credentials.find((integration) => integration.type === "office365_calendar") || null, + title: "Office 365 / Outlook.com Calendar", + imageSrc: "integrations/office-365.png", + description: "For personal and business calendars", + }, + { + installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET), + type: "zoom_video", + credential: credentials.find((integration) => integration.type === "zoom_video") || null, + title: "Zoom", + imageSrc: "integrations/zoom.png", + description: "Video Conferencing", + }, + ]; - return { - props: {integrations}, - } + return { + props: { integrations }, + }; } diff --git a/pages/settings/billing.tsx b/pages/settings/billing.tsx index 4f171776..e5c04318 100644 --- a/pages/settings/billing.tsx +++ b/pages/settings/billing.tsx @@ -13,11 +13,16 @@ export default function Billing(props) { return ( +
+

+ Manage your billing information and cancel your subscription. +

+
Billing | Calendso -
+

Change your Subscription @@ -63,4 +68,4 @@ export async function getServerSideProps(context) { return { props: {user}, // will be passed to the page component as props } -} \ No newline at end of file +} diff --git a/pages/settings/embed.tsx b/pages/settings/embed.tsx index 99ec1c89..8fd371d4 100644 --- a/pages/settings/embed.tsx +++ b/pages/settings/embed.tsx @@ -1,105 +1,120 @@ -import Head from 'next/head'; -import Link from 'next/link'; -import { useState } from 'react'; -import { useRouter } from 'next/router'; -import prisma from '../../lib/prisma'; -import Modal from '../../components/Modal'; -import Shell from '../../components/Shell'; -import SettingsShell from '../../components/Settings'; -import Avatar from '../../components/Avatar'; -import { signIn, useSession, getSession } from 'next-auth/client'; -import TimezoneSelect from 'react-timezone-select'; +import Head from "next/head"; +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/router"; +import prisma from "../../lib/prisma"; +import Modal from "../../components/Modal"; +import Shell from "../../components/Shell"; +import SettingsShell from "../../components/Settings"; +import Avatar from "../../components/Avatar"; +import { signIn, useSession, getSession } from "next-auth/client"; +import TimezoneSelect from "react-timezone-select"; export default function Embed(props) { - const [ session, loading ] = useSession(); - const router = useRouter(); + const [session, loading] = useSession(); + const router = useRouter(); - if (loading) { - return
; - } + if (loading) { + return
; + } - return( - - - Embed | Calendso - - - -
-
-

Iframe Embed

-

- The easiest way to embed Calendso on your website. -

-
-
-
- -
- + className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm">
@@ -152,7 +152,7 @@ export default function Settings(props) { id="timeZone" value={selectedTimeZone} onChange={setSelectedTimeZone} - className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" + className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm" />
@@ -165,7 +165,7 @@ export default function Settings(props) { id="weekStart" value={selectedWeekStartDay} 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" + className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm" options={[ { value: "Sunday", label: "Sunday" }, { value: "Monday", label: "Monday" }, @@ -183,11 +183,11 @@ export default function Settings(props) { 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" + className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm" options={themeOptions} />
-
+
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" + className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm" />
@@ -214,7 +214,7 @@ export default function Settings(props) { type="checkbox" ref={hideBrandingRef} defaultChecked={props.user.hideBranding} - className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" + className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm" />
@@ -238,13 +238,13 @@ export default function Settings(props) { aria-hidden="true">
- {/*
-
+ {/*
+
- +
*/}
@@ -254,12 +254,12 @@ export default function Settings(props) {
} + fallback={
} /> {/* */}
@@ -272,7 +272,7 @@ export default function Settings(props) { name="avatar" id="avatar" placeholder="URL" - className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm" defaultValue={props.user.avatar} />
@@ -282,7 +282,7 @@ export default function Settings(props) {
diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index c129939f..78f8e95e 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -58,21 +58,22 @@ export default function Teams() { return ( +
+

+ Create and manage teams to use collaborative features. +

+
Teams | Calendso
-
+
-

Your teams

-

- View, edit and create teams to organise relationships between users -

{!(invites.length || teams.length) && ( -
+

Create a team to get started @@ -94,7 +95,7 @@ export default function Teams() {

{!!(invites.length || teams.length) && (
-
@@ -143,10 +144,10 @@ export default function Teams() { ​ -
+
-
- +
+
diff --git a/public/calendso-logo-word.svg b/public/calendso-logo-word.svg index 4a01ade1..cfa67015 100644 --- a/public/calendso-logo-word.svg +++ b/public/calendso-logo-word.svg @@ -1,41 +1,11 @@ - - - - - + + + + + + + + + + diff --git a/public/integrations/google-calendar.png b/public/integrations/google-calendar.png deleted file mode 100644 index a600dcb4df58b803469f2a201089d4c4cba73c80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26889 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 diff --git a/styles/globals.css b/styles/globals.css index b7dcbf0f..b2e17113 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -7,84 +7,84 @@ @layer components { /* Primary buttons */ .btn-xs.btn-primary { - @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-sm.btn-primary { - @apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn.btn-primary { - @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-lg.btn-primary { - @apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-xl.btn-primary { - @apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-wide.btn-primary { - @apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } /* Secondary buttons */ .btn-xs.btn-secondary { - @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-sm.btn-secondary { - @apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn.btn-secondary { - @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-lg.btn-secondary { - @apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-xl.btn-secondary { - @apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-wide.btn-secondary { - @apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } /* White buttons */ .btn-xs.btn-white { - @apply inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-sm.btn-white { - @apply inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn.btn-white { - @apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-lg.btn-white { - @apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-xl.btn-white { - @apply inline-flex items-center px-6 py-3 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply inline-flex items-center px-6 py-3 border border-gray-300 shadow-sm text-base font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } .btn-wide.btn-white { - @apply w-full text-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500; + @apply w-full text-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500; } } .loader { margin: 80px auto; border: 8px solid #f3f3f3; /* Light grey */ - border-top: 8px solid #039be5; /* Blue */ + border-top: 8px solid #039be5; /* neutral */ border-radius: 50%; width: 60px; height: 60px; @@ -105,11 +105,11 @@ nav#nav--settings > a svg { } nav#nav--settings > a.active { - @apply bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700; + @apply bg-neutral-50 border-neutral-500 text-neutral-700 hover:bg-neutral-50 hover:text-neutral-700; } nav#nav--settings > a.active svg { - @apply text-blue-500; + @apply text-neutral-500; } @@ -155,4 +155,4 @@ body { rgba(3, 169, 244, var(--tw-border-opacity)) rgba(3, 169, 244, var(--tw-border-opacity)) white; -} \ No newline at end of file +} diff --git a/tailwind.config.js b/tailwind.config.js index a6063b5d..ae0c2adc 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,27 +5,77 @@ module.exports = { theme: { extend: { colors: { - gray: { - 100: "#EBF1F5", - 200: "#D9E3EA", - 300: "#C5D2DC", - 400: "#9BA9B4", - 500: "#707D86", - 600: "#55595F", - 700: "#33363A", - 800: "#25282C", - 900: "#151719", + neutral: { + 50: "#F7F8F9", + 100: "#F4F5F6", + 200: "#EAEEF2", + 300: "#C6CCD5", + 400: "#9BA6B6", + 500: "#708097", + 600: "#657388", + 700: "#373F4A", + 800: "#1F2937", + 900: "#1A1A1A", }, - blue: { - 100: "#b3e5fc", - 200: "#81d4fa", - 300: "#4fc3f7", - 400: "#29b6f6", - 500: "#03a9f4", - 600: "#039be5", - 700: "#0288d1", - 800: "#0277bd", - 900: "#01579b", + primary: { + 50: "#F4F4F4", + 100: "#E8E8E8", + 200: "#C6C6C6", + 300: "#A3A3A3", + 400: "#5F5F5F", + 500: "#1A1A1A", + 600: "#171717", + 700: "#141414", + 800: "#101010", + 900: "#0D0D0D", + }, + secondary: { + 50: "#F5F8F7", + 100: "#EBF0F0", + 200: "#CDDAD9", + 300: "#AEC4C2", + 400: "#729894", + 500: "#356C66", + 600: "#30615C", + 700: "#28514D", + 800: "#20413D", + 900: "#223B41", + }, + red: { + 50: "#FEF2F2", + 100: "#FEE2E2", + 200: "#FECACA", + 300: "#FCA5A5", + 400: "#F87171", + 500: "#EF4444", + 600: "#DC2626", + 700: "#B91C1C", + 800: "#991B1B", + 900: "#7F1D1D", + }, + orange: { + 50: "#FFF7ED", + 100: "#FFEDD5", + 200: "#FED7AA", + 300: "#FDBA74", + 400: "#FB923C", + 500: "#F97316", + 600: "#EA580C", + 700: "#C2410C", + 800: "#9A3412", + 900: "#7C2D12", + }, + green: { + 50: "#ECFDF5", + 100: "#D1FAE5", + 200: "#A7F3D0", + 300: "#6EE7B7", + 400: "#34D399", + 500: "#10B981", + 600: "#059669", + 700: "#047857", + 800: "#065F46", + 900: "#064E3B", }, }, maxHeight: (theme) => ({ From 15225c5f6796273511c255e48f73b634cc834550 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Sat, 31 Jul 2021 18:29:54 +0200 Subject: [PATCH 02/28] redesign: tiny changes --- components/Shell.tsx | 14 +++++++++----- tailwind.config.js | 4 ++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/components/Shell.tsx b/components/Shell.tsx index f9b951e6..bd3b5bb6 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Fragment, useEffect, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { signOut, useSession } from "next-auth/client"; import { Dialog, Menu, Transition } from "@headlessui/react"; @@ -180,8 +180,10 @@ export default function Shell(props) { {/* Sidebar component, swap this element with another sidebar if you like */}

-
- Calendso +
+

+ Calendso +

diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index 8a2f1823..78e8e12d 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -1135,7 +1135,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, query enabled: credentials.find((integration) => integration.type === "google_calendar") != null, type: "google_calendar", title: "Google Calendar", - imageSrc: "integrations/google-calendar.png", + imageSrc: "integrations/google-calendar.svg", description: "For personal and business accounts", }, { @@ -1143,7 +1143,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, query type: "office365_calendar", enabled: credentials.find((integration) => integration.type === "office365_calendar") != null, title: "Office 365 / Outlook.com Calendar", - imageSrc: "integrations/office-365.png", + imageSrc: "integrations/outlook.svg", description: "For personal and business accounts", }, ]; diff --git a/pages/event-types/index.tsx b/pages/event-types/index.tsx index 36eb5c27..d3ded115 100644 --- a/pages/event-types/index.tsx +++ b/pages/event-types/index.tsx @@ -65,7 +65,7 @@ export default function Availability({ user, types }) { } if (loading) { - return
; + return
; } return ( @@ -74,18 +74,17 @@ export default function Availability({ user, types }) { Event Types | Calendso - -
-

- Create events to share for people to book on your calendar. -

-
- + + + New event type + + }>
    {types.map((type) => ( diff --git a/pages/index.tsx b/pages/index.tsx index 264e4d8e..ec876b44 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -13,7 +13,7 @@ function classNames(...classes) { export default function Home(props) { const [session, loading] = useSession(); if (loading) { - return
    ; + return
    ; } function convertMinsToHrsMins(mins) { @@ -227,19 +227,19 @@ export default function Home(props) { {integration.type == "google_calendar" && ( Google Calendar )} {integration.type == "office365_calendar" && ( Office 365 / Outlook.com Calendar )} {integration.type == "zoom_video" && ( - Zoom + Zoom )}
    {integration.type == "office365_calendar" && ( diff --git a/pages/integrations/[integration].tsx b/pages/integrations/[integration].tsx index ba89ebbb..52f14144 100644 --- a/pages/integrations/[integration].tsx +++ b/pages/integrations/[integration].tsx @@ -12,7 +12,7 @@ export default function integration(props) { const [showAPIKey, setShowAPIKey] = useState(false); if (loading) { - return
    ; + return
    ; } function toggleShowAPIKey() { diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index 891fc4ef..0740271a 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -71,9 +71,9 @@ export default function Home({ integrations }) { function getCalendarIntegrationImage(integrationType: string) { switch (integrationType) { case "google_calendar": - return "integrations/google-calendar.png"; + return "integrations/google-calendar.svg"; case "office365_calendar": - return "integrations/office-365.png"; + return "integrations/outlook.svg"; default: return ""; } @@ -86,7 +86,7 @@ export default function Home({ integrations }) { useEffect(loadCalendars, [integrations]); if (loading) { - return
    ; + return
    ; } return ( @@ -96,10 +96,10 @@ export default function Home({ integrations }) { - -
    -

    Connect your favourite apps.

    -
    +