diff options
| author | Simon Orzel <simonorzel@gmail.com> | 2025-07-20 18:35:06 +0200 |
|---|---|---|
| committer | Simon Orzel <simonorzel@gmail.com> | 2025-07-20 18:35:06 +0200 |
| commit | b03287df8d6fa14153af264c6a3e3da5b2a06153 (patch) | |
| tree | 100b2c2a729295419dce61655bca418e74195ec4 /apps/web/src/components/ui/phone-input.tsx | |
| parent | 061624ab588afff18073ae62fbac425c1aa5cf3a (diff) | |
| parent | ba3da43728bc051aa033dc7b45042b0f0f788e63 (diff) | |
| download | opencut-b03287df8d6fa14153af264c6a3e3da5b2a06153.tar.gz | |
Merge branch 'main' into feat/project-preview-images
Diffstat (limited to 'apps/web/src/components/ui/phone-input.tsx')
| -rw-r--r-- | apps/web/src/components/ui/phone-input.tsx | 334 |
1 files changed, 167 insertions, 167 deletions
diff --git a/apps/web/src/components/ui/phone-input.tsx b/apps/web/src/components/ui/phone-input.tsx index d682c813..95ad9131 100644 --- a/apps/web/src/components/ui/phone-input.tsx +++ b/apps/web/src/components/ui/phone-input.tsx @@ -1,167 +1,167 @@ -import * as React from "react"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; -import * as RPNInput from "react-phone-number-input"; -import flags from "react-phone-number-input/flags"; - -import { Button } from "./button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "./command"; -import { Input } from "./input"; -import { Popover, PopoverContent, PopoverTrigger } from "./popover"; -import { ScrollArea } from "./scroll-area"; -import { cn } from "../../lib/utils"; - -type PhoneInputProps = Omit< - React.ComponentProps<"input">, - "onChange" | "value" | "ref" -> & - Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & { - onChange?: (value: RPNInput.Value) => void; - }; - -const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = - React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>( - ({ className, onChange, defaultCountry = "US", ...props }, ref) => { - return ( - <RPNInput.default - ref={ref} - className={cn("flex", className)} - flagComponent={FlagComponent} - countrySelectComponent={CountrySelect} - inputComponent={InputComponent} - defaultCountry={defaultCountry} - international - addInternationalOption - smartCaret={false} - /** - * Handles the onChange event. - * - * react-phone-number-input might trigger the onChange event as undefined - * when a valid phone number is not entered. To prevent this, - * the value is coerced to an empty string. - * - * @param {E164Number | undefined} value - The entered value - */ - onChange={(value) => onChange?.(value || ("" as RPNInput.Value))} - {...props} - /> - ); - }, - ); -PhoneInput.displayName = "PhoneInput"; - -const InputComponent = React.forwardRef< - HTMLInputElement, - React.ComponentProps<"input"> ->(({ className, ...props }, ref) => ( - <Input - className={cn("rounded-e-lg rounded-s-none", className)} - {...props} - ref={ref} - /> -)); -InputComponent.displayName = "InputComponent"; - -type CountryEntry = { label: string; value: RPNInput.Country | undefined }; - -type CountrySelectProps = { - disabled?: boolean; - value: RPNInput.Country; - options: CountryEntry[]; - onChange: (country: RPNInput.Country) => void; -}; - -const CountrySelect = ({ - disabled, - value: selectedCountry, - options: countryList, - onChange, -}: CountrySelectProps) => { - return ( - <Popover> - <PopoverTrigger asChild> - <Button - type="button" - variant="outline" - className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10" - disabled={disabled} - > - <FlagComponent - country={selectedCountry} - countryName={selectedCountry} - /> - <ChevronsUpDown - className={cn( - "-mr-2 size-4 opacity-50", - disabled ? "hidden" : "opacity-100", - )} - /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-[300px] p-0"> - <Command> - <CommandInput placeholder="Search country..." /> - <CommandList> - <ScrollArea className="h-72"> - <CommandEmpty>No country found.</CommandEmpty> - <CommandGroup> - {countryList.map(({ value, label }) => - value ? ( - <CountrySelectOption - key={value} - country={value} - countryName={label} - selectedCountry={selectedCountry} - onChange={onChange} - /> - ) : null, - )} - </CommandGroup> - </ScrollArea> - </CommandList> - </Command> - </PopoverContent> - </Popover> - ); -}; - -interface CountrySelectOptionProps extends RPNInput.FlagProps { - selectedCountry: RPNInput.Country; - onChange: (country: RPNInput.Country) => void; -} - -const CountrySelectOption = ({ - country, - countryName, - selectedCountry, - onChange, -}: CountrySelectOptionProps) => { - return ( - <CommandItem className="gap-2" onSelect={() => onChange(country)}> - <FlagComponent country={country} countryName={countryName} /> - <span className="flex-1 text-sm">{countryName}</span> - <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span> - <CheckIcon - className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`} - /> - </CommandItem> - ); -}; - -const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => { - const Flag = flags[country]; - - return ( - <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full"> - {Flag && <Flag title={countryName} />} - </span> - ); -}; - -export { PhoneInput }; +import * as React from "react";
+import { CheckIcon, ChevronsUpDown } from "lucide-react";
+import * as RPNInput from "react-phone-number-input";
+import flags from "react-phone-number-input/flags";
+
+import { Button } from "./button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "./command";
+import { Input } from "./input";
+import { Popover, PopoverContent, PopoverTrigger } from "./popover";
+import { ScrollArea } from "./scroll-area";
+import { cn } from "../../lib/utils";
+
+type PhoneInputProps = Omit<
+ React.ComponentProps<"input">,
+ "onChange" | "value" | "ref"
+> &
+ Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
+ onChange?: (value: RPNInput.Value) => void;
+ };
+
+const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
+ React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
+ ({ className, onChange, defaultCountry = "US", ...props }, ref) => {
+ return (
+ <RPNInput.default
+ ref={ref}
+ className={cn("flex", className)}
+ flagComponent={FlagComponent}
+ countrySelectComponent={CountrySelect}
+ inputComponent={InputComponent}
+ defaultCountry={defaultCountry}
+ international
+ addInternationalOption
+ smartCaret={false}
+ /**
+ * Handles the onChange event.
+ *
+ * react-phone-number-input might trigger the onChange event as undefined
+ * when a valid phone number is not entered. To prevent this,
+ * the value is coerced to an empty string.
+ *
+ * @param {E164Number | undefined} value - The entered value
+ */
+ onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
+ {...props}
+ />
+ );
+ }
+ );
+PhoneInput.displayName = "PhoneInput";
+
+const InputComponent = React.forwardRef<
+ HTMLInputElement,
+ React.ComponentProps<"input">
+>(({ className, ...props }, ref) => (
+ <Input
+ className={cn("rounded-e-lg rounded-s-none", className)}
+ {...props}
+ ref={ref}
+ />
+));
+InputComponent.displayName = "InputComponent";
+
+type CountryEntry = { label: string; value: RPNInput.Country | undefined };
+
+type CountrySelectProps = {
+ disabled?: boolean;
+ value: RPNInput.Country;
+ options: CountryEntry[];
+ onChange: (country: RPNInput.Country) => void;
+};
+
+const CountrySelect = ({
+ disabled,
+ value: selectedCountry,
+ options: countryList,
+ onChange,
+}: CountrySelectProps) => {
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ type="button"
+ variant="outline"
+ className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
+ disabled={disabled}
+ >
+ <FlagComponent
+ country={selectedCountry}
+ countryName={selectedCountry}
+ />
+ <ChevronsUpDown
+ className={cn(
+ "-mr-2 size-4 opacity-50",
+ disabled ? "hidden" : "opacity-100"
+ )}
+ />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[300px] p-0">
+ <Command>
+ <CommandInput placeholder="Search country..." />
+ <CommandList>
+ <ScrollArea className="h-72">
+ <CommandEmpty>No country found.</CommandEmpty>
+ <CommandGroup>
+ {countryList.map(({ value, label }) =>
+ value ? (
+ <CountrySelectOption
+ key={value}
+ country={value}
+ countryName={label}
+ selectedCountry={selectedCountry}
+ onChange={onChange}
+ />
+ ) : null
+ )}
+ </CommandGroup>
+ </ScrollArea>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+};
+
+interface CountrySelectOptionProps extends RPNInput.FlagProps {
+ selectedCountry: RPNInput.Country;
+ onChange: (country: RPNInput.Country) => void;
+}
+
+const CountrySelectOption = ({
+ country,
+ countryName,
+ selectedCountry,
+ onChange,
+}: CountrySelectOptionProps) => {
+ return (
+ <CommandItem className="gap-2" onSelect={() => onChange(country)}>
+ <FlagComponent country={country} countryName={countryName} />
+ <span className="flex-1 text-sm">{countryName}</span>
+ <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
+ <CheckIcon
+ className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
+ />
+ </CommandItem>
+ );
+};
+
+const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
+ const Flag = flags[country];
+
+ return (
+ <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full">
+ {Flag && <Flag title={countryName} />}
+ </span>
+ );
+};
+
+export { PhoneInput };
|
