Inital UI + layout setup

feat/troubleshooter-v2
Sean Brydon 2023-10-19 14:20:45 +01:00
parent efc3e864bb
commit f93d2d83e5
9 changed files with 387 additions and 130 deletions

View File

@ -1,139 +1,23 @@
import dayjs from "@calcom/dayjs";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import { getLayout } from "@calcom/features/troubleshooter/layout";
import PageWrapper from "@components/PageWrapper";
type User = RouterOutputs["viewer"]["me"];
export interface IBusySlot {
start: string | Date;
end: string | Date;
title?: string;
source?: string | null;
}
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
const selectedDate = dayjs(date);
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
username: user.username || "",
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
{
enabled: !!user.username,
}
);
const overrides =
data?.dateOverrides.reduce((acc, override) => {
if (
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
)
return acc;
acc.push({ ...override, source: "Date override" });
return acc;
}, [] as IBusySlot[]) || [];
function TroubleshooterPage() {
// TODO: carry out the same logic for size based on the screen size
const extraDays = 7;
const startDate = dayjs().toDate();
const endDate = dayjs(startDate)
.add(extraDays - 1, "day")
.toDate();
return (
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline h-8 border-none bg-inherit p-0"
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(e.target.value);
}}
/>
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{(() => {
if (isLoading)
return (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
);
if (data && (data.busy.length > 0 || overrides.length > 0))
return [...data.busy, ...overrides]
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="bg-subtle overflow-hidden rounded-md"
data-testid="troubleshooter-busy-time">
<div className="text-emphasis px-4 py-5 sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
));
return (
<div className="bg-subtle overflow-hidden rounded-md">
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
})()}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
</div>
</div>
</div>
</div>
</div>
);
};
export default function Troubleshoot() {
const { data, isLoading } = trpc.viewer.me.useQuery();
const { t } = useLocale();
return (
<div>
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}>
{!isLoading && data && <AvailabilityView user={data} />}
</Shell>
<div className="h-full [--calendar-dates-sticky-offset:66px]">
<Calendar startHour={0} endHour={23} startDate={startDate} endDate={endDate} events={[]} />
</div>
);
}
Troubleshoot.PageWrapper = PageWrapper;
function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60);
const m = mins % 60;
const hs = h < 10 ? `0${h}` : h;
const ms = m < 10 ? `0${m}` : m;
return `${hs}:${ms}`;
}
TroubleshooterPage.getLayout = getLayout;
TroubleshooterPage.PageWrapper = PageWrapper;
export default TroubleshooterPage;

View File

@ -0,0 +1,139 @@
import dayjs from "@calcom/dayjs";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import PageWrapper from "@components/PageWrapper";
type User = RouterOutputs["viewer"]["me"];
export interface IBusySlot {
start: string | Date;
end: string | Date;
title?: string;
source?: string | null;
}
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
const selectedDate = dayjs(date);
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
username: user.username || "",
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
{
enabled: !!user.username,
}
);
const overrides =
data?.dateOverrides.reduce((acc, override) => {
if (
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
)
return acc;
acc.push({ ...override, source: "Date override" });
return acc;
}, [] as IBusySlot[]) || [];
return (
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline h-8 border-none bg-inherit p-0"
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(e.target.value);
}}
/>
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{(() => {
if (isLoading)
return (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
);
if (data && (data.busy.length > 0 || overrides.length > 0))
return [...data.busy, ...overrides]
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="bg-subtle overflow-hidden rounded-md"
data-testid="troubleshooter-busy-time">
<div className="text-emphasis px-4 py-5 sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
));
return (
<div className="bg-subtle overflow-hidden rounded-md">
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
})()}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
</div>
</div>
</div>
</div>
</div>
);
};
export default function Troubleshoot() {
const { data, isLoading } = trpc.viewer.me.useQuery();
const { t } = useLocale();
return (
<div>
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}>
{!isLoading && data && <AvailabilityView user={data} />}
</Shell>
</div>
);
}
Troubleshoot.PageWrapper = PageWrapper;
function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60);
const m = mins % 60;
const hs = h < 10 ? `0${h}` : h;
const ms = m < 10 ? `0${m}` : m;
return `${hs}:${ms}`;
}

View File

@ -2095,5 +2095,10 @@
"overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"troubleshooting":"Troubleshooting",
"calendars_were_checking_for_conflicts":"Calendars were checking for conflicts",
"availabilty_schedules":"Availability schedules",
"manage calendars":"Manage calendars",
"manage_availability_schedules":"Manage availability schedules",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -0,0 +1,38 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Button, Switch } from "@calcom/ui";
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
function AvailabiltyItem() {
const { t } = useLocale();
return (
<TroubleshooterListItemContainer
title="Office Hours"
subtitle="Mon-Fri; 9:00 AM - 5:00 PM"
suffixSlot={
<div>
<Badge variant="green" withDot size="sm">
Connected
</Badge>
</div>
}>
<div className="flex flex-col gap-3">
<p className="text-subtle text-sm font-medium leading-none">{t("date_overrides")}</p>
<Switch label="google@calendar.com" />
</div>
</TroubleshooterListItemContainer>
);
}
export function AvailabiltySchedulesContainer() {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-3">
<p className="text-sm font-medium leading-none">{t("availabilty_schedules")}</p>
<AvailabiltyItem />
<Button color="secondary" className="justify-center gap-2">
{t("manage_availabilty_schedules")}
</Button>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Button, Switch } from "@calcom/ui";
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
function CalendarToggleItem() {
return (
<TroubleshooterListItemContainer
title="Google Cal"
subtitle="google@calendar.com"
prefixSlot={
<>
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
</>
}
suffixSlot={
<div>
<Badge variant="green" withDot size="sm">
Connected
</Badge>
</div>
}>
<div className="flex flex-col gap-3">
<Switch label="google@calendar.com" />
<Switch label="google@calendar.com" />
</div>
</TroubleshooterListItemContainer>
);
}
export function CalendarToggleContainer() {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-3">
<p className="text-sm font-medium leading-none">{t("calendars_were_checking_for_conflicts")}</p>
<CalendarToggleItem />
<CalendarToggleItem />
<Button color="secondary" className="justify-center gap-2">
{t("manage_calendars")}
</Button>
</div>
);
}

View File

@ -0,0 +1,38 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
function ConnectedAppsItem() {
return (
<TroubleshooterListItemHeader
title="Google Cal"
subtitle="google@calendar.com"
prefixSlot={
<>
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
</>
}
suffixSlot={
<div>
<Badge variant="green" withDot size="sm">
Connected
</Badge>
</div>
}
/>
);
}
export function ConnectedAppsContainer() {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-3">
<p className="text-sm font-medium leading-none">{t("other_apps")}</p>
<div className="[&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md [&>*:last-child]:border-b">
<ConnectedAppsItem />
<ConnectedAppsItem />
</div>
</div>
);
}

View File

@ -0,0 +1,38 @@
import type { PropsWithChildren } from "react";
interface TroubleshooterListItemContainerProps {
title: string;
subtitle: string;
suffixSlot?: React.ReactNode;
prefixSlot?: React.ReactNode;
}
export function TroubleshooterListItemHeader({
prefixSlot,
title,
subtitle,
suffixSlot,
}: TroubleshooterListItemContainerProps) {
return (
<div className="border-subtle flex max-w-full gap-3 border border-b-0 px-4 py-2">
{prefixSlot}
<div className="flex h-full max-w-full flex-1 flex-col flex-nowrap truncate text-sm leading-4">
<p className="font-semibold">{title}</p>
<p className="font-normal">{subtitle}</p>
</div>
{suffixSlot}
</div>
);
}
export function TroubleshooterListItemContainer({
children,
...rest
}: PropsWithChildren<TroubleshooterListItemContainerProps>) {
return (
<div className="[&>*:first-child]:rounded-t-md ">
<TroubleshooterListItemHeader {...rest} />
<div className="border-subtle flex flex-col space-y-3 rounded-b-md border p-4">{children}</div>
</div>
);
}

View File

@ -0,0 +1,72 @@
import Link from "next/link";
import type { ComponentProps } from "react";
import React, { Suspense } from "react";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ErrorBoundary, Skeleton } from "@calcom/ui";
import { ArrowLeft, Loader } from "@calcom/ui/components/icon";
import { AvailabiltySchedulesContainer } from "./components/AvailabilitySchedulesContainer";
import { CalendarToggleContainer } from "./components/CalendarToggleContainer";
import { ConnectedAppsContainer } from "./components/ConnectedAppsContainer";
const BackButtonInSidebar = ({ name }: { name: string }) => {
return (
<Link
href="/"
className="tracking-none leading-full sticky inline-flex items-center gap-2 self-stretch text-xl font-semibold">
<ArrowLeft className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
<Skeleton title={name} as="p" className="min-h-4 truncate" loadingClassName="ms-3">
{name}
</Skeleton>
</Link>
);
};
interface SettingsSidebarContainerProps {
className?: string;
}
const SettingsSidebarContainer = ({ className = "" }: SettingsSidebarContainerProps) => {
const { t } = useLocale();
return (
<nav
className={classNames(
"scroll-bar bg-default border-subtle flex max-h-full max-h-screen w-1/4 flex-col flex-col gap-6 space-y-1 overflow-x-hidden overflow-y-scroll border-r p-6",
className
)}
aria-label="Tabs">
<>
<BackButtonInSidebar name={t("troubleshooting")} />
<CalendarToggleContainer />
<AvailabiltySchedulesContainer />
<ConnectedAppsContainer />
</>
</nav>
);
};
export default function TroubleshooterLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell
withoutSeo={true}
flexChildrenContainer
hideHeadingOnMobile
{...rest}
SidebarContainer={<SettingsSidebarContainer />}>
<div className="flex flex-1 [&>*]:flex-1">
<ErrorBoundary>
<Suspense fallback={<Loader />}>{children}</Suspense>
</ErrorBoundary>
</div>
</Shell>
);
}
export const getLayout = (page: React.ReactElement) => <TroubleshooterLayout>{page}</TroubleshooterLayout>;