Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ability to add phone number in profile πŸ“² #482

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 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 Down Expand Up @@ -52,6 +54,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
'genderPronouns',
'headline',
'lastName',
'phoneNumber',
'preferredName',
])
.executeTakeFirstOrThrow();
Expand All @@ -73,6 +76,7 @@ const UpdateGeneralInformation = Student.pick({
headline: true,
lastName: true,
preferredName: true,
phoneNumber: true,
}).extend({
currentLocation: Student.shape.currentLocation.unwrap(),
currentLocationLatitude: Student.shape.currentLocationLatitude.unwrap(),
Expand Down Expand Up @@ -174,6 +178,19 @@ export default function UpdateGeneralInformationSection() {
longitudeName={keys.currentLocationLongitude}
/>

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

ramiAbdou marked this conversation as resolved.
Show resolved Hide resolved
<Button.Group>
<Button.Submit>Save</Button.Submit>
</Button.Group>
Expand Down
12 changes: 12 additions & 0 deletions packages/db/src/migrations/20240924211318_phone_number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type Kysely } from 'kysely';

export async function up(db: Kysely<any>) {
await db.schema
.alterTable('students')
.addColumn('phone_number', 'text')
.execute();
}

export async function down(db: Kysely<any>) {
await db.schema.alterTable('students').dropColumn('phone_number').execute();
}
15 changes: 15 additions & 0 deletions packages/types/src/domain/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,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
3 changes: 3 additions & 0 deletions packages/ui/src/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type InputFieldProps = FieldProps<string> &
Pick<FormFieldProps, 'description' | 'label' | 'required'> &
Pick<InputProps, 'disabled' | 'placeholder'>;

/**
* @deprecated Instead, just compose the `Form.Field` and `Input` together.
*/
export function InputField({
defaultValue,
description,
Expand Down
71 changes: 70 additions & 1 deletion 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 Down Expand Up @@ -42,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="(555) 123-4567"
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