Skip to content

Commit

Permalink
feat(core): add new address for customer
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-alexsaiannyi committed Apr 23, 2024
1 parent cd51301 commit db6fa78
Show file tree
Hide file tree
Showing 17 changed files with 966 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use server';

import { revalidatePath } from 'next/cache';

import {
addCustomerAddress,
AddCustomerAddressInput,
} from '~/client/mutations/add-customer-address';

const isAddCustomerAddressInput = (data: unknown): data is AddCustomerAddressInput => {
if (typeof data === 'object' && data !== null && 'address1' in data) {
return true;
}

return false;
};

export const addAddress = async ({
formData,
reCaptchaToken,
}: {
formData: FormData;
reCaptchaToken?: string;
}) => {
try {
const parsed: unknown = [...formData.entries()].reduce<{
[key: string]: FormDataEntryValue | { [key: string]: FormDataEntryValue };
}>((parsedData, [name, value]) => {
const key = name.split('-').at(-1) ?? '';
const sections = name.split('-').slice(0, -1);

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

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

return parsedData;
}, {});

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

const response = await addCustomerAddress({
input: parsed,
reCaptchaToken,
});

revalidatePath('/account/addresses', 'page');

if (response.errors.length === 0) {
return { status: 'success', message: 'The address has been added' };
}

return {
status: 'error',
message: response.errors.map((error) => error.message).join('\n'),
};
} catch (error: unknown) {
if (error instanceof Error) {
return {
status: 'error',
message: error.message,
};
}

return { status: 'error', message: 'Unknown error' };
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const deleteAddress = async (addressId: number) => {
revalidatePath('/account/addresses', 'page');

if (response.customer.deleteCustomerAddress.errors.length === 0) {
return { status: 'success', message: 'The address has been deleted' };
return { status: 'success', message: 'This address has been deleted' };
}

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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,7 @@
'use client';

export * from './text';
export * from './password';
export * from './picklist';
export * from './picklist-or-text';
export * from './field-wrapper';
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 { AddressFields, FieldNameToFieldId } from '..';

type PasswordType = Extract<
NonNullable<AddressFields>[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 { AddressFields, FieldNameToFieldId } from '..';

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

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

export const PicklistOrText = ({
defaultValue,
field,
name,
onChange,
options,
pending,
variant,
}: PicklistOrTextProps) => {
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.label}
defaultValue={defaultValue}
disabled={pending}
id={`field-${field.entityId}`}
key={defaultValue}
placeholder={field.label}
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>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Field, FieldControl, FieldLabel } from '@bigcommerce/components/form';
import { Select, SelectContent, SelectItem } from '@bigcommerce/components/select';

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

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

interface PicklistProps {
defaultValue?: string;
field: PicklistType;
name: string;
onChange?: (value: string) => Promise<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>
{field.entityId === FieldNameToFieldId.countryCode &&
options.map(({ label, entityId }) => (
<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,56 @@
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 { AddressFields, FieldNameToFieldId } from '..';

type TextType = 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'); // TODO: update later

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 ?? '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-red-200"
match="valueMissing"
>
{t('emptyTextValidatoinMessage')}
</FieldMessage>
)}
{FieldNameToFieldId[field.entityId] === 'email' && (
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-xs font-normal text-red-200"
match="typeMismatch"
>
{t('emailValidationMessage')}
</FieldMessage>
)}
</Field>
);
};
Loading

0 comments on commit db6fa78

Please sign in to comment.