diff --git a/components/eventtype/EventTypeList.tsx b/components/eventtype/EventTypeList.tsx
deleted file mode 100644
index de2cf33a..00000000
--- a/components/eventtype/EventTypeList.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-// TODO: replace headlessui with radix-ui
-import { Menu, Transition } from "@headlessui/react";
-import { DotsHorizontalIcon, ExternalLinkIcon, LinkIcon } from "@heroicons/react/solid";
-import Link from "next/link";
-import React, { Fragment } from "react";
-
-import classNames from "@lib/classNames";
-import { useLocale } from "@lib/hooks/useLocale";
-import showToast from "@lib/notification";
-
-import { Tooltip } from "@components/Tooltip";
-import EventTypeDescription from "@components/eventtype/EventTypeDescription";
-import AvatarGroup from "@components/ui/AvatarGroup";
-
-interface Props {
- profile: { slug: string };
- readOnly: boolean;
- types: {
- $disabled: boolean;
- hidden: boolean;
- id: number;
- slug: string;
- title: string;
- users: {
- name: string;
- avatar: string;
- }[];
- };
-}
-
-const EventTypeList = ({ readOnly, types, profile }: Props): JSX.Element => {
- const { t } = useLocale();
- return (
-
-
- {types.map((type) => (
- -
-
-
-
-
-
-
-
- ))}
-
-
- );
-};
-
-export default EventTypeList;
diff --git a/components/eventtype/EventTypeListHeading.tsx b/components/eventtype/EventTypeListHeading.tsx
deleted file mode 100644
index cbf4b884..00000000
--- a/components/eventtype/EventTypeListHeading.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-// TODO: replace headlessui with radix-ui
-import { UsersIcon } from "@heroicons/react/solid";
-import Link from "next/link";
-import React from "react";
-
-import Avatar from "@components/ui/Avatar";
-import Badge from "@components/ui/Badge";
-
-interface Props {
- profile: {
- slug?: string | null;
- name?: string | null;
- image?: string | null;
- };
- membershipCount: number;
-}
-
-const EventTypeListHeading = ({ profile, membershipCount }: Props): JSX.Element => (
-
-);
-
-export default EventTypeListHeading;
diff --git a/components/eventtype/CustomInputTypeForm.tsx b/components/pages/eventtypes/CustomInputTypeForm.tsx
similarity index 100%
rename from components/eventtype/CustomInputTypeForm.tsx
rename to components/pages/eventtypes/CustomInputTypeForm.tsx
diff --git a/lib/mutations/event-types/create-event-type.ts b/lib/mutations/event-types/create-event-type.ts
index 54a1e738..9849da3e 100644
--- a/lib/mutations/event-types/create-event-type.ts
+++ b/lib/mutations/event-types/create-event-type.ts
@@ -1,10 +1,11 @@
-import { EventType } from "@prisma/client";
-
import * as fetch from "@lib/core/http/fetch-wrapper";
-import { CreateEventType } from "@lib/types/event-type";
+import { CreateEventType, CreateEventTypeResponse } from "@lib/types/event-type";
const createEventType = async (data: CreateEventType) => {
- const response = await fetch.post("/api/availability/eventtype", data);
+ const response = await fetch.post(
+ "/api/availability/eventtype",
+ data
+ );
return response;
};
diff --git a/lib/types/event-type.ts b/lib/types/event-type.ts
index a535e0c0..337d83b6 100644
--- a/lib/types/event-type.ts
+++ b/lib/types/event-type.ts
@@ -1,4 +1,4 @@
-import { SchedulingType } from "@prisma/client";
+import { SchedulingType, EventType } from "@prisma/client";
export type OpeningHours = {
days: number[];
@@ -49,9 +49,14 @@ export type CreateEventType = {
slug: string;
description: string;
length: number;
+ teamId?: number;
schedulingType?: SchedulingType;
};
+export type CreateEventTypeResponse = {
+ eventType: EventType;
+};
+
export type EventTypeInput = AdvancedOptions & {
id: number;
title: string;
diff --git a/package.json b/package.json
index 57a6d53d..4f1ae066 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"test": "jest",
"test-playwright": "jest --config jest.playwright.config.js",
"test-playwright-lcov": "cross-env PLAYWRIGHT_HEADLESS=1 PLAYWRIGHT_COVERAGE=1 yarn test-playwright && nyc report --reporter=lcov",
+ "type-check": "tsc --pretty --noEmit",
"build": "next build",
"start": "next start",
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\"",
diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx
index 759044cb..ae890c86 100644
--- a/pages/event-types/[type].tsx
+++ b/pages/event-types/[type].tsx
@@ -50,7 +50,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog";
import Shell from "@components/Shell";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
-import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
+import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
import Button from "@components/ui/Button";
import { Scheduler } from "@components/ui/Scheduler";
import Switch from "@components/ui/Switch";
diff --git a/pages/event-types/index.tsx b/pages/event-types/index.tsx
index 32f5367c..aa082a19 100644
--- a/pages/event-types/index.tsx
+++ b/pages/event-types/index.tsx
@@ -1,31 +1,33 @@
// TODO: replace headlessui with radix-ui
+import { Menu, Transition } from "@headlessui/react";
+import { UsersIcon } from "@heroicons/react/solid";
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
-import { Prisma, SchedulingType } from "@prisma/client";
-import { GetServerSidePropsContext } from "next";
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { DotsHorizontalIcon, ExternalLinkIcon, LinkIcon } from "@heroicons/react/solid";
+import { SchedulingType } from "@prisma/client";
import Head from "next/head";
+import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useRef } from "react";
import { useMutation } from "react-query";
-import { asStringOrNull } from "@lib/asStringOrNull";
-import { getSession } from "@lib/auth";
+import { QueryCell } from "@lib/QueryCell";
+import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
-import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
-import { ONBOARDING_NEXT_REDIRECT, shouldShowOnboarding } from "@lib/getting-started";
import { useLocale } from "@lib/hooks/useLocale";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import createEventType from "@lib/mutations/event-types/create-event-type";
import showToast from "@lib/notification";
-import prisma from "@lib/prisma";
-import { inferSSRProps } from "@lib/types/inferSSRProps";
+import { inferQueryOutput, trpc } from "@lib/trpc";
+import { CreateEventType } from "@lib/types/event-type";
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
import Shell from "@components/Shell";
-import EventTypeList from "@components/eventtype/EventTypeList";
-import EventTypeListHeading from "@components/eventtype/EventTypeListHeading";
+import { Tooltip } from "@components/Tooltip";
+import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import { Alert } from "@components/ui/Alert";
import Avatar from "@components/ui/Avatar";
+import AvatarGroup from "@components/ui/AvatarGroup";
+import Badge from "@components/ui/Badge";
import { Button } from "@components/ui/Button";
import Dropdown, {
DropdownMenuContent,
@@ -37,22 +39,236 @@ import Dropdown, {
import * as RadioArea from "@components/ui/form/radio-area";
import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration";
-type PageProps = inferSSRProps;
-type Profile = PageProps["profiles"][number];
+type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"];
-const EventTypesPage = (props: PageProps) => {
+interface CreateEventTypeProps {
+ canAddEvents: boolean;
+ profiles: Profiles;
+}
+
+const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => {
const { t } = useLocale();
- const CreateFirstEventTypeView = () => (
+ return (
{t("new_event_type_heading")}
{t("new_event_type_description")}
-
+
);
+};
+
+type EventTypeGroup = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"][number];
+type EventType = EventTypeGroup["eventTypes"][number];
+interface EventTypeListProps {
+ profile: { slug: string | null };
+ readOnly: boolean;
+ types: EventType[];
+}
+const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.Element => {
+ const { t } = useLocale();
+ return (
+
+
+ {types.map((type) => (
+ -
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+interface EventTypeListHeadingProps {
+ profile: Profile;
+ membershipCount: number;
+}
+const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => (
+
+);
+
+const EventTypesPage = () => {
+ const { t } = useLocale();
+ const query = trpc.useQuery(["viewer.eventTypes"]);
return (
@@ -64,50 +280,60 @@ const EventTypesPage = (props: PageProps) => {
heading={t("event_types_page_title")}
subtitle={t("event_types_page_subtitle")}
CTA={
- props.eventTypes.length !== 0 && (
-
+ query.data &&
+ query.data.eventTypeGroups.length !== 0 && (
+
)
}>
- {props.user.plan === "FREE" && !props.canAddEvents && (
-
{t("plan_upgrade")}>}
- message={
- <>
- {t("to_upgrade_go_to")}{" "}
-
- {"https://cal.com/upgrade"}
-
- >
- }
- className="my-4"
- />
- )}
- {props.eventTypes &&
- props.eventTypes.map((input) => (
-
- {/* hide list heading when there is only one (current user) */}
- {(props.eventTypes.length !== 1 || input.teamId) && (
- (
+ <>
+ {data.user.plan === "FREE" && !data.canAddEvents && (
+ {t("plan_upgrade")}>}
+ message={
+ <>
+ {t("to_upgrade_go_to")}{" "}
+
+ {"https://cal.com/upgrade"}
+
+ >
+ }
+ className="my-4"
/>
)}
-
-
- ))}
+ {data.eventTypeGroups &&
+ data.eventTypeGroups.map((input) => (
+
+ {/* hide list heading when there is only one (current user) */}
+ {(data.eventTypeGroups.length !== 1 || input.teamId) && (
+
+ )}
+
+
+ ))}
- {props.eventTypes.length === 0 && }
+ {data.eventTypeGroups.length === 0 && (
+
+ )}
+ >
+ )}
+ />
);
};
-const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[]; canAddEvents: boolean }) => {
+const CreateNewEventButton = ({ profiles, canAddEvents }: CreateEventTypeProps) => {
const router = useRouter();
const teamId: number | null = Number(router.query.teamId) || null;
const modalOpen = useToggleQuery("new");
@@ -173,12 +399,7 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[];
},
})
}>
-
+
{profile.name ? profile.name : profile.slug}
))}
@@ -203,7 +424,7 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[];
{ value: string }
>;
- const payload = {
+ const payload: CreateEventType = {
title: target.title.value,
slug: target.slug.value,
description: target.description.value,
@@ -211,8 +432,8 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[];
};
if (router.query.teamId) {
- payload.teamId = parseInt(asStringOrNull(router.query.teamId), 10);
- payload.schedulingType = target.schedulingType.value;
+ payload.teamId = parseInt(`${router.query.teamId}`, 10);
+ payload.schedulingType = target.schedulingType.value as SchedulingType;
}
createMutation.mutate(payload);
@@ -325,188 +546,4 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[];
);
};
-export async function getServerSideProps(context: GetServerSidePropsContext) {
- const session = await getSession(context);
- const locale = await getOrSetUserLocaleFromHeaders(context.req);
-
- if (!session?.user?.id) {
- return { redirect: { permanent: false, destination: "/auth/login" } };
- }
-
- /**
- * This makes the select reusable and type safe.
- * @url https://www.prisma.io/docs/concepts/components/prisma-client/advanced-type-safety/prisma-validator#using-the-prismavalidator
- * */
- const eventTypeSelect = Prisma.validator()({
- id: true,
- title: true,
- description: true,
- length: true,
- schedulingType: true,
- slug: true,
- hidden: true,
- price: true,
- currency: true,
- users: {
- select: {
- id: true,
- avatar: true,
- name: true,
- },
- },
- });
-
- const user = await prisma.user.findUnique({
- where: {
- id: session.user.id,
- },
- select: {
- id: true,
- username: true,
- name: true,
- startTime: true,
- endTime: true,
- bufferTime: true,
- avatar: true,
- completedOnboarding: true,
- createdDate: true,
- plan: true,
- teams: {
- where: {
- accepted: true,
- },
- select: {
- role: true,
- team: {
- select: {
- id: true,
- name: true,
- slug: true,
- logo: true,
- members: {
- select: {
- userId: true,
- },
- },
- eventTypes: {
- select: eventTypeSelect,
- },
- },
- },
- },
- },
- eventTypes: {
- where: {
- team: null,
- },
- select: eventTypeSelect,
- },
- },
- });
-
- if (!user) {
- // this shouldn't happen
- return {
- redirect: {
- permanent: false,
- destination: "/auth/login",
- },
- };
- }
-
- if (
- shouldShowOnboarding({ completedOnboarding: user.completedOnboarding, createdDate: user.createdDate })
- ) {
- return ONBOARDING_NEXT_REDIRECT;
- }
-
- // backwards compatibility, TMP:
- const typesRaw = await prisma.eventType.findMany({
- where: {
- userId: session.user.id,
- },
- select: eventTypeSelect,
- });
-
- type EventTypeGroup = {
- teamId?: number | null;
- profile?: {
- slug: typeof user["username"];
- name: typeof user["name"];
- image: typeof user["avatar"];
- };
- metadata: {
- membershipCount: number;
- readOnly: boolean;
- };
- eventTypes: (typeof user.eventTypes[number] & { $disabled?: boolean })[];
- };
-
- let eventTypeGroups: EventTypeGroup[] = [];
- const eventTypesHashMap = user.eventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
- const oldItem = hashMap[newItem.id] || {};
- hashMap[newItem.id] = { ...oldItem, ...newItem };
- return hashMap;
- }, {} as Record);
- const mergedEventTypes = Object.values(eventTypesHashMap).map((et, index) => ({
- ...et,
- $disabled: user.plan === "FREE" && index > 0,
- }));
-
- eventTypeGroups.push({
- teamId: null,
- profile: {
- slug: user.username,
- name: user.name,
- image: user.avatar,
- },
- eventTypes: mergedEventTypes,
- metadata: {
- membershipCount: 1,
- readOnly: false,
- },
- });
-
- eventTypeGroups = ([] as EventTypeGroup[]).concat(
- eventTypeGroups,
- user.teams.map((membership) => ({
- teamId: membership.team.id,
- profile: {
- name: membership.team.name,
- image: membership.team.logo || "",
- slug: "team/" + membership.team.slug,
- },
- metadata: {
- membershipCount: membership.team.members.length,
- readOnly: membership.role !== "OWNER",
- },
- eventTypes: membership.team.eventTypes,
- }))
- );
-
- const userObj = Object.assign({}, user, {
- createdDate: user.createdDate.toString(),
- });
-
- const canAddEvents = user.plan !== "FREE" || eventTypeGroups[0].eventTypes.length < 1;
-
- return {
- props: {
- session,
- localeProp: locale,
- canAddEvents,
- user: userObj,
- // don't display event teams without event types,
- eventTypes: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length),
- // so we can show a dropdown when the user has teams
- profiles: eventTypeGroups.map((group) => ({
- teamId: group.teamId,
- ...group.profile,
- ...group.metadata,
- })),
- ...(await serverSideTranslations(locale, ["common"])),
- },
- };
-}
-
export default EventTypesPage;
diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx
index 3209286d..e58451f3 100644
--- a/server/routers/viewer.tsx
+++ b/server/routers/viewer.tsx
@@ -71,6 +71,156 @@ const loggedInViewerRouter = createProtectedRouter()
return me;
},
})
+ .query("eventTypes", {
+ async resolve({ ctx }) {
+ const { prisma } = ctx;
+ const eventTypeSelect = Prisma.validator()({
+ id: true,
+ title: true,
+ description: true,
+ length: true,
+ schedulingType: true,
+ slug: true,
+ hidden: true,
+ price: true,
+ currency: true,
+ users: {
+ select: {
+ id: true,
+ avatar: true,
+ name: true,
+ },
+ },
+ });
+
+ const user = await prisma.user.findUnique({
+ where: {
+ id: ctx.user.id,
+ },
+ select: {
+ id: true,
+ username: true,
+ name: true,
+ startTime: true,
+ endTime: true,
+ bufferTime: true,
+ avatar: true,
+ plan: true,
+ teams: {
+ where: {
+ accepted: true,
+ },
+ select: {
+ role: true,
+ team: {
+ select: {
+ id: true,
+ name: true,
+ slug: true,
+ logo: true,
+ members: {
+ select: {
+ userId: true,
+ },
+ },
+ eventTypes: {
+ select: eventTypeSelect,
+ },
+ },
+ },
+ },
+ },
+ eventTypes: {
+ where: {
+ team: null,
+ },
+ select: eventTypeSelect,
+ },
+ },
+ });
+
+ if (!user) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+
+ // backwards compatibility, TMP:
+ const typesRaw = await prisma.eventType.findMany({
+ where: {
+ userId: ctx.user.id,
+ },
+ select: eventTypeSelect,
+ });
+
+ type EventTypeGroup = {
+ teamId?: number | null;
+ profile: {
+ slug: typeof user["username"];
+ name: typeof user["name"];
+ image: typeof user["avatar"];
+ };
+ metadata: {
+ membershipCount: number;
+ readOnly: boolean;
+ };
+ eventTypes: (typeof user.eventTypes[number] & { $disabled?: boolean })[];
+ };
+
+ let eventTypeGroups: EventTypeGroup[] = [];
+ const eventTypesHashMap = user.eventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
+ const oldItem = hashMap[newItem.id] || {};
+ hashMap[newItem.id] = { ...oldItem, ...newItem };
+ return hashMap;
+ }, {} as Record);
+ const mergedEventTypes = Object.values(eventTypesHashMap).map((et, index) => ({
+ ...et,
+ $disabled: user.plan === "FREE" && index > 0,
+ }));
+
+ eventTypeGroups.push({
+ teamId: null,
+ profile: {
+ slug: user.username,
+ name: user.name,
+ image: user.avatar,
+ },
+ eventTypes: mergedEventTypes,
+ metadata: {
+ membershipCount: 1,
+ readOnly: false,
+ },
+ });
+
+ eventTypeGroups = ([] as EventTypeGroup[]).concat(
+ eventTypeGroups,
+ user.teams.map((membership) => ({
+ teamId: membership.team.id,
+ profile: {
+ name: membership.team.name,
+ image: membership.team.logo || "",
+ slug: "team/" + membership.team.slug,
+ },
+ metadata: {
+ membershipCount: membership.team.members.length,
+ readOnly: membership.role !== "OWNER",
+ },
+ eventTypes: membership.team.eventTypes,
+ }))
+ );
+
+ const canAddEvents = user.plan !== "FREE" || eventTypeGroups[0].eventTypes.length < 1;
+
+ return {
+ canAddEvents,
+ user,
+ // don't display event teams without event types,
+ eventTypeGroups: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length),
+ // so we can show a dropdown when the user has teams
+ profiles: eventTypeGroups.map((group) => ({
+ teamId: group.teamId,
+ ...group.profile,
+ ...group.metadata,
+ })),
+ };
+ },
+ })
.query("bookings", {
input: z.object({
status: z.enum(["upcoming", "past", "cancelled"]),