Inital UI + layout setup
parent
efc3e864bb
commit
f93d2d83e5
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
}
|
|
@ -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 we’re 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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>;
|
Loading…
Reference in New Issue