Compare commits

...

2 Commits

Author SHA1 Message Date
sean-brydon 616e83d54b Add migration 2022-05-04 13:43:11 +01:00
sean-brydon 9be492f5af Current UI refactor 2022-05-04 11:44:55 +01:00
5 changed files with 273 additions and 191 deletions

View File

@ -1,36 +1,47 @@
import React, { forwardRef, InputHTMLAttributes } from "react";
type Props = InputHTMLAttributes<HTMLInputElement> & {
label: React.ReactNode;
label?: React.ReactNode;
description: string;
descriptionAsLabel?: boolean;
};
const CheckboxField = forwardRef<HTMLInputElement, Props>(({ label, description, ...rest }, ref) => {
return (
<div className="block items-center sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700">
{label}
</label>
</div>
<div className="w-full">
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
/>
const CheckboxField = forwardRef<HTMLInputElement, Props>(
({ label, description, descriptionAsLabel, ...rest }, ref) => {
return (
<div className="block items-center sm:flex">
{label && (
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700">
{label}
</label>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
<p className="text-neutral-900">{description}</p>
)}
<div className="w-full">
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
/>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
{!descriptionAsLabel ? (
<p className="text-neutral-900">{description}</p>
) : (
<label className="text-neutral-900" htmlFor={rest.id}>
{description}
</label>
)}
</div>
</div>
</div>
</div>
</div>
);
});
);
}
);
CheckboxField.displayName = "CheckboxField";

View File

@ -274,6 +274,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [periodTypeLimitsVisible, setPeriodTypeLimitsVisible] = useState(periodType?.type !== "UNLIMITED");
useEffect(() => {
const fetchTokens = async () => {
@ -1411,23 +1412,201 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
/>
<hr className="my-2 border-neutral-200" />
<div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label
htmlFor="bufferTime"
className="mt-2.5 flex text-sm font-medium text-neutral-700">
{t("buffer_and_limits")}
</label>
</div>
<div className="block sm:flex sm:space-y-2 sm:space-x-2 md:space-y-0 ">
<div className="w-full">
<label
htmlFor="beforeBufferTime"
className="mb-2 flex text-sm font-medium text-neutral-700">
{t("before_event")}
</label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) ||
beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<label
htmlFor="afterBufferTime"
className="mb-2 flex text-sm font-medium text-neutral-700">
{t("after_event")}
</label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) ||
afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<label
htmlFor="minimumBookingNotice"
className="mb-2 flex text-sm font-medium text-neutral-700">
{t("minimum_booking_notice")}
</label>
<Controller
name="minimumBookingNotice"
control={formMethods.control}
defaultValue={eventType.minimumBookingNotice}
render={() => (
<MinutesField
required
min="0"
placeholder="120"
defaultValue={eventType.minimumBookingNotice}
onChange={(e) => {
formMethods.setValue("minimumBookingNotice", Number(e.target.value));
}}
/>
)}
/>
</div>
</div>
</div>
<Controller
name="minimumBookingNotice"
name="periodType"
control={formMethods.control}
defaultValue={eventType.minimumBookingNotice}
defaultValue={periodType?.type}
render={() => (
<MinutesField
label={t("minimum_booking_notice")}
required
min="0"
placeholder="120"
defaultValue={eventType.minimumBookingNotice}
onChange={(e) => {
formMethods.setValue("minimumBookingNotice", Number(e.target.value));
}}
/>
<>
<CheckboxField
id="limitPeriodType"
name="limitPeriodType"
description={t("limit_future_booking_checkbox")}
descriptionAsLabel
defaultChecked={periodType?.type !== "UNLIMITED" ? true : false}
onChange={(e) => {
setPeriodTypeLimitsVisible(e?.target.checked);
}}
/>
{periodTypeLimitsVisible && (
<RadioGroup.Root
className="mt-3 bg-gray-100 p-4"
defaultValue={periodType?.type}
onValueChange={(val) =>
formMethods.setValue("periodType", val as PeriodType)
}>
{PERIOD_TYPES.map((period) => (
<div className="mb-2 flex items-center text-sm " key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="inline-flex">
<input
type="number"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className=" block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
endDate,
});
}}
/>
)}
/>
</div>
)}
{period.suffix ? (
<span className="ltr:ml-2 rtl:mr-2">&nbsp;{period.suffix}</span>
) : null}
</div>
))}
</RadioGroup.Root>
)}
</>
)}
/>
<hr className="my-2 border-neutral-200" />
<div className="block items-center sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
@ -1480,163 +1659,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{t("invitees_can_schedule")}
</label>
</div>
<div className="w-full">
<Controller
name="periodType"
control={formMethods.control}
defaultValue={periodType?.type}
render={() => (
<RadioGroup.Root
defaultValue={periodType?.type}
onValueChange={(val) =>
formMethods.setValue("periodType", val as PeriodType)
}>
{PERIOD_TYPES.map((period) => (
<div className="mb-2 flex items-center text-sm" key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="inline-flex">
<input
type="number"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className=" block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", { startDate, endDate });
}}
/>
)}
/>
</div>
)}
{period.suffix ? (
<span className="ltr:ml-2 rtl:mr-2">&nbsp;{period.suffix}</span>
) : null}
</div>
))}
</RadioGroup.Root>
)}
/>
</div>
<div className="w-full"></div>
</div>
<hr className="border-neutral-200" />
<div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label
htmlFor="bufferTime"
className="mt-2.5 flex text-sm font-medium text-neutral-700">
{t("buffer_time")}
</label>
</div>
<div className="w-full">
<div className="inline-flex w-full space-x-2">
<div className="w-full">
<label
htmlFor="beforeBufferTime"
className="mb-2 flex text-sm font-medium text-neutral-700">
{t("before_event")}
</label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) ||
beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<label
htmlFor="afterBufferTime"
className="mb-2 flex text-sm font-medium text-neutral-700">
{t("after_event")}
</label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) ||
afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div>
</div>
</div>
<SuccessRedirectEdit<typeof formMethods>
formMethods={formMethods}
eventType={eventType}></SuccessRedirectEdit>
@ -2085,6 +2111,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
price: true,
currency: true,
destinationCalendar: true,
bookingPeriodLimit: {
select: {
id: true,
period: true,
},
},
},
});

View File

@ -781,5 +781,7 @@
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions"
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
"buffers_and_limts" : "Buffers & Limits",
"limit_future_booking_checkbox":"Limit how far in the future people can book a time"
}

View File

@ -0,0 +1,20 @@
-- CreateEnum
CREATE TYPE "BookingPeriodFrequency" AS ENUM ('DAY', 'WEEK', 'MONTH', 'YEAR');
-- CreateTable
CREATE TABLE "BookingPeriodLimit" (
"id" TEXT NOT NULL,
"period" "BookingPeriodFrequency" NOT NULL,
"eventTypeId" INTEGER NOT NULL,
CONSTRAINT "BookingPeriodLimit_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BookingPeriodLimit_id_key" ON "BookingPeriodLimit"("id");
-- CreateIndex
CREATE UNIQUE INDEX "BookingPeriodLimit_eventTypeId_period_key" ON "BookingPeriodLimit"("eventTypeId", "period");
-- AddForeignKey
ALTER TABLE "BookingPeriodLimit" ADD CONSTRAINT "BookingPeriodLimit_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -71,6 +71,7 @@ model EventType {
slotInterval Int?
metadata Json?
successRedirectUrl String?
bookingPeriodLimit BookingPeriodLimit[]
@@unique([userId, slug])
@@unique([teamId, slug])
@ -471,3 +472,19 @@ model App {
Webhook Webhook[]
ApiKey ApiKey[]
}
enum BookingPeriodFrequency {
DAY
WEEK
MONTH
YEAR
}
model BookingPeriodLimit {
id String @id @unique @default(cuid())
period BookingPeriodFrequency
eventType EventType @relation(fields: [eventTypeId], references: [id])
eventTypeId Int
@@unique([eventTypeId,period])
}