🧹 Cleaned up older v1 components for availability, settings and auth. (#5288)
* Cleaned up older v1 components for availability, settings and auth. * Updated path.
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
const form = useForm<{
|
||||
name: string;
|
||||
}>();
|
||||
const { register } = form;
|
||||
|
||||
const createMutation = trpc.useMutation("viewer.availability.schedule.create", {
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push("/availability/" + schedule.id);
|
||||
showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not able to create this event`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid={name} StartIcon={Icon.FiPlus}>
|
||||
{t("new_schedule_btn")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{t("add_new_schedule")}
|
||||
</h3>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-2">
|
||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||
{t("name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
placeholder={t("default_schedule_name")}
|
||||
{...register("name")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="secondary">{t("cancel")}</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
import classNames from "classnames";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { GroupBase, Props } from "react-select";
|
||||
|
||||
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
|
||||
import { defaultDayRange } from "@lib/availability";
|
||||
import { weekdayNames } from "@lib/core/i18n/weekday";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import { TimeRange } from "@lib/types/schedule";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
/** Begin Time Increments For Select */
|
||||
const increment = 15;
|
||||
|
||||
type Option = {
|
||||
readonly label: string;
|
||||
readonly value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of times on a 15 minute interval from
|
||||
* 00:00:00 (Start of day) to
|
||||
* 23:45:00 (End of day with enough time for 15 min booking)
|
||||
*/
|
||||
const useOptions = () => {
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const { timeFormat } = query.data || { timeFormat: null };
|
||||
|
||||
const [filteredOptions, setFilteredOptions] = useState<Option[]>([]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const end = dayjs().utc().endOf("day");
|
||||
let t: Dayjs = dayjs().utc().startOf("day");
|
||||
|
||||
const options: Option[] = [];
|
||||
while (t.isBefore(end)) {
|
||||
options.push({
|
||||
value: t.toDate().valueOf(),
|
||||
label: dayjs(t)
|
||||
.utc()
|
||||
.format(timeFormat === 12 ? "h:mma" : "HH:mm"),
|
||||
});
|
||||
t = t.add(increment, "minutes");
|
||||
}
|
||||
return options;
|
||||
}, [timeFormat]);
|
||||
|
||||
const filter = useCallback(
|
||||
({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => {
|
||||
if (current) {
|
||||
const currentOption = options.find((option) => option.value === dayjs(current).toDate().valueOf());
|
||||
if (currentOption) setFilteredOptions([currentOption]);
|
||||
} else
|
||||
setFilteredOptions(
|
||||
options.filter((option) => {
|
||||
const time = dayjs(option.value);
|
||||
return (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset));
|
||||
})
|
||||
);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
return { options: filteredOptions, filter };
|
||||
};
|
||||
|
||||
type TimeRangeFieldProps = {
|
||||
name: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const LazySelect = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
...props
|
||||
}: Omit<Props<Option, false, GroupBase<Option>>, "value"> & {
|
||||
value: ConfigType;
|
||||
min?: ConfigType;
|
||||
max?: ConfigType;
|
||||
}) => {
|
||||
// Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
|
||||
const { options, filter } = useOptions();
|
||||
|
||||
useEffect(() => {
|
||||
filter({ current: value });
|
||||
}, [filter, value]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
onMenuOpen={() => {
|
||||
if (min) filter({ offset: min });
|
||||
if (max) filter({ limit: max });
|
||||
}}
|
||||
value={options.find((option) => option.value === dayjs(value).toDate().valueOf())}
|
||||
onMenuClose={() => filter({ current: value })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TimeRangeField = ({ name, className }: TimeRangeFieldProps) => {
|
||||
const { watch } = useFormContext();
|
||||
const minEnd = watch(`${name}.start`);
|
||||
const maxStart = watch(`${name}.end`);
|
||||
return (
|
||||
<div className={classNames("flex flex-grow items-center space-x-3", className)}>
|
||||
<Controller
|
||||
name={`${name}.start`}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<LazySelect
|
||||
className="w-[120px]"
|
||||
value={value}
|
||||
max={maxStart}
|
||||
onChange={(option) => {
|
||||
onChange(new Date(option?.value as number));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>-</span>
|
||||
<Controller
|
||||
name={`${name}.end`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<LazySelect
|
||||
className="flex-grow sm:w-[120px]"
|
||||
value={value}
|
||||
min={minEnd}
|
||||
onChange={(option) => {
|
||||
onChange(new Date(option?.value as number));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ScheduleBlockProps = {
|
||||
day: number;
|
||||
weekday: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (selected: number[]) => void }) => {
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const { i18n, t } = useLocale();
|
||||
return (
|
||||
<div className="m-4 space-y-2 py-4">
|
||||
<p className="h6 text-xs font-medium uppercase text-neutral-400">Copy times to</p>
|
||||
<ol className="space-y-2">
|
||||
{weekdayNames(i18n.language).map((weekday, num) => (
|
||||
<li key={weekday}>
|
||||
<label className="flex w-full items-center justify-between">
|
||||
<span>{weekday}</span>
|
||||
<input
|
||||
value={num}
|
||||
defaultChecked={disabled.includes(num)}
|
||||
disabled={disabled.includes(num)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && !selected.includes(num)) {
|
||||
setSelected(selected.concat([num]));
|
||||
} else if (!e.target.checked && selected.includes(num)) {
|
||||
setSelected(selected.slice(selected.indexOf(num), 1));
|
||||
}
|
||||
}}
|
||||
type="checkbox"
|
||||
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="pt-2">
|
||||
<Button className="w-full justify-center" color="primary" onClick={() => onApply(selected)}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
export const DayRanges = ({
|
||||
name,
|
||||
defaultValue = [defaultDayRange],
|
||||
}: {
|
||||
name: string;
|
||||
defaultValue?: TimeRange[];
|
||||
}) => {
|
||||
const { setValue, watch } = useFormContext();
|
||||
// XXX: Hack to make copying times work; `fields` is out of date until save.
|
||||
const watcher = watch(name);
|
||||
const { t } = useLocale();
|
||||
const { fields, replace, append, remove } = useFieldArray({
|
||||
name,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue.length && !fields.length) {
|
||||
replace(defaultValue);
|
||||
}
|
||||
}, [replace, defaultValue, fields.length]);
|
||||
|
||||
const handleAppend = () => {
|
||||
// FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499
|
||||
const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end);
|
||||
const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour");
|
||||
|
||||
if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) {
|
||||
return append({
|
||||
start: nextRangeStart.toDate(),
|
||||
end: nextRangeEnd.toDate(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center rtl:space-x-reverse">
|
||||
<div className="flex flex-grow space-x-1 sm:flex-grow-0">
|
||||
<TimeRangeField name={`${name}.${index}`} />
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiTrash}
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<div className="absolute top-2 right-0 text-right sm:relative sm:top-0 sm:flex-grow">
|
||||
<Tooltip content={t("add_time_availability") as string}>
|
||||
<Button
|
||||
className="text-neutral-400"
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiPlus}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Dropdown>
|
||||
<Tooltip content={t("duplicate") as string}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent>
|
||||
<CopyTimes
|
||||
disabled={[parseInt(name.substring(name.lastIndexOf(".") + 1), 10)]}
|
||||
onApply={(selected) =>
|
||||
selected.forEach((day) => {
|
||||
// TODO: Figure out why this is different?
|
||||
// console.log(watcher, fields);
|
||||
setValue(name.substring(0, name.lastIndexOf(".") + 1) + day, watcher);
|
||||
})
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const form = useFormContext();
|
||||
const watchAvailable = form.watch(`${name}.${day}`, []);
|
||||
|
||||
return (
|
||||
<fieldset className="relative flex flex-col justify-between space-y-2 py-5 sm:flex-row sm:space-y-0">
|
||||
<label
|
||||
className={classNames(
|
||||
"flex space-x-2 rtl:space-x-reverse",
|
||||
!watchAvailable.length ? "w-full" : "w-1/3"
|
||||
)}>
|
||||
<div className={classNames(!watchAvailable.length ? "w-1/3" : "w-full")}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={watchAvailable.length}
|
||||
onChange={(e) => {
|
||||
form.setValue(`${name}.${day}`, e.target.checked ? [defaultDayRange] : []);
|
||||
}}
|
||||
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
|
||||
/>
|
||||
<span className="ml-2 inline-block text-sm capitalize">{weekday}</span>
|
||||
</div>
|
||||
{!watchAvailable.length && (
|
||||
<div className="flex-grow text-right text-sm text-gray-500 sm:flex-shrink">
|
||||
{t("no_availability")}
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
{!!watchAvailable.length && (
|
||||
<div className="flex-grow">
|
||||
<DayRanges name={`${name}.${day}`} defaultValue={[]} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
const Schedule = ({ name }: { name: string }) => {
|
||||
const { i18n } = useLocale();
|
||||
return (
|
||||
<fieldset className="divide-y divide-gray-200">
|
||||
{weekdayNames(i18n.language).map((weekday, num) => (
|
||||
<ScheduleBlock key={num} name={name} weekday={weekday} day={num} />
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
export default Schedule;
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||
*/
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Availability } from "@calcom/prisma/client";
|
||||
import { inferQueryOutput } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
/**
|
||||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||
*/
|
||||
export function ScheduleListItem({
|
||||
schedule,
|
||||
deleteFunction,
|
||||
isDeleting = false,
|
||||
}: {
|
||||
schedule: inferQueryOutput<"viewer.availability.list">["schedules"][number];
|
||||
deleteFunction: ({ scheduleId }: { scheduleId: number }) => void;
|
||||
isDeleting: boolean;
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
|
||||
return (
|
||||
<li key={schedule.id}>
|
||||
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
||||
<div className="group flex w-full items-center justify-between hover:bg-neutral-50 sm:px-6">
|
||||
<Link href={"/availability/" + schedule.id}>
|
||||
<a className="flex-grow truncate text-sm" title={schedule.name}>
|
||||
<div>
|
||||
<span className="truncate font-medium text-neutral-900">{schedule.name}</span>
|
||||
{schedule.isDefault && (
|
||||
<span className="ml-2 inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
|
||||
{t("default")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-neutral-500">
|
||||
{schedule.availability.map((availability: Availability) => (
|
||||
<Fragment key={availability.id}>
|
||||
{availabilityAsString(availability, { locale: i18n.language })}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
|
||||
<Icon.FiMoreHorizontal className="h-5 w-5 group-hover:text-gray-800" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
deleteFunction({
|
||||
scheduleId: schedule.id,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
color="warn"
|
||||
className="w-full font-normal"
|
||||
StartIcon={isDeleting ? undefined : Icon.FiTrash}
|
||||
loading={isDeleting}>
|
||||
{isDeleting ? t("deleting") : t("delete_schedule")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
/** @deprecated Use `apps/web/components/v2/availability/SkeletonLoader.tsx` */
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
@@ -20,26 +21,30 @@ function SkeletonItem() {
|
||||
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText width="32" height="2" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-2 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText width="12" height="6" />
|
||||
<SkeletonText className="h-6 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const AvailabilitySelectSkeletonLoader = () => {
|
||||
export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
|
||||
<li
|
||||
className={classNames(
|
||||
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
|
||||
className
|
||||
)}>
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText width="4" height="4" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
/** @deprecated Use `apps/web/components/v2/bookings/SkeletonLoader.tsx` */
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||
@@ -21,15 +20,15 @@ function SkeletonItem() {
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText width="16" height="5" />
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText className="h-5 w-16" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText width="16" height="6" />
|
||||
<SkeletonText width="32" height="6" />
|
||||
<SkeletonText className="h-6 w-16" />
|
||||
<SkeletonText className="h-6 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -17,27 +17,25 @@ interface Props {
|
||||
|
||||
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col justify-center border-gray-200 bg-neutral-50 py-12 sm:px-6 lg:px-8">
|
||||
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={props.title} description={props.description} />
|
||||
{props.showLogo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" />
|
||||
)}
|
||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||
{props.showLogo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="mx-auto h-6" src={LOGO} alt="Cal.com Logo" />
|
||||
)}
|
||||
{props.heading && (
|
||||
<h2 className="font-cal mt-6 text-center text-3xl text-neutral-900">{props.heading}</h2>
|
||||
)}
|
||||
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
||||
</div>
|
||||
{props.loading && (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="mx-2 rounded-md border border-gray-300 bg-white px-4 py-8 sm:px-10">
|
||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkeletonLoader;
|
||||
|
||||
function SkeletonItem() {
|
||||
return (
|
||||
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-2 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText className="h-6 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<li
|
||||
className={classNames(
|
||||
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
|
||||
className
|
||||
)}>
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
<SkeletonItem />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkeletonLoader;
|
||||
|
||||
function SkeletonItem() {
|
||||
return (
|
||||
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText className="h-5 w-16" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText className="h-6 w-16" />
|
||||
<SkeletonText className="h-6 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import Button from "@calcom/ui/v2/core/Button";
|
||||
import Select from "@calcom/ui/v2/core/form/select";
|
||||
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader";
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
type AvailabilityOption = {
|
||||
label: string;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { LOGO } from "@calcom/lib/constants";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
footerText?: React.ReactNode | string;
|
||||
showLogo?: boolean;
|
||||
heading?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={props.title} description={props.description} />
|
||||
{props.showLogo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" />
|
||||
)}
|
||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
||||
</div>
|
||||
{props.loading && (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { useRouter } from "next/router";
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
import Shell from "@calcom/ui/v2/core/Shell";
|
||||
import AppCard from "@calcom/ui/v2/core/apps/AppCard";
|
||||
|
||||
@@ -21,7 +21,7 @@ import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||
import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
|
||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
|
||||
function ConnectOrDisconnectIntegrationButton(props: {
|
||||
credentialIds: number[];
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Button, Input, TextField } from "@calcom/ui/v2";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { EmailField, Button } from "@calcom/ui/v2";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
const { t, i18n } = useLocale();
|
||||
|
||||
@@ -23,7 +23,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import { Skeleton, SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import EditableHeading from "@components/ui/EditableHeading";
|
||||
import { SelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader";
|
||||
|
||||
const querySchema = z.object({
|
||||
schedule: stringOrNumber,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { EmptyScreen, showToast } from "@calcom/ui/v2";
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
|
||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
|
||||
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -15,7 +15,7 @@ import BookingLayout from "@calcom/ui/v2/core/layouts/BookingLayout";
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
|
||||
import BookingListItem from "@components/booking/BookingListItem";
|
||||
import SkeletonLoader from "@components/v2/bookings/SkeletonLoader";
|
||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||
|
||||
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
|
||||
type BookingOutput = inferQueryOutput<"viewer.bookings">["bookings"][0];
|
||||
|
||||
@@ -1,741 +0,0 @@
|
||||
// @@DEPRECATED, use new getting-started.tsx instead
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import classnames from "classnames";
|
||||
import debounce from "lodash/debounce";
|
||||
import omit from "lodash/omit";
|
||||
import { NextPageContext } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||
import { DOCS_URL } from "@calcom/lib/constants";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { Schedule as ScheduleType } from "@lib/types/schedule";
|
||||
|
||||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import Loader from "@components/Loader";
|
||||
import Schedule from "@components/availability/Schedule";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
||||
|
||||
// Embed isn't applicable to onboarding, so ignore the rule
|
||||
/* eslint-disable @calcom/eslint/avoid-web-storage */
|
||||
|
||||
export interface ScheduleFormValues {
|
||||
schedule: ScheduleType;
|
||||
}
|
||||
|
||||
let mutationComplete: ((err: Error | null) => void) | null;
|
||||
|
||||
export default function Onboarding(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const { user } = props;
|
||||
const router = useRouter();
|
||||
const utils = trpc.useContext();
|
||||
const telemetry = useTelemetry();
|
||||
const [hasErrors, setHasErrors] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const { data: eventTypes } = trpc.useQuery(["viewer.eventTypes.list"]);
|
||||
const onSuccessMutation = async () => {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
setHasErrors(false); // dismiss any open errors
|
||||
await utils.invalidateQueries(["viewer.me"]);
|
||||
};
|
||||
|
||||
const onErrorMutation = (error: TRPCClientErrorLike<AppRouter>) => {
|
||||
setHasErrors(true);
|
||||
setErrorMessage(error.message);
|
||||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
const mutation = trpc.useMutation("viewer.updateProfile", {
|
||||
onSuccess: async () => {
|
||||
setSubmitting(true);
|
||||
setEnteredName(nameRef.current?.value || "");
|
||||
setInputUsernameValue(usernameRef.current?.value || "");
|
||||
if (mutationComplete) {
|
||||
mutationComplete(null);
|
||||
mutationComplete = null;
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
onError: (err: TRPCClientErrorLike<AppRouter>) => {
|
||||
setError(new Error(err.message));
|
||||
if (mutationComplete) {
|
||||
mutationComplete(new Error(err.message));
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_EVENT_TYPES = [
|
||||
{
|
||||
title: t("15min_meeting"),
|
||||
slug: "15min",
|
||||
length: 15,
|
||||
},
|
||||
{
|
||||
title: t("30min_meeting"),
|
||||
slug: "30min",
|
||||
length: 30,
|
||||
},
|
||||
{
|
||||
title: t("secret_meeting"),
|
||||
slug: "secret",
|
||||
length: 15,
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||
const [enteredName, setEnteredName] = React.useState("");
|
||||
const [currentUsername, setCurrentUsername] = useState(user.username || undefined);
|
||||
const [inputUsernameValue, setInputUsernameValue] = useState(currentUsername);
|
||||
|
||||
const { status } = useSession();
|
||||
const loading = status === "loading";
|
||||
const [ready, setReady] = useState(false);
|
||||
const [selectedImport, setSelectedImport] = useState("");
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const updateUser = useCallback(
|
||||
async (data: Prisma.UserUpdateInput) => {
|
||||
const res = await fetch(`/api/user/${user.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ data: { ...data } }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error((await res.json()).message);
|
||||
}
|
||||
const responseData = await res.json();
|
||||
return responseData.data;
|
||||
},
|
||||
[user.id]
|
||||
);
|
||||
|
||||
const createEventType = trpc.useMutation("viewer.eventTypes.create");
|
||||
|
||||
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", {
|
||||
onError: (error: TRPCClientErrorLike<AppRouter>) => {
|
||||
throw new Error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
/** Name */
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
/** Username */
|
||||
const usernameRef = useRef<HTMLInputElement>(null!);
|
||||
const bioRef = useRef<HTMLInputElement>(null);
|
||||
/** End Name */
|
||||
/** TimeZone */
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||
/** End TimeZone */
|
||||
|
||||
/** Onboarding Steps */
|
||||
const [currentStep, setCurrentStep] = useState(props.initialStep);
|
||||
const handleConfirmStep = async () => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const onComplete = steps[currentStep]?.onComplete;
|
||||
if (onComplete) {
|
||||
await onComplete();
|
||||
}
|
||||
incrementStep();
|
||||
setSubmitting(false);
|
||||
} catch (error) {
|
||||
setSubmitting(false);
|
||||
setError(error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleConfirmStep = debounce(handleConfirmStep, 850);
|
||||
|
||||
const handleSkipStep = () => {
|
||||
incrementStep();
|
||||
};
|
||||
|
||||
const incrementStep = () => {
|
||||
const nextStep = currentStep + 1;
|
||||
|
||||
if (nextStep >= steps.length) {
|
||||
completeOnboarding();
|
||||
return;
|
||||
}
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const decrementStep = () => {
|
||||
const previous = currentStep - 1;
|
||||
|
||||
if (previous < 0) {
|
||||
return;
|
||||
}
|
||||
setCurrentStep(previous);
|
||||
};
|
||||
|
||||
const goToStep = (step: number) => {
|
||||
setCurrentStep(step);
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete Onboarding finalizes the onboarding flow for a new user.
|
||||
*
|
||||
* Here, 3 event types are pre-created for the user as well.
|
||||
* Set to the availability the user enter during the onboarding.
|
||||
*
|
||||
* If a user skips through the Onboarding flow,
|
||||
* then the default availability is applied.
|
||||
*/
|
||||
const completeOnboarding = async () => {
|
||||
setSubmitting(true);
|
||||
if (eventTypes?.length === 0) {
|
||||
await Promise.all(
|
||||
DEFAULT_EVENT_TYPES.map(async (event) => {
|
||||
return createEventType.mutate(event);
|
||||
})
|
||||
);
|
||||
}
|
||||
await updateUser({
|
||||
completedOnboarding: true,
|
||||
});
|
||||
|
||||
setSubmitting(false);
|
||||
router.push("/event-types");
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
const formMethods = useForm<{
|
||||
token: string;
|
||||
}>({ resolver: zodResolver(schema), mode: "onSubmit" });
|
||||
|
||||
// Should update username on user when being redirected from sign up and doing google/saml
|
||||
useEffect(() => {
|
||||
async function validateAndSave(username: string) {
|
||||
const { data } = await fetchUsername(username);
|
||||
|
||||
// Only persist username if its available and not premium
|
||||
// premium usernames are saved via stripe webhook
|
||||
if (data.available && !data.premium) {
|
||||
await updateUser({
|
||||
username,
|
||||
});
|
||||
}
|
||||
// Remove it from localStorage
|
||||
window.localStorage.removeItem("username");
|
||||
return;
|
||||
}
|
||||
|
||||
// Looking for username on localStorage
|
||||
const username = window.localStorage.getItem("username");
|
||||
if (username) {
|
||||
validateAndSave(username);
|
||||
}
|
||||
}, [updateUser]);
|
||||
|
||||
const availabilityForm = useForm({ defaultValues: { schedule: DEFAULT_SCHEDULE } });
|
||||
const steps = [
|
||||
{
|
||||
id: t("welcome"),
|
||||
title: t("welcome_to_calcom"),
|
||||
description: t("welcome_instructions"),
|
||||
Component: (
|
||||
<>
|
||||
{/** @NOTE: Hiding temporarily
|
||||
* @URL related: https://github.com/calcom/cal.com/issues/3941
|
||||
*/}
|
||||
{/* {selectedImport == "" && (
|
||||
<div className="mb-4 grid grid-cols-2 gap-x-4">
|
||||
<Button color="secondary" onClick={() => setSelectedImport("calendly")}>
|
||||
{t("import_from")} Calendly
|
||||
</Button>
|
||||
<Button color="secondary" onClick={() => setSelectedImport("savvycal")}>
|
||||
{t("import_from")} SavvyCal
|
||||
</Button>
|
||||
</div>
|
||||
)} */}
|
||||
{/* {selectedImport && (
|
||||
<div>
|
||||
<h2 className="font-cal text-2xl text-gray-900">
|
||||
{t("import_from")} {selectedImport === "calendly" ? "Calendly" : "SavvyCal"}
|
||||
</h2>
|
||||
<p className="mb-2 text-sm text-gray-500">
|
||||
{t("you_will_need_to_generate")}. Find out how to do this{" "}
|
||||
<a href={`${DOCS_URL}/import`}>here</a>.
|
||||
</p>
|
||||
<form
|
||||
className="flex"
|
||||
onSubmit={formMethods.handleSubmit(async (values) => {
|
||||
// track the number of imports. Without personal data/payload
|
||||
telemetry.event(telemetryEventTypes.importSubmitted, {
|
||||
...collectPageParameters(),
|
||||
selectedImport,
|
||||
});
|
||||
setSubmitting(true);
|
||||
const response = await fetch(`/api/import/${selectedImport}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
token: values.token,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (response.status === 201) {
|
||||
setSubmitting(false);
|
||||
handleSkipStep();
|
||||
} else {
|
||||
await response.json().catch((e) => {
|
||||
console.log("Error: response.json invalid: " + e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}
|
||||
})}>
|
||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
||||
|
||||
<input
|
||||
onChange={async (e) => {
|
||||
formMethods.setValue("token", e.target.value);
|
||||
}}
|
||||
type="text"
|
||||
name="token"
|
||||
id="token"
|
||||
placeholder={t("access_token")}
|
||||
required
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||
/>
|
||||
<Button type="submit" className="mt-1 ml-4 h-10">
|
||||
{t("import")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative my-4">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-white px-2 text-sm text-gray-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
<form className="sm:mx-auto sm:w-full">
|
||||
<section className="space-y-8">
|
||||
<fieldset>
|
||||
{user.username !== "" && (
|
||||
<UsernameAvailability
|
||||
currentUsername={currentUsername}
|
||||
setCurrentUsername={setCurrentUsername}
|
||||
inputUsernameValue={inputUsernameValue}
|
||||
usernameRef={usernameRef}
|
||||
setInputUsernameValue={setInputUsernameValue}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder={t("your_name")}
|
||||
defaultValue={user.name ?? enteredName}
|
||||
required
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<section className="flex justify-between">
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<p className="text-sm leading-tight text-gray-500 dark:text-white">
|
||||
{t("current_time")}:
|
||||
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
|
||||
</p>
|
||||
</section>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={({ value }) => setSelectedTimeZone(value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
/>
|
||||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
</>
|
||||
),
|
||||
hideConfirm: false,
|
||||
confirmText: t("continue"),
|
||||
showCancel: true,
|
||||
cancelText: t("set_up_later"),
|
||||
onComplete: async () => {
|
||||
mutationComplete = null;
|
||||
setError(null);
|
||||
const mutationAsync = new Promise((resolve, reject) => {
|
||||
mutationComplete = (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
|
||||
const userUpdateData = {
|
||||
name: nameRef.current?.value,
|
||||
username: usernameRef.current?.value,
|
||||
timeZone: selectedTimeZone,
|
||||
};
|
||||
|
||||
mutation.mutate(userUpdateData);
|
||||
|
||||
if (mutationComplete) {
|
||||
await mutationAsync;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "connect-calendar",
|
||||
title: t("connect_your_calendar"),
|
||||
description: t("connect_your_calendar_instructions"),
|
||||
Component: (
|
||||
<section>
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<CalendarListContainer heading={false} fromOnboarding={true} />
|
||||
</ClientSuspense>
|
||||
<footer className="flex flex-col space-y-6 py-6 sm:mx-auto sm:w-full">
|
||||
<Button
|
||||
className="justify-center"
|
||||
EndIcon={Icon.FiArrowRight}
|
||||
type="button"
|
||||
onClick={handleConfirmStep}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
),
|
||||
hideConfirm: true,
|
||||
confirmText: t("continue"),
|
||||
showCancel: true,
|
||||
cancelText: t("continue_without_calendar"),
|
||||
},
|
||||
{
|
||||
id: "set-availability",
|
||||
title: t("set_availability"),
|
||||
description: t("set_availability_instructions"),
|
||||
Component: (
|
||||
<Form<ScheduleFormValues>
|
||||
className="mx-auto max-w-lg bg-white text-black dark:bg-opacity-5 dark:text-white"
|
||||
form={availabilityForm}
|
||||
handleSubmit={async (values) => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await createSchedule.mutate({
|
||||
name: t("default_schedule_name"),
|
||||
...values,
|
||||
});
|
||||
debouncedHandleConfirmStep();
|
||||
setSubmitting(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setError(error);
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<section>
|
||||
<Schedule name="schedule" />
|
||||
<footer className="flex flex-col space-y-6 py-6 sm:mx-auto sm:w-full">
|
||||
<Button className="justify-center" EndIcon={Icon.FiArrowRight} type="submit">
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
</Form>
|
||||
),
|
||||
hideConfirm: true,
|
||||
showCancel: false,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
title: t("nearly_there"),
|
||||
description: t("nearly_there_instructions"),
|
||||
Component: (
|
||||
<form className="sm:mx-auto sm:w-full" id="ONBOARDING_STEP_4">
|
||||
<section className="space-y-4">
|
||||
<fieldset>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder={t("your_name")}
|
||||
defaultValue={user.name || enteredName}
|
||||
required
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="bio" className="block text-sm font-medium text-gray-700">
|
||||
{t("about")}
|
||||
</label>
|
||||
<input
|
||||
ref={bioRef}
|
||||
type="text"
|
||||
name="bio"
|
||||
id="bio"
|
||||
required
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||
defaultValue={user.bio || undefined}
|
||||
/>
|
||||
<p className="mt-2 text-sm leading-tight text-gray-500 dark:text-white">
|
||||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
</fieldset>
|
||||
</section>
|
||||
</form>
|
||||
),
|
||||
hideConfirm: false,
|
||||
confirmText: t("finish"),
|
||||
showCancel: true,
|
||||
cancelText: t("set_up_later"),
|
||||
onComplete: async () => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await updateUser({
|
||||
bio: bioRef.current?.value,
|
||||
});
|
||||
setSubmitting(false);
|
||||
} catch (error) {
|
||||
setError(error as Error);
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
/** End Onboarding Steps */
|
||||
|
||||
useEffect(() => {
|
||||
setReady(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (loading || !ready) {
|
||||
return <div className="loader" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-brand min-h-screen" data-testid="onboarding">
|
||||
<Head>
|
||||
<title>Cal.com - {t("getting_started")}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
{isSubmitting && (
|
||||
<div className="fixed z-10 flex h-full w-full flex-col content-center items-center justify-center bg-white bg-opacity-25">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-auto px-4 py-24">
|
||||
<article className="relative">
|
||||
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||
<header>
|
||||
<p className="font-cal mb-2 text-3xl tracking-wider text-white">{steps[currentStep].title}</p>
|
||||
<p className="text-sm font-normal text-white">{steps[currentStep].description}</p>
|
||||
</header>
|
||||
<section className="space-y-2 pt-4">
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-white">
|
||||
Step {currentStep + 1} of {steps.length}
|
||||
</p>
|
||||
|
||||
{error && <Alert severity="error" message={error?.message} />}
|
||||
|
||||
<section className="flex w-full space-x-2 rtl:space-x-reverse">
|
||||
{steps.map((s, index) => {
|
||||
return index <= currentStep ? (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
onClick={() => goToStep(index)}
|
||||
className={classnames(
|
||||
"h-1 w-1/4 bg-white",
|
||||
index < currentStep ? "cursor-pointer" : ""
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div key={`step-${index}`} className="h-1 w-1/4 bg-white bg-opacity-25" />
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section className="mx-auto mt-10 max-w-xl rounded-sm bg-white p-10">
|
||||
{steps[currentStep].Component}
|
||||
|
||||
{!steps[currentStep].hideConfirm && (
|
||||
<footer className="mt-8 flex flex-col space-y-6 sm:mx-auto sm:w-full">
|
||||
<Button
|
||||
className="justify-center"
|
||||
disabled={isSubmitting}
|
||||
onClick={debouncedHandleConfirmStep}
|
||||
EndIcon={Icon.FiArrowRight}
|
||||
data-testid={`continue-button-${currentStep}`}>
|
||||
{steps[currentStep].confirmText}
|
||||
</Button>
|
||||
</footer>
|
||||
)}
|
||||
</section>
|
||||
<section className="mx-auto max-w-xl py-8">
|
||||
<div className="flex flex-row-reverse justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSkipStep}
|
||||
className="text-sm leading-tight text-gray-500 dark:text-white">
|
||||
{t("next_step")}
|
||||
</button>
|
||||
{currentStep !== 0 && (
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
onClick={decrementStep}
|
||||
className="text-sm leading-tight text-gray-500 dark:text-white">
|
||||
{t("prev_step")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/auth/login",
|
||||
},
|
||||
};
|
||||
}
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
timeZone: true,
|
||||
identityProvider: true,
|
||||
completedOnboarding: true,
|
||||
weekStart: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
plan: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
metadata: true,
|
||||
timeFormat: true,
|
||||
allowDynamicBooking: true,
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
externalId: true,
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error(`Signed in as ${session.user.id} but cannot be found in db`);
|
||||
}
|
||||
|
||||
if (user.completedOnboarding) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/event-types",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const integrations = getApps(user.credentials)
|
||||
.filter((item) => item.type.endsWith("_calendar"))
|
||||
.map((item) => omit(item, "key"));
|
||||
const { schedules } = user;
|
||||
const hasConfigureCalendar = integrations.some((integration) => integration.credential !== null);
|
||||
const hasSchedules = schedules && schedules.length > 0;
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
user,
|
||||
initialStep: hasSchedules ? (hasConfigureCalendar ? 2 : 3) : 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/Disconnect
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import { CalendarSwitch } from "@components/v2/settings/CalendarSwitch";
|
||||
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
|
||||
@@ -8,8 +8,8 @@ import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import Switch from "@calcom/ui/v2/core/Switch";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
|
||||
|
||||
import DisableTwoFactorModal from "@components/v2/settings/DisableTwoFactorModal";
|
||||
import EnableTwoFactorModal from "@components/v2/settings/EnableTwoFactorModal";
|
||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||
|
||||
const TwoFactorAuthView = () => {
|
||||
const utils = trpc.useContext();
|
||||
|
||||
Reference in New Issue
Block a user