Starting to take shape

This commit is contained in:
Alex van Andel 2022-09-28 23:54:41 +01:00
parent a792d31939
commit 8d111cede5
8 changed files with 217 additions and 414 deletions

View File

@ -1,191 +0,0 @@
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/[schedule].tsx` instead
*/
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { BadgeCheckIcon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import Switch from "@calcom/ui/Switch";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import Schedule from "@components/availability/Schedule";
import EditableHeading from "@components/ui/EditableHeading";
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/[schedule].tsx` instead
*/
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const form = useForm({
defaultValues: {
schedule: props.availability || DEFAULT_SCHEDULE,
isDefault: !!props.isDefault,
timeZone: props.timeZone,
},
});
const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
onSuccess: async ({ schedule }) => {
await utils.invalidateQueries(["viewer.availability.schedule"]);
await router.push("/availability");
showToast(
t("availability_updated_successfully", {
scheduleName: schedule.name,
}),
"success"
);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
return (
<Form
form={form}
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: parseInt(router.query.schedule as string, 10),
name: props.schedule.name,
...values,
});
}}
className="grid grid-cols-3 gap-2">
<div className="col-span-3 space-y-2 lg:col-span-2">
<div className="divide-y rounded-sm border border-gray-200 bg-white px-4 py-5 sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
<Schedule name="schedule" />
</div>
<div className="space-x-2 text-right">
<Button color="secondary" href="/availability" tabIndex={-1}>
{t("cancel")}
</Button>
<Button>{t("save")}</Button>
</div>
</div>
<div className="min-w-40 col-span-3 ml-2 space-y-2 lg:col-span-1">
{props.isDefault ? (
<div className="inline-block cursor-default rounded border border-gray-300 bg-gray-200 px-2 py-0.5 pl-1.5 text-sm font-medium text-neutral-800">
<span className="flex items-center">
<BadgeCheckIcon className="mr-1 h-4 w-4" /> {t("default")}
</span>
</div>
) : (
<Controller
name="isDefault"
render={({ field: { onChange, value } }) => (
<Switch label={t("set_to_default")} onCheckedChange={onChange} checked={value} />
)}
/>
)}
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<div className="mt-1">
<Controller
name="timeZone"
render={({ field: { onChange, value } }) => (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
)}
/>
</div>
</div>
<div className="mt-2 rounded-sm border border-gray-200 px-4 py-5 sm:p-6 ">
<h3 className="text-base font-medium leading-6 text-gray-900">
{t("something_doesnt_look_right")}
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>{t("troubleshoot_availability")}</p>
</div>
<div className="mt-5">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
</div>
</div>
</Form>
);
}
const querySchema = z.object({
schedule: stringOrNumber,
});
export default function Availability() {
const router = useRouter();
const { i18n } = useLocale();
const { schedule: scheduleId } = router.isReady ? querySchema.parse(router.query) : { schedule: -1 };
const query = trpc.useQuery(["viewer.availability.schedule", { scheduleId }], { enabled: router.isReady });
const [name, setName] = useState<string>();
return (
<div>
<QueryCell
query={query}
success={({ data }) => {
return (
<Shell
heading={<EditableHeading title={name || data.schedule.name} onChange={setName} />}
subtitle={data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language })}
<br />
</span>
))}>
<AvailabilityForm
{...{ ...data, schedule: { ...data.schedule, name: name || data.schedule.name } }}
/>
</Shell>
);
}}
/>
</div>
);
}
export const getStaticProps: GetStaticProps = (ctx) => {
const params = querySchema.safeParse(ctx.params);
if (!params.success) return { notFound: true };
return {
props: {
schedule: params.data.schedule,
},
revalidate: 10, // seconds
};
};
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: "blocking",
};
};

View File

@ -1,74 +0,0 @@
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/index.tsx` instead
*/
import { ScheduleListItem } from "@calcom/features/schedules/components/ScheduleListItem";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { Icon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
import SkeletonLoader from "@components/availability/SkeletonLoader";
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/index.tsx` instead
*/
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t } = useLocale();
const utils = trpc.useContext();
const deleteMutation = trpc.useMutation("viewer.availability.schedule.delete", {
onSuccess: async () => {
await utils.invalidateQueries(["viewer.availability.list"]);
showToast(t("schedule_deleted_successfully"), "success");
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
return (
<>
{schedules.length === 0 ? (
<EmptyScreen
Icon={Icon.FiClock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
/>
) : (
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
<ul className="divide-y divide-neutral-200" data-testid="schedules">
{schedules.map((schedule) => (
<ScheduleListItem
key={schedule.id}
schedule={schedule}
deleteFunction={deleteMutation.mutate}
/>
))}
</ul>
</div>
)}
</>
);
}
const WithQuery = withQuery(["viewer.availability.list"]);
export default function AvailabilityPage() {
const { t } = useLocale();
return (
<div>
<Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
</Shell>
</div>
);
}

View File

@ -1,121 +0,0 @@
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
*/
import type { IBusySlot } from "pages/v2/availability/troubleshoot";
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Shell from "@calcom/ui/Shell";
import { QueryCell } from "@lib/QueryCell";
import Loader from "@components/Loader";
type User = inferQueryOutput<"viewer.me">;
/**
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
*/
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const [selectedDate, setSelectedDate] = useState(dayjs());
const { data, isLoading } = trpc.useQuery(
[
"viewer.availability.user",
{
username: user.username!,
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
],
{
enabled: !!user.username,
}
);
return (
<div className="max-w-xl overflow-hidden rounded-sm bg-white shadow">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline h-8 border-none p-0"
defaultValue={selectedDate.format("YYYY-MM-DD")}
onChange={(e) => {
if (e.target.value) setSelectedDate(dayjs(e.target.value));
}}
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-sm">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{isLoading ? (
<Loader />
) : data && data.busy.length > 0 ? (
data.busy
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={`${slot.start}-${slot.title ?? "untitled"}`}
className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{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>
))
) : (
<div className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
)}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-sm">
<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 query = trpc.useQuery(["viewer.me"]);
const { t } = useLocale();
return (
<div>
<Shell heading={t("troubleshoot")} subtitle={t("troubleshoot_description")}>
<QueryCell query={query} success={({ data }) => <AvailabilityView user={data} />} />
</Shell>
</div>
);
}
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

@ -1,6 +1,16 @@
import {
DropdownMenuCheckboxItem as PrimitiveDropdownMenuCheckboxItem,
DropdownMenuCheckboxItemProps,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@radix-ui/react-dropdown-menu";
import classNames from "classnames";
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { useEffect } from "react";
import React from "react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@ -14,6 +24,7 @@ import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import { Icon } from "@calcom/ui";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import Button from "@calcom/ui/v2/core/Button";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/v2/core/Dropdown";
import Shell from "@calcom/ui/v2/core/Shell";
import Switch from "@calcom/ui/v2/core/Switch";
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
@ -36,6 +47,101 @@ type AvailabilityFormValues = {
isDefault: boolean;
};
const DropdownMenuCheckboxItem = React.forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
({ children }, ref) => (
<PrimitiveDropdownMenuCheckboxItem ref={ref}>
<label className="flex w-60 items-center justify-between">
{children}
<input
type="checkbox"
onClick={(e) => e.stopPropagation()}
onChange={(e) => e.stopPropagation()}
className="inline-block rounded-[4px] border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
/>
</label>
</PrimitiveDropdownMenuCheckboxItem>
)
);
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
const ActiveOnEventTypeSelect = () => {
const { t } = useLocale();
const [isOpen, setOpen] = useState(false);
const { data } = trpc.useQuery(["viewer.eventTypes"]);
const eventTypeGroups = data?.eventTypeGroups.reduce((aggregate, eventTypeGroups) => {
if (eventTypeGroups.eventTypes[0].team !== null) {
aggregate.push({
groupName: eventTypeGroups.eventTypes[0].team.name || "",
eventTypeNames: [...eventTypeGroups.eventTypes.map((eventType) => eventType.title)],
});
} else {
aggregate.push({
groupName: eventTypeGroups.eventTypes[0].users[0].name || "",
eventTypeNames: [...eventTypeGroups.eventTypes.map((eventType) => eventType.title)],
});
}
return aggregate;
}, [] as { groupName: string; eventTypeNames: string[] }[]);
return (
<Dropdown onOpenChange={setOpen} open={isOpen}>
<DropdownMenuTrigger asChild>
<Button
size="base"
color="secondary"
className="w-full px-3 !font-light"
EndIcon={({ className, ...props }) =>
isOpen ? (
<Icon.FiChevronUp
{...props}
className={classNames(className, "!h-5 !w-5 !font-extrabold text-gray-300")}
/>
) : (
<Icon.FiChevronDown
{...props}
className={classNames(className, "!h-5 !w-5 !font-extrabold text-gray-300")}
/>
)
}>
{t("nr_event_type", { count: 3 })}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{(eventTypeGroups || []).map((eventTypeGroup) => (
<DropdownMenuGroup key={eventTypeGroup.groupName} className="space-y-3 p-4 px-3">
<DropdownMenuLabel className="h6 pb-3 pl-1 text-xs font-medium uppercase text-neutral-400">
{eventTypeGroup.groupName}
</DropdownMenuLabel>
{eventTypeGroup.eventTypeNames.map((eventTypeTitle) => (
<DropdownMenuCheckboxItem key={eventTypeTitle}>
<span className="w-[200px] truncate">{eventTypeTitle}</span>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
))}
<DropdownMenuSeparator asChild>
<hr />
</DropdownMenuSeparator>
<DropdownMenuItem className="flex justify-end space-x-2 px-4 pt-3 pb-2">
<Button color="minimalSecondary" onClick={() => setOpen(false)}>
{t("cancel")}
</Button>
<Button
color="primary"
onClick={() => {
console.log("do nothing");
}}>
{t("apply")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
);
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const router = useRouter();
@ -130,7 +236,7 @@ export default function Availability({ schedule }: { schedule: number }) {
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<div className="mb-4 rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
@ -148,25 +254,31 @@ export default function Availability({ schedule }: { schedule: number }) {
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SkeletonText className="h-6 w-full" />
)
}
/>
<div className="xl:max-w-80 w-full space-y-4 pr-4 sm:p-0">
<div className="space-y-4">
<div className="sm:w-full md:w-1/2 lg:w-full">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SkeletonText className="h-6 w-full" />
)
}
/>
</div>
<Label className="mt-1 cursor-pointer space-y-2 sm:w-full md:w-1/2 lg:w-full">
<span>Active on</span>
<ActiveOnEventTypeSelect />
</Label>
</div>
<hr className="my-8" />
<div className="rounded-md">

View File

@ -281,6 +281,7 @@ const loggedInViewerRouter = createProtectedRouter()
successRedirectUrl: true,
hashedLink: true,
destinationCalendar: true,
scheduleId: true,
team: true,
users: {
select: {

View File

@ -87,7 +87,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
ref: forwardedRef,
className: classNames(
// base styles independent what type of button it is
"inline-flex items-center text-sm font-medium relative",
"inline-flex place-content-between items-center text-sm font-medium relative",
// different styles depending on size
size === "base" && "h-9 px-4 py-2.5 ",
size === "lg" && "h-[36px] px-4 py-2.5 ",

View File

@ -50,7 +50,7 @@ function Select<
Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
>({ className, noInputStyle, ...props }: SelectProps<Option, IsMulti, Group> & { noInputStyle?: boolean }) {
return (
<ReactSelect
className={classNames(
@ -71,6 +71,12 @@ function Select<
styles={{
control: (base) => ({
...base,
...(noInputStyle
? {
border: 0,
backgroundColor: "transparent !important",
}
: {}),
// Brute force to remove focus outline of input
"& .react-select__input": {
borderWidth: 0,

View File

@ -1,5 +1,5 @@
import { useId } from "@radix-ui/react-id";
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useState } from "react";
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useEffect, useState } from "react";
import { Check, Circle, Info, X, Eye, EyeOff } from "react-feather";
import {
FieldErrors,
@ -13,8 +13,10 @@ import {
import classNames from "@calcom/lib/classNames";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Skeleton, Tooltip } from "@calcom/ui/v2";
import Select from "@calcom/ui/v2/core/form/Select";
import showToast from "@calcom/ui/v2/core/notifications";
import { Skeleton } from "@calcom/ui/v2/core/skeleton";
import { Tooltip } from "@calcom/ui/v2/core/tooltip";
import { Alert } from "../../../Alert";
@ -216,7 +218,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
"h-9 border border-gray-300",
addOnFilled && "bg-gray-100",
addOnLeading && "rounded-l-md border-r-0 px-3",
addOnSuffix && "rounded-r-md border-l-0 px-3"
addOnSuffix && "rounded-r-md border-l-0"
)}>
<div
className={classNames(
@ -446,6 +448,74 @@ export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
);
}
export const MinutesField = forwardRef<HTMLInputElement, InputFieldProps>(function MinutesField(props, ref) {
return <InputField ref={ref} type="number" min={0} {...props} addOnSuffix="mins" />;
export const MinutesField = (props: Omit<InputFieldProps, "name"> & { name: string }) => {
const form = useFormContext();
const [timeUnit, setTimeUnit] = useState<typeof TIME_UNIT_OPTIONS[number]>();
return (
<TimeUnitField
onTimeUnitChange={setTimeUnit}
{...form.register(props.name, {
required: true,
setValueAs: (value) => Number(value / (timeUnit?.asNumber || 1)),
})}
{...props}
/>
);
};
const TIME_UNIT_OPTIONS = [
{ value: "day", label: "days", asNumber: 1440 },
{ value: "hour", label: "hours", asNumber: 60 },
{ value: "minute", label: "mins", asNumber: 1 },
];
export const TimeUnitField = forwardRef<
HTMLInputElement,
Omit<InputFieldProps, "name"> & {
onTimeUnitChange: (timeUnit: typeof TIME_UNIT_OPTIONS[number]) => void;
name: string;
}
>(function TimeUnitField({ onTimeUnitChange, ...props }, ref) {
const { watch, setValue } = useFormContext();
const [displayValue, setDisplayValue] = useState<number>();
const [selectedTimeUnit, setSelectedTimeUnit] = useState<typeof TIME_UNIT_OPTIONS[number]>(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
TIME_UNIT_OPTIONS.find((option) => option.value === "minute")!
);
useEffect(() => {
setValue(props.name, displayValue * selectedTimeUnit.asNumber);
}, [displayValue, selectedTimeUnit, props.name, setValue]);
console.log(watch(props.name), displayValue);
return (
<>
{/* Actual value (in minutes) of input field */}
<input type="hidden" ref={ref} />
<InputField
type="number"
onKeyUp={(e) => {
setValue(props.name, Number(e.target.value) * selectedTimeUnit.asNumber);
}}
value={displayValue}
onChange={(e) => setDisplayValue(Number(e.target.value))}
min={0}
addOnSuffix={
<Select
className="focus:outline-none"
noInputStyle
value={selectedTimeUnit}
onChange={(newValue) => {
if (!newValue) return;
setSelectedTimeUnit(newValue);
onTimeUnitChange(newValue);
}}
options={TIME_UNIT_OPTIONS}
/>
}
/>
</>
);
});