Skip to content

Commit

Permalink
feat(core): create register customer page (#730)
Browse files Browse the repository at this point in the history
  • Loading branch information
yurytut1993 committed May 13, 2024
1 parent b13fecf commit 15e4b82
Show file tree
Hide file tree
Showing 15 changed files with 934 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-news-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

create register customer page
11 changes: 2 additions & 9 deletions core/app/[locale]/(default)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,8 @@ export default async function Login({ params: { locale }, searchParams }: Props)
<li>{t('CreateAccount.ordersTracking')}</li>
<li>{t('CreateAccount.wishlists')}</li>
</ul>
<Button asChild className="w-fit items-center px-8 py-2">
<Link
href={{
pathname: '/login',
query: { action: 'create_account' },
}}
>
{t('CreateAccount.createLink')}
</Link>
<Button asChild className="w-fit items-center px-8 py-2 hover:text-white">
<Link href="/login/register-customer">{t('CreateAccount.createLink')}</Link>
</Button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use server';

import { isRedirectError } from 'next/dist/client/components/redirect';

import { signIn } from '~/auth';

export const login = async (
email: FormDataEntryValue | null,
password: FormDataEntryValue | null,
) => {
try {
return await signIn('credentials', {
email,
password,
redirectTo: '/account',
});
} catch (error: unknown) {
if (isRedirectError(error)) {
throw error;
}

return {
status: 'error',
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use server';

import {
Input,
registerCustomer as registerCustomerClient,
} from '~/client/mutations/register-customer';

interface RegisterCustomerForm {
formData: FormData;
reCaptchaToken?: string;
}

const isRegisterCustomerInput = (data: unknown): data is Input => {
if (typeof data === 'object' && data !== null && 'email' in data) {
return true;
}

return false;
};

export const registerCustomer = async ({ formData, reCaptchaToken }: RegisterCustomerForm) => {
formData.delete('customer-confirmPassword');

const parsedData = Array.from(formData.entries()).reduce<{
[key: string]: FormDataEntryValue | { [key: string]: FormDataEntryValue };
address: { [key: string]: FormDataEntryValue };
}>(
(acc, [name, value]) => {
const key = name.split('-').at(-1) ?? '';
const sections = name.split('-').slice(0, -1);

if (sections.includes('customer')) {
acc[key] = value;
}

if (sections.includes('address')) {
acc.address[key] = value;
}

return acc;
},
{ address: {} },
);

if (!isRegisterCustomerInput(parsedData)) {
return {
status: 'error',
error: 'Something went wrong with proccessing user input',
};
}

const response = await registerCustomerClient({
formFields: parsedData,
reCaptchaToken,
});

if (response.errors.length === 0) {
return { status: 'success', data: parsedData };
}

return {
status: 'error',
error: response.errors.map((error) => error.message).join('\n'),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useTranslations } from 'next-intl';
import { ChangeEvent } from 'react';

import { Field, FieldControl, FieldLabel, FieldMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';

import { CustomerFields, FieldNameToFieldId } from '..';

type PasswordType = Extract<
NonNullable<CustomerFields>[number],
{ __typename: 'PasswordFormField' }
>;

interface PasswordProps {
field: PasswordType;
isValid?: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
name: string;
}

export const Password = ({ field, isValid, name, onChange }: PasswordProps) => {
const t = useTranslations('Account.Register.validationMessages');
const fieldName = FieldNameToFieldId[field.entityId];

return (
<Field className="relative space-y-2 pb-7" name={name}>
<FieldLabel htmlFor={`field-${field.entityId}`} isRequired={field.isRequired}>
{field.label}
</FieldLabel>
<FieldControl asChild>
<Input
defaultValue={field.defaultText ?? undefined}
id={`field-${field.entityId}`}
onChange={onChange}
onInvalid={onChange}
required={field.isRequired}
type="password"
variant={isValid === false ? 'error' : undefined}
/>
</FieldControl>
{field.isRequired && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-error-secondary"
match="valueMissing"
>
{t('password')}
</FieldMessage>
)}
{fieldName === 'confirmPassword' && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-error-secondary"
match={() => {
return !isValid;
}}
>
{t('confirmPassword')}
</FieldMessage>
)}
</Field>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useTranslations } from 'next-intl';

import { Field, FieldControl, FieldLabel } from '~/components/ui/form';
import { Input } from '~/components/ui/input';
import { Select, SelectContent, SelectItem } from '~/components/ui/select';

import { AddressFields } from '..';

type PicklistOrTextType = Extract<
NonNullable<AddressFields>[number],
{ __typename: 'PicklistOrTextFormField' }
>;

interface PicklistOrTextProps {
defaultValue?: string;
field: PicklistOrTextType;
name: string;
options: Array<{ label: string; entityId: string | number }>;
variant?: 'error';
}

export const PicklistOrText = ({ defaultValue, field, name, options }: PicklistOrTextProps) => {
const t = useTranslations('Account.Register');

return (
<Field className="relative space-y-2 pb-7" name={name}>
<FieldLabel
htmlFor={`field-${field.entityId}`}
isRequired={options.length !== 0 ? field.isRequired : false}
>
{field.label}
</FieldLabel>
<FieldControl asChild>
{options.length === 0 ? (
<Input id={`field-${field.entityId}`} type="text" />
) : (
<Select
aria-label={t('stateProvincePrefix')}
defaultValue={defaultValue}
id={`field-${field.entityId}`}
key={defaultValue}
placeholder={t('stateProvincePrefix')}
required={field.isRequired}
>
<SelectContent position="item-aligned">
{options.map(({ entityId, label }) => {
return (
<SelectItem key={entityId} value={entityId.toString()}>
{label}
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
</FieldControl>
</Field>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Field, FieldControl, FieldLabel } from '~/components/ui/form';
import { Select, SelectContent, SelectItem } from '~/components/ui/select';

import { AddressFields, FieldNameToFieldId } from '..';

type PicklistType = Extract<
NonNullable<AddressFields>[number],
{ __typename: 'PicklistFormField' }
>;

interface PicklistProps {
defaultValue?: string;
field: PicklistType;
name: string;
onChange?: (value: string) => void;
options: Array<{ label: string; entityId: string | number }>;
}

export const Picklist = ({ defaultValue, field, name, onChange, options }: PicklistProps) => {
return (
<Field className="relative space-y-2 pb-7" name={name}>
<FieldLabel htmlFor={`field-${field.entityId}`} isRequired={field.isRequired}>
{field.label}
</FieldLabel>
<FieldControl asChild>
<Select
aria-label={field.choosePrefix}
defaultValue={defaultValue}
id={`field-${field.entityId}`}
onValueChange={field.entityId === FieldNameToFieldId.countryCode ? onChange : undefined}
placeholder={field.choosePrefix}
required={field.isRequired}
>
<SelectContent position="item-aligned">
{options.map(({ entityId, label }) => (
<SelectItem key={entityId} value={entityId.toString()}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</FieldControl>
</Field>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { PropsWithChildren } from 'react';

export enum FieldNameToFieldId {
email = 1,
password,
confirmPassword,
firstName,
lastName,
company,
phone,
address1,
address2,
city,
countryCode,
stateOrProvince,
postalCode,
currentPassword = 24,
exclusiveOffers = 25,
}

const LAYOUT_SINGLE_LINE_FIELDS = [
FieldNameToFieldId.email,
FieldNameToFieldId.company,
FieldNameToFieldId.phone,
];

export const FieldWrapper = ({ children, fieldId }: { fieldId: number } & PropsWithChildren) => {
if (LAYOUT_SINGLE_LINE_FIELDS.includes(fieldId)) {
return (
<div className="grid grid-cols-1 gap-y-6 lg:col-span-2 lg:grid-cols-2 lg:gap-x-6 lg:gap-y-2">
{children}
</div>
);
}

return children;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useTranslations } from 'next-intl';
import { ChangeEvent } from 'react';

import { Field, FieldControl, FieldLabel, FieldMessage } from '~/components/ui/form';
import { Input } from '~/components/ui/input';

import { AddressFields, CustomerFields, FieldNameToFieldId } from '..';

type TextType =
| Extract<NonNullable<CustomerFields>[number], { __typename: 'TextFormField' }>
| Extract<NonNullable<AddressFields>[number], { __typename: 'TextFormField' }>;

interface TextProps {
field: TextType;
isValid?: boolean;
name: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
type?: string;
}

export const Text = ({ field, isValid, name, onChange, type }: TextProps) => {
const t = useTranslations('Account.Register.validationMessages');
const fieldName = FieldNameToFieldId[field.entityId];

return (
<Field className="relative space-y-2 pb-7" name={name}>
<FieldLabel htmlFor={`field-${field.entityId}`} isRequired={field.isRequired}>
{field.label}
</FieldLabel>
<FieldControl asChild>
<Input
defaultValue={field.defaultText ?? undefined}
id={`field-${field.entityId}`}
maxLength={field.maxLength ?? undefined}
onChange={field.isRequired ? onChange : undefined}
onInvalid={field.isRequired ? onChange : undefined}
required={field.isRequired}
type={type === 'email' ? 'email' : 'text'}
variant={isValid === false ? 'error' : undefined}
/>
</FieldControl>
{field.isRequired && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-error-secondary"
match="valueMissing"
>
{t(fieldName ?? 'empty')}
</FieldMessage>
)}
{fieldName === 'email' && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-error-secondary"
match="typeMismatch"
>
{t('email')}
</FieldMessage>
)}
</Field>
);
};
Loading

0 comments on commit 15e4b82

Please sign in to comment.