chore: Proposal for new module based file structure (#13396)

This commit is contained in:
Omar López
2024-01-25 17:31:08 -07:00
committed by GitHub
parent 3d8b5390df
commit 210bc9fc8c
11 changed files with 285 additions and 271 deletions
@@ -1,9 +1,10 @@
import { type PageProps, getServerSideProps } from "@pages/booking/[uid]";
import OldPage from "@pages/booking/[uid]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import OldPage from "~/bookings/views/bookings-single-view";
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
+3 -2
View File
@@ -1,4 +1,3 @@
import OldPage, { type PageProps } from "@pages/booking/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
@@ -8,9 +7,11 @@ import { cookies, headers } from "next/headers";
import { BookingStatus } from "@calcom/prisma/enums";
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import OldPage from "~/bookings/views/bookings-single-view";
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
@@ -1,4 +1,3 @@
import Page from "@pages/bookings/[status]";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
@@ -7,9 +6,9 @@ import type { InferGetStaticPropsType } from "next";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import { getStaticProps } from "@lib/bookings/[status]/getStaticProps";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
import { validStatuses } from "~/bookings/lib/validStatuses";
import Page from "~/bookings/views/bookings-listing-view";
import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
type Y = InferGetStaticPropsType<typeof getStaticProps>;
const getData = withAppDirSsg<Y>(getStaticProps);
@@ -0,0 +1 @@
export const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
@@ -3,7 +3,7 @@ import { z } from "zod";
import { getTranslations } from "@server/lib/getTranslations";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
import { validStatuses } from "~/bookings/lib/validStatuses";
const querySchema = z.object({
status: z.enum(validStatuses),
@@ -0,0 +1,252 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Fragment, useState } from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import dayjs from "@calcom/dayjs";
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import useMeQuery from "@lib/hooks/useMeQuery";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { validStatuses } from "~/bookings/lib/validStatuses";
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
cancelled: "cancelled_bookings",
unconfirmed: "unconfirmed_bookings",
};
const querySchema = z.object({
status: z.enum(validStatuses),
});
export default function Bookings() {
const params = useParamsWithFallback();
const { data: filterQuery } = useFilterQuery();
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
const { t } = useLocale();
const user = useMeQuery().data;
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
const query = trpc.viewer.bookings.get.useInfiniteQuery(
{
limit: 10,
filters: {
...filterQuery,
status: filterQuery.status ?? status,
},
},
{
// first render has status `undefined`
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
// Animate page (tab) transitions to look smoothing
const buttonInView = useInViewObserver(() => {
if (!query.isFetching && query.hasNextPage && query.status === "success") {
query.fetchNextPage();
}
});
const isEmpty = !query.data?.pages[0]?.bookings.length;
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
if (
shownBookings[booking.recurringEventId] !== undefined &&
shownBookings[booking.recurringEventId].length > 0
) {
shownBookings[booking.recurringEventId].push(booking);
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
}
return true;
};
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<HorizontalTabs tabs={tabs} />
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "pending" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</div>
</main>
</div>
</ShellMain>
);
}
@@ -9,6 +9,8 @@ import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUi
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
const stringToBoolean = z
@@ -29,6 +31,8 @@ const querySchema = z.object({
seatReferenceUid: z.string().optional(),
});
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export async function getServerSideProps(context: GetServerSidePropsContext) {
// this is needed to prevent bundling of lib/booking to the client bundle
// usually functions that are used in getServerSideProps are tree shaken from client bundle
@@ -44,17 +44,13 @@ import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import { timeZone } from "@lib/clock";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
export { getServerSideProps };
export type PageProps = inferSSRProps<typeof getServerSideProps>;
import type { PageProps } from "./bookings-single-view.getServerSideProps";
const stringToBoolean = z
.string()
@@ -837,7 +833,7 @@ type RecurringBookingsProps = {
tz: string;
};
export function RecurringBookings({
function RecurringBookings({
eventType,
recurringBookings,
duration,
+3 -4
View File
@@ -1,8 +1,7 @@
"use client";
import { getServerSideProps as _getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
export { default } from "../[uid]";
import { getServerSideProps as _getServerSideProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
export { default } from "~/bookings/views/bookings-single-view";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "~/bookings/views/bookings-single-view";
export { getServerSideProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
+11 -252
View File
@@ -1,263 +1,22 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { GetStaticPaths } from "next";
import { Fragment, useState } from "react";
import React from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import dayjs from "@calcom/dayjs";
import { getLayout } from "@calcom/features/MainLayout";
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { HorizontalTabs } from "@calcom/ui";
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { getStaticProps } from "@lib/bookings/[status]/getStaticProps";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import useMeQuery from "@lib/hooks/useMeQuery";
import PageWrapper from "@components/PageWrapper";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
import { validStatuses } from "~/bookings/lib/validStatuses";
import BookingsListingView from "~/bookings/views/bookings-listing-view";
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
export { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const BookingsListingPage = new Proxy<{
(): JSX.Element;
PageWrapper?: typeof PageWrapper;
getLayout?: typeof getLayout;
}>(BookingsListingView, {});
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
cancelled: "cancelled_bookings",
unconfirmed: "unconfirmed_bookings",
};
const querySchema = z.object({
status: z.enum(validStatuses),
});
export default function Bookings() {
const params = useParamsWithFallback();
const { data: filterQuery } = useFilterQuery();
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
const { t } = useLocale();
const user = useMeQuery().data;
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
const query = trpc.viewer.bookings.get.useInfiniteQuery(
{
limit: 10,
filters: {
...filterQuery,
status: filterQuery.status ?? status,
},
},
{
// first render has status `undefined`
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
// Animate page (tab) transitions to look smoothing
const buttonInView = useInViewObserver(() => {
if (!query.isFetching && query.hasNextPage && query.status === "success") {
query.fetchNextPage();
}
});
const isEmpty = !query.data?.pages[0]?.bookings.length;
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
if (
shownBookings[booking.recurringEventId] !== undefined &&
shownBookings[booking.recurringEventId].length > 0
) {
shownBookings[booking.recurringEventId].push(booking);
return false;
}
shownBookings[booking.recurringEventId] = [booking];
} else if (status === "upcoming") {
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
}
return true;
};
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
);
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<HorizontalTabs tabs={tabs} />
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "pending" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</div>
</main>
</div>
</ShellMain>
);
}
Bookings.PageWrapper = PageWrapper;
Bookings.getLayout = getLayout;
BookingsListingPage.PageWrapper = PageWrapper;
BookingsListingPage.getLayout = getLayout;
export const getStaticPaths: GetStaticPaths = () => {
return {
@@ -269,4 +28,4 @@ export const getStaticPaths: GetStaticPaths = () => {
};
};
export { getStaticProps };
export default BookingsListingPage;