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}
|
||||
options={options}
|
||||
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) => {
|
||||
setSelectedOption(option);
|
||||
if (!option) {
|
||||
|
|
|
@ -60,7 +60,19 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
|||
createMutation.mutate(values);
|
||||
}}>
|
||||
<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 className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
||||
import React, { FC } from "react";
|
||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
||||
import Select from "react-select";
|
||||
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
interface OptionTypeBase {
|
||||
label: string;
|
||||
value: EventTypeCustomInputType;
|
||||
|
@ -55,7 +56,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||
defaultValue={selectedInputOption}
|
||||
options={inputOptions}
|
||||
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)}
|
||||
value={selectedInputOption}
|
||||
onBlur={field.onBlur}
|
||||
|
@ -73,7 +74,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||
type="text"
|
||||
id="label"
|
||||
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}
|
||||
{...register("label", { required: true })}
|
||||
/>
|
||||
|
@ -89,7 +90,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||
<input
|
||||
type="text"
|
||||
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}
|
||||
{...register("placeholder")}
|
||||
/>
|
||||
|
|
|
@ -74,7 +74,7 @@ const ChangePasswordSection = () => {
|
|||
name="current_password"
|
||||
id="current_password"
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
|
@ -91,7 +91,7 @@ const ChangePasswordSection = () => {
|
|||
value={newPassword}
|
||||
required
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -70,7 +70,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||
required
|
||||
value={password}
|
||||
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>
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
required
|
||||
value={password}
|
||||
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>
|
||||
|
||||
|
@ -172,7 +172,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
minLength={6}
|
||||
inputMode="numeric"
|
||||
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>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MembershipRole } from "@prisma/client";
|
||||
import { useState } from "react";
|
||||
import React, { SyntheticEvent } from "react";
|
||||
import React, { SyntheticEvent, useEffect } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
@ -8,6 +8,14 @@ import Button from "@calcom/ui/Button";
|
|||
import { trpc } from "@lib/trpc";
|
||||
|
||||
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: {
|
||||
isOpen: boolean;
|
||||
|
@ -16,7 +24,16 @@ export default function MemberChangeRoleModal(props: {
|
|||
initialRole: MembershipRole;
|
||||
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 { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
|
@ -37,7 +54,7 @@ export default function MemberChangeRoleModal(props: {
|
|||
changeRoleMutation.mutate({
|
||||
teamId: props.teamId,
|
||||
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">
|
||||
{t("role")}
|
||||
</label>
|
||||
<select
|
||||
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={options}
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value as MembershipRole)}
|
||||
onChange={(option) => option && setRole(option)}
|
||||
id="role"
|
||||
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black 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>
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<p className="text-sm text-red-700">
|
||||
<span className="font-bold">Error: </span>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { UserIcon } from "@heroicons/react/outline";
|
||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { useState } from "react";
|
||||
import React, { SyntheticEvent } from "react";
|
||||
import React, { useState, useEffect, SyntheticEvent } from "react";
|
||||
|
||||
import Button from "@calcom/ui/Button";
|
||||
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 { trpc } from "@lib/trpc";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type MemberInvitationModalProps = {
|
||||
isOpen: boolean;
|
||||
team: TeamWithMembers | null;
|
||||
onExit: () => void;
|
||||
};
|
||||
|
||||
type MembershipRoleOption = {
|
||||
value: MembershipRole;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }];
|
||||
|
||||
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const { t, i18n } = useLocale();
|
||||
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", {
|
||||
async onSuccess() {
|
||||
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">
|
||||
{t("role")}
|
||||
</label>
|
||||
<select
|
||||
<Select
|
||||
options={options}
|
||||
id="role"
|
||||
className="focus:border-brand mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm">
|
||||
<option value="MEMBER">{t("member")}</option>
|
||||
<option value="ADMIN">{t("admin")}</option>
|
||||
</select>
|
||||
name="role"
|
||||
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-5 items-center">
|
||||
|
@ -97,7 +112,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
name="sendInviteEmail"
|
||||
defaultChecked
|
||||
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 className="text-sm ltr:ml-2 rtl:mr-2">
|
||||
|
|
|
@ -63,7 +63,7 @@ export default function TeamCreate(props: Props) {
|
|||
id="name"
|
||||
placeholder="Acme Inc."
|
||||
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>
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
|
|
|
@ -112,7 +112,7 @@ export default function TeamSettings(props: Props) {
|
|||
id="name"
|
||||
placeholder={t("your_team_name")}
|
||||
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}
|
||||
/>
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ export default function TeamSettings(props: Props) {
|
|||
name="about"
|
||||
rows={3}
|
||||
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>
|
||||
</>
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ export default function TeamSettings(props: Props) {
|
|||
name="avatar"
|
||||
id="avatar"
|
||||
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}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
<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}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
|
|
|
@ -14,7 +14,7 @@ type Props = {
|
|||
export const DateRangePicker = ({ startDate, endDate, onDatesChange }: Props) => {
|
||||
return (
|
||||
<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}
|
||||
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" />}
|
||||
|
|
|
@ -22,7 +22,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
|
|||
ref={ref}
|
||||
type="number"
|
||||
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
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
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";
|
||||
|
||||
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<
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({ className, ...props }: Props<Option, IsMulti, Group>) {
|
||||
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||
return (
|
||||
<ReactSelect
|
||||
theme={(theme) => ({
|
||||
|
@ -15,26 +34,28 @@ function Select<
|
|||
borderRadius: 2,
|
||||
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))",
|
||||
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||
},
|
||||
})}
|
||||
styles={{
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
color: state.isSelected ? "var(--brand-text-color)" : "black",
|
||||
":active": {
|
||||
backgroundColor: state.isSelected ? "" : "rgba(17, 17, 17, var(--tw-bg-opacity))",
|
||||
color: "#ffffff",
|
||||
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
|
||||
color: "var(--brand-text-color)",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
components={{
|
||||
...components,
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("focus:border-primary-500 text-sm shadow-sm", className)}
|
||||
className={classNames("text-sm shadow-sm", className)}
|
||||
{...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"
|
||||
value={selectedTimeZone}
|
||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -73,8 +72,7 @@ export default function TeamAvailabilityModal(props: Props) {
|
|||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
|
|
|
@ -80,7 +80,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
value={selectedTimeZone}
|
||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||
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 className="hidden sm:block">
|
||||
|
@ -92,8 +92,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
|
|
|
@ -51,7 +51,7 @@ function IframeEmbedContainer() {
|
|||
<div className="text-right">
|
||||
<input
|
||||
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")}
|
||||
defaultValue={iframeTemplate}
|
||||
readOnly
|
||||
|
@ -76,7 +76,7 @@ function IframeEmbedContainer() {
|
|||
<div>
|
||||
<input
|
||||
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")}
|
||||
defaultValue={htmlTemplate}
|
||||
readOnly
|
||||
|
|
|
@ -2,7 +2,6 @@ import { BadgeCheckIcon } from "@heroicons/react/solid";
|
|||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
|
||||
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -18,6 +17,7 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
|
|||
import Shell from "@components/Shell";
|
||||
import Schedule from "@components/availability/Schedule";
|
||||
import EditableHeading from "@components/ui/EditableHeading";
|
||||
import TimezoneSelect from "@components/ui/form/TimezoneSelect";
|
||||
|
||||
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
|
||||
const { t } = useLocale();
|
||||
|
@ -99,7 +99,7 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.sc
|
|||
render={({ field: { onChange, value } }) => (
|
||||
<TimezoneSelect
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -25,7 +25,6 @@ import { useRouter } from "next/router";
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, Noop, useForm, UseFormReturn } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import Select, { Props as SelectProps } from "react-select";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -62,6 +61,7 @@ import CheckboxField from "@components/ui/form/CheckboxField";
|
|||
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
||||
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
||||
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 WebhookListContainer from "@components/webhook/WebhookListContainer";
|
||||
|
||||
|
@ -122,7 +122,7 @@ const SuccessRedirectEdit = <T extends UseFormReturn<any, any>>({
|
|||
}}
|
||||
readOnly={proUpgradeRequired}
|
||||
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")}
|
||||
defaultValue={eventType.successRedirectUrl || ""}
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
|
@ -172,11 +172,7 @@ const AvailabilitySelect = ({
|
|||
options={options}
|
||||
isSearchable={false}
|
||||
onChange={props.onChange}
|
||||
classNamePrefix="react-select"
|
||||
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
|
||||
)}
|
||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm sm:text-sm", className)}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
|
@ -356,7 +352,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{...locationFormMethods.register("locationAddress")}
|
||||
id="address"
|
||||
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={
|
||||
formMethods
|
||||
.getValues("locations")
|
||||
|
@ -378,7 +374,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{...locationFormMethods.register("locationLink")}
|
||||
id="address"
|
||||
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={
|
||||
formMethods.getValues("locations").find((location) => location.type === LocationType.Link)
|
||||
?.link
|
||||
|
@ -524,8 +520,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<Select
|
||||
options={locationOptions}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||
onChange={(e) => {
|
||||
if (e?.value) {
|
||||
const newLocationType: LocationType = e.value;
|
||||
|
@ -958,7 +953,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
id="slug"
|
||||
aria-labelledby="slug-label"
|
||||
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}
|
||||
{...formMethods.register("slug", {
|
||||
setValueAs: (v) => slugify(v),
|
||||
|
@ -1024,7 +1019,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="w-full">
|
||||
<textarea
|
||||
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")}
|
||||
{...formMethods.register("description")}
|
||||
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")} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="schedule"
|
||||
control={formMethods.control}
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => field.onChange(selected?.value || null)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
name="schedule"
|
||||
control={formMethods.control}
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => field.onChange(selected?.value || null)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1177,7 +1170,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<input
|
||||
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")}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventName")}
|
||||
|
@ -1199,7 +1192,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{
|
||||
<input
|
||||
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")}
|
||||
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
|
||||
{...formMethods.register("smartContractAddress")}
|
||||
|
@ -1362,45 +1355,40 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{t("slot_interval")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const slotIntervalOptions = [
|
||||
{
|
||||
label: t("slot_interval_default"),
|
||||
value: -1,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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) => {
|
||||
formMethods.setValue(
|
||||
"slotInterval",
|
||||
val && (val.value || 0) > 0 ? val.value : null
|
||||
);
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
(option) => option.value === eventType.slotInterval
|
||||
) || slotIntervalOptions[0]
|
||||
}
|
||||
options={slotIntervalOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
render={() => {
|
||||
const slotIntervalOptions = [
|
||||
{
|
||||
label: t("slot_interval_default"),
|
||||
value: -1,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||
onChange={(val) => {
|
||||
formMethods.setValue(
|
||||
"slotInterval",
|
||||
val && (val.value || 0) > 0 ? val.value : null
|
||||
);
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
(option) => option.value === eventType.slotInterval
|
||||
) || slotIntervalOptions[0]
|
||||
}
|
||||
options={slotIntervalOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-2 border-neutral-200" />
|
||||
|
||||
|
@ -1436,14 +1424,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="inline-flex">
|
||||
<input
|
||||
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"
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
/>
|
||||
<select
|
||||
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")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
|
@ -1514,8 +1502,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
|
@ -1553,8 +1540,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
|
@ -1602,7 +1588,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
id="requirePayment"
|
||||
name="requirePayment"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1639,7 +1625,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
min="0.5"
|
||||
type="number"
|
||||
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"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.valueAsNumber * 100);
|
||||
|
@ -1781,8 +1767,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
defaultValue={selectedLocation}
|
||||
options={locationOptions}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className=" my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
|
|
|
@ -4,8 +4,6 @@ import { GetServerSidePropsContext } from "next";
|
|||
import { signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
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 { Alert } from "@calcom/ui/Alert";
|
||||
|
@ -31,6 +29,8 @@ import Avatar from "@components/ui/Avatar";
|
|||
import Badge from "@components/ui/Badge";
|
||||
import InfoBadge from "@components/ui/InfoBadge";
|
||||
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";
|
||||
|
||||
|
@ -225,7 +225,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
autoComplete="given-name"
|
||||
placeholder={t("your_name")}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
@ -241,7 +241,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
name="email"
|
||||
id="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}
|
||||
/>
|
||||
<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")}
|
||||
rows={3}
|
||||
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>
|
||||
|
@ -314,8 +314,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
id="languageSelect"
|
||||
value={selectedLanguage || props.localeProp}
|
||||
onChange={(v) => v && setSelectedLanguage(v)}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||
options={localeOptions}
|
||||
/>
|
||||
</div>
|
||||
|
@ -329,8 +328,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(v) => v && setSelectedTimeZone(v)}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="mt-1 block w-full rounded-sm shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -343,8 +341,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
id="timeFormatSelect"
|
||||
value={selectedTimeFormat || props.user.timeFormat}
|
||||
onChange={(v) => v && setSelectedTimeFormat(v)}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||
options={timeFormatOptions}
|
||||
/>
|
||||
</div>
|
||||
|
@ -358,8 +355,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
id="weekStart"
|
||||
value={selectedWeekStartDay}
|
||||
onChange={(v) => v && setSelectedWeekStartDay(v)}
|
||||
classNamePrefix="react-select"
|
||||
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"
|
||||
className="mt-1 block w-full rounded-sm capitalize shadow-sm sm:text-sm"
|
||||
options={[
|
||||
{ value: "Sunday", label: nameOfDay(props.localeProp, 0) },
|
||||
{ value: "Monday", label: nameOfDay(props.localeProp, 1) },
|
||||
|
@ -375,7 +371,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
type="checkbox"
|
||||
ref={allowDynamicGroupBookingRef}
|
||||
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 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]}
|
||||
value={selectedTheme || themeOptions[0]}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
@ -409,7 +405,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
type="checkbox"
|
||||
onChange={(e) => setSelectedTheme(e.target.checked ? undefined : themeOptions[0])}
|
||||
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 className="text-sm ltr:ml-3 rtl:mr-3">
|
||||
|
@ -420,19 +416,18 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{t("light_brand_color")}
|
||||
</label>
|
||||
<ColorPicker defaultValue={props.user.brandColor} onChange={setBrandColor} />
|
||||
</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">
|
||||
{t("dark_brand_color")}
|
||||
</label>
|
||||
<ColorPicker defaultValue={props.user.darkBrandColor} onChange={setDarkBrandColor} />
|
||||
</div>
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
|
|
|
@ -9,6 +9,31 @@
|
|||
--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"] {
|
||||
@apply bg-gray-900;
|
||||
}
|
||||
|
@ -36,7 +61,7 @@ button[role="switch"][data-state="checked"] span {
|
|||
|
||||
.react-daterange-picker > .react-daterange-picker__wrapper input {
|
||||
/* 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" /> */
|
||||
|
@ -192,7 +217,7 @@ button[role="switch"][data-state="checked"] span {
|
|||
}
|
||||
|
||||
.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] {
|
||||
|
@ -238,34 +263,6 @@ button[role="switch"][data-state="checked"] span {
|
|||
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 */
|
||||
.DateRangePickerInput__withBorder {
|
||||
border: 0 !important;
|
||||
|
|
Loading…
Reference in a new issue