diff --git a/components/BookingsShell.tsx b/components/BookingsShell.tsx new file mode 100644 index 0000000000..2457a25a65 --- /dev/null +++ b/components/BookingsShell.tsx @@ -0,0 +1,28 @@ +import NavTabs from "./NavTabs"; + +export default function BookingsShell(props) { + const tabs = [ + { + name: "Upcoming", + href: "/bookings/upcoming", + }, + { + name: "Past", + href: "/bookings/past", + }, + { + name: "Cancelled", + href: "/bookings/cancelled", + }, + ]; + + return ( +
+
+ +
+
+
{props.children}
+
+ ); +} diff --git a/components/NavTabs.tsx b/components/NavTabs.tsx new file mode 100644 index 0000000000..81e2c8837d --- /dev/null +++ b/components/NavTabs.tsx @@ -0,0 +1,50 @@ +import Link, { LinkProps } from "next/link"; +import { useRouter } from "next/router"; +import React, { ElementType, FC } from "react"; + +import classNames from "@lib/classNames"; + +interface Props { + tabs: { + name: string; + href: string; + icon?: ElementType; + }[]; + linkProps?: Omit; +} + +const NavTabs: FC = ({ tabs, linkProps }) => { + const router = useRouter(); + return ( + + ); +}; + +export default NavTabs; diff --git a/components/Settings.tsx b/components/Settings.tsx deleted file mode 100644 index 85869e669d..0000000000 --- a/components/Settings.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { CodeIcon, CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -import classNames from "@lib/classNames"; - -export default function SettingsShell(props) { - const router = useRouter(); - - const tabs = [ - { - name: "Profile", - href: "/settings/profile", - icon: UserIcon, - current: router.pathname == "/settings/profile", - }, - { - name: "Security", - href: "/settings/security", - icon: KeyIcon, - current: router.pathname == "/settings/security", - }, - { name: "Embed", href: "/settings/embed", icon: CodeIcon, current: router.pathname == "/settings/embed" }, - { - name: "Teams", - href: "/settings/teams", - icon: UserGroupIcon, - current: router.pathname == "/settings/teams", - }, - { - name: "Billing", - href: "/settings/billing", - icon: CreditCardIcon, - current: router.pathname == "/settings/billing", - }, - ]; - - return ( -
-
- -
-
-
{props.children}
-
- ); -} diff --git a/components/SettingsShell.tsx b/components/SettingsShell.tsx new file mode 100644 index 0000000000..b8e7af3e48 --- /dev/null +++ b/components/SettingsShell.tsx @@ -0,0 +1,39 @@ +import { CodeIcon, CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid"; + +import NavTabs from "./NavTabs"; + +export default function SettingsShell(props) { + const tabs = [ + { + name: "Profile", + href: "/settings/profile", + icon: UserIcon, + }, + { + name: "Security", + href: "/settings/security", + icon: KeyIcon, + }, + { name: "Embed", href: "/settings/embed", icon: CodeIcon }, + { + name: "Teams", + href: "/settings/teams", + icon: UserGroupIcon, + }, + { + name: "Billing", + href: "/settings/billing", + icon: CreditCardIcon, + }, + ]; + + return ( +
+
+ +
+
+
{props.children}
+
+ ); +} diff --git a/components/Shell.tsx b/components/Shell.tsx index 28dc4c71c2..f13fe436b0 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -77,37 +77,37 @@ export default function Shell(props: { name: "Event Types", href: "/event-types", icon: LinkIcon, - current: router.pathname.startsWith("/event-types"), + current: router.asPath.startsWith("/event-types"), }, { name: "Bookings", - href: "/bookings", + href: "/bookings/upcoming", icon: ClockIcon, - current: router.pathname.startsWith("/bookings"), + current: router.asPath.startsWith("/bookings"), }, { name: "Availability", href: "/availability", icon: CalendarIcon, - current: router.pathname.startsWith("/availability"), + current: router.asPath.startsWith("/availability"), }, { name: "Integrations", href: "/integrations", icon: PuzzleIcon, - current: router.pathname.startsWith("/integrations"), + current: router.asPath.startsWith("/integrations"), }, { name: "Settings", href: "/settings/profile", icon: CogIcon, - current: router.pathname.startsWith("/settings"), + current: router.asPath.startsWith("/settings"), }, ]; useEffect(() => { telemetry.withJitsu((jitsu) => { - return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname)); + return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath)); }); }, [telemetry]); diff --git a/components/booking/TimeOptions.tsx b/components/booking/TimeOptions.tsx index b53c55f5a2..407a33e150 100644 --- a/components/booking/TimeOptions.tsx +++ b/components/booking/TimeOptions.tsx @@ -1,13 +1,18 @@ // TODO: replace headlessui with radix-ui import { Switch } from "@headlessui/react"; -import { useEffect, useState } from "react"; -import TimezoneSelect from "react-timezone-select"; +import { FC, useEffect, useState } from "react"; +import TimezoneSelect, { ITimezoneOption } from "react-timezone-select"; import classNames from "@lib/classNames"; import { is24h, timeZone } from "../../lib/clock"; -const TimeOptions = (props) => { +type Props = { + onSelectTimeZone: (selectedTimeZone: string) => void; + onToggle24hClock: (is24hClock: boolean) => void; +}; + +const TimeOptions: FC = (props) => { const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [is24hClock, setIs24hClock] = useState(false); @@ -27,47 +32,45 @@ const TimeOptions = (props) => { props.onToggle24hClock(is24h(is24hClock)); }; - return ( - selectedTimeZone !== "" && ( -
-
-
Time Options
-
- - - am/pm - - +
+
Time Options
+
+ + + am/pm + + + Use setting + - - 24h - - -
+ is24hClock ? "translate-x-3" : "translate-x-0", + "pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200" + )} + /> + + + 24h + +
- setSelectedTimeZone(tz.value)} - className="mb-2 shadow-sm focus:ring-black focus:border-black mt-1 block w-full sm:text-sm border-gray-300 rounded-md" - />
- ) - ); + setSelectedTimeZone(tz.value)} + className="mb-2 shadow-sm focus:ring-black focus:border-black mt-1 block w-full sm:text-sm border-gray-300 rounded-md" + /> +
+ ) : null; }; export default TimeOptions; diff --git a/components/booking/pages/AvailabilityPage.tsx b/components/booking/pages/AvailabilityPage.tsx index e3932d0618..d4c9a33e67 100644 --- a/components/booking/pages/AvailabilityPage.tsx +++ b/components/booking/pages/AvailabilityPage.tsx @@ -22,11 +22,14 @@ import AvatarGroup from "@components/ui/AvatarGroup"; import PoweredByCal from "@components/ui/PoweredByCal"; import { AvailabilityPageProps } from "../../../pages/[user]/[type]"; +import { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]"; dayjs.extend(utc); dayjs.extend(customParseFormat); -const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPageProps) => { +type Props = AvailabilityTeamPageProps | AvailabilityPageProps; + +const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => { const router = useRouter(); const { rescheduleUid } = router.query; const { isReady } = useTheme(profile.theme); diff --git a/components/booking/pages/BookingPage.tsx b/components/booking/pages/BookingPage.tsx index da44475d55..7439581308 100644 --- a/components/booking/pages/BookingPage.tsx +++ b/components/booking/pages/BookingPage.tsx @@ -264,6 +264,7 @@ const BookingPage = (props: BookingPageProps) => { type="email" name="email" id="email" + inputMode="email" required className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm" placeholder="you@example.com" diff --git a/components/ui/TableActions.tsx b/components/ui/TableActions.tsx new file mode 100644 index 0000000000..2e5eccef48 --- /dev/null +++ b/components/ui/TableActions.tsx @@ -0,0 +1,93 @@ +import { Menu, Transition } from "@headlessui/react"; +import { DotsHorizontalIcon } from "@heroicons/react/solid"; +import React, { FC, Fragment } from "react"; + +import classNames from "@lib/classNames"; +import { SVGComponent } from "@lib/types/SVGComponent"; + +import Button from "./Button"; + +type ActionType = { + id: string; + icon: SVGComponent; + label: string; + disabled?: boolean; +} & ({ href?: never; onClick: () => any } | { href: string; onClick?: never }); + +interface Props { + actions: ActionType[]; +} + +const TableActions: FC = ({ actions }) => { + return ( + <> +
+ {actions.map((action) => ( + + ))} +
+ + {({ open }) => ( + <> +
+ + Open options + +
+ + + +
+ {actions.map((action) => { + const Element = typeof action.onClick === "function" ? "span" : "a"; + return ( + + {({ active }) => ( + + + )} + + ); + })} +
+
+
+ + )} +
+ + ); +}; + +export default TableActions; diff --git a/components/ui/form/CheckedSelect.tsx b/components/ui/form/CheckedSelect.tsx index 12cba0f345..a4a8b87cd8 100644 --- a/components/ui/form/CheckedSelect.tsx +++ b/components/ui/form/CheckedSelect.tsx @@ -1,21 +1,26 @@ -import { XIcon, CheckIcon } from "@heroicons/react/outline"; +import { CheckIcon, XIcon } from "@heroicons/react/outline"; import React, { ForwardedRef, useEffect, useState } from "react"; -import { OptionsType } from "react-select/lib/types"; import Avatar from "@components/ui/Avatar"; import Select from "@components/ui/form/Select"; +type CheckedSelectValue = { + avatar: string; + label: string; + value: string; +}[]; + export type CheckedSelectProps = { - defaultValue?: []; + defaultValue?: CheckedSelectValue; placeholder?: string; name?: string; - options: []; - onChange: (options: OptionsType) => void; - disabled: []; + options: CheckedSelectValue; + onChange: (options: CheckedSelectValue) => void; + disabled: boolean; }; export const CheckedSelect = React.forwardRef((props: CheckedSelectProps, ref: ForwardedRef) => { - const [selectedOptions, setSelectedOptions] = useState<[]>(props.defaultValue || []); + const [selectedOptions, setSelectedOptions] = useState(props.defaultValue || []); useEffect(() => { props.onChange(selectedOptions); @@ -38,7 +43,7 @@ export const CheckedSelect = React.forwardRef((props: CheckedSelectProps, ref: F disabled: !!selectedOptions.find((selectedOption) => selectedOption.value === option.value), })); - const removeOption = (value) => + const removeOption = (value: string) => setSelectedOptions(selectedOptions.filter((option) => option.value !== value)); const changeHandler = (selections) => diff --git a/components/ui/form/DateRangePicker.tsx b/components/ui/form/DateRangePicker.tsx index 121618412c..9b022abf87 100644 --- a/components/ui/form/DateRangePicker.tsx +++ b/components/ui/form/DateRangePicker.tsx @@ -6,8 +6,8 @@ import React from "react"; import "react-calendar/dist/Calendar.css"; type Props = { - startDate: string; - endDate: string; + startDate: Date; + endDate: Date; onDatesChange?: ((arg: { startDate: Date; endDate: Date }) => void) | undefined; }; diff --git a/components/ui/form/Select.tsx b/components/ui/form/Select.tsx index 0d72d9ab2e..a7f768dfbd 100644 --- a/components/ui/form/Select.tsx +++ b/components/ui/form/Select.tsx @@ -7,7 +7,7 @@ export const SelectComp = (props: PropsWithChildren) => ( [number]; + +function BookingListItem(booking: BookingItem) { + const utils = trpc.useContext(); + const mutation = useMutation( + async (confirm: boolean) => { + const res = await fetch("/api/book/confirm", { + method: "PATCH", + body: JSON.stringify({ id: booking.id, confirmed: confirm }), + headers: { + "Content-Type": "application/json", + }, + }); + if (!res.ok) { + throw new HttpError({ statusCode: res.status }); + } + }, + { + async onSettled() { + await utils.invalidateQuery(["viewer.bookings"]); + }, + } + ); + const isUpcoming = new Date(booking.endTime) >= new Date(); + const isCancelled = booking.status === BookingStatus.CANCELLED; + + const pendingActions = [ + { + id: "confirm", + label: "Confirm", + onClick: () => mutation.mutate(true), + icon: CheckIcon, + disabled: mutation.isLoading, + }, + { + id: "reject", + label: "Reject", + onClick: () => mutation.mutate(false), + icon: BanIcon, + disabled: mutation.isLoading, + }, + ]; + + const bookedActions = [ + { + id: "cancel", + label: "Cancel", + href: `/cancel/${booking.uid}`, + icon: XIcon, + }, + { + id: "reschedule", + label: "Reschedule", + href: `/reschedule/${booking.uid}`, + icon: ClockIcon, + }, + ]; + + return ( + + + {!booking.confirmed && !booking.rejected && ( + + Unconfirmed + + )} +
+ {booking.eventType?.team && {booking.eventType.team.name}: } + {booking.title} +
+
+
+ {dayjs(booking.startTime).format("D MMMM YYYY")}:{" "} + + {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} + +
+
+ {booking.description && ( +
+ "{booking.description}" +
+ )} + {booking.attendees.length !== 0 && ( + + )} + + +
{dayjs(booking.startTime).format("D MMMM YYYY")}
+
+ {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} +
+ + + {isUpcoming && !isCancelled ? ( + <> + {!booking.confirmed && !booking.rejected && } + {booking.confirmed && !booking.rejected && } + {!booking.confirmed && booking.rejected &&
Rejected
} + + ) : null} + + + ); +} + +export default function Bookings() { + const router = useRouter(); + const query = trpc.useQuery(["viewer.bookings"]); + const filtersByStatus = { + upcoming: (booking: BookingItem) => + new Date(booking.endTime) >= new Date() && booking.status !== BookingStatus.CANCELLED, + past: (booking: BookingItem) => new Date(booking.endTime) < new Date(), + cancelled: (booking: BookingItem) => booking.status === BookingStatus.CANCELLED, + } as const; + const filterKey = (router.query?.status as string as keyof typeof filtersByStatus) || "upcoming"; + const appliedFilter = filtersByStatus[filterKey]; + const bookings = query.data?.filter(appliedFilter); + + return ( + + +
+
+
+ {query.status === "error" && ( + + )} + {query.status === "loading" && } + {bookings && + (bookings.length === 0 ? ( + + ) : ( +
+ + + {bookings.map((booking) => ( + + ))} + +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/pages/bookings/index.tsx b/pages/bookings/index.tsx index 19258f00e6..f63871c1c9 100644 --- a/pages/bookings/index.tsx +++ b/pages/bookings/index.tsx @@ -1,277 +1,16 @@ -// TODO: replace headlessui with radix-ui -import { Menu, Transition } from "@headlessui/react"; -import { BanIcon, CalendarIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline"; -import { DotsHorizontalIcon } from "@heroicons/react/solid"; -import { BookingStatus } from "@prisma/client"; -import dayjs from "dayjs"; -import { Fragment } from "react"; -import { useMutation } from "react-query"; +import { getSession } from "@lib/auth"; -import classNames from "@lib/classNames"; -import { HttpError } from "@lib/core/http/error"; -import { inferQueryOutput, trpc } from "@lib/trpc"; - -import EmptyScreen from "@components/EmptyScreen"; -import Loader from "@components/Loader"; -import Shell from "@components/Shell"; -import { Alert } from "@components/ui/Alert"; -import { Button } from "@components/ui/Button"; - -type BookingItem = inferQueryOutput<"viewer.bookings">[number]; - -function BookingListItem(booking: BookingItem) { - const utils = trpc.useContext(); - const mutation = useMutation( - async (confirm: boolean) => { - const res = await fetch("/api/book/confirm", { - method: "PATCH", - body: JSON.stringify({ id: booking.id, confirmed: confirm }), - headers: { - "Content-Type": "application/json", - }, - }); - if (!res.ok) { - throw new HttpError({ statusCode: res.status }); - } - }, - { - async onSettled() { - await utils.invalidateQuery(["viewer.bookings"]); - }, - } - ); - return ( - - - {!booking.confirmed && !booking.rejected && ( - - Unconfirmed - - )} -
- {booking.eventType?.team && {booking.eventType.team.name}: } - {booking.title} -
-
-
- {dayjs(booking.startTime).format("D MMMM YYYY")}:{" "} - - {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} - -
-
- {booking.attendees.length !== 0 && ( - - )} - - -
{dayjs(booking.startTime).format("D MMMM YYYY")}
-
- {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} -
- - - {!booking.confirmed && !booking.rejected && ( - <> -
- - -
- - {({ open }) => ( - <> -
- - Open options - -
- - -
- - {({ active }) => ( - mutation.mutate(true)} - className={classNames( - active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700", - "group flex items-center px-4 py-2 text-sm font-medium" - )}> - - )} - - - {({ active }) => ( - mutation.mutate(false)} - className={classNames( - active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700", - "group flex items-center px-4 py-2 text-sm w-full font-medium" - )}> - - )} - -
-
-
- - )} -
- - )} - {booking.confirmed && !booking.rejected && ( - <> -
- - -
- - {({ open }) => ( - <> -
- - Open options - -
- - - -
- - {({ active }) => ( - - - )} - - - {({ active }) => ( - - - )} - -
-
-
- - )} -
- - )} - {!booking.confirmed && booking.rejected &&
Rejected
} - - - ); +function RedirectPage() { + return null; } -export default function Bookings() { - const query = trpc.useQuery(["viewer.bookings"]); - const bookings = query.data; +export async function getServerSideProps(context) { + const session = await getSession(context); + if (!session?.user?.id) { + return { redirect: { permanent: false, destination: "/auth/login" } }; + } - return ( - -
-
-
- {query.status === "error" && ( - - )} - {query.status === "loading" && } - {bookings && - (bookings.length === 0 ? ( - - ) : ( -
- - - {bookings - .filter((booking) => booking.status !== BookingStatus.CANCELLED) - .map((booking) => ( - - ))} - -
-
- ))} -
-
-
-
- ); + return { redirect: { permanent: false, destination: "/bookings/upcoming" } }; } + +export default RedirectPage; diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index 05d4728ec4..d5473b01c4 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -27,7 +27,12 @@ import Select, { OptionTypeBase } from "react-select"; import { StripeData } from "@ee/lib/stripe/server"; -import { asNumberOrThrow, asNumberOrUndefined, asStringOrThrow } from "@lib/asStringOrNull"; +import { + asNumberOrThrow, + asNumberOrUndefined, + asStringOrThrow, + asStringOrUndefined, +} from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; @@ -137,13 +142,13 @@ const EventTypePage = (props: inferSSRProps) => { const isAdvancedSettingsVisible = !!eventNameRef.current; useEffect(() => { - setSelectedTimeZone(eventType.timeZone); + setSelectedTimeZone(eventType.timeZone || ""); }, []); - async function updateEventTypeHandler(event) { + async function updateEventTypeHandler(event: React.FormEvent) { event.preventDefault(); - const formData = Object.fromEntries(new FormData(event.target).entries()); + const formData = Object.fromEntries(new FormData(event.currentTarget).entries()); const enteredTitle: string = titleRef.current!.value; @@ -191,7 +196,7 @@ const EventTypePage = (props: inferSSRProps) => { updateMutation.mutate(payload); } - async function deleteEventTypeHandler(event) { + async function deleteEventTypeHandler(event: React.MouseEvent) { event.preventDefault(); const payload = { id: eventType.id }; @@ -218,33 +223,34 @@ const EventTypePage = (props: inferSSRProps) => { setSuccessModalOpen(false); }; - const updateLocations = (e) => { + const updateLocations = (e: React.FormEvent) => { e.preventDefault(); + const newLocation = e.currentTarget.location.value; let details = {}; - if (e.target.location.value === LocationType.InPerson) { - details = { address: e.target.address.value }; + if (newLocation === LocationType.InPerson) { + details = { address: e.currentTarget.address.value }; } - const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type); + const existingIdx = locations.findIndex((loc) => newLocation === 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 })); + setLocations(locations.concat({ type: newLocation, ...details })); } setShowLocationModal(false); }; - const removeLocation = (selectedLocation) => { + const removeLocation = (selectedLocation: typeof eventType.locations[number]) => { setLocations(locations.filter((location) => location.type !== selectedLocation.type)); }; const openEditCustomModel = (customInput: EventTypeCustomInput) => { setSelectedCustomInput(customInput); - setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)); + setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)!); setShowAddCustomModal(true); }; @@ -283,14 +289,16 @@ const EventTypePage = (props: inferSSRProps) => { return null; }; - const updateCustom = (e) => { + const updateCustom = (e: React.FormEvent) => { e.preventDefault(); const customInput: EventTypeCustomInput = { - label: e.target.label.value, - placeholder: e.target.placeholder?.value, - required: e.target.required.checked, - type: e.target.type.value, + id: -1, + eventTypeId: -1, + label: e.currentTarget.label.value, + placeholder: e.currentTarget.placeholder?.value, + required: e.currentTarget.required.checked, + type: e.currentTarget.type.value, }; if (selectedCustomInput) { @@ -309,7 +317,7 @@ const EventTypePage = (props: inferSSRProps) => { setCustomInputs([...customInputs]); }; - const schedulingTypeOptions: { value: string; label: string }[] = [ + const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [ { value: SchedulingType.COLLECTIVE, label: "Collective", @@ -327,6 +335,24 @@ const EventTypePage = (props: inferSSRProps) => { endDate: new Date(eventType.periodEndDate || Date.now()), }); + const permalink = `${process.env.NEXT_PUBLIC_APP_URL}/${ + team ? `team/${team.slug}` : eventType.users[0].username + }/${eventType.slug}`; + + const mapUserToValue = ({ + id, + name, + avatar, + }: { + id: number | null; + name: string | null; + avatar: string | null; + }) => ({ + value: `${id || ""}`, + label: `${name || ""}`, + avatar: `${avatar || ""}`, + }); + return (
) => { defaultValue={eventType.title} /> } - subtitle={eventType.description}> + subtitle={eventType.description || ""}>
@@ -403,10 +429,10 @@ const EventTypePage = (props: inferSSRProps) => { name="location" id="location" options={locationOptions} - isSearchable="false" + isSearchable={false} classNamePrefix="react-select" className="flex-1 block w-full min-w-0 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm" - onChange={(e) => openLocationModal(e.value)} + onChange={(e) => openLocationModal(e?.value)} />
)} @@ -534,7 +560,7 @@ const EventTypePage = (props: inferSSRProps) => { id="description" className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm" placeholder="A quick video meeting." - defaultValue={eventType.description}> + defaultValue={asStringOrUndefined(eventType.description)}>
@@ -551,7 +577,7 @@ const EventTypePage = (props: inferSSRProps) => {
@@ -564,17 +590,9 @@ const EventTypePage = (props: inferSSRProps) => {
setUsers(options.map((option) => option.value))} - defaultValue={eventType.users.map((user: User) => ({ - value: user.id, - label: user.name, - avatar: user.avatar, - }))} - options={teamMembers.map((user: User) => ({ - value: user.id, - label: user.name, - avatar: user.avatar, - }))} + onChange={(options) => setUsers(options.map((option) => option.value))} + defaultValue={eventType.users.map(mapUserToValue)} + options={teamMembers.map(mapUserToValue)} id="users" placeholder="Add attendees" /> @@ -921,7 +939,7 @@ const EventTypePage = (props: inferSSRProps) => { label="Hide event type" /> @@ -930,12 +948,7 @@ const EventTypePage = (props: inferSSRProps) => {
@@ -51,18 +53,16 @@ function TeamPage({ team }: InferGetServerSidePropsType ); + const teamName = team.name || "Nameless Team"; + return ( isReady && (
- +
- - {team.name} + + {teamName}
{(showMembers.isOn || !team.eventTypes.length) && } {!showMembers.isOn && team.eventTypes.length && ( @@ -97,10 +97,19 @@ function TeamPage({ team }: InferGetServerSidePropsType { +export const getServerSideProps = async (context: GetServerSidePropsContext) => { const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug; - const teamSelectInput = { + const userSelect = Prisma.validator()({ + username: true, + avatar: true, + email: true, + name: true, + id: true, + bio: true, + }); + + const teamSelect = Prisma.validator()({ id: true, name: true, slug: true, @@ -108,13 +117,7 @@ export const getServerSideProps = async (context) => { members: { select: { user: { - select: { - username: true, - avatar: true, - name: true, - id: true, - bio: true, - }, + select: userSelect, }, }, }, @@ -129,36 +132,29 @@ export const getServerSideProps = async (context) => { length: true, slug: true, schedulingType: true, + price: true, + currency: true, users: { - select: { - id: true, - name: true, - avatar: true, - email: true, - }, + select: userSelect, }, }, }, - }; + }); const team = await prisma.team.findUnique({ where: { slug, }, - select: teamSelectInput, + select: teamSelect, }); - if (!team) { - return { - notFound: true, - }; - } + if (!team) return { notFound: true }; team.eventTypes = team.eventTypes.map((type) => ({ ...type, users: type.users.map((user) => ({ ...user, - avatar: user.avatar || defaultAvatarSrc({ email: user.email }), + avatar: user.avatar || defaultAvatarSrc({ email: user.email || "" }), })), })); diff --git a/pages/team/[slug]/[type].tsx b/pages/team/[slug]/[type].tsx index d114484449..440e7db086 100644 --- a/pages/team/[slug]/[type].tsx +++ b/pages/team/[slug]/[type].tsx @@ -1,20 +1,24 @@ -import { Availability, EventType } from "@prisma/client"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrNull } from "@lib/asStringOrNull"; +import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; +import { inferSSRProps } from "@lib/types/inferSSRProps"; import AvailabilityPage from "@components/booking/pages/AvailabilityPage"; -export default function TeamType(props: InferGetServerSidePropsType) { +export type AvailabilityTeamPageProps = inferSSRProps; + +export default function TeamType(props: AvailabilityTeamPageProps) { return ; } export const getServerSideProps = async (context: GetServerSidePropsContext) => { - // get query params and typecast them to string - // (would be even better to assert them instead of typecasting) + const locale = await extractLocaleInfo(context.req); const slugParam = asStringOrNull(context.query.slug); const typeParam = asStringOrNull(context.query.type); + const dateParam = asStringOrNull(context.query.date); if (!slugParam || !typeParam) { throw new Error(`File is not named [idOrSlug]/[user]`); @@ -49,6 +53,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => description: true, length: true, schedulingType: true, + periodStartDate: true, + periodEndDate: true, }, }, }, @@ -57,23 +63,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (!team || team.eventTypes.length != 1) { return { notFound: true, - } as const; + }; } - const profile = { - name: team.name, - slug: team.slug, - image: team.logo || null, - }; + const [eventType] = team.eventTypes; - const eventType: EventType = team.eventTypes[0]; + type Availability = typeof eventType["availability"]; + const getWorkingHours = (availability: Availability) => (availability?.length ? availability : null); + const workingHours = getWorkingHours(eventType.availability) || []; - const getWorkingHours = (providesAvailability: { availability: Availability[] }) => - providesAvailability.availability && providesAvailability.availability.length - ? providesAvailability.availability - : null; - - const workingHours = getWorkingHours(eventType) || []; workingHours.sort((a, b) => a.startTime - b.startTime); const eventTypeObject = Object.assign({}, eventType, { @@ -83,10 +81,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { - profile, - team, + localeProp: locale, + profile: { + name: team.name, + slug: team.slug, + image: team.logo || null, + theme: null, + }, + date: dateParam, eventType: eventTypeObject, workingHours, + ...(await serverSideTranslations(locale, ["common"])), }, }; };