Styling tweaks to inputs and Select (+ TimezoneSelect) (#2453)
Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
parent
d91f667d0c
commit
5fdc5078cc
24 changed files with 265 additions and 561 deletions
|
@ -68,7 +68,7 @@ const DestinationCalendarSelector = ({
|
||||||
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="focus:ring-primary-500 focus:border-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
|
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
setSelectedOption(option);
|
setSelectedOption(option);
|
||||||
if (!option) {
|
if (!option) {
|
||||||
|
|
|
@ -60,7 +60,19 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values);
|
||||||
}}>
|
}}>
|
||||||
<div className="mt-3 space-y-4">
|
<div className="mt-3 space-y-4">
|
||||||
<TextField placeholder={t("default_schedule_name")} label={t("name")} {...register("name")} />
|
<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 shadow-sm"
|
||||||
|
placeholder={t("default_schedule_name")}
|
||||||
|
{...register("name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||||
<Button type="submit" loading={createMutation.isLoading}>
|
<Button type="submit" loading={createMutation.isLoading}>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
interface OptionTypeBase {
|
interface OptionTypeBase {
|
||||||
label: string;
|
label: string;
|
||||||
value: EventTypeCustomInputType;
|
value: EventTypeCustomInputType;
|
||||||
|
@ -55,7 +56,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
defaultValue={selectedInputOption}
|
defaultValue={selectedInputOption}
|
||||||
options={inputOptions}
|
options={inputOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
|
className="mt-1 mb-2 block w-full min-w-0 flex-1 sm:text-sm"
|
||||||
onChange={(option) => option && field.onChange(option.value)}
|
onChange={(option) => option && field.onChange(option.value)}
|
||||||
value={selectedInputOption}
|
value={selectedInputOption}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
@ -73,7 +74,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
type="text"
|
type="text"
|
||||||
id="label"
|
id="label"
|
||||||
required
|
required
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
defaultValue={selectedCustomInput?.label}
|
defaultValue={selectedCustomInput?.label}
|
||||||
{...register("label", { required: true })}
|
{...register("label", { required: true })}
|
||||||
/>
|
/>
|
||||||
|
@ -89,7 +90,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="placeholder"
|
id="placeholder"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
defaultValue={selectedCustomInput?.placeholder}
|
||||||
{...register("placeholder")}
|
{...register("placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -74,7 +74,7 @@ const ChangePasswordSection = () => {
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password"
|
id="current_password"
|
||||||
required
|
required
|
||||||
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
placeholder={t("your_old_password")}
|
placeholder={t("your_old_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,7 +91,7 @@ const ChangePasswordSection = () => {
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
required
|
required
|
||||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||||
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
placeholder={t("super_secure_new_password")}
|
placeholder={t("super_secure_new_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,7 +70,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
minLength={6}
|
minLength={6}
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
onInput={(e) => setTotpCode(e.currentTarget.value)}
|
onInput={(e) => setTotpCode(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@prisma/client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import React, { SyntheticEvent } from "react";
|
import React, { SyntheticEvent, useEffect } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
@ -8,6 +8,14 @@ import Button from "@calcom/ui/Button";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import ModalContainer from "@components/ui/ModalContainer";
|
import ModalContainer from "@components/ui/ModalContainer";
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
type MembershipRoleOption = {
|
||||||
|
value: MembershipRole;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }];
|
||||||
|
|
||||||
export default function MemberChangeRoleModal(props: {
|
export default function MemberChangeRoleModal(props: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -16,7 +24,16 @@ export default function MemberChangeRoleModal(props: {
|
||||||
initialRole: MembershipRole;
|
initialRole: MembershipRole;
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [role, setRole] = useState(props.initialRole || MembershipRole.MEMBER);
|
useEffect(() => {
|
||||||
|
options.forEach((option, i) => {
|
||||||
|
options[i].label = t(option.value.toLowerCase());
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [role, setRole] = useState(
|
||||||
|
options.find((option) => option.value === props.initialRole || MembershipRole.MEMBER)!
|
||||||
|
);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
@ -37,7 +54,7 @@ export default function MemberChangeRoleModal(props: {
|
||||||
changeRoleMutation.mutate({
|
changeRoleMutation.mutate({
|
||||||
teamId: props.teamId,
|
teamId: props.teamId,
|
||||||
memberId: props.memberId,
|
memberId: props.memberId,
|
||||||
role,
|
role: role.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,17 +73,16 @@ export default function MemberChangeRoleModal(props: {
|
||||||
<label className="mb-2 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
<label className="mb-2 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
||||||
{t("role")}
|
{t("role")}
|
||||||
</label>
|
</label>
|
||||||
<select
|
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
options={options}
|
||||||
value={role}
|
value={role}
|
||||||
onChange={(e) => setRole(e.target.value as MembershipRole)}
|
onChange={(option) => option && setRole(option)}
|
||||||
id="role"
|
id="role"
|
||||||
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm">
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm sm:text-sm"
|
||||||
<option value="MEMBER">{t("member")}</option>
|
/>
|
||||||
<option value="ADMIN">{t("admin")}</option>
|
|
||||||
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<p className="text-sm text-red-700">
|
<p className="text-sm text-red-700">
|
||||||
<span className="font-bold">Error: </span>
|
<span className="font-bold">Error: </span>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { UserIcon } from "@heroicons/react/outline";
|
import { UserIcon } from "@heroicons/react/outline";
|
||||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@prisma/client";
|
||||||
import { useState } from "react";
|
import React, { useState, useEffect, SyntheticEvent } from "react";
|
||||||
import React, { SyntheticEvent } from "react";
|
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
|
@ -12,17 +11,33 @@ import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { TeamWithMembers } from "@lib/queries/teams";
|
import { TeamWithMembers } from "@lib/queries/teams";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
type MemberInvitationModalProps = {
|
type MemberInvitationModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
team: TeamWithMembers | null;
|
team: TeamWithMembers | null;
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MembershipRoleOption = {
|
||||||
|
value: MembershipRole;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }];
|
||||||
|
|
||||||
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
|
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
options.forEach((option, i) => {
|
||||||
|
options[i].label = t(option.value.toLowerCase());
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const inviteMemberMutation = trpc.useMutation("viewer.teams.inviteMember", {
|
const inviteMemberMutation = trpc.useMutation("viewer.teams.inviteMember", {
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await utils.invalidateQueries(["viewer.teams.get"]);
|
await utils.invalidateQueries(["viewer.teams.get"]);
|
||||||
|
@ -83,12 +98,12 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
<label className="mb-1 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
<label className="mb-1 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
||||||
{t("role")}
|
{t("role")}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<Select
|
||||||
|
options={options}
|
||||||
id="role"
|
id="role"
|
||||||
className="focus:border-brand mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm">
|
name="role"
|
||||||
<option value="MEMBER">{t("member")}</option>
|
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
<option value="ADMIN">{t("admin")}</option>
|
/>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
|
@ -97,7 +112,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
name="sendInviteEmail"
|
name="sendInviteEmail"
|
||||||
defaultChecked
|
defaultChecked
|
||||||
id="sendInviteEmail"
|
id="sendInviteEmail"
|
||||||
className="focus:border-brand rounded-sm border-gray-300 text-black shadow-sm focus:ring-black sm:text-sm"
|
className="rounded-sm border-gray-300 text-black shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm ltr:ml-2 rtl:mr-2">
|
<div className="text-sm ltr:ml-2 rtl:mr-2">
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default function TeamCreate(props: Props) {
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Acme Inc."
|
placeholder="Acme Inc."
|
||||||
required
|
required
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
|
|
|
@ -112,7 +112,7 @@ export default function TeamSettings(props: Props) {
|
||||||
id="name"
|
id="name"
|
||||||
placeholder={t("your_team_name")}
|
placeholder={t("your_team_name")}
|
||||||
required
|
required
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
|
||||||
defaultValue={team?.name as string}
|
defaultValue={team?.name as string}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ export default function TeamSettings(props: Props) {
|
||||||
name="about"
|
name="about"
|
||||||
rows={3}
|
rows={3}
|
||||||
defaultValue={team?.bio as string}
|
defaultValue={team?.bio as string}
|
||||||
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"></textarea>
|
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"></textarea>
|
||||||
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ export default function TeamSettings(props: Props) {
|
||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
|
||||||
defaultValue={team?.logo ?? undefined}
|
defaultValue={team?.logo ?? undefined}
|
||||||
/>
|
/>
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
import { TrashIcon } from "@heroicons/react/outline";
|
|
||||||
import { Availability } from "@prisma/client";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import timezone from "dayjs/plugin/timezone";
|
|
||||||
import utc from "dayjs/plugin/utc";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
|
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import { WeekdaySelect } from "./WeekdaySelect";
|
|
||||||
import SetTimesModal from "./modal/SetTimesModal";
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
|
|
||||||
type AvailabilityInput = Pick<Availability, "days" | "startTime" | "endTime">;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
timeZone: string;
|
|
||||||
availability: Availability[];
|
|
||||||
setTimeZone: (timeZone: string) => void;
|
|
||||||
setAvailability: (schedule: {
|
|
||||||
openingHours: AvailabilityInput[];
|
|
||||||
dateOverrides: AvailabilityInput[];
|
|
||||||
}) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export const Scheduler = ({ availability, setAvailability, timeZone, setTimeZone }: Props) => {
|
|
||||||
const { t, i18n } = useLocale();
|
|
||||||
const [editSchedule, setEditSchedule] = useState(-1);
|
|
||||||
const [openingHours, setOpeningHours] = useState<Availability[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpeningHours(availability.filter((item: Availability) => item.days.length !== 0));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAvailability({ openingHours, dateOverrides: [] });
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [openingHours]);
|
|
||||||
|
|
||||||
const addNewSchedule = () => setEditSchedule(openingHours.length);
|
|
||||||
|
|
||||||
const applyEditSchedule = (changed: Availability) => {
|
|
||||||
// new entry
|
|
||||||
if (!changed.days) {
|
|
||||||
changed.days = [1, 2, 3, 4, 5]; // Mon - Fri
|
|
||||||
setOpeningHours(openingHours.concat(changed));
|
|
||||||
} else {
|
|
||||||
// update
|
|
||||||
const replaceWith = { ...openingHours[editSchedule], ...changed };
|
|
||||||
openingHours.splice(editSchedule, 1, replaceWith);
|
|
||||||
setOpeningHours([...openingHours]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeScheduleAt = (toRemove: number) => {
|
|
||||||
openingHours.splice(toRemove, 1);
|
|
||||||
setOpeningHours([...openingHours]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OpeningHours = ({ idx, item }: { idx: number; item: Availability }) => (
|
|
||||||
<li className="flex justify-between border-b py-2">
|
|
||||||
<div className="flex flex-col space-y-4 lg:inline-flex">
|
|
||||||
<WeekdaySelect defaultValue={item.days} onSelect={(selected: number[]) => (item.days = selected)} />
|
|
||||||
<button
|
|
||||||
className="rounded-sm bg-neutral-100 px-3 py-2 text-sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setEditSchedule(idx)}>
|
|
||||||
{item.startTime.toLocaleTimeString(i18n.language, {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
timeZone: "UTC",
|
|
||||||
})}
|
|
||||||
{t("until")}
|
|
||||||
{item.endTime.toLocaleTimeString(i18n.language, {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
timeZone: "UTC",
|
|
||||||
})}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeScheduleAt(idx)}
|
|
||||||
className="btn-sm ml-1 bg-transparent px-2 py-1">
|
|
||||||
<TrashIcon className="-mt-1 inline h-5 w-5 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="w-full">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("timezone")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<TimezoneSelect
|
|
||||||
id="timeZone"
|
|
||||||
value={timeZone}
|
|
||||||
onChange={(tz: ITimezoneOption) => setTimeZone(tz.value)}
|
|
||||||
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul>
|
|
||||||
{openingHours.map((item, idx) => (
|
|
||||||
<OpeningHours key={idx} idx={idx} item={item} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<Button type="button" onClick={addNewSchedule} className="mt-2" color="secondary" size="sm">
|
|
||||||
{t("add_another")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{editSchedule >= 0 && (
|
|
||||||
<SetTimesModal
|
|
||||||
isOpen={true}
|
|
||||||
startTime={
|
|
||||||
openingHours[editSchedule]
|
|
||||||
? new Date(openingHours[editSchedule].startTime).getUTCHours() * 60 +
|
|
||||||
new Date(openingHours[editSchedule].startTime).getUTCMinutes()
|
|
||||||
: 540
|
|
||||||
}
|
|
||||||
endTime={
|
|
||||||
openingHours[editSchedule]
|
|
||||||
? new Date(openingHours[editSchedule].endTime).getUTCHours() * 60 +
|
|
||||||
new Date(openingHours[editSchedule].endTime).getUTCMinutes()
|
|
||||||
: 1020
|
|
||||||
}
|
|
||||||
onChange={(times: { startTime: number; endTime: number }) =>
|
|
||||||
applyEditSchedule({
|
|
||||||
...(openingHours[editSchedule] || {}),
|
|
||||||
startTime: new Date(
|
|
||||||
new Date().setUTCHours(Math.floor(times.startTime / 60), times.startTime % 60, 0, 0)
|
|
||||||
),
|
|
||||||
endTime: new Date(
|
|
||||||
new Date().setUTCHours(Math.floor(times.endTime / 60), times.endTime % 60, 0, 0)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onExit={() => setEditSchedule(-1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -87,7 +87,7 @@ const ColorPicker = (props: ColorPickerProps) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<HexColorInput
|
<HexColorInput
|
||||||
className="ml-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
|
className="ml-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
|
||||||
color={color}
|
color={color}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setColor(val);
|
setColor(val);
|
||||||
|
|
|
@ -14,7 +14,7 @@ type Props = {
|
||||||
export const DateRangePicker = ({ startDate, endDate, onDatesChange }: Props) => {
|
export const DateRangePicker = ({ startDate, endDate, onDatesChange }: Props) => {
|
||||||
return (
|
return (
|
||||||
<PrimitiveDateRangePicker
|
<PrimitiveDateRangePicker
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 rounded-sm border-gray-300 sm:text-sm"
|
className="rounded-sm border-gray-300 sm:text-sm"
|
||||||
clearIcon={null}
|
clearIcon={null}
|
||||||
calendarIcon={<CalendarIcon className="h-5 w-5 text-gray-500" />}
|
calendarIcon={<CalendarIcon className="h-5 w-5 text-gray-500" />}
|
||||||
rangeDivider={<ArrowRightIcon className="h-4 w-4 text-gray-400 ltr:mr-2 rtl:ml-2" />}
|
rangeDivider={<ArrowRightIcon className="h-4 w-4 text-gray-400 ltr:mr-2 rtl:ml-2" />}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="number"
|
type="number"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm",
|
"block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm",
|
||||||
rest.className
|
rest.className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactSelect, { components, GroupBase, Props } from "react-select";
|
import ReactSelect, { components, GroupBase, Props, InputProps } from "react-select";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
export type SelectProps<
|
||||||
|
Option,
|
||||||
|
IsMulti extends boolean = false,
|
||||||
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
|
> = Props<Option, IsMulti, Group>;
|
||||||
|
|
||||||
|
export const InputComponent = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({
|
||||||
|
inputClassName,
|
||||||
|
...props
|
||||||
|
}: InputProps<Option, IsMulti, Group>) => {
|
||||||
|
return (
|
||||||
|
<components.Input
|
||||||
|
// disables our default form focus hightlight on the react-select input element
|
||||||
|
inputClassName={classNames("focus:ring-0 focus:ring-offset-0", inputClassName)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function Select<
|
function Select<
|
||||||
Option,
|
Option,
|
||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
>({ className, ...props }: Props<Option, IsMulti, Group>) {
|
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
theme={(theme) => ({
|
theme={(theme) => ({
|
||||||
|
@ -15,26 +34,28 @@ function Select<
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
colors: {
|
colors: {
|
||||||
...theme.colors,
|
...theme.colors,
|
||||||
primary: "rgba(17, 17, 17, var(--tw-bg-opacity))",
|
primary: "var(--brand-color)",
|
||||||
|
|
||||||
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||||
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
styles={{
|
styles={{
|
||||||
option: (base, state) => ({
|
option: (provided, state) => ({
|
||||||
...base,
|
...provided,
|
||||||
|
color: state.isSelected ? "var(--brand-text-color)" : "black",
|
||||||
":active": {
|
":active": {
|
||||||
backgroundColor: state.isSelected ? "" : "rgba(17, 17, 17, var(--tw-bg-opacity))",
|
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
|
||||||
color: "#ffffff",
|
color: "var(--brand-text-color)",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
...components,
|
...components,
|
||||||
IndicatorSeparator: () => null,
|
IndicatorSeparator: () => null,
|
||||||
|
Input: InputComponent,
|
||||||
}}
|
}}
|
||||||
className={classNames("focus:border-primary-500 text-sm shadow-sm", className)}
|
className={classNames("text-sm shadow-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
43
apps/web/components/ui/form/TimezoneSelect.tsx
Normal file
43
apps/web/components/ui/form/TimezoneSelect.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { components } from "react-select";
|
||||||
|
import BaseSelect, { ITimezone, Props as SelectProps } from "react-timezone-select";
|
||||||
|
|
||||||
|
import { InputComponent } from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
function TimezoneSelect({ className, ...props }: SelectProps) {
|
||||||
|
return (
|
||||||
|
<BaseSelect
|
||||||
|
theme={(theme) => ({
|
||||||
|
...theme,
|
||||||
|
borderRadius: 2,
|
||||||
|
colors: {
|
||||||
|
...theme.colors,
|
||||||
|
primary: "var(--brand-color)",
|
||||||
|
|
||||||
|
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||||
|
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
styles={{
|
||||||
|
option: (provided, state) => ({
|
||||||
|
...provided,
|
||||||
|
color: state.isSelected ? "var(--brand-text-color)" : "black",
|
||||||
|
":active": {
|
||||||
|
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
|
||||||
|
color: "var(--brand-text-color)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
...components,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
Input: InputComponent,
|
||||||
|
}}
|
||||||
|
className={classNames("text-sm shadow-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimezoneSelect;
|
||||||
|
export type { ITimezone };
|
|
@ -1,222 +0,0 @@
|
||||||
import { ClockIcon } from "@heroicons/react/outline";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
|
|
||||||
|
|
||||||
dayjs.extend(customParseFormat);
|
|
||||||
|
|
||||||
interface SetTimesModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
onChange: (times: { startTime: number; endTime: number }) => void;
|
|
||||||
onExit: (...p: unknown[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SetTimesModal(props: SetTimesModalProps) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [startHours, startMinutes] = [Math.floor(props.startTime / 60), props.startTime % 60];
|
|
||||||
const [endHours, endMinutes] = [Math.floor(props.endTime / 60), props.endTime % 60];
|
|
||||||
const startHoursRef = useRef<HTMLInputElement>(null!);
|
|
||||||
const startMinsRef = useRef<HTMLInputElement>(null!);
|
|
||||||
const endHoursRef = useRef<HTMLInputElement>(null!);
|
|
||||||
const endMinsRef = useRef<HTMLInputElement>(null!);
|
|
||||||
const [endMinuteDisable, setEndMinuteDisable] = useState(false);
|
|
||||||
const [maximumStartTime, setMaximumStartTime] = useState({ hour: endHours, minute: 59 });
|
|
||||||
const [minimumEndTime, setMinimumEndTime] = useState({ hour: startHours, minute: 59 });
|
|
||||||
|
|
||||||
const STEP = 15;
|
|
||||||
|
|
||||||
const isValidTime = (startTime: number, endTime: number) => {
|
|
||||||
if (new Date(startTime) > new Date(endTime)) {
|
|
||||||
showToast(t("error_end_time_before_start_time"), "error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (endTime > 1440) {
|
|
||||||
showToast(t("error_end_time_next_day"), "error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// compute dynamic range for minimum and maximum allowed hours/minutes.
|
|
||||||
const setEdgeTimes = (
|
|
||||||
(step) =>
|
|
||||||
(
|
|
||||||
startHoursRef: React.MutableRefObject<HTMLInputElement>,
|
|
||||||
startMinsRef: React.MutableRefObject<HTMLInputElement>,
|
|
||||||
endHoursRef: React.MutableRefObject<HTMLInputElement>,
|
|
||||||
endMinsRef: React.MutableRefObject<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
//parse all the refs
|
|
||||||
const startHour = parseInt(startHoursRef.current.value);
|
|
||||||
let startMinute = parseInt(startMinsRef.current.value);
|
|
||||||
const endHour = parseInt(endHoursRef.current.value);
|
|
||||||
let endMinute = parseInt(endMinsRef.current.value);
|
|
||||||
|
|
||||||
//convert to dayjs object
|
|
||||||
const startTime = dayjs(`${startHour}:${startMinute}`, "hh:mm");
|
|
||||||
const endTime = dayjs(`${endHour}:${endMinute}`, "hh:mm");
|
|
||||||
|
|
||||||
//compute minimin and maximum allowed
|
|
||||||
const maximumStartTime = endTime.subtract(step, "minute");
|
|
||||||
const maximumStartHour = maximumStartTime.hour();
|
|
||||||
const maximumStartMinute = startHour === endHour ? maximumStartTime.minute() : 59;
|
|
||||||
|
|
||||||
const minimumEndTime = startTime.add(step, "minute");
|
|
||||||
const minimumEndHour = minimumEndTime.hour();
|
|
||||||
const minimumEndMinute = startHour === endHour ? minimumEndTime.minute() : 0;
|
|
||||||
|
|
||||||
//check allow min/max minutes when the end/start hour matches
|
|
||||||
if (startHoursRef.current.value === endHoursRef.current.value) {
|
|
||||||
if (parseInt(startMinsRef.current.value) >= maximumStartMinute)
|
|
||||||
startMinsRef.current.value = maximumStartMinute.toString();
|
|
||||||
if (parseInt(endMinsRef.current.value) <= minimumEndMinute)
|
|
||||||
endMinsRef.current.value = minimumEndMinute.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
//save into state
|
|
||||||
setMaximumStartTime({ hour: maximumStartHour, minute: maximumStartMinute });
|
|
||||||
setMinimumEndTime({ hour: minimumEndHour, minute: minimumEndMinute });
|
|
||||||
}
|
|
||||||
)(STEP);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
|
|
||||||
<DialogContent>
|
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
|
||||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<ClockIcon className="h-6 w-6 text-black" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
|
||||||
{t("change_bookings_availability")}
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">{t("set_work_schedule")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4 flex">
|
|
||||||
<label className="block w-1/4 pt-2 text-sm font-medium text-gray-700">{t("start_time")}</label>
|
|
||||||
<div className="w-1/6">
|
|
||||||
<label htmlFor="startHours" className="sr-only">
|
|
||||||
{t("hours")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={startHoursRef}
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max={maximumStartTime.hour}
|
|
||||||
minLength={2}
|
|
||||||
name="hours"
|
|
||||||
id="startHours"
|
|
||||||
className="focus:border-brand block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
|
||||||
placeholder="9"
|
|
||||||
defaultValue={startHours}
|
|
||||||
onChange={() => setEdgeTimes(startHoursRef, startMinsRef, endHoursRef, endMinsRef)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="mx-2 pt-1">:</span>
|
|
||||||
<div className="w-1/6">
|
|
||||||
<label htmlFor="startMinutes" className="sr-only">
|
|
||||||
{t("minutes")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={startMinsRef}
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max={maximumStartTime.minute}
|
|
||||||
step={STEP}
|
|
||||||
maxLength={2}
|
|
||||||
name="minutes"
|
|
||||||
id="startMinutes"
|
|
||||||
className="focus:border-brand block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
|
||||||
placeholder="30"
|
|
||||||
defaultValue={startMinutes}
|
|
||||||
onChange={() => setEdgeTimes(startHoursRef, startMinsRef, endHoursRef, endMinsRef)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<label className="block w-1/4 pt-2 text-sm font-medium text-gray-700">{t("end_time")}</label>
|
|
||||||
<div className="w-1/6">
|
|
||||||
<label htmlFor="endHours" className="sr-only">
|
|
||||||
{t("hours")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={endHoursRef}
|
|
||||||
type="number"
|
|
||||||
min={minimumEndTime.hour}
|
|
||||||
max="24"
|
|
||||||
maxLength={2}
|
|
||||||
name="hours"
|
|
||||||
id="endHours"
|
|
||||||
className="focus:border-brand block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
|
||||||
placeholder="17"
|
|
||||||
defaultValue={endHours}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (endHoursRef.current.value === "24") endMinsRef.current.value = "0";
|
|
||||||
setEdgeTimes(startHoursRef, startMinsRef, endHoursRef, endMinsRef);
|
|
||||||
setEndMinuteDisable(endHoursRef.current.value === "24");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="mx-2 pt-1">:</span>
|
|
||||||
<div className="w-1/6">
|
|
||||||
<label htmlFor="endMinutes" className="sr-only">
|
|
||||||
{t("minutes")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={endMinsRef}
|
|
||||||
type="number"
|
|
||||||
min={minimumEndTime.minute}
|
|
||||||
max="59"
|
|
||||||
maxLength={2}
|
|
||||||
step={STEP}
|
|
||||||
name="minutes"
|
|
||||||
id="endMinutes"
|
|
||||||
className="focus:border-brand block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
|
||||||
placeholder="30"
|
|
||||||
defaultValue={endMinutes}
|
|
||||||
disabled={endMinuteDisable}
|
|
||||||
onChange={() => setEdgeTimes(startHoursRef, startMinsRef, endHoursRef, endMinsRef)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const enteredStartHours = parseInt(startHoursRef.current.value);
|
|
||||||
const enteredStartMins = parseInt(startMinsRef.current.value);
|
|
||||||
const enteredEndHours = parseInt(endHoursRef.current.value);
|
|
||||||
const enteredEndMins = parseInt(endMinsRef.current.value);
|
|
||||||
|
|
||||||
if (
|
|
||||||
isValidTime(enteredStartHours * 60 + enteredStartMins, enteredEndHours * 60 + enteredEndMins)
|
|
||||||
) {
|
|
||||||
props.onChange({
|
|
||||||
startTime: enteredStartHours * 60 + enteredStartMins,
|
|
||||||
endTime: enteredEndHours * 60 + enteredEndMins,
|
|
||||||
});
|
|
||||||
props.onExit(0);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="submit">
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={props.onExit} type="button" color="secondary" className="ltr:mr-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -60,8 +60,7 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||||
classNamePrefix="react-select"
|
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
|
||||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -73,8 +72,7 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
{ value: 60, label: "60 minutes" },
|
{ value: 60, label: "60 minutes" },
|
||||||
]}
|
]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||||
classNamePrefix="react-select"
|
classNamePrefix="react-select"
|
||||||
className="react-select-container w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
className="react-select-container w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
|
@ -92,8 +92,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
||||||
{ value: 60, label: "60 minutes" },
|
{ value: 60, label: "60 minutes" },
|
||||||
]}
|
]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -51,7 +51,7 @@ function IframeEmbedContainer() {
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<input
|
<input
|
||||||
id="iframe"
|
id="iframe"
|
||||||
className="focus:border-brand px-2 py-1 text-sm text-gray-500 focus:ring-black"
|
className="px-2 py-1 text-sm text-gray-500 "
|
||||||
placeholder={t("loading")}
|
placeholder={t("loading")}
|
||||||
defaultValue={iframeTemplate}
|
defaultValue={iframeTemplate}
|
||||||
readOnly
|
readOnly
|
||||||
|
@ -76,7 +76,7 @@ function IframeEmbedContainer() {
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
id="fullscreen"
|
id="fullscreen"
|
||||||
className="focus:border-brand px-2 py-1 text-sm text-gray-500 focus:ring-black"
|
className="px-2 py-1 text-sm text-gray-500 "
|
||||||
placeholder={t("loading")}
|
placeholder={t("loading")}
|
||||||
defaultValue={htmlTemplate}
|
defaultValue={htmlTemplate}
|
||||||
readOnly
|
readOnly
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { BadgeCheckIcon } from "@heroicons/react/solid";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import TimezoneSelect from "react-timezone-select";
|
|
||||||
|
|
||||||
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -18,6 +17,7 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import Schedule from "@components/availability/Schedule";
|
import Schedule from "@components/availability/Schedule";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
import TimezoneSelect from "@components/ui/form/TimezoneSelect";
|
||||||
|
|
||||||
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
|
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -99,7 +99,7 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.sc
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
value={value}
|
value={value}
|
||||||
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm sm:text-sm"
|
||||||
onChange={(timezone) => onChange(timezone.value)}
|
onChange={(timezone) => onChange(timezone.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { useRouter } from "next/router";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Controller, Noop, useForm, UseFormReturn } from "react-hook-form";
|
import { Controller, Noop, useForm, UseFormReturn } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import Select, { Props as SelectProps } from "react-select";
|
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
@ -62,6 +61,7 @@ import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
||||||
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
||||||
import MinutesField from "@components/ui/form/MinutesField";
|
import MinutesField from "@components/ui/form/MinutesField";
|
||||||
|
import Select, { SelectProps } from "@components/ui/form/Select";
|
||||||
import * as RadioArea from "@components/ui/form/radio-area";
|
import * as RadioArea from "@components/ui/form/radio-area";
|
||||||
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ const SuccessRedirectEdit = <T extends UseFormReturn<any, any>>({
|
||||||
}}
|
}}
|
||||||
readOnly={proUpgradeRequired}
|
readOnly={proUpgradeRequired}
|
||||||
type="url"
|
type="url"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
className=" block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
|
@ -172,11 +172,7 @@ const AvailabilitySelect = ({
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
classNamePrefix="react-select"
|
className={classNames("block w-full min-w-0 flex-1 rounded-sm sm:text-sm", className)}
|
||||||
className={classNames(
|
|
||||||
"react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -356,7 +352,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{...locationFormMethods.register("locationAddress")}
|
{...locationFormMethods.register("locationAddress")}
|
||||||
id="address"
|
id="address"
|
||||||
required
|
required
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className=" block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
defaultValue={
|
defaultValue={
|
||||||
formMethods
|
formMethods
|
||||||
.getValues("locations")
|
.getValues("locations")
|
||||||
|
@ -378,7 +374,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{...locationFormMethods.register("locationLink")}
|
{...locationFormMethods.register("locationLink")}
|
||||||
id="address"
|
id="address"
|
||||||
required
|
required
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
className=" block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
defaultValue={
|
defaultValue={
|
||||||
formMethods.getValues("locations").find((location) => location.type === LocationType.Link)
|
formMethods.getValues("locations").find((location) => location.type === LocationType.Link)
|
||||||
?.link
|
?.link
|
||||||
|
@ -524,8 +520,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<Select
|
<Select
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e?.value) {
|
if (e?.value) {
|
||||||
const newLocationType: LocationType = e.value;
|
const newLocationType: LocationType = e.value;
|
||||||
|
@ -958,7 +953,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
id="slug"
|
id="slug"
|
||||||
aria-labelledby="slug-label"
|
aria-labelledby="slug-label"
|
||||||
required
|
required
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
className=" block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
{...formMethods.register("slug", {
|
{...formMethods.register("slug", {
|
||||||
setValueAs: (v) => slugify(v),
|
setValueAs: (v) => slugify(v),
|
||||||
|
@ -1024,7 +1019,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className=" block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
placeholder={t("quick_video_meeting")}
|
placeholder={t("quick_video_meeting")}
|
||||||
{...formMethods.register("description")}
|
{...formMethods.register("description")}
|
||||||
defaultValue={asStringOrUndefined(eventType.description)}></textarea>
|
defaultValue={asStringOrUndefined(eventType.description)}></textarea>
|
||||||
|
@ -1043,20 +1038,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{t("availability")} <InfoBadge content={t("you_can_manage_your_schedules")} />
|
{t("availability")} <InfoBadge content={t("you_can_manage_your_schedules")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<Controller
|
||||||
<Controller
|
name="schedule"
|
||||||
name="schedule"
|
control={formMethods.control}
|
||||||
control={formMethods.control}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<AvailabilitySelect
|
||||||
<AvailabilitySelect
|
value={field.value}
|
||||||
value={field.value}
|
onBlur={field.onBlur}
|
||||||
onBlur={field.onBlur}
|
name={field.name}
|
||||||
name={field.name}
|
onChange={(selected) => field.onChange(selected?.value || null)}
|
||||||
onChange={(selected) => field.onChange(selected?.value || null)}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1177,7 +1170,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className=" block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user")}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
|
@ -1199,7 +1192,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{
|
{
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
className=" block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||||
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
|
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
|
||||||
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
|
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
|
||||||
{...formMethods.register("smartContractAddress")}
|
{...formMethods.register("smartContractAddress")}
|
||||||
|
@ -1362,45 +1355,40 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{t("slot_interval")}
|
{t("slot_interval")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<Controller
|
||||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
name="slotInterval"
|
||||||
<Controller
|
control={formMethods.control}
|
||||||
name="slotInterval"
|
render={() => {
|
||||||
control={formMethods.control}
|
const slotIntervalOptions = [
|
||||||
render={() => {
|
{
|
||||||
const slotIntervalOptions = [
|
label: t("slot_interval_default"),
|
||||||
{
|
value: -1,
|
||||||
label: t("slot_interval_default"),
|
},
|
||||||
value: -1,
|
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||||
},
|
label: minutes + " " + t("minutes"),
|
||||||
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
value: minutes,
|
||||||
label: minutes + " " + t("minutes"),
|
})),
|
||||||
value: minutes,
|
];
|
||||||
})),
|
return (
|
||||||
];
|
<Select
|
||||||
return (
|
isSearchable={false}
|
||||||
<Select
|
className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
isSearchable={false}
|
onChange={(val) => {
|
||||||
classNamePrefix="react-select"
|
formMethods.setValue(
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
"slotInterval",
|
||||||
onChange={(val) => {
|
val && (val.value || 0) > 0 ? val.value : null
|
||||||
formMethods.setValue(
|
);
|
||||||
"slotInterval",
|
}}
|
||||||
val && (val.value || 0) > 0 ? val.value : null
|
defaultValue={
|
||||||
);
|
slotIntervalOptions.find(
|
||||||
}}
|
(option) => option.value === eventType.slotInterval
|
||||||
defaultValue={
|
) || slotIntervalOptions[0]
|
||||||
slotIntervalOptions.find(
|
}
|
||||||
(option) => option.value === eventType.slotInterval
|
options={slotIntervalOptions}
|
||||||
) || slotIntervalOptions[0]
|
/>
|
||||||
}
|
);
|
||||||
options={slotIntervalOptions}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-2 border-neutral-200" />
|
<hr className="my-2 border-neutral-200" />
|
||||||
|
|
||||||
|
@ -1436,14 +1424,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="inline-flex">
|
<div className="inline-flex">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-12 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
className="block w-12 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
defaultValue={eventType.periodDays || 30}
|
defaultValue={eventType.periodDays || 30}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
id=""
|
id=""
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
|
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")}
|
{...formMethods.register("periodCountCalendarDays")}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||||
<option value="1">{t("calendar_days")}</option>
|
<option value="1">{t("calendar_days")}</option>
|
||||||
|
@ -1514,8 +1502,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
|
@ -1553,8 +1540,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
|
@ -1602,7 +1588,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
id="requirePayment"
|
id="requirePayment"
|
||||||
name="requirePayment"
|
name="requirePayment"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
className="text-primary-600 h-4 w-4 rounded border-gray-300"
|
||||||
defaultChecked={requirePayment}
|
defaultChecked={requirePayment}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1639,7 +1625,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
min="0.5"
|
min="0.5"
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm"
|
className=" block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm"
|
||||||
placeholder="Price"
|
placeholder="Price"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e.target.valueAsNumber * 100);
|
field.onChange(e.target.valueAsNumber * 100);
|
||||||
|
@ -1781,8 +1767,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
defaultValue={selectedLocation}
|
defaultValue={selectedLocation}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
classNamePrefix="react-select"
|
className=" my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
locationFormMethods.setValue("locationType", val.value);
|
||||||
|
|
|
@ -4,8 +4,6 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
|
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Select from "react-select";
|
|
||||||
import TimezoneSelect, { ITimezone } from "react-timezone-select";
|
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
|
@ -31,6 +29,8 @@ import Avatar from "@components/ui/Avatar";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
import InfoBadge from "@components/ui/InfoBadge";
|
import InfoBadge from "@components/ui/InfoBadge";
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
import ColorPicker from "@components/ui/colorpicker";
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
import TimezoneSelect, { ITimezone } from "@components/ui/form/TimezoneSelect";
|
||||||
|
|
||||||
import { UpgradeToProDialog } from "../../components/UpgradeToProDialog";
|
import { UpgradeToProDialog } from "../../components/UpgradeToProDialog";
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
placeholder={t("your_name")}
|
placeholder={t("your_name")}
|
||||||
required
|
required
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
|
||||||
defaultValue={props.user.name || undefined}
|
defaultValue={props.user.name || undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -241,7 +241,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
name="email"
|
name="email"
|
||||||
id="email"
|
id="email"
|
||||||
placeholder={t("your_email")}
|
placeholder={t("your_email")}
|
||||||
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||||
defaultValue={props.user.email}
|
defaultValue={props.user.email}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
@ -262,7 +262,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
placeholder={t("little_something_about")}
|
placeholder={t("little_something_about")}
|
||||||
rows={3}
|
rows={3}
|
||||||
defaultValue={props.user.bio || undefined}
|
defaultValue={props.user.bio || undefined}
|
||||||
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"></textarea>
|
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -314,8 +314,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
id="languageSelect"
|
id="languageSelect"
|
||||||
value={selectedLanguage || props.localeProp}
|
value={selectedLanguage || props.localeProp}
|
||||||
onChange={(v) => v && setSelectedLanguage(v)}
|
onChange={(v) => v && setSelectedLanguage(v)}
|
||||||
classNamePrefix="react-select"
|
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 capitalize shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
|
||||||
options={localeOptions}
|
options={localeOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -329,8 +328,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(v) => v && setSelectedTimeZone(v)}
|
onChange={(v) => v && setSelectedTimeZone(v)}
|
||||||
classNamePrefix="react-select"
|
className="mt-1 block w-full rounded-sm shadow-sm sm:text-sm"
|
||||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -343,8 +341,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
id="timeFormatSelect"
|
id="timeFormatSelect"
|
||||||
value={selectedTimeFormat || props.user.timeFormat}
|
value={selectedTimeFormat || props.user.timeFormat}
|
||||||
onChange={(v) => v && setSelectedTimeFormat(v)}
|
onChange={(v) => v && setSelectedTimeFormat(v)}
|
||||||
classNamePrefix="react-select"
|
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 capitalize shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
|
||||||
options={timeFormatOptions}
|
options={timeFormatOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -358,8 +355,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
id="weekStart"
|
id="weekStart"
|
||||||
value={selectedWeekStartDay}
|
value={selectedWeekStartDay}
|
||||||
onChange={(v) => v && setSelectedWeekStartDay(v)}
|
onChange={(v) => v && setSelectedWeekStartDay(v)}
|
||||||
classNamePrefix="react-select"
|
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 capitalize shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
|
||||||
options={[
|
options={[
|
||||||
{ value: "Sunday", label: nameOfDay(props.localeProp, 0) },
|
{ value: "Sunday", label: nameOfDay(props.localeProp, 0) },
|
||||||
{ value: "Monday", label: nameOfDay(props.localeProp, 1) },
|
{ value: "Monday", label: nameOfDay(props.localeProp, 1) },
|
||||||
|
@ -375,7 +371,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
ref={allowDynamicGroupBookingRef}
|
ref={allowDynamicGroupBookingRef}
|
||||||
defaultChecked={props.user.allowDynamicBooking || false}
|
defaultChecked={props.user.allowDynamicBooking || false}
|
||||||
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-800"
|
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 "
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
||||||
|
@ -397,7 +393,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
defaultValue={selectedTheme || themeOptions[0]}
|
defaultValue={selectedTheme || themeOptions[0]}
|
||||||
value={selectedTheme || themeOptions[0]}
|
value={selectedTheme || themeOptions[0]}
|
||||||
onChange={(v) => v && setSelectedTheme(v)}
|
onChange={(v) => v && setSelectedTheme(v)}
|
||||||
className="| { value: string } mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
className="mt-1 block w-full rounded-sm shadow-sm sm:text-sm"
|
||||||
options={themeOptions}
|
options={themeOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -409,7 +405,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(e) => setSelectedTheme(e.target.checked ? undefined : themeOptions[0])}
|
onChange={(e) => setSelectedTheme(e.target.checked ? undefined : themeOptions[0])}
|
||||||
checked={!selectedTheme}
|
checked={!selectedTheme}
|
||||||
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-800"
|
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 "
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
||||||
|
@ -420,19 +416,18 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="block rtl:space-x-reverse sm:flex sm:space-x-2">
|
<div className="block rtl:space-x-reverse sm:flex sm:space-x-2">
|
||||||
<div className="mb-6 w-full sm:w-1/2">
|
<div className="mb-2 sm:w-1/2">
|
||||||
<label htmlFor="brandColor" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="brandColor" className="block text-sm font-medium text-gray-700">
|
||||||
{t("light_brand_color")}
|
{t("light_brand_color")}
|
||||||
</label>
|
</label>
|
||||||
<ColorPicker defaultValue={props.user.brandColor} onChange={setBrandColor} />
|
<ColorPicker defaultValue={props.user.brandColor} onChange={setBrandColor} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 w-full sm:w-1/2">
|
<div className="mb-2 sm:w-1/2">
|
||||||
<label htmlFor="darkBrandColor" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="darkBrandColor" className="block text-sm font-medium text-gray-700">
|
||||||
{t("dark_brand_color")}
|
{t("dark_brand_color")}
|
||||||
</label>
|
</label>
|
||||||
<ColorPicker defaultValue={props.user.darkBrandColor} onChange={setDarkBrandColor} />
|
<ColorPicker defaultValue={props.user.darkBrandColor} onChange={setDarkBrandColor} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-6" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
|
|
|
@ -9,6 +9,31 @@
|
||||||
--brand-text-color-dark-mode: #292929;
|
--brand-text-color-dark-mode: #292929;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Override the default tailwindcss-forms styling (default is: 'colors.blue.600')
|
||||||
|
* @see: https://github.com/tailwindlabs/tailwindcss-forms/issues/14#issuecomment-1005376006
|
||||||
|
*/
|
||||||
|
[type='text']:focus,
|
||||||
|
[type='email']:focus,
|
||||||
|
[type='url']:focus,
|
||||||
|
[type='password']:focus,
|
||||||
|
[type='number']:focus,
|
||||||
|
[type='date']:focus,
|
||||||
|
[type='datetime-local']:focus,
|
||||||
|
[type='month']:focus,
|
||||||
|
[type='search']:focus,
|
||||||
|
[type='tel']:focus,
|
||||||
|
[type='checkbox']:focus,
|
||||||
|
[type='radio']:focus,
|
||||||
|
[type='time']:focus,
|
||||||
|
[type='week']:focus,
|
||||||
|
[multiple]:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
--tw-ring-color: var(--brand-color);
|
||||||
|
border-color: var(--brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
button[role="switch"][data-state="checked"] {
|
button[role="switch"][data-state="checked"] {
|
||||||
@apply bg-gray-900;
|
@apply bg-gray-900;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +61,7 @@ button[role="switch"][data-state="checked"] span {
|
||||||
|
|
||||||
.react-daterange-picker > .react-daterange-picker__wrapper input {
|
.react-daterange-picker > .react-daterange-picker__wrapper input {
|
||||||
/* Makes sure the on-focus behaviour is like Cal.com's */
|
/* Makes sure the on-focus behaviour is like Cal.com's */
|
||||||
@apply focus:border-primary-500 focus:ring-primary-500 my-0.5 h-auto rounded-sm py-0.5;
|
@apply my-0.5 h-auto rounded-sm py-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* note(PeerRich): TODO move @layer components into proper React Components: <Button color="primary" size="xs" /> */
|
/* note(PeerRich): TODO move @layer components into proper React Components: <Button color="primary" size="xs" /> */
|
||||||
|
@ -192,7 +217,7 @@ button[role="switch"][data-state="checked"] span {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-multi-email > [type="text"] {
|
.react-multi-email > [type="text"] {
|
||||||
@apply focus:border-brand block w-full rounded-md border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:text-sm;
|
@apply block w-full rounded-md border-gray-300 shadow-sm dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-multi-email [data-tag] {
|
.react-multi-email [data-tag] {
|
||||||
|
@ -238,34 +263,6 @@ button[role="switch"][data-state="checked"] span {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* !important to override react-select */
|
|
||||||
.react-select__value-container {
|
|
||||||
border: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__input input {
|
|
||||||
border: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__option--is-focused {
|
|
||||||
background: #000 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__control {
|
|
||||||
border: 2px solid transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__control.react-select__control--is-focused {
|
|
||||||
border: 2px solid #000 !important;
|
|
||||||
border-color: #000 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* !important to override react-dates */
|
/* !important to override react-dates */
|
||||||
.DateRangePickerInput__withBorder {
|
.DateRangePickerInput__withBorder {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
|
|
Loading…
Reference in a new issue