-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): create register customer page
- Loading branch information
1 parent
ab67b34
commit 9684f20
Showing
14 changed files
with
924 additions
and
1 deletion.
There are no files selected for viewing
86 changes: 86 additions & 0 deletions
86
apps/core/app/[locale]/(default)/account/register-customer/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
28
apps/core/components/register-customer-form/_actions/login.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}; | ||
} | ||
}; |
65 changes: 65 additions & 0 deletions
65
apps/core/components/register-customer-form/_actions/register-customer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
61
apps/core/components/register-customer-form/fields/password.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
94 changes: 94 additions & 0 deletions
94
apps/core/components/register-customer-form/fields/picklist-with-text.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
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} | ||
name={name} | ||
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> | ||
); | ||
}; |
Oops, something went wrong.