2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import cx from "@calcom/lib/classNames";
import { Icon } from "..";
import { Input } from "../components/form";
export type AddressInputProps = {
value: string;
id?: string;
placeholder?: string;
required?: boolean;
onChange: (val: string) => void;
className?: string;
};
function AddressInput({ value, onChange, ...rest }: AddressInputProps) {
return (
<div className="relative flex items-center">
<Icon
name="map-pin"
className="text-muted absolute left-0.5 ml-3 h-4 w-4 -translate-y-1/2"
style={{ top: "44%" }}
/>
<Input
{...rest}
value={value}
onChange={(e) => {
onChange(e.target.value);
}}
className={cx("pl-10", rest?.className)}
/>
</div>
);
}
export default AddressInput;

View File

@@ -0,0 +1,8 @@
import dynamic from "next/dynamic";
/** These are like 40kb that not every user needs */
const AddressInput = dynamic(
() => import("./AddressInput")
) as unknown as typeof import("./AddressInput").default;
export default AddressInput;

View File

@@ -0,0 +1,87 @@
import { isSupportedCountry } from "libphonenumber-js";
import { useState, useEffect } from "react";
import PhoneInput from "react-phone-input-2";
import "react-phone-input-2/lib/style.css";
import { classNames } from "@calcom/lib";
import { trpc } from "@calcom/trpc/react";
export type PhoneInputProps = {
value?: string;
id?: string;
placeholder?: string;
required?: boolean;
className?: string;
name?: string;
disabled?: boolean;
onChange: (value: string) => void;
};
function BasePhoneInput({ name, className = "", onChange, value, ...rest }: PhoneInputProps) {
const defaultCountry = useDefaultCountry();
return (
<PhoneInput
{...rest}
value={value ? value.trim().replace(/^\+?/, "+") : undefined}
country={value ? undefined : defaultCountry}
enableSearch
disableSearchIcon
inputProps={{
name: name,
required: rest.required,
placeholder: rest.placeholder,
}}
onChange={(value) => {
onChange(`+${value}`);
}}
containerClass={classNames(
"hover:border-emphasis dark:focus:border-emphasis border-default !bg-default rounded-md border focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-default disabled:cursor-not-allowed",
className
)}
inputClass="text-sm focus:ring-0 !bg-default text-default"
buttonClass="text-emphasis !bg-default hover:!bg-emphasis"
searchClass="!text-default !bg-default hover:!bg-emphasis"
dropdownClass="!text-default !bg-default"
inputStyle={{ width: "inherit", border: 0 }}
searchStyle={{
display: "flex",
flexDirection: "row",
alignItems: "center",
padding: "6px 12px",
gap: "8px",
width: "296px",
height: "28px",
marginLeft: "-4px",
}}
dropdownStyle={{ width: "max-content" }}
/>
);
}
const useDefaultCountry = () => {
const [defaultCountry, setDefaultCountry] = useState("us");
const query = trpc.viewer.public.countryCode.useQuery(undefined, {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
});
useEffect(
function refactorMeWithoutEffect() {
const data = query.data;
if (!data?.countryCode) {
return;
}
isSupportedCountry(data?.countryCode)
? setDefaultCountry(data.countryCode.toLowerCase())
: setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us");
},
[query.data]
);
return defaultCountry;
};
export default BasePhoneInput;

View File

@@ -0,0 +1,8 @@
import dynamic from "next/dynamic";
/** These are like 40kb that not every user needs */
const PhoneInputLazy = dynamic(
() => import("./PhoneInput")
) as unknown as typeof import("./PhoneInput").default;
export default PhoneInputLazy;

View File

@@ -0,0 +1,106 @@
import type { GroupBase, InputProps, Props } from "react-select";
import ReactSelect, { components } from "react-select";
import classNames from "@calcom/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 }: SelectProps<Option, IsMulti, Group>) {
return (
<ReactSelect
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={className}
{...props}
/>
);
}
export default Select;
export function UnstyledSelect<
Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
>({ ...props }: SelectProps<Option, IsMulti, Group>) {
return (
<ReactSelect
{...props}
isSearchable={false}
theme={(theme) => ({ ...theme, borderRadius: 0, border: "none" })}
components={{
IndicatorSeparator: () => null,
Input: InputComponent,
}}
styles={{
container: (provided) => ({
...provided,
width: "100%",
}),
control: (provided) => ({
...provided,
backgroundColor: " transparent",
border: "none",
boxShadow: "none",
}),
option: (provided, state) => ({
...provided,
color: state.isSelected ? "var(--brand-text-color)" : "black",
":active": {
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
color: "var(--brand-text-color)",
},
}),
indicatorSeparator: () => ({
display: "hidden",
color: "black",
}),
}}
/>
);
}

View File

@@ -0,0 +1,263 @@
import { useId } from "@radix-ui/react-id";
import type { ReactElement, ReactNode, Ref } from "react";
import React, { forwardRef } from "react";
import type { FieldValues, SubmitHandler, UseFormReturn } from "react-hook-form";
import { FormProvider, useFormContext } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, showToast } from "../";
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return (
<input
{...props}
ref={ref}
className={classNames(
"border-default mt-1 block w-full rounded-sm border px-3 py-2 shadow-sm focus:border-neutral-800 focus:outline-none focus:ring-1 focus:ring-neutral-800 sm:text-sm",
props.className
)}
/>
);
});
export function Label(props: JSX.IntrinsicElements["label"]) {
return (
<label {...props} className={classNames("text-default block text-sm font-medium", props.className)}>
{props.children}
</label>
);
}
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
return (
<span className="bg-muted border-default text-subtle inline-flex flex-shrink-0 items-center rounded-l-sm border border-r-0 px-3 sm:text-sm">
{props.children}
</span>
);
}
type InputFieldProps = {
label?: ReactNode;
hint?: ReactNode;
addOnLeading?: ReactNode;
} & React.ComponentProps<typeof Input> & {
labelProps?: React.ComponentProps<typeof Label>;
};
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name),
labelProps,
placeholder = t(`${props.name}_placeholder`) !== `${props.name}_placeholder`
? t(`${props.name}_placeholder`)
: "",
className,
addOnLeading,
hint,
...passThrough
} = props;
return (
<div>
{!!props.name && (
<Label htmlFor={id} {...labelProps}>
{label}
</Label>
)}
{addOnLeading ? (
<div className="mt-1 flex rounded-md shadow-sm">
{addOnLeading}
<Input
id={id}
placeholder={placeholder}
className={classNames("mt-0", props.addOnLeading && "rounded-l-none", className)}
{...passThrough}
ref={ref}
/>
</div>
) : (
<Input id={id} placeholder={placeholder} className={className} {...passThrough} ref={ref} />
)}
{hint}
{methods?.formState?.errors[props.name]?.message && (
<Alert
className="mt-1"
severity="error"
message={<>{methods.formState.errors[props.name]?.message}</>}
/>
)}
</div>
);
});
export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
return <InputField ref={ref} {...props} />;
});
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
) {
return (
<InputField data-testid="password" type="password" placeholder="•••••••••••••" ref={ref} {...props} />
);
});
export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
return (
<Input
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
return (
<InputField
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
type TextAreaProps = Omit<JSX.IntrinsicElements["textarea"], "name"> & { name: string };
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextAreaInput(props, ref) {
return (
<textarea
ref={ref}
{...props}
className={classNames(
"border-default block w-full rounded-sm shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm",
props.className
)}
/>
);
});
type TextAreaFieldProps = {
label?: ReactNode;
} & React.ComponentProps<typeof TextArea> & {
labelProps?: React.ComponentProps<typeof Label>;
};
export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(function TextField(
props,
ref
) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name as string),
labelProps,
placeholder = t(`${props.name}_placeholder`) !== `${props.name}_placeholder`
? t(`${props.name}_placeholder`)
: "",
...passThrough
} = props;
return (
<div>
{!!props.name && (
<Label htmlFor={id} {...labelProps}>
{label}
</Label>
)}
<TextArea ref={ref} placeholder={placeholder} {...passThrough} />
{methods?.formState?.errors[props.name]?.message && (
<Alert
className="mt-1"
severity="error"
message={<>{methods.formState.errors[props.name]?.message}</>}
/>
)}
</div>
);
});
type FormProps<T extends object> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return (
<FormProvider {...form}>
<form
ref={ref}
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form
.handleSubmit(handleSubmit)(event)
.catch((err) => {
showToast(`${getErrorFromUnknown(err).message}`, "error");
});
}}
{...passThrough}>
{
/* @see https://react-hook-form.com/advanced-usage/#SmartFormComponent */
React.Children.map(props.children, (child) => {
return typeof child !== "string" &&
typeof child !== "number" &&
typeof child !== "boolean" &&
child &&
"props" in child &&
child.props.name
? React.createElement(child.type, {
...{
...child.props,
register: form.register,
key: child.props.name,
},
})
: child;
})
}
</form>
</FormProvider>
);
};
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return (
<legend {...props} className={classNames("text-default text-sm font-medium", props.className)}>
{props.children}
</legend>
);
}
export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
return (
<div
{...props}
className={classNames("bg-default border-default space-y-2 rounded-sm border p-2", props.className)}>
{props.children}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import type { ReactNode } from "react";
import React from "react";
import classNames from "@calcom/lib/classNames";
export const Group = (props: RadioGroupPrimitive.RadioGroupProps & { children: ReactNode }) => (
<RadioGroupPrimitive.Root {...props}>{props.children}</RadioGroupPrimitive.Root>
);
export const Radio = (props: RadioGroupPrimitive.RadioGroupItemProps & { children: ReactNode }) => (
<RadioGroupPrimitive.Item
{...props}
className={classNames(
"hover:bg-subtle border-default dark:checked:bg-brand-default dark:bg-darkgray-100 dark:hover:bg-subtle dark:checked:hover:bg-brand-default focus:ring-brand-default hover:border-emphasis me-1.5 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border text-[--cal-brand] focus:border-0 focus:ring-1",
props.disabled && "opacity-60"
)}>
{props.children}
</RadioGroupPrimitive.Item>
);
export const Indicator = ({ disabled }: { disabled?: boolean }) => (
<RadioGroupPrimitive.Indicator
className={classNames(
"after:bg-default dark:after:bg-brand-accent relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
disabled ? "after:bg-muted" : "bg-brand-default"
)}
/>
);
export const Label = (props: JSX.IntrinsicElements["label"] & { disabled?: boolean }) => (
<label
{...props}
className={classNames(
"text-emphasis ms-2 w-full text-sm font-medium leading-5",
props.disabled && "text-subtle"
)}
/>
);
export const RadioField = ({
label,
disabled,
id,
value,
className,
}: {
label: string | ReactNode;
disabled?: boolean;
id: string;
value: string;
className?: string;
}) => (
<div className={classNames("flex items-start", className)}>
<Radio value={value} disabled={disabled} id={id}>
<Indicator disabled={disabled} />
</Radio>
<Label htmlFor={id} disabled={disabled}>
{label}
</Label>
</div>
);

View File

@@ -0,0 +1,59 @@
import { useId } from "@radix-ui/react-id";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import type { ReactNode } from "react";
import classNames from "@calcom/lib/classNames";
type RadioAreaProps = RadioGroupPrimitive.RadioGroupItemProps & {
children: ReactNode;
classNames?: { container?: string };
};
const RadioArea = ({ children, className, classNames: innerClassNames, ...props }: RadioAreaProps) => {
const radioAreaId = useId();
const id = props.id ?? radioAreaId;
return (
<div
className={classNames(
"border-subtle [&:has(input:checked)]:border-emphasis relative flex items-start rounded-md border ",
className
)}>
<RadioGroupPrimitive.Item
id={id}
{...props}
className={classNames(
"hover:bg-subtle disabled:hover:bg-default border-default focus:ring-emphasis absolute left-3 top-[0.9rem] mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2 disabled:cursor-not-allowed",
props.disabled && "opacity-60"
)}>
<RadioGroupPrimitive.Indicator
className={classNames(
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
props.disabled ? "after:bg-muted" : "bg-black"
)}
/>
</RadioGroupPrimitive.Item>
<label htmlFor={id} className={classNames("text-default p-4 pl-10 pt-3", innerClassNames?.container)}>
{children}
</label>
</div>
);
};
const RadioAreaGroup = ({
children,
className,
onValueChange,
...passThroughProps
}: RadioGroupPrimitive.RadioGroupProps) => {
return (
<RadioGroupPrimitive.Root className={className} onValueChange={onValueChange} {...passThroughProps}>
{children}
</RadioGroupPrimitive.Root>
);
};
const Item = RadioArea;
const Group = RadioAreaGroup;
export { RadioArea, RadioAreaGroup, Item, Group };

View File

@@ -0,0 +1,2 @@
export * as RadioGroup from "./RadioAreaGroup";
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";