Skip to content

Commit

Permalink
move phone number input to ui package
Browse files Browse the repository at this point in the history
  • Loading branch information
ramiAbdou committed Sep 24, 2024
1 parent 0984e56 commit 0c379c8
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 72 deletions.
20 changes: 14 additions & 6 deletions apps/member-profile/app/routes/_profile.profile.general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import { Student } from '@oyster/types';
import {
Button,
Divider,
Form,
getErrors,
InputField,
PhoneNumberInput,
validateForm,
} from '@oyster/ui';

Expand All @@ -28,7 +30,6 @@ import {
} from '@/shared/components/profile';
import {
CurrentLocationField,
PhoneNumberField,
PreferredNameField,
} from '@/shared/components/profile.general';
import { getMember } from '@/shared/queries/index';
Expand All @@ -53,8 +54,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
'genderPronouns',
'headline',
'lastName',
'preferredName',
'phoneNumber',
'preferredName',
])
.executeTakeFirstOrThrow();

Expand Down Expand Up @@ -177,11 +178,18 @@ export default function UpdateGeneralInformationSection() {
longitudeName={keys.currentLocationLongitude}
/>

<PhoneNumberField
defaultValue={student.phoneNumber || undefined}
<Form.Field
description="Enter your 10-digit phone number. We'll use this to send you important ColorStack updates."
error={errors.phoneNumber}
name={keys.phoneNumber}
/>
label="Phone Number"
labelFor={keys.phoneNumber}
>
<PhoneNumberInput
defaultValue={student.phoneNumber || undefined}
id={keys.phoneNumber}
name={keys.phoneNumber}
/>
</Form.Field>

<Button.Group>
<Button.Submit>Save</Button.Submit>
Expand Down
43 changes: 1 addition & 42 deletions apps/member-profile/app/shared/components/profile.general.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';

import { CityCombobox, type CityComboboxProps } from '@oyster/core/location/ui';
import { type FieldProps, Form, Input, InputField, Text } from '@oyster/ui';
import { type FieldProps, Form, Input, Text } from '@oyster/ui';

export function CurrentLocationField({
defaultValue,
Expand Down Expand Up @@ -62,44 +62,3 @@ export function PreferredNameField({
</Form.Field>
);
}

const formatPhoneNumber = (input: string) => {
const cleaned = input.replace(/\D/g, '');

if (cleaned.length == 0) {
return '';
} else if (cleaned.length <= 3) {
return `(${cleaned}`;
} else if (cleaned.length <= 6) {
return `(${cleaned.slice(0, 3)})-${cleaned.slice(3)}`;
} else {
return `(${cleaned.slice(0, 3)})-${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
}
};

export const PhoneNumberField = ({
defaultValue,
error,
name,
}: FieldProps<string>) => {
const [phoneNumber, setPhoneNumber] = useState(defaultValue || '');

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value);

setPhoneNumber(formatted);
};

return (
<InputField
value={phoneNumber}
onChange={handleChange}
error={error}
label="Phone Number"
name={name}
description="Enter your 10-digit phone number."
placeholder="(555)-123-4567"
type="tel"
/>
);
};
23 changes: 15 additions & 8 deletions packages/types/src/domain/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,6 @@ export const Student = Entity.merge(StudentSocialLinks)
joinedMemberDirectoryAt: z.coerce.date().nullable(),
joinedSlackAt: z.coerce.date().optional(),
lastName: z.string().trim().min(1),
phoneNumber: z
.string()
.trim()
.regex(
/^\(\d{3}\)-\d{3}-\d{4}$/,
'Phone Number must be in the format (555)-123-4567'
)
.optional(),

/**
* Enum that represents all of the accepted majors from the ColorStack
Expand All @@ -167,6 +159,21 @@ export const Student = Entity.merge(StudentSocialLinks)
otherMajor: z.string().optional(),
otherSchool: z.string().optional(),

/**
* A 10-digit phone number without any formatting. Note that since we only
* serve US and Canadian students, we will not worry about asking for nor
* storing the country code, it is by default +1. We will only store
* 10-digit values without any formatting (ie: parentheses, dashes, etc).
*
* @example "1112223333"
* @example "1234567890"
*/
phoneNumber: z
.string()
.trim()
.regex(/^\d{10}$/, 'Must be a 10-digit number.')
.optional(),

/**
* The preferred name that a member would like to go by. This will typically
* just be a first name.
Expand Down
16 changes: 4 additions & 12 deletions packages/ui/src/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ Form.Field = function FormField({

type InputFieldProps = FieldProps<string> &
Pick<FormFieldProps, 'description' | 'label' | 'required'> &
Pick<
InputProps,
'disabled' | 'placeholder' | 'type' | 'pattern' | 'onChange' | 'value'
>;
Pick<InputProps, 'disabled' | 'placeholder'>;

/**
* @deprecated Instead, just compose the `Form.Field` and `Input` together.
*/
export function InputField({
defaultValue,
description,
Expand All @@ -85,10 +85,6 @@ export function InputField({
name,
placeholder,
required,
pattern,
type = 'text',
onChange,
value,
}: InputFieldProps) {
return (
<Form.Field
Expand All @@ -105,10 +101,6 @@ export function InputField({
name={name}
placeholder={placeholder}
required={required}
type={type}
pattern={pattern}
onChange={onChange}
value={value}
/>
</Form.Field>
);
Expand Down
74 changes: 71 additions & 3 deletions packages/ui/src/components/input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type HTMLInputTypeAttribute } from 'react';
import React, { type HTMLInputTypeAttribute, useState } from 'react';

import { cx } from '../utils/cx';

Expand All @@ -22,9 +22,8 @@ export type InputProps = Pick<
| 'required'
| 'type'
| 'value'
| 'pattern'
> & {
type?: Extract<HTMLInputTypeAttribute, 'email' | 'number' | 'text' | 'tel'>;
type?: Extract<HTMLInputTypeAttribute, 'email' | 'number' | 'text'>;
};

export const Input = React.forwardRef(
Expand All @@ -43,6 +42,75 @@ export const Input = React.forwardRef(
}
);

type PhoneNumberInputProps = Pick<InputProps, 'id' | 'name' | 'required'> & {
defaultValue?: string; // Limit the default value to a string.
};

export function PhoneNumberInput({
defaultValue,
name,
...rest
}: PhoneNumberInputProps) {
const [value, setValue] = useState(defaultValue || '');

const formattedValue = formatPhoneNumber(value);
const rawValue = formattedValue.replace(/\D/g, '');

return (
<>
<input
className={getInputCn()}
onChange={(e) => setValue(e.target.value)}
pattern="\(\d{3}\) \d{3}-\d{4}"
placeholder="(123) 456-7890"
type="tel"
value={formattedValue}
{...rest}
/>

<input name={name} type="hidden" value={rawValue} />
</>
);
}

/**
* Formats a phone number to the format: (xxx) xxx-xxxx.
*
* @param number - The phone number to format.
* @returns The formatted phone number.
*
* @example
* formatPhoneNumber("") => ""
* formatPhoneNumber("1") => "(1"
* formatPhoneNumber("12") => "(12"
* formatPhoneNumber("123") => "(123"
* formatPhoneNumber("1234") => "(123) 4"
* formatPhoneNumber("12345") => "(123) 45"
* formatPhoneNumber("123456") => "(123) 456"
* formatPhoneNumber("1234567") => "(123) 456-7"
* formatPhoneNumber("12345678") => "(123) 456-78"
* formatPhoneNumber("123456789") => "(123) 456-789"
* formatPhoneNumber("1234567890") => "(123) 456-7890"
* formatPhoneNumber("1234567890123") => "(123) 456-7890"
*/
function formatPhoneNumber(input: string): string {
const digits = input.replace(/\D/g, '');

if (digits.length === 0) {
return '';
}

if (digits.length <= 3) {
return `(${digits}`;
}

if (digits.length <= 6) {
return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
}

return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}

export function getInputCn() {
return cx(
'w-full rounded-lg border border-gray-300 p-2',
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export { MB_IN_BYTES, FileUploader } from './components/file-uploader';
export { Form, getErrors, InputField, validateForm } from './components/form';
export type { DescriptionProps, FieldProps } from './components/form';
export { IconButton, getIconButtonCn } from './components/icon-button';
export { Input, getInputCn } from './components/input';
export { Input, PhoneNumberInput, getInputCn } from './components/input';
export type { InputProps } from './components/input';
export { Link } from './components/link';
export { Login } from './components/login';
Expand Down

0 comments on commit 0c379c8

Please sign in to comment.