Skip to content

Commit

Permalink
feat(core): create register customer page
Browse files Browse the repository at this point in the history
  • Loading branch information
yurytut1993 committed Apr 11, 2024
1 parent ab67b34 commit 9cf15f0
Show file tree
Hide file tree
Showing 13 changed files with 927 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { redirect } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';

import { auth } from '~/auth';
import { FormFieldSortInput } from '~/client/generated/graphql';
import { getCountries } from '~/client/management/get-countries';
import { getAddressFormFields } from '~/client/queries/get-address-form-fields';
import { getCustomerFormFields } from '~/client/queries/get-customer-form-fields';
import { getReCaptchaSettings } from '~/client/queries/get-recaptcha-settings';
import { getStoreCountry } from '~/client/queries/get-store-country';
import { RegisterCustomerForm } from '~/components/register-customer-form';
import { LocaleType } from '~/i18n';

import { getShippingStates } from '../../cart/_actions/get-shipping-states';

interface Props {
params: {
locale: LocaleType;
};
}

const MOCKED_STATE_PROVINCE_FIELD = {
entityId: 12,
label: 'State/Province',
sortOrder: 10,
isBuiltIn: true,
isRequired: true,
__typename: 'PicklistWithTextFormField' as const,
choosePrefix: 'Choose your State or Province',
};

const FALLBACK_COUNTRY = {
id: 226,
name: 'United States',
country_iso2: 'US',
};

export type PicklistWithTextFormField = typeof MOCKED_STATE_PROVINCE_FIELD;

export default async function RegisterCustomer({ params: { locale } }: Props) {
const session = await auth();

if (session) {
redirect('/account');
}

const messages = await getMessages({ locale });
const Account = messages.Account ?? {};
const t = await getTranslations({ locale, namespace: 'Account.Register' });

const defaultCountry = (await getStoreCountry()) || FALLBACK_COUNTRY.name;

const countries = await getCountries();

const { country_iso2 = FALLBACK_COUNTRY.country_iso2, id = FALLBACK_COUNTRY.id } =
countries.find(({ country }) => country === defaultCountry) || {};

const defaultCountryStates = (await getShippingStates(id)).data || [];

const customerFields = await getCustomerFormFields({
sortBy: FormFieldSortInput.SortOrder,
});

const addressFields = await getAddressFormFields({ sortBy: FormFieldSortInput.SortOrder });

const addressFieldsWithMocked = [...(addressFields ?? []), MOCKED_STATE_PROVINCE_FIELD];

const reCaptchaSettings = await getReCaptchaSettings();

return (
<div className="mx-auto mb-10 mt-8 text-base lg:w-2/3">
<h1 className="my-6 my-8 text-4xl font-black lg:my-8 lg:text-5xl">{t('heading')}</h1>
<NextIntlClientProvider locale={locale} messages={{ Account }}>
<RegisterCustomerForm
addressFields={addressFieldsWithMocked}
countries={countries}
customerFields={customerFields}
defaultCountry={{ id, code: country_iso2, states: defaultCountryStates }}
fallbackCountryId={FALLBACK_COUNTRY.id}
reCaptchaSettings={reCaptchaSettings}
/>
</NextIntlClientProvider>
</div>
);
}
26 changes: 26 additions & 0 deletions apps/core/client/queries/get-store-country.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { cache } from 'react';

import { client } from '..';
import { graphql } from '../graphql';
import { revalidate } from '../revalidate-target';

const GET_STORE_COUNTRY_QUERY = graphql(`
query getStoreSettings {
site {
settings {
contact {
country
}
}
}
}
`);

export const getStoreCountry = cache(async () => {
const response = await client.fetch({
document: GET_STORE_COUNTRY_QUERY,
fetchOptions: { next: { revalidate } },
});

return response.data.site.settings?.contact?.country;
});
28 changes: 28 additions & 0 deletions apps/core/components/register-customer-form/_actions/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'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 {
const singin = await signIn('credentials', {
email,
password,
redirectTo: '/account',
});

return singin;
} 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 { RegisterCustomerInput } from '~/client/generated/graphql';
import { registerCustomer as registerCustomerClient } from '~/client/mutations/register-customer';

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

const isRegisterCustomerInput = (data: unknown): data is RegisterCustomerInput => {
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: {} },
);

console.log(parsedData, 'parsedData');

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'),
};
};
61 changes: 61 additions & 0 deletions apps/core/components/register-customer-form/fields/password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Field, FieldControl, FieldLabel, FieldMessage } from '@bigcommerce/components/form';
import { Input } from '@bigcommerce/components/input';
import { useTranslations } from 'next-intl';
import { ChangeEvent } from 'react';

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;
variant?: 'error';
}

export const Password = ({ field, isValid, name, onChange, variant }: PasswordProps) => {
const t = useTranslations('Account.Register');

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={onChange}
onInvalid={onChange}
required={field.isRequired}
type="text"
variant={variant}
/>
</FieldControl>
{field.isRequired && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-red-200"
match="valueMissing"
>
{t('emptyPasswordValidatoinMessage')}
</FieldMessage>
)}
{FieldNameToFieldId[field.entityId] === 'confirmPassword' && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-red-200"
match={() => {
return !isValid;
}}
>
{t('equalPasswordValidatoinMessage')}
</FieldMessage>
)}
</Field>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Field, FieldControl, FieldLabel, FieldMessage } from '@bigcommerce/components/form';
import { Input } from '@bigcommerce/components/input';
import { Select, SelectContent, SelectItem } from '@bigcommerce/components/select';
import { Loader2 as Spinner } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { ChangeEvent } from 'react';

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

type PicklistWithTextType = Extract<
NonNullable<AddressFieldsWithMocked>[number],
{ __typename: 'PicklistWithTextFormField' }
>;

interface PicklistWithTextProps {
defaultValue?: string;
field: PicklistWithTextType;
name: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
options: Array<{ label: string; entityId: string | number }>;
pending?: boolean;
variant?: 'error';
}

export const PicklistWithText = ({
defaultValue,
field,
name,
onChange,
options,
pending,
variant,
}: PicklistWithTextProps) => {
const t = useTranslations('Account.Register');

return (
<Field className="relative space-y-2 pb-7" name={name}>
<FieldLabel htmlFor={`field-${field.entityId}`} isRequired={field.isRequired}>
<span className="flex justify-start">
{field.label}
{pending && field.entityId === FieldNameToFieldId.stateOrProvince && (
<span className="ms-1 text-primary">
<Spinner aria-hidden="true" className="animate-spin" />
<span className="sr-only">{t('loadingStates')}</span>
</span>
)}
</span>
</FieldLabel>
<FieldControl asChild>
{field.entityId === FieldNameToFieldId.stateOrProvince && options.length === 0 ? (
<Input
disabled={pending}
id={`field-${field.entityId}`}
onChange={field.isRequired ? onChange : undefined}
onInvalid={field.isRequired ? onChange : undefined}
required={field.isRequired}
type="text"
variant={variant}
/>
) : (
<Select
aria-label={field.choosePrefix}
defaultValue={defaultValue}
disabled={pending}
id={`field-${field.entityId}`}
key={defaultValue}
placeholder={field.choosePrefix}
required={field.isRequired}
>
<SelectContent>
{field.entityId === FieldNameToFieldId.stateOrProvince &&
options.map(({ entityId, label }) => {
return (
<SelectItem key={entityId} value={entityId.toString()}>
{label}
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
</FieldControl>
{field.isRequired && options.length === 0 && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-red-200"
match="valueMissing"
>
{t('emptyTextValidatoinMessage')}
</FieldMessage>
)}
</Field>
);
};
Loading

0 comments on commit 9cf15f0

Please sign in to comment.