Merge branch 'release-stable' of https://github.com/KieIO/grocery-vercel-commerce into feauture/m4-fixbug-blog-list

This commit is contained in:
QuangNhan
2021-10-24 23:41:32 +07:00
69 changed files with 2009 additions and 599 deletions

View File

@@ -1,4 +1,5 @@
import type { Discount, Measurement, Image } from './common' import { ShippingMethod } from '@framework/schema';
import type { Discount, Image, Measurement } from './common';
export type SelectedOption = { export type SelectedOption = {
// The option's id. // The option's id.
@@ -83,6 +84,50 @@ export type Cart = {
discounts?: Discount[] discounts?: Discount[]
} }
export type CartCheckout = {
id: string
// ID of the customer to which the cart belongs.
customerId?: string
customer?: {
firstName: string,
lastName: string,
emailAddress: string,
}
shippingAddress?: {
streetLine1: string,
city: string,
province: string,
postalCode: string,
countryCode: string,
phoneNumber: string,
}
// The email assigned to this cart
email?: string
// The date and time when the cart was created.
createdAt: string
// The currency used for this cart
currency: { code: string }
// Specifies if taxes are included in the line items.
taxesIncluded: boolean
lineItems: LineItem[]
// The sum of all the prices of all the items in the cart.
// Duties, taxes, shipping and discounts excluded.
lineItemsSubtotalPrice: number
// Price of the cart before duties, shipping and taxes.
subtotalPrice: number
// The sum of all the prices of all the items in the cart.
// Duties, taxes and discounts included.
totalPrice: number
totalQuantity: number
// Discounts that have been applied on the cart.
discounts?: Discount[]
totalDiscount: number
shippingLine?: {
priceWithTax: number
shippingMethod: ShippingMethod
}
}
/** /**
* Base cart item body used for cart mutations * Base cart item body used for cart mutations
*/ */

View File

@@ -1,6 +1,7 @@
export type Discount = { export type Discount = {
// The value of the discount, can be an amount or percentage // The value of the discount, can be an amount or percentage
value: number value: number
description?: string
} }
export type Measurement = { export type Measurement = {

View File

@@ -2,7 +2,7 @@ import { Product } from '@commerce/types/product'
import { OperationContext } from '@commerce/api/operations' import { OperationContext } from '@commerce/api/operations'
import { Provider, VendureConfig } from '../' import { Provider, VendureConfig } from '../'
import { GetProductQuery } from '../../schema' import { GetProductQuery } from '../../schema'
import { getProductQuery, getProductDetailQuery } from '../../utils/queries/get-product-query' import { getProductQuery } from '../../utils/queries/get-product-query'
export default function getProductOperation({ export default function getProductOperation({
commerce, commerce,

View File

@@ -306,9 +306,44 @@ export type MutationResetPasswordArgs = {
password: Scalars['String'] password: Scalars['String']
} }
export type ActiveOrderCustomerFragment = Pick<Order, 'id'> & {
customer?: Maybe<Pick<Customer, 'id' | 'emailAddress' | 'firstName' | 'lastName'>>;
lines: Array<Pick<OrderLine, 'id'>>;
};
export type SetCustomerForOrderMutationVariables = Exact<{
input: CreateCustomerInput;
}>;
export type SetCustomerForOrderMutation = { __typename?: 'Mutation' } & {
setCustomerForOrder:
| ({ __typename: 'ActiveOrderCustomerFragment' } & Pick<ActiveOrderCustomerFragment, 'customer', 'lines'>)
| ({ __typename: 'AlreadyLoggedInError' } & Pick<
AlreadyLoggedInError,
'errorCode' | 'message'
>)
| ({ __typename: 'EmailAddressConflictError' } & Pick<
EmailAddressConflictError,
'errorCode' | 'message'
>)
| ({ __typename: 'NoActiveOrderError' } & Pick<
NoActiveOrderError,
'errorCode' | 'message'
>)
}
export type SetOrderShippingAddressMutation = { __typename?: 'Mutation' } & {
setOrderShippingAddress:
| ({ __typename: 'Order' } & Pick<Order, 'id' | 'total' | 'totalQuantity' | 'code' | 'shippingAddress'>)
| ({ __typename: 'NoActiveOrderError' } & Pick<
NoActiveOrderError,
'errorCode' | 'message'
>)
}
export type Address = Node & { export type Address = Node & {
updateCustomerAddress: updateCustomerAddress:
| { | {
__typename?: 'Address' __typename?: 'Address'
id: Scalars['ID'] id: Scalars['ID']
} }
@@ -330,8 +365,24 @@ export type Address = Node & {
customFields?: Maybe<Scalars['JSON']> customFields?: Maybe<Scalars['JSON']>
} }
export type SetShippingMethodMutationVariables = Exact<{
id: Scalars['ID'];
}>;
export type SetShippingMethodMutation = {
setOrderShippingMethod:
| TestOrderFragmentFragment
| Pick<OrderModificationError, 'errorCode' | 'message'>
| Pick<IneligibleShippingMethodError, 'errorCode' | 'message'>
| Pick<NoActiveOrderError, 'errorCode' | 'message'>;
};
export type GetEligibleMethodsQuery = {
eligibleShippingMethods: Array<
Pick<ShippingMethodQuote, 'id' | 'name' | 'description' | 'price' | 'priceWithTax' | 'metadata'>
>;
};
export type Asset = Node & { export type Asset = Node & {
__typename?: 'Asset' __typename?: 'Asset'
@@ -1471,7 +1522,7 @@ export type CustomerListOptions = {
export type Customer = Node & { export type Customer = Node & {
updateCustomer: updateCustomer:
| { | {
__typename?: 'Customer' __typename?: 'Customer'
id: Scalars['ID'] id: Scalars['ID']
} }
@@ -1482,7 +1533,7 @@ export type Customer = Node & {
title?: Maybe<Scalars['String']> title?: Maybe<Scalars['String']>
firstName: Scalars['String'] firstName: Scalars['String']
lastName: Scalars['String'] lastName: Scalars['String']
phoneNumber?: Maybe<Scalars['String']> phoneNumber?: Maybe<Scalars['String']>
emailAddress: Scalars['String'] emailAddress: Scalars['String']
addresses?: Maybe<Array<Address>> addresses?: Maybe<Array<Address>>
orders: OrderList orders: OrderList
@@ -3066,56 +3117,65 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
| 'totalWithTax' | 'totalWithTax'
| 'currencyCode' | 'currencyCode'
> & { > & {
customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>> shippingAddress?: Maybe<{ __typename?: 'OrderAddress' } & Pick<OrderAddress, 'streetLine1' | 'fullName' | 'city' | 'province' | 'postalCode' | 'countryCode' | 'phoneNumber'>>
lines: Array< discounts: Array<
{ __typename?: 'OrderLine' } & Pick< { __typename?: 'Discount' } & Pick<Discount, 'type' | 'description' | 'amount' | 'amountWithTax'>
OrderLine, >
| 'id' customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id' | 'firstName' | 'lastName' | 'emailAddress'>>
| 'quantity' shippingLines: Array<
| 'linePriceWithTax' Pick<ShippingLine, 'priceWithTax'> & {
| 'discountedLinePriceWithTax' shippingMethod: Pick<ShippingMethod, 'id' | 'code' | 'name' | 'description'>;
| 'unitPriceWithTax'
| 'discountedUnitPriceWithTax'
> & {
featuredAsset?: Maybe<
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>
>
discounts: Array<
{ __typename?: 'Discount' } & Pick<
Discount,
'description' | 'amount'
>
>
productVariant: { __typename?: 'ProductVariant' } & Pick<
ProductVariant,
| 'id'
| 'name'
| 'sku'
| 'price'
| 'priceWithTax'
| 'stockLevel'
| 'productId'
> & { product: { __typename?: 'Product' } & Pick<Product, 'slug'> }
} }
> >
} lines: Array<
{ __typename?: 'OrderLine' } & Pick<
OrderLine,
| 'id'
| 'quantity'
| 'linePriceWithTax'
| 'discountedLinePriceWithTax'
| 'unitPriceWithTax'
| 'discountedUnitPriceWithTax'
> & {
featuredAsset?: Maybe<
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>
>
discounts: Array<
{ __typename?: 'Discount' } & Pick<
Discount,
'description' | 'amount'
>
>
productVariant: { __typename?: 'ProductVariant' } & Pick<
ProductVariant,
| 'id'
| 'name'
| 'sku'
| 'price'
| 'priceWithTax'
| 'stockLevel'
| 'productId'
> & { product: { __typename?: 'Product' } & Pick<Product, 'slug'> }
}
>
}
export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick< export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
SearchResult, SearchResult,
'productId' | 'sku' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode' 'productId' | 'sku' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
| 'productAsset' | 'price' | 'priceWithTax' | 'currencyCode' | 'productAsset' | 'price' | 'priceWithTax' | 'currencyCode'
| 'collectionIds' | 'productVariantId' | 'facetValueIds' | "productVariantName" | 'collectionIds' | 'productVariantId' | 'facetValueIds' | "productVariantName"
> & { > & {
productAsset?: Maybe< productAsset?: Maybe<
{ __typename?: 'SearchResultAsset' } & Pick< { __typename?: 'SearchResultAsset' } & Pick<
SearchResultAsset, SearchResultAsset,
'id' | 'preview' 'id' | 'preview'
>
> >
priceWithTax: >
| ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>) priceWithTax:
| ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>) | ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
} | ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>)
}
export type AddItemToOrderMutationVariables = Exact<{ export type AddItemToOrderMutationVariables = Exact<{
variantId: Scalars['ID'] variantId: Scalars['ID']
@@ -3124,23 +3184,23 @@ export type AddItemToOrderMutationVariables = Exact<{
export type AddItemToOrderMutation = { __typename?: 'Mutation' } & { export type AddItemToOrderMutation = { __typename?: 'Mutation' } & {
addItemToOrder: addItemToOrder:
| ({ __typename: 'Order' } & CartFragment) | ({ __typename: 'Order' } & CartFragment)
| ({ __typename: 'OrderModificationError' } & Pick< | ({ __typename: 'OrderModificationError' } & Pick<
OrderModificationError, OrderModificationError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'OrderLimitError' } & Pick< | ({ __typename: 'OrderLimitError' } & Pick<
OrderLimitError, OrderLimitError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NegativeQuantityError' } & Pick< | ({ __typename: 'NegativeQuantityError' } & Pick<
NegativeQuantityError, NegativeQuantityError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'InsufficientStockError' } & Pick< | ({ __typename: 'InsufficientStockError' } & Pick<
InsufficientStockError, InsufficientStockError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type AdjustOrderLineMutationVariables = Exact<{ export type AdjustOrderLineMutationVariables = Exact<{
@@ -3150,23 +3210,53 @@ export type AdjustOrderLineMutationVariables = Exact<{
export type AdjustOrderLineMutation = { __typename?: 'Mutation' } & { export type AdjustOrderLineMutation = { __typename?: 'Mutation' } & {
adjustOrderLine: adjustOrderLine:
| ({ __typename: 'Order' } & CartFragment) | ({ __typename: 'Order' } & CartFragment)
| ({ __typename: 'OrderModificationError' } & Pick< | ({ __typename: 'OrderModificationError' } & Pick<
OrderModificationError, OrderModificationError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'OrderLimitError' } & Pick< | ({ __typename: 'OrderLimitError' } & Pick<
OrderLimitError, OrderLimitError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NegativeQuantityError' } & Pick< | ({ __typename: 'NegativeQuantityError' } & Pick<
NegativeQuantityError, NegativeQuantityError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'InsufficientStockError' } & Pick< | ({ __typename: 'InsufficientStockError' } & Pick<
InsufficientStockError, InsufficientStockError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
}
export type ApplyCouponCodeMutationVariables = Exact<{
couponCode: Scalars['String'];
}>;
export type ApplyCouponCodeMutation = {
applyCouponCode:
| TestOrderFragmentFragment
| Pick<CouponCodeExpiredError, 'errorCode' | 'message'>
| Pick<CouponCodeInvalidError, 'errorCode' | 'message'>
| Pick<CouponCodeLimitError, 'errorCode' | 'message'>;
};
export type ApplyCouponCodeMutation = { __typename?: 'Mutation' } & {
applyCouponCode:
| ({ __typename: 'Order' } & CartFragment)
| ({ __typename: 'CouponCodeExpiredError' } & Pick<
CouponCodeExpiredError,
'errorCode' | 'message'
>)
| ({ __typename: 'CouponCodeInvalidError' } & Pick<
CouponCodeInvalidError,
'errorCode' | 'message'
>)
| ({ __typename: 'CouponCodeLimitError' } & Pick<
CouponCodeLimitError,
'errorCode' | 'message'
>)
} }
export type LoginMutationVariables = Exact<{ export type LoginMutationVariables = Exact<{
@@ -3176,49 +3266,49 @@ export type LoginMutationVariables = Exact<{
export type LoginMutation = { __typename?: 'Mutation' } & { export type LoginMutation = { __typename?: 'Mutation' } & {
login: login:
| ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>) | ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
| ({ __typename: 'InvalidCredentialsError' } & Pick< | ({ __typename: 'InvalidCredentialsError' } & Pick<
InvalidCredentialsError, InvalidCredentialsError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NotVerifiedError' } & Pick< | ({ __typename: 'NotVerifiedError' } & Pick<
NotVerifiedError, NotVerifiedError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NativeAuthStrategyError' } & Pick< | ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError, NativeAuthStrategyError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type ResetPasswordMutation = { __typename?: 'Mutation' } & { export type ResetPasswordMutation = { __typename?: 'Mutation' } & {
resetPassword: resetPassword:
| ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>) | ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
| ({ __typename: 'PasswordResetTokenInvalidError' } & Pick< | ({ __typename: 'PasswordResetTokenInvalidError' } & Pick<
PasswordResetTokenInvalidError, PasswordResetTokenInvalidError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'PasswordResetTokenExpiredError' } & Pick< | ({ __typename: 'PasswordResetTokenExpiredError' } & Pick<
PasswordResetTokenExpiredError, PasswordResetTokenExpiredError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NativeAuthStrategyError' } & Pick< | ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError, NativeAuthStrategyError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type SignupMutation = { __typename?: 'Mutation' } & { export type SignupMutation = { __typename?: 'Mutation' } & {
registerCustomerAccount: registerCustomerAccount:
| ({ __typename: 'Success' } & Pick<Success, 'success'>) | ({ __typename: 'Success' } & Pick<Success, 'success'>)
| ({ __typename: 'MissingPasswordError' } & Pick< | ({ __typename: 'MissingPasswordError' } & Pick<
MissingPasswordError, MissingPasswordError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NativeAuthStrategyError' } & Pick< | ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError, NativeAuthStrategyError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type VerifyCustomerAccountVariables = Exact<{ export type VerifyCustomerAccountVariables = Exact<{
@@ -3228,27 +3318,27 @@ export type VerifyCustomerAccountVariables = Exact<{
export type VerifyCustomerAccountMutation = { __typename?: 'Mutation' } & { export type VerifyCustomerAccountMutation = { __typename?: 'Mutation' } & {
verifyCustomerAccount: verifyCustomerAccount:
| ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>) | ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
| ({ __typename: 'VerificationTokenInvalidError' } & Pick< | ({ __typename: 'VerificationTokenInvalidError' } & Pick<
VerificationTokenInvalidError, VerificationTokenInvalidError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'VerificationTokenExpiredError' } & Pick< | ({ __typename: 'VerificationTokenExpiredError' } & Pick<
VerificationTokenExpiredError, VerificationTokenExpiredError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'MissingPasswordError' } & Pick< | ({ __typename: 'MissingPasswordError' } & Pick<
MissingPasswordError, MissingPasswordError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'PasswordAlreadySetError' } & Pick< | ({ __typename: 'PasswordAlreadySetError' } & Pick<
PasswordAlreadySetError, PasswordAlreadySetError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NativeAuthStrategyError' } & Pick< | ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError, NativeAuthStrategyError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type LogoutMutationVariables = Exact<{ [key: string]: never }> export type LogoutMutationVariables = Exact<{ [key: string]: never }>
@@ -3263,11 +3353,11 @@ export type RemoveOrderLineMutationVariables = Exact<{
export type RemoveOrderLineMutation = { __typename?: 'Mutation' } & { export type RemoveOrderLineMutation = { __typename?: 'Mutation' } & {
removeOrderLine: removeOrderLine:
| ({ __typename: 'Order' } & CartFragment) | ({ __typename: 'Order' } & CartFragment)
| ({ __typename: 'OrderModificationError' } & Pick< | ({ __typename: 'OrderModificationError' } & Pick<
OrderModificationError, OrderModificationError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
export type SignupMutationVariables = Exact<{ export type SignupMutationVariables = Exact<{
@@ -3277,15 +3367,15 @@ export type SignupMutationVariables = Exact<{
export type RequestPasswordReset = { __typename?: 'Mutation' } & { export type RequestPasswordReset = { __typename?: 'Mutation' } & {
requestPasswordReset: requestPasswordReset:
| ({ __typename: 'Success' } & Pick<Success, 'success'>) | ({ __typename: 'Success' } & Pick<Success, 'success'>)
| ({ __typename: 'MissingPasswordError' } & Pick< | ({ __typename: 'MissingPasswordError' } & Pick<
MissingPasswordError, MissingPasswordError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
| ({ __typename: 'NativeAuthStrategyError' } & Pick< | ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError, NativeAuthStrategyError,
'errorCode' | 'message' 'errorCode' | 'message'
>) >)
} }
@@ -3297,7 +3387,7 @@ export type ActiveCustomerQuery = { __typename?: 'Query' } & {
{ __typename?: 'Customer' } & Pick< { __typename?: 'Customer' } & Pick<
Customer, Customer,
Favorite, Favorite,
'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber'| 'orders' 'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber' | 'orders'
> >
> >
} }
@@ -3316,7 +3406,7 @@ export type FavoriteList = PaginatedList & {
totalItems: Int! totalItems: Int!
} }
type Favorite = Node & { type Favorite = Node & {
id: ID! id: ID!
createdAt: DateTime! createdAt: DateTime!
updatedAt: DateTime! updatedAt: DateTime!
@@ -3324,9 +3414,28 @@ type Favorite = Node & {
customer: Customer! customer: Customer!
} }
export type GetAvailableCountriesQueryVariables = Exact<{ [key: string]: never; }>;
type FavouriteOption = Customer & { // export type GetAvailableCountriesQuery = { countries: (
// { __typename?: 'CountryList' }
// & { items: Array<(
// { __typename?: 'Country' }
// & Pick<Country, 'id' | 'code' | 'name' | 'enabled'>
// )> }
// ) };
export type GetAvailableCountriesQuery = {
availableCountries:
{ __typename?: 'CountryList' }
& Array<(
{ __typename?: 'Country' }
& Pick<Country, 'id' | 'code' | 'name' | 'enabled'>
)>
};
type FavouriteOption = Customer & {
favorites(options: FavoriteListOptions): FavoriteList! favorites(options: FavoriteListOptions): FavoriteList!
} }
@@ -3357,13 +3466,13 @@ export type GetAllFacetsQuery = { __typename?: 'Query' } & {
{ __typename?: 'Facet' } & Pick< { __typename?: 'Facet' } & Pick<
Facet, Facet,
'id' | 'name' | 'code' | 'values' 'id' | 'name' | 'code' | 'values'
> >
& { & {
parent?: Maybe<{ __typename?: 'Facet' } & Pick<Facet, 'id'>> parent?: Maybe<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
children?: Maybe< children?: Maybe<
Array<{ __typename?: 'Facet' } & Pick<Facet, 'id'>> Array<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
> >
} }
>, >,
'totalItems' 'totalItems'
} }
@@ -3376,11 +3485,11 @@ export type GetAllCollectionsQuery = { __typename?: 'Query' } & {
Collection, Collection,
'id' | 'name' | 'slug' 'id' | 'name' | 'slug'
> & { > & {
parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>> parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
children?: Maybe< children?: Maybe<
Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>> Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
> >
} }
>, >,
'totalItems' 'totalItems'
} }
@@ -3401,15 +3510,15 @@ export type GetCollectionsQuery = { __typename?: 'Query' } & {
Collection, Collection,
'id' | 'name' | 'description' | 'slug' 'id' | 'name' | 'description' | 'slug'
> & { > & {
productVariants: { __typename?: 'ProductVariantList' } & Pick< productVariants: { __typename?: 'ProductVariantList' } & Pick<
ProductVariantList, ProductVariantList,
'totalItems' 'totalItems'
> >
parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>> parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
children?: Maybe< children?: Maybe<
Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>> Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
> >
} }
> >
} }
} }
@@ -3424,60 +3533,60 @@ export type GetProductQuery = { __typename?: 'Query' } & {
Product, Product,
'id' | 'name' | 'slug' | 'description' 'id' | 'name' | 'slug' | 'description'
> & { > & {
assets: Array< assets: Array<
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview' | 'name'> { __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview' | 'name'>
> >
variants: Array< variants: Array<
{ __typename?: 'ProductVariant' } & Pick< { __typename?: 'ProductVariant' } & Pick<
ProductVariant, ProductVariant,
'id' | 'priceWithTax' | 'currencyCode' | 'price' | "name" 'id' | 'priceWithTax' | 'currencyCode' | 'price' | "name"
> & { > & {
options: Array< options: Array<
{ __typename?: 'ProductOption' } & Pick< { __typename?: 'ProductOption' } & Pick<
ProductOption, ProductOption,
'id' | 'name' | 'code' | 'groupId' 'id' | 'name' | 'code' | 'groupId'
> & { > & {
group: { __typename?: 'ProductOptionGroup' } & Pick< group: { __typename?: 'ProductOptionGroup' } & Pick<
ProductOptionGroup, ProductOptionGroup,
'id' 'id'
> & { > & {
options: Array< options: Array<
{ __typename?: 'ProductOption' } & Pick< { __typename?: 'ProductOption' } & Pick<
ProductOption, ProductOption,
'name' 'name'
> >
>
}
}
>
}
>
optionGroups: Array<
{ __typename?: 'ProductOptionGroup' } & Pick<
ProductOptionGroup,
'id' | 'code' | 'name'
> & {
options: Array<
{ __typename?: 'ProductOption' } & Pick<
ProductOption,
'id' | 'name'
> >
> }
} }
>
facetValues: Array<
{ __typename?: 'FacetValue' } & Pick<
FacetValue,
'id'
> >
> }
collections: Array< >
{ __typename?: 'Collection' } & Pick< optionGroups: Array<
Collection, { __typename?: 'ProductOptionGroup' } & Pick<
'id'|"name" ProductOptionGroup,
'id' | 'code' | 'name'
> & {
options: Array<
{ __typename?: 'ProductOption' } & Pick<
ProductOption,
'id' | 'name'
>
> >
}
>
facetValues: Array<
{ __typename?: 'FacetValue' } & Pick<
FacetValue,
'id'
> >
} >
collections: Array<
{ __typename?: 'Collection' } & Pick<
Collection,
'id' | "name"
>
>
}
> >
} }

View File

@@ -0,0 +1,21 @@
export const applyCouponCodeMutation = /* GraphQL */ `
mutation applyCouponCode($couponCode: String!) {
applyCouponCode(couponCode: $couponCode) {
__typename
... on Order {
id
createdAt
updatedAt
discounts {
type
amount
amountWithTax
}
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1,24 @@
export const setCustomerForOrderMutation = /* GraphQL */ `
mutation setCustomerForOrder($input: CreateCustomerInput!) {
setCustomerForOrder(input: $input) {
__typename
... on Order {
id
createdAt
updatedAt
code
customer {
id
firstName
lastName
emailAddress
phoneNumber
}
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1,25 @@
export const setOrderShippingAddressMutation = /* GraphQL */ `
mutation setOrderShippingAddress($input: CreateAddressInput!) {
setOrderShippingAddress(input: $input) {
__typename
... on Order {
id
createdAt
updatedAt
code
shippingAddress {
streetLine1
city
province
postalCode
countryCode
phoneNumber
}
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1,35 @@
export const setShippingMethodMutation = /* GraphQL */ `
mutation SetShippingMethod($id: ID!) {
setOrderShippingMethod(shippingMethodId: $id) {
...Cart
...ErrorResult
__typename
}
}
fragment Cart on Order {
id
code
state
active
shippingLines {
priceWithTax
shippingMethod {
id
code
name
description
__typename
}
__typename
}
__typename
}
fragment ErrorResult on ErrorResult {
errorCode
message
__typename
}
`

View File

@@ -1,6 +1,6 @@
import { Cart } from '@commerce/types/cart' import { Cart, CartCheckout } from '@commerce/types/cart'
import { ProductCard, Product } from '@commerce/types/product' import { Product, ProductCard } from '@commerce/types/product'
import { CartFragment, SearchResultFragment,Favorite, BlogList } from '../schema' import { CartFragment, Favorite, SearchResultFragment, ShippingMethod, BlogList } from '../schema'
export function normalizeSearchResult(item: SearchResultFragment): ProductCard { export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
return { return {
@@ -11,10 +11,10 @@ export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
price: (item.priceWithTax as any).min / 100, price: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode, currencyCode: item.currencyCode,
productVariantId: item.productVariantId, productVariantId: item.productVariantId,
productVariantName:item.productVariantName, productVariantName: item.productVariantName,
facetValueIds: item.facetValueIds, facetValueIds: item.facetValueIds,
collectionIds: item.collectionIds, collectionIds: item.collectionIds,
// TODO: // TODO:
// oldPrice: item.price // oldPrice: item.price
// discount // discount
@@ -70,6 +70,63 @@ export function normalizeCart(order: CartFragment): Cart {
} }
} }
export function normalizeCartForCheckout(order: CartFragment): CartCheckout {
return {
id: order.id.toString(),
createdAt: order.createdAt,
taxesIncluded: true,
totalQuantity: order.totalQuantity,
lineItemsSubtotalPrice: order.subTotalWithTax / 100,
currency: { code: order.currencyCode },
subtotalPrice: order.subTotalWithTax / 100,
totalPrice: order.totalWithTax / 100,
customerId: order.customer?.id,
customer: {
firstName: order.customer?.firstName || '',
lastName: order.customer?.lastName || '',
emailAddress: order.customer?.emailAddress || '',
},
shippingAddress: {
streetLine1: order.shippingAddress?.streetLine1 || '',
city: order.shippingAddress?.city || '',
province: order.shippingAddress?.province || '',
postalCode: order.shippingAddress?.postalCode || '',
countryCode: order.shippingAddress?.countryCode || '',
phoneNumber: order.shippingAddress?.phoneNumber || '',
},
shippingLine: order.shippingLines[0] ? {
priceWithTax: order.shippingLines[0]?.priceWithTax / 100,
shippingMethod: order.shippingLines[0]?.shippingMethod as ShippingMethod
}: undefined,
totalDiscount: order.discounts?.reduce((total, item) => total + item.amountWithTax, 0) / 100 || 0,
discounts: order.discounts.map(item => {
return { value: item.amountWithTax, description: item.description }
}),
lineItems: order.lines?.map((l) => ({
id: l.id,
name: l.productVariant.name,
quantity: l.quantity,
slug: l.productVariant.product.slug,
variantId: l.productVariant.id,
productId: l.productVariant.productId,
images: [{ url: l.featuredAsset?.preview + '?preset=thumb' || '' }],
discounts: l.discounts.map((d) => ({ value: d.amount / 100 })),
path: '',
variant: {
id: l.productVariant.id,
name: l.productVariant.name,
sku: l.productVariant.sku,
price: l.discountedUnitPriceWithTax / 100,
listPrice: l.unitPriceWithTax / 100,
image: {
url: l.featuredAsset?.preview + '?preset=thumb' || '',
},
requiresShipping: true,
},
})),
}
}
export function normalizeProductCard(product: Product): ProductCard { export function normalizeProductCard(product: Product): ProductCard {
return { return {
id: product.id, id: product.id,
@@ -79,7 +136,7 @@ export function normalizeProductCard(product: Product): ProductCard {
price: product.price, price: product.price,
currencyCode: product.currencyCode, currencyCode: product.currencyCode,
productVariantId: product.variants?.[0].id.toString(), productVariantId: product.variants?.[0].id.toString(),
productVariantName:product.variants?.[0].name, productVariantName: product.variants?.[0].name,
facetValueIds: product.facetValueIds, facetValueIds: product.facetValueIds,
collectionIds: product.collectionIds, collectionIds: product.collectionIds,
} }

View File

@@ -0,0 +1,116 @@
export const getActiveOrderForCheckoutQuery = /* GraphQL */ `
query getActiveOrderForCheckout {
activeOrder {
...Cart
shippingAddress {
...OrderAddress
__typename
}
__typename
}
}
fragment Cart on Order {
id
code
state
active
customer {
id
firstName
lastName
emailAddress
}
lines {
id
featuredAsset {
...Asset
__typename
}
unitPrice
unitPriceWithTax
quantity
linePriceWithTax
discountedLinePriceWithTax
unitPriceWithTax
discountedUnitPriceWithTax
productVariant {
id
name
price
priceWithTax
stockLevel
productId
product {
slug
}
__typename
}
discounts {
amount
amountWithTax
description
adjustmentSource
type
__typename
}
__typename
}
totalQuantity
subTotal
subTotalWithTax
total
totalWithTax
shipping
shippingWithTax
currencyCode
shippingLines {
priceWithTax
shippingMethod {
id
code
name
description
__typename
}
__typename
}
discounts {
amount
amountWithTax
description
adjustmentSource
type
__typename
}
__typename
}
fragment Asset on Asset {
id
width
height
name
preview
focalPoint {
x
y
__typename
}
__typename
}
fragment OrderAddress on OrderAddress {
fullName
company
streetLine1
streetLine2
city
province
postalCode
country
phoneNumber
__typename
}
`

View File

@@ -0,0 +1,16 @@
export const availableCountriesQuery = /* GraphQL */ `
query availableCountriesQuery {
availableCountries {
...Country
__typename
}
}
fragment Country on Country {
id
code
name
enabled
__typename
}
`

View File

@@ -0,0 +1,13 @@
export const getEligibleShippingMethods = /* GraphQL */ `
query getEligibleShippingMethods {
eligibleShippingMethods {
id
name
description
price
priceWithTax
metadata
__typename
}
}
`

View File

@@ -2,8 +2,9 @@
"name": "nextjs-commerce", "name": "nextjs-commerce",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "NODE_OPTIONS='--inspect' PORT=3005 next dev", "dev": "set NODE_OPTIONS='--inspect' && set PORT=3005 && next dev",
"dev-windows": "set NODE_OPTIONS='--inspect' && set PORT=3005 && next dev", "dev-windows": "set NODE_OPTIONS='--inspect' && set PORT=3005 && next dev",
"dev-macos": "set NODE_OPTIONS='--inspect' && set PORT=3005 && next dev",
"build": "next build", "build": "next build",
"start": "PORT=3005 next start", "start": "PORT=3005 next start",
"analyze": "BUNDLE_ANALYZE=both yarn build", "analyze": "BUNDLE_ANALYZE=both yarn build",

View File

@@ -4,7 +4,8 @@ import { CheckoutPage } from 'src/components/modules/checkout';
export default function Checkout() { export default function Checkout() {
return ( return (
<> <>
<CheckoutPage/> <CheckoutPage />
</> </>
) )
} }

View File

@@ -31,8 +31,8 @@ export default function Home({ featuredAndDiscountFacetsValue, veggie,
<HomeBanner /> <HomeBanner />
<HomeFeature /> <HomeFeature />
<HomeCategories /> <HomeCategories />
<HomeCollection data = {veggie}/>
<FreshProducts data={freshProducts} collections={collections} /> <FreshProducts data={freshProducts} collections={collections} />
<HomeCollection data = {veggie}/>
<HomeVideo /> <HomeVideo />
{spiceProducts.length>0 && <HomeSpice data={spiceProducts}/>} {spiceProducts.length>0 && <HomeSpice data={spiceProducts}/>}
<FeaturedProductsCarousel data={featuredProducts} featuredFacetsValue={featuredAndDiscountFacetsValue} /> <FeaturedProductsCarousel data={featuredProducts} featuredFacetsValue={featuredAndDiscountFacetsValue} />

View File

@@ -1,7 +1,7 @@
@import "../../../styles/utilities"; @import "../../../styles/utilities";
.buttonCommon { .buttonCommon {
@apply shape-common; @apply shape-common h-full;
&:hover { &:hover {
.inner { .inner {
@apply shadow-md; @apply shadow-md;

View File

@@ -1,24 +1,29 @@
import { LineItem } from '@commerce/types/cart'
import React from 'react' import React from 'react'
import { ImgWithLink } from '..'
import s from "./CardItemCheckout.module.scss" import s from "./CardItemCheckout.module.scss"
import { ProductProps } from 'src/utils/types.utils' export interface CardItemCheckoutProps extends LineItem {
export interface CardItemCheckoutProps extends ProductProps { currency: { code: string }
quantity:number
} }
const CardItemCheckout = ({imageSrc,name,price,weight,quantity,category}: CardItemCheckoutProps) => { const CardItemCheckout = ({
quantity,
variant,
name,
currency }: CardItemCheckoutProps) => {
return ( return (
<div className={s.warpper}> <div className={s.warpper}>
<div className={s.image}> <div className={s.image}>
<img src={imageSrc} alt="image" /> <ImgWithLink src={variant?.image?.url ?? ''} alt={name} />
</div> </div>
<div className={s.right}> <div className={s.right}>
<div className={s.name}> <div className={s.name}>
{`${name} (${weight})`} {name} {variant?.weight ? `(${variant.weight})` : ''}
</div> </div>
<div className={s.quantity}> <div className={s.quantity}>
Quantity: Quantity:
<div className={s.price}> <div className={s.price}>
{`${quantity} x ${price}`} {`${quantity} x ${variant?.price} ${currency?.code}`}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -74,7 +74,8 @@ const ProductCartItem = ({
{discounts.length > 0 && ( {discounts.length > 0 && (
<div className={s.old}> <div className={s.old}>
{/* <span className={s.number}>{oldPrice}</span> */} {/* <span className={s.number}>{oldPrice}</span> */}
<LabelCommon type="discount">{discounts[0]}</LabelCommon> {/* TODO: edit the value */}
<LabelCommon type="discount">{discounts[0]?.value}</LabelCommon>
</div> </div>
)} )}
<div className={s.current}>{variant?.price} {currency?.code}</div> <div className={s.current}>{variant?.price} {currency?.code}</div>

View File

@@ -1,51 +1,52 @@
.warpper{ .warpper {
padding: 2.4rem 0; padding: 2.4rem 0;
@apply border-b border-solid border-line; @apply border-b border-solid border-line;
.note{ .note {
@apply cursor-pointer;
font-size: 1.2rem; font-size: 1.2rem;
line-height: 2rem; line-height: 2rem;
letter-spacing: 0.01em; letter-spacing: 0.01em;
color: var(--text-label); color: var(--text-label);
padding: 0 5.6rem; padding: 0 5.6rem;
} }
.header{ .header {
@apply flex justify-between; @apply flex justify-between cursor-pointer;
.left{ .left {
@apply flex items-center; @apply flex items-center;
.number{ .number {
width: 3.2rem; width: 3.2rem;
height: 3.2rem; height: 3.2rem;
border-radius: 100%; border-radius: 100%;
border: 1px solid var(--text-active); border: 1px solid var(--text-active);
color: var(--text-active); color: var(--text-active);
@apply flex justify-center items-center font-bold; @apply flex justify-center items-center font-bold;
&.visible{ &.visible {
background-color: var(--text-active); background-color: var(--text-active);
border: none; border: none;
color: var(--white); color: var(--white);
} }
&.done{ &.done {
@apply border-2 border-solid border-primary; @apply border-2 border-solid border-primary;
} }
} }
.title{ .title {
padding-left: 2.4rem; padding-left: 2.4rem;
@apply font-bold select-none cursor-pointer; @apply font-bold select-none;
color: var(--text-active); color: var(--text-active);
} }
} }
.edit{ .edit {
@apply font-bold cursor-pointer; @apply font-bold cursor-pointer;
text-decoration-line: underline; text-decoration-line: underline;
margin-right: 5.6rem; margin-right: 5.6rem;
} }
} }
.body{ .body {
height: 0; height: 0;
@apply overflow-hidden; @apply overflow-hidden;
&.show{ &.show {
margin-top: 3.2rem; margin-top: 3.2rem;
height: initial; height: initial;
} }
} }
} }

View File

@@ -1,8 +1,6 @@
import classNames from 'classnames' import classNames from 'classnames'
import { divide } from 'lodash'
import React from 'react' import React from 'react'
import { IconDoneCheckout } from 'src/components/icons' import { IconDoneCheckout } from 'src/components/icons'
import { CheckOutForm } from 'src/utils/types.utils'
import s from './CheckoutCollapse.module.scss' import s from './CheckoutCollapse.module.scss'
interface CheckoutCollapseProps { interface CheckoutCollapseProps {
visible: boolean visible: boolean
@@ -10,10 +8,11 @@ interface CheckoutCollapseProps {
children: React.ReactNode children: React.ReactNode
title: string title: string
isEdit: boolean isEdit: boolean
onClose?: (id:number) => void onClose: (id: number) => void
onOpen?: (id:number) => void onOpen?: (id: number) => void
onEditClick?:(id:number) => void onEditClick?: (id: number) => void
note?:string note?: string
disableEdit?: boolean
} }
const CheckoutCollapse = ({ const CheckoutCollapse = ({
@@ -24,33 +23,34 @@ const CheckoutCollapse = ({
visible, visible,
note, note,
onOpen, onOpen,
onClose, onClose,
onEditClick onEditClick,
disableEdit,
}: CheckoutCollapseProps) => { }: CheckoutCollapseProps) => {
const handleTitleClick = () => { const handleToggle = () => {
if(visible){ if (visible) {
onClose && onClose(id) isEdit && onClose(id)
}else{ } else if (!disableEdit) {
onOpen && onOpen(id) isEdit && onEditClick && onEditClick(id)
} }
} }
const handleEdit = () => { const handleEdit = () => {
onEditClick && onEditClick(id) onEditClick && onEditClick(id)
} }
return ( return (
<div className={s.warpper}> <div className={s.warpper}>
<div className={s.header}> <div className={s.header} onClick={handleToggle}>
<div className={s.left}> <div className={s.left}>
<div className={classNames(s.number, { [s.visible]: visible, [s.done]:isEdit })}> <div className={classNames(s.number, { [s.visible]: visible, [s.done]: isEdit })}>
{isEdit?<IconDoneCheckout/>:id} {isEdit ? <IconDoneCheckout /> : id}
</div> </div>
<div className={s.title} onClick={handleTitleClick}> <div className={s.title}>
{title} {title}
</div> </div>
</div> </div>
{isEdit && <div className={s.edit} onClick={handleEdit}>{'Edit'}</div>} {!disableEdit && isEdit && <div className={s.edit} onClick={handleEdit}>{'Edit'}</div>}
</div> </div>
{(!visible && isEdit) && (<div className={s.note}>{note}</div>) } {(!visible && isEdit) && (<div className={s.note} onClick={handleToggle}>{note}</div>)}
<div className={classNames(s.body, { [`${s.show}`]: visible })}>{children}</div> <div className={classNames(s.body, { [`${s.show}`]: visible })}>{children}</div>
</div> </div>
) )

View File

@@ -1,7 +1,6 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FC } from 'react' import { FC } from 'react'
import { useMessage } from 'src/components/contexts' import { useMessage } from 'src/components/contexts'
import { useModalCommon } from 'src/components/hooks'
import { FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils' import { FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils'
import { CartDrawer, Footer, MessageCommon, ScrollToTop } from '../..' import { CartDrawer, Footer, MessageCommon, ScrollToTop } from '../..'
import Header from '../../Header/Header' import Header from '../../Header/Header'

View File

@@ -4,8 +4,11 @@
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
> main { > main {
display: flex;
flex: 1; flex: 1;
position: relative; position: relative;
max-width: min(100%, 1536px);
margin: auto;
} }
.footer { .footer {
@apply spacing-horizontal; @apply spacing-horizontal;

View File

@@ -1,3 +1,4 @@
import { MessageProvider } from 'src/components/contexts'
import s from './LayoutCheckout.module.scss' import s from './LayoutCheckout.module.scss'
interface Props { interface Props {
@@ -6,14 +7,18 @@ interface Props {
const LayoutCheckout = ({ children }: Props) => { const LayoutCheckout = ({ children }: Props) => {
return ( return (
<div className={s.layoutCheckout}> <>
<main>{children}</main> <MessageProvider>
<footer className={s.footer}> <div className={s.layoutCheckout}>
<div> <main>{children}</main>
© 2021 Online Grocery <footer className={s.footer}>
<div>
© 2021 Online Grocery
</div>
</footer>
</div> </div>
</footer> </MessageProvider>
</div> </>
) )
} }

View File

@@ -1,4 +1,4 @@
import React, { memo, useEffect } from 'react' import React, { memo } from 'react'
import s from './MessageCommon.module.scss' import s from './MessageCommon.module.scss'
import MessageItem, { MessageItemProps } from './MessageItem/MessageItem' import MessageItem, { MessageItemProps } from './MessageItem/MessageItem'

View File

@@ -12,9 +12,11 @@ interface Props {
visible: boolean visible: boolean
closeModal: () => void closeModal: () => void
mode?: '' | 'register' mode?: '' | 'register'
initialEmail?: string
disableRedirect ?: boolean
} }
const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => { const ModalAuthenticate = ({ visible, mode, closeModal, initialEmail, disableRedirect }: Props) => {
const [isLogin, setIsLogin] = useState<boolean>(true) const [isLogin, setIsLogin] = useState<boolean>(true)
const { customer } = useActiveCustomer() const { customer } = useActiveCustomer()
const router = useRouter() const router = useRouter()
@@ -30,9 +32,11 @@ const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => {
useEffect(() => { useEffect(() => {
if (visible && customer) { if (visible && customer) {
closeModal() closeModal()
router.push(ROUTE.ACCOUNT) if (!disableRedirect) {
router.push(ROUTE.ACCOUNT)
}
} }
}, [customer, visible, closeModal, router]) }, [customer, visible, closeModal, router, disableRedirect])
const onSwitch = () => { const onSwitch = () => {
setIsLogin(!isLogin) setIsLogin(!isLogin)
@@ -51,7 +55,7 @@ const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => {
[s.register]: !isLogin, [s.register]: !isLogin,
})} })}
> >
<FormLogin isHide={!isLogin} onSwitch={onSwitch} /> <FormLogin isHide={!isLogin} onSwitch={onSwitch} initialEmail={initialEmail} />
<FormRegister isHide={isLogin} onSwitch={onSwitch} /> <FormRegister isHide={isLogin} onSwitch={onSwitch} />
</div> </div>
</section> </section>

View File

@@ -15,16 +15,18 @@ import styles from './FormLogin.module.scss'
interface Props { interface Props {
isHide: boolean isHide: boolean
onSwitch: () => void onSwitch: () => void
initialEmail?: string
} }
const DisplayingErrorMessagesSchema = Yup.object().shape({ const displayingErrorMessagesSchema = Yup.object().shape({
email: Yup.string().email('Your email was wrong').required('Required'), email: Yup.string().email(LANGUAGE.MESSAGE.INVALID_EMAIL).required(LANGUAGE.MESSAGE.REQUIRED),
password: Yup.string() password: Yup.string()
.max(30, 'Password is too long') .max(30, 'Password is too long')
.required('Required'), .required(LANGUAGE.MESSAGE.REQUIRED),
}) })
const FormLogin = ({ onSwitch, isHide }: Props) => { const FormLogin = ({ onSwitch, isHide, initialEmail = ''}: Props) => {
const emailRef = useRef<CustomInputCommon>(null) const emailRef = useRef<CustomInputCommon>(null)
const { loading, login } = useLogin() const { loading, login } = useLogin()
const { showMessageSuccess, showMessageError } = useMessage() const { showMessageSuccess, showMessageError } = useMessage()
@@ -54,9 +56,9 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
<Formik <Formik
initialValues={{ initialValues={{
password: '', password: '',
email: '', email: initialEmail,
}} }}
validationSchema={DisplayingErrorMessagesSchema} validationSchema={displayingErrorMessagesSchema}
onSubmit={onLogin} onSubmit={onLogin}
> >

View File

@@ -5,7 +5,7 @@ import s from './ModalConfirm.module.scss'
interface ModalConfirmProps extends ModalCommonProps { interface ModalConfirmProps extends ModalCommonProps {
okText?: String okText?: String
cancelText?: String cancelText?: String
loading?:boolean loading?: boolean
onOk?: () => void onOk?: () => void
onCancel?: () => void onCancel?: () => void
} }
@@ -18,16 +18,17 @@ const ModalConfirm = ({
children, children,
title = 'Confirm', title = 'Confirm',
loading, loading,
onClose,
...props ...props
}: ModalConfirmProps) => { }: ModalConfirmProps) => {
return ( return (
<ModalCommon {...props} title={title}> <ModalCommon onClose={onClose} title={title} {...props}>
{children} {children}
<div className={s.footer}> <div className={s.footer}>
<div className="mr-4"> <div className="mr-4">
<ButtonCommon onClick={onCancel} type="light"> {cancelText}</ButtonCommon> <ButtonCommon onClick={onCancel || onClose} type="light" size="small"> {cancelText}</ButtonCommon>
</div> </div>
<ButtonCommon onClick={onOk} loading={loading}>{okText}</ButtonCommon> <ButtonCommon onClick={onOk} loading={loading} size="small">{okText}</ButtonCommon>
</div> </div>
</ModalCommon> </ModalCommon>
) )

View File

@@ -0,0 +1,19 @@
@import "../../../styles/form";
.inputWrap {
@extend .formInputWrap;
.inputInner {
select {
@apply w-full;
background-color: var(--white);
padding: 1.6rem 1.6rem;
border: 1px solid var(--border-line);
border-radius: 0.8rem;
&:focus {
outline: none;
border: 1px solid var(--primary);
@apply shadow-md;
}
}
}
}

View File

@@ -0,0 +1,106 @@
import classNames from "classnames"
import { Field } from "formik"
import { useMemo } from "react"
import { IconCheck, IconError } from "src/components/icons"
import s from './SelectFieldInForm.module.scss'
interface Props {
placeholder?: string
styleType?: 'default' | 'custom'
backgroundTransparent?: boolean
icon?: React.ReactNode
isIconSuffix?: boolean
isShowIconSuccess?: boolean
name: string
error?: string
options: any[]
keyNameOption?: string[]
keyValueOption?: string
nameSeperator?: string
}
const SelectFieldInForm = ({
name,
placeholder,
options,
styleType = 'default',
icon,
backgroundTransparent = false,
isIconSuffix = true,
isShowIconSuccess,
error,
keyNameOption = ['name'],
keyValueOption = 'value',
nameSeperator = " ",
}: Props) => {
const iconElement = useMemo(() => {
if (error) {
return (
<span className={s.icon}>
<IconError />{' '}
</span>
)
} else if (isShowIconSuccess) {
return (
<span className={s.icon}>
<IconCheck />{' '}
</span>
)
} else if (icon) {
return <span className={s.icon}>{icon} </span>
}
return <></>
}, [icon, error, isShowIconSuccess])
return (
<div
className={classNames({
[s.inputWrap]: true,
[s[styleType]]: true,
[s.bgTransparent]: backgroundTransparent,
})}
>
<div
className={classNames({
[s.inputInner]: true,
[s.preserve]: isIconSuffix,
[s.success]: isShowIconSuccess,
[s.error]: !!error,
})}
>
{iconElement}
<Field
as="select"
name={name}
placeholder={placeholder}
>
{
options.map((item) => {
let name = ''
keyNameOption.map((key) => {
if (name) {
name += nameSeperator
}
name += item[key]
})
name = name.trim()
return <option
key={item[keyValueOption]}
value={item[keyValueOption]}
>
{name}
</option>
})
}
</Field>
</div>
{error && <div className={s.errorMessage}>{error}</div>}
</div>
)
}
export default SelectFieldInForm

View File

@@ -50,6 +50,7 @@ export { default as RecommendedRecipes} from './RecommendedRecipes/RecommendedRe
export { default as LayoutCheckout} from './LayoutCheckout/LayoutCheckout' export { default as LayoutCheckout} from './LayoutCheckout/LayoutCheckout'
export { default as InputPasswordFiledInForm} from './InputPasswordFiledInForm/InputPasswordFiledInForm' export { default as InputPasswordFiledInForm} from './InputPasswordFiledInForm/InputPasswordFiledInForm'
export { default as InputFiledInForm} from './InputFiledInForm/InputFiledInForm' export { default as InputFiledInForm} from './InputFiledInForm/InputFiledInForm'
export { default as SelectFieldInForm} from './SelectFieldInForm/SelectFieldInForm'
export { default as MessageCommon} from './MessageCommon/MessageCommon' export { default as MessageCommon} from './MessageCommon/MessageCommon'
export { default as FormForgot} from './ForgotPassword/FormForgot/FormForgot' export { default as FormForgot} from './ForgotPassword/FormForgot/FormForgot'
export { default as FormResetPassword} from './ForgotPassword/FormResetPassword/FormResetPassword' export { default as FormResetPassword} from './ForgotPassword/FormResetPassword/FormResetPassword'

View File

@@ -6,7 +6,7 @@ export type MessageContextType = {
removeMessage: (id: number) => void removeMessage: (id: number) => void
showMessageSuccess: (content: string, timeout?: number) => void showMessageSuccess: (content: string, timeout?: number) => void
showMessageInfo: (content: string, timeout?: number) => void showMessageInfo: (content: string, timeout?: number) => void
showMessageError: (content: string, timeout?: number) => void showMessageError: (content?: string, timeout?: number) => void
showMessageWarning: (content: string, timeout?: number) => void showMessageWarning: (content: string, timeout?: number) => void
} }
export const DEFAULT_MESSAGE_CONTEXT: MessageContextType = { export const DEFAULT_MESSAGE_CONTEXT: MessageContextType = {

View File

@@ -1,5 +1,6 @@
import { ReactNode, useCallback, useState } from 'react' import { ReactNode, useState } from 'react'
import { MessageItemProps } from 'src/components/common/MessageCommon/MessageItem/MessageItem' import { MessageItemProps } from 'src/components/common/MessageCommon/MessageItem/MessageItem'
import { LANGUAGE } from 'src/utils/language.utils'
import { MessageContext } from './MessageContext' import { MessageContext } from './MessageContext'
type Props = { type Props = {
@@ -33,8 +34,8 @@ export function MessageProvider({ children }: Props) {
createNewMessage(content, timeout, 'info') createNewMessage(content, timeout, 'info')
} }
const showMessageError = (content: string, timeout?: number) => { const showMessageError = (content?: string, timeout?: number) => {
createNewMessage(content, timeout, 'error') createNewMessage(content || LANGUAGE.MESSAGE.ERROR, timeout, 'error')
} }
const showMessageWarning = (content: string, timeout?: number) => { const showMessageWarning = (content: string, timeout?: number) => {

View File

@@ -6,6 +6,7 @@ import { LoginMutation } from '@framework/schema'
import { LOCAL_STORAGE_KEY } from 'src/utils/constanst.utils' import { LOCAL_STORAGE_KEY } from 'src/utils/constanst.utils'
import { errorMapping } from 'src/utils/errrorMapping' import { errorMapping } from 'src/utils/errrorMapping'
import { loginMutation } from '@framework/utils/mutations/log-in-mutation' import { loginMutation } from '@framework/utils/mutations/log-in-mutation'
import { useGetActiveOrder } from '../cart'
interface LoginInput { interface LoginInput {
username: string username: string
@@ -16,6 +17,7 @@ const useLogin = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null) const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useActiveCustomer() const { mutate } = useActiveCustomer()
const { mutate: mutateOrder } = useGetActiveOrder()
const login = (options: LoginInput, const login = (options: LoginInput,
fCallBack: (isSuccess: boolean, message?: string) => void fCallBack: (isSuccess: boolean, message?: string) => void
@@ -34,6 +36,7 @@ const useLogin = () => {
if (authToken != null) { if (authToken != null) {
localStorage.setItem(LOCAL_STORAGE_KEY.TOKEN, authToken) localStorage.setItem(LOCAL_STORAGE_KEY.TOKEN, authToken)
mutate() mutate()
mutateOrder()
} }
fCallBack(true) fCallBack(true)
}) })

View File

@@ -1,4 +1,3 @@
import { Cart } from '@commerce/types/cart'
import { ActiveOrderQuery } from '@framework/schema' import { ActiveOrderQuery } from '@framework/schema'
import { cartFragment } from '@framework/utils/fragments/cart-fragment' import { cartFragment } from '@framework/utils/fragments/cart-fragment'
import { normalizeCart } from '@framework/utils/normalize' import { normalizeCart } from '@framework/utils/normalize'
@@ -16,8 +15,7 @@ const query = gql`
const useGetActiveOrder = () => { const useGetActiveOrder = () => {
const { data, ...rest } = useSWR<ActiveOrderQuery>([query], gglFetcher) const { data, ...rest } = useSWR<ActiveOrderQuery>([query], gglFetcher)
return { order: data?.activeOrder ? normalizeCart(data!.activeOrder) : null, ...rest } return { order: data?.activeOrder ? normalizeCart(data!.activeOrder) : null, ...rest }
} }
export default useGetActiveOrder export default useGetActiveOrder

View File

@@ -1,11 +1,10 @@
import { RemoveOrderLineMutation, RemoveOrderLineMutationVariables } from '@framework/schema'
import { removeOrderLineMutation } from '@framework/utils/mutations/remove-order-line-mutation'
import { useState } from 'react' import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError' import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { AdjustOrderLineMutationVariables,AdjustOrderLineMutation, RemoveOrderLineMutation, RemoveOrderLineMutationVariables } from '@framework/schema'
import { errorMapping } from 'src/utils/errrorMapping' import { errorMapping } from 'src/utils/errrorMapping'
import rawFetcher from 'src/utils/rawFetcher'
import { useGetActiveOrder } from '.' import { useGetActiveOrder } from '.'
import { adjustOrderLineMutation } from '@framework/utils/mutations/adjust-order-line-mutation'
import { removeOrderLineMutation } from '@framework/utils/mutations/remove-order-line-mutation'
const useRemoveProductInCart = () => { const useRemoveProductInCart = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

View File

@@ -0,0 +1,8 @@
export { default as useSetCustomerForOrder } from './useSetCustomerForOrder'
export { default as useSetOrderShippingAddress } from './useSetOrderShippingAddress'
export { default as useApplyCouponCode } from './useApplyCouponCode'
export { default as useAvailableCountries } from './useAvailableCountries'
export { default as useSetOrderShippingMethod } from './useSetOrderShippingMethod'
export { default as useGetActiveOrderForCheckout } from './useGetActiveOrderForCheckout'
export { default as useEligibleShippingMethods } from './useEligibleShippingMethods'

View File

@@ -0,0 +1,41 @@
import { ApplyCouponCodeMutation } from '@framework/schema'
import { applyCouponCodeMutation } from '@framework/utils/mutations/apply-coupon-code-mutation'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { useGetActiveOrderForCheckout } from '.'
const useApplyCouponCode = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrderForCheckout()
const applyCouponCode = (couponCode: string,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<ApplyCouponCodeMutation>({
query: applyCouponCodeMutation,
variables: { couponCode },
})
.then(({ data }) => {
if (data.applyCouponCode.__typename === 'Order') {
fCallBack(true)
mutate()
} else {
fCallBack(false, data.applyCouponCode.message)
}
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, applyCouponCode, error }
}
export default useApplyCouponCode

View File

@@ -0,0 +1,14 @@
import { GetAvailableCountriesQuery } from '@framework/schema'
import { availableCountriesQuery } from '@framework/utils/queries/available-countries-query'
import gglFetcher from 'src/utils/gglFetcher'
import useSWR from 'swr'
const useAvailableCountries = () => {
const { data, isValidating } = useSWR<GetAvailableCountriesQuery>([availableCountriesQuery], gglFetcher)
return {
countries: data?.availableCountries,
loading: isValidating,
}
}
export default useAvailableCountries

View File

@@ -0,0 +1,14 @@
import { GetEligibleMethodsQuery, ShippingMethodQuote } from '@framework/schema'
import { getEligibleShippingMethods } from '@framework/utils/queries/eligible-shipping-methods-query'
import gglFetcher from 'src/utils/gglFetcher'
import useSWR from 'swr'
const useEligibleShippingMethods = () => {
const { data, isValidating } = useSWR<GetEligibleMethodsQuery>([getEligibleShippingMethods], gglFetcher)
return {
eligibleShippingMethods: data?.eligibleShippingMethods as ShippingMethodQuote[],
loading: isValidating,
}
}
export default useEligibleShippingMethods

View File

@@ -0,0 +1,13 @@
import { ActiveOrderQuery } from '@framework/schema'
import { normalizeCartForCheckout } from '@framework/utils/normalize'
import { getActiveOrderForCheckoutQuery } from '@framework/utils/queries/active-order-for-checkout-query'
import gglFetcher from 'src/utils/gglFetcher'
import useSWR from 'swr'
const useGetActiveOrderForCheckout = () => {
const { data, ...rest } = useSWR<ActiveOrderQuery>([getActiveOrderForCheckoutQuery], gglFetcher)
return { order: data?.activeOrder ? normalizeCartForCheckout(data!.activeOrder) : null, ...rest }
}
export default useGetActiveOrderForCheckout

View File

@@ -0,0 +1,42 @@
import { CreateCustomerInput, SetCustomerForOrderMutation } from '@framework/schema'
import { setCustomerForOrderMutation } from '@framework/utils/mutations/set-customer-order-mutation'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { useGetActiveOrderForCheckout } from '.'
const useSetCustomerForOrder = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrderForCheckout()
const setCustomerForOrder = (input: CreateCustomerInput,
fCallBack: (isSuccess: boolean, message?: CommonError) => void
) => {
setError(null)
setLoading(true)
rawFetcher<SetCustomerForOrderMutation>({
query: setCustomerForOrderMutation,
variables: { input },
})
.then(({ data }) => {
if (data.setCustomerForOrder.__typename === 'Order') {
fCallBack(true)
mutate()
} else {
fCallBack(false, data.setCustomerForOrder)
}
})
.catch((error) => {
setError(error)
fCallBack(false, error)
})
.finally(() => setLoading(false))
}
return { loading, setCustomerForOrder, error }
}
export default useSetCustomerForOrder

View File

@@ -0,0 +1,42 @@
import { CreateAddressInput, SetOrderShippingAddressMutation } from '@framework/schema'
import { setOrderShippingAddressMutation } from '@framework/utils/mutations/set-order-shipping-address-mutation'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { useGetActiveOrderForCheckout } from '.'
const useSetOrderShippingAddress = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrderForCheckout()
const setOrderShippingAddress = (input: CreateAddressInput,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<SetOrderShippingAddressMutation>({
query: setOrderShippingAddressMutation,
variables: { input },
})
.then(({ data }) => {
if (data.setOrderShippingAddress.__typename === 'Order') {
fCallBack(true)
mutate()
} else {
fCallBack(false, data.setOrderShippingAddress.message)
}
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, setOrderShippingAddress, error }
}
export default useSetOrderShippingAddress

View File

@@ -0,0 +1,41 @@
import { SetShippingMethodMutation } from '@framework/schema'
import { setShippingMethodMutation } from '@framework/utils/mutations/set-order-shipping-method-mutation'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { useGetActiveOrderForCheckout } from '.'
const useSetOrderShippingMethod = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrderForCheckout()
const setOrderShippingMethod = (id: string,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<SetShippingMethodMutation>({
query: setShippingMethodMutation,
variables: { id },
})
.then(({ data }) => {
if (data.setOrderShippingMethod.__typename === 'Order') {
fCallBack(true)
mutate()
} else {
fCallBack(false, data.setOrderShippingMethod.message)
}
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, setOrderShippingMethod, error }
}
export default useSetOrderShippingMethod

View File

@@ -1,6 +1,7 @@
.warpper { .warpper {
padding: 3.2rem; padding: 3.2rem;
min-width: 100%; min-width: 100%;
min-height: 100%;
@screen lg { @screen lg {
max-width: 56.3rem; max-width: 56.3rem;
@apply flex justify-between flex-col; @apply flex justify-between flex-col;
@@ -14,15 +15,14 @@
display: block; display: block;
} }
} }
.empty {
@apply flex flex-col justify-center items-center;
}
.list { .list {
min-height: 52.8rem; // min-height: 52.8rem;
} }
.bot { .bot {
.promo { margin-top: auto;
// padding: 3.2rem;
@apply bg-gray flex justify-between items-center;
min-height: 6.4rem;
}
.price { .price {
margin-top: 3.2rem; margin-top: 3.2rem;
.line { .line {

View File

@@ -1,41 +1,59 @@
import { CartCheckout } from '@commerce/types/cart'
import Link from 'next/link'
import React from 'react' import React from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import { LANGUAGE } from 'src/utils/language.utils'
import { ButtonCommon, CardItemCheckout, EmptyCommon } from '../../../common'
import s from './CheckoutBill.module.scss' import s from './CheckoutBill.module.scss'
import { CardItemCheckout } from '../../../common' import FormPromotionCode from './FormPromotionCode/FormPromotionCode'
import { CardItemCheckoutProps } from '../../../common/CardItemCheckout/CardItemCheckout'
import { IconCirclePlus } from 'src/components/icons'
interface CheckoutBillProps { interface CheckoutBillProps {
data: CardItemCheckoutProps[] data: CartCheckout | null
} }
const CheckoutBill = ({ data }: CheckoutBillProps) => { const CheckoutBill = ({ data }: CheckoutBillProps) => {
return ( return (
<div className={s.warpper}> <div className={s.warpper}>
<div className = {s.title}> <div className={s.title}>
Your cart ({data.length}) Your cart ({data?.totalQuantity || 0})
</div> </div>
{
!data?.totalQuantity && <div className={s.empty}>
<EmptyCommon description="Your cart is empty" />
<Link href={ROUTE.HOME}>
<a>
<ButtonCommon>{LANGUAGE.BUTTON_LABEL.SHOP_NOW}</ButtonCommon>
</a>
</Link>
</div>
}
<div className={s.list}> <div className={s.list}>
{data.map((item) => { {data?.lineItems?.map((item) => {
return <CardItemCheckout {...item} key={item.slug} /> return <CardItemCheckout {...item} key={item.slug} currency={data?.currency} />
})} })}
</div> </div>
<div className={s.bot}> <div className={s.bot}>
<div className={s.promo}> <FormPromotionCode />
Apply Promotion Code
<IconCirclePlus />
</div>
<div className={s.price}> <div className={s.price}>
<div className={s.line}>
Discount {(data?.discounts?.length || 0) > 0 && `(${data?.discounts?.map(item => item.description).join(",")})`}
<div className={s.shipping}>
{data?.totalDiscount || 0} {data?.currency?.code}
</div>
</div>
<div className={s.line}> <div className={s.line}>
Subtotal Subtotal
<div className={s.subTotal}>RP 120.500</div> <div className={s.subTotal}>
{data?.subtotalPrice || 0} {data?.currency?.code}
</div>
</div> </div>
<div className={s.line}> <div className={s.line}>
Shipping Shipping
<div className={s.shipping}>Free</div> <div className={s.shipping}>{data?.shippingLine?.priceWithTax || 0} {data?.currency?.code}</div>
</div> </div>
<div className={s.line}> <div className={s.line}>
Estimated Total Estimated Total
<div className={s.total}>RP 120.500</div> <div className={s.total}>{data?.totalPrice || 0} {data?.currency?.code}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,19 @@
.promo {
@apply bg-gray flex justify-between items-center;
min-height: 6.4rem;
.modalPromotion {
min-width: 40rem;
.bottom {
@apply flex justify-end items-center;
margin-top: 3.2rem;
button {
&:first-child {
margin-right: 0.8rem;
@screen md {
margin-right: 3.2rem;
}
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
import { Form, Formik } from 'formik';
import React, { useEffect, useRef } from 'react';
import { ButtonCommon, InputFiledInForm, ModalCommon } from 'src/components/common';
import { useMessage } from 'src/components/contexts';
import { useModalCommon } from 'src/components/hooks';
import { useApplyCouponCode } from 'src/components/hooks/order';
import { IconCirclePlus } from 'src/components/icons';
import { LANGUAGE } from 'src/utils/language.utils';
import { CustomInputCommon } from 'src/utils/type.utils';
import * as Yup from 'yup';
import s from './FormPromotionCode.module.scss';
const displayingErrorMessagesSchema = Yup.object().shape({
couponCode: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
})
const FormPromotionCode = () => {
const { visible, openModal, closeModal } = useModalCommon({ initialValue: false })
const { showMessageError, showMessageSuccess } = useMessage()
const { applyCouponCode, loading } = useApplyCouponCode()
const inputRef = useRef<CustomInputCommon>(null)
useEffect(() => {
setTimeout(() => {
if (visible) {
inputRef.current?.focus()
}
}, 500);
}, [visible])
const handleSubmit = (values: { couponCode: string }) => {
applyCouponCode(values.couponCode, onSubmitCalBack)
}
const onSubmitCalBack = (isSuccess: boolean, msg?: string) => {
// TODO:
if (isSuccess) {
showMessageSuccess("Applied coupon code successfully.", 5000)
closeModal()
} else {
showMessageError(msg)
}
}
return (
<div className={s.promo}>
Apply Promotion Code
<button className={s.buttonAdd} onClick={openModal}>
<IconCirclePlus />
</button>
<ModalCommon
visible={visible}
onClose={closeModal}
>
<div className={s.modalPromotion}>
<Formik
initialValues={{
couponCode: ''
}}
validationSchema={displayingErrorMessagesSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isValid, submitForm }) => (
<Form className="u-form">
<div className="body">
<InputFiledInForm
name="couponCode"
placeholder="Promotion code"
error={
touched.couponCode && errors.couponCode
? errors.couponCode.toString()
: ''
}
isShowIconSuccess={touched.couponCode && !errors.couponCode}
onEnter={isValid ? submitForm : undefined}
ref={inputRef}
/>
</div>
<div className={s.bottom}>
<ButtonCommon disabled={loading} onClick={closeModal} type='light' size='small'>
{LANGUAGE.BUTTON_LABEL.CANCEL}
</ButtonCommon>
<ButtonCommon HTMLType='submit' loading={loading} size='small'>
Apply promotion code
</ButtonCommon>
</div>
</Form>
)}
</Formik>
</div>
</ModalCommon>
</div>
);
};
export default FormPromotionCode;

View File

@@ -1,50 +1,100 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { Logo } from 'src/components/common' import { Logo } from 'src/components/common'
import CheckoutCollapse from 'src/components/common/CheckoutCollapse/CheckoutCollapse' import CheckoutCollapse from 'src/components/common/CheckoutCollapse/CheckoutCollapse'
import { removeItem } from 'src/utils/funtion.utils' import { useActiveCustomer } from 'src/components/hooks/auth'
import { CheckOutForm } from 'src/utils/types.utils' import { useGetActiveOrderForCheckout } from 'src/components/hooks/order'
import s from './CheckoutInfo.module.scss' import s from './CheckoutInfo.module.scss'
import CustomerInfoForm from './components/CustomerInfoForm/CustomerInfoForm' import CustomerInfoForm from './components/CustomerInfoForm/CustomerInfoForm'
import PaymentInfoForm from './components/PaymentInfoForm/PaymentInfoForm' import PaymentInfoForm from './components/PaymentInfoForm/PaymentInfoForm'
import ShippingInfoForm from './components/ShippingInfoForm/ShippingInfoForm' import ShippingInfoForm from './components/ShippingInfoForm/ShippingInfoForm'
import ShippingMethod from './components/ShippingMethod/ShippingMethod'
interface CheckoutInfoProps { interface CheckoutInfoProps {
onViewCart:()=>void onViewCart: () => void
currency?: string
} }
const CheckoutInfo = ({onViewCart}: CheckoutInfoProps) => { export enum CheckoutStep {
const [active, setActive] = useState(1) CustomerInfo = 1,
const [done, setDone] = useState<number[]>([]) ShippingAddressInfo = 2,
const [info, setInfo] = useState<CheckOutForm>({}) ShippingMethodInfo = 3,
PaymentInfo = 4,
}
const onEdit = (id:number) => { const CheckoutInfo = ({ onViewCart, currency = "" }: CheckoutInfoProps) => {
setActive(id) const [activeStep, setActiveStep] = useState(1)
setDone(removeItem<number>(done,id)) const [doneSteps, setDoneSteps] = useState<CheckoutStep[]>([])
} const { order } = useGetActiveOrderForCheckout()
const { customer } = useActiveCustomer()
const onConfirm = (id:number,formInfo:CheckOutForm) => { useEffect(() => {
if(id+1>formList.length){ if (customer) {
console.log({...info,...formInfo}) if (!doneSteps.includes(CheckoutStep.CustomerInfo)) {
}else{
if(done.length>0){ if (doneSteps.length > 0) {
for (let i = id+1; i <= formList.length; i++) { for (let i = CheckoutStep.CustomerInfo + 1; i <= Object.keys(CheckoutStep).length; i++) {
if(!done.includes(i)){ if (!doneSteps.includes(i)) {
setActive(i) setActiveStep(i)
}
} }
} else {
setActiveStep(CheckoutStep.CustomerInfo + 1)
} }
}else{
setActive(id+1)
}
setDone([...done,id])
}
setInfo({...info,...formInfo})
}
const getNote = (id:number) => { setDoneSteps([...doneSteps, CheckoutStep.CustomerInfo])
}
}
}, [customer, doneSteps])
const onEdit = (id: CheckoutStep) => {
setActiveStep(id)
}
const updateActiveStep = (step: CheckoutStep) => {
if (doneSteps.length > 0) {
for (let i = step + 1; i < Object.keys(CheckoutStep).length; i++) {
if (!doneSteps.includes(i)) {
setActiveStep(i)
return
}
}
} else {
setActiveStep(step + 1)
}
}
const onConfirm = (step: CheckoutStep) => {
if (step + 1 > formList.length) {
// TODO: checkout
console.log("finish: ", order)
} else {
updateActiveStep(step)
setDoneSteps([...doneSteps, step])
}
}
const getNote = (id: CheckoutStep) => {
switch (id) { switch (id) {
case 1: case CheckoutStep.CustomerInfo:
return `${info.name}, ${info.email}` if (order?.customer?.emailAddress) {
case 2: return `${order?.customer?.firstName} ${order?.customer?.lastName}, ${order?.customer?.emailAddress}`
return `${info.address}, ${info.state}, ${info.city}, ${info.code}, ${info.phone}, ` } else if (customer) {
return `${customer.firstName} ${customer.lastName}, ${customer.emailAddress}`
} else {
return ''
}
case CheckoutStep.ShippingAddressInfo:
if (order?.shippingAddress) {
const { streetLine1, city, province, postalCode, countryCode, phoneNumber } = order.shippingAddress
return `${streetLine1}, ${city}, ${province}, ${postalCode}, ${countryCode}, ${phoneNumber}`
}
return ''
case CheckoutStep.ShippingMethodInfo:
if (order?.shippingLine) {
return `${order?.shippingLine.shippingMethod.name}, ${order?.shippingLine.priceWithTax ? `${order?.shippingLine.priceWithTax} ${currency}` : 'Free'}` || ''
}
return ''
default: default:
return "" return ""
} }
@@ -52,21 +102,27 @@ const CheckoutInfo = ({onViewCart}: CheckoutInfoProps) => {
const formList = [ const formList = [
{ {
id: 1, id: CheckoutStep.CustomerInfo,
title: 'Customer Information', title: 'Customer Information',
form: <CustomerInfoForm onConfirm={onConfirm} id={1}/>, form: <CustomerInfoForm onConfirm={onConfirm} id={CheckoutStep.CustomerInfo} activeStep={activeStep} />,
}, },
{ {
id: 2, id: CheckoutStep.ShippingAddressInfo,
title: 'Shipping Information', title: 'Shipping Address Information',
form: <ShippingInfoForm onConfirm={onConfirm} id={2}/>, form: <ShippingInfoForm onConfirm={onConfirm} id={CheckoutStep.ShippingAddressInfo} activeStep={activeStep} />,
}, },
{ {
id: 3, id: CheckoutStep.ShippingMethodInfo,
title: 'Shipping Method Information',
form: <ShippingMethod onConfirm={onConfirm} currency={currency} />,
},
{
id: CheckoutStep.PaymentInfo,
title: 'Payment Information', title: 'Payment Information',
form: <PaymentInfoForm onConfirm={onConfirm} id={3}/>, form: <PaymentInfoForm onConfirm={onConfirm} id={CheckoutStep.PaymentInfo} />,
}, },
] ]
return ( return (
<div className={s.warpper}> <div className={s.warpper}>
<div className={s.title}> <div className={s.title}>
@@ -76,13 +132,15 @@ const CheckoutInfo = ({onViewCart}: CheckoutInfoProps) => {
{formList.map((item) => { {formList.map((item) => {
let note = getNote(item.id) let note = getNote(item.id)
return <CheckoutCollapse return <CheckoutCollapse
key={item.title} key={item.title}
id={item.id} id={item.id}
visible={item.id === active} visible={item.id === activeStep}
title={item.title} title={item.title}
onEditClick={onEdit} onEditClick={onEdit}
isEdit={done.includes(item.id)} isEdit={doneSteps.includes(item.id)}
onClose={onConfirm}
note={note} note={note}
disableEdit={customer && item.id === CheckoutStep.CustomerInfo}
> >
{item.form} {item.form}
</CheckoutCollapse> </CheckoutCollapse>

View File

@@ -0,0 +1,6 @@
@import "../../../../../../styles/utilities";
.chekoutNotePolicy {
@apply caption;
margin-bottom: 1.6rem;
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link'
import React, { memo } from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import s from './ChekoutNotePolicy.module.scss'
const ChekoutNotePolicy = memo(() => {
return (
<div className={s.chekoutNotePolicy}>
By clicking continue you agree to Casper's{' '}
{
<Link href={ROUTE.TERM_CONDITION}>
<a>
<strong>terms and conditions</strong>
</a>
</Link>
}{' '}
and{' '}
{
<Link href={ROUTE.PRIVACY_POLICY}>
<a>
<strong>privacy policy</strong>
</a>
</Link>
}
.
</div>
)
})
ChekoutNotePolicy.displayName = 'ChekoutNotePolicy'
export default ChekoutNotePolicy

View File

@@ -8,18 +8,10 @@
} }
} }
.bottom{ .bottom{
@apply flex flex-col items-start;
margin-top: 2.4rem; margin-top: 2.4rem;
@apply flex justify-between items-center; button {
.note{ margin-left: auto;
font-size: 1.2rem;
line-height: 2rem;
} }
@screen sm-only {
@apply flex-col items-start;
.button {
padding-top: 2rem;
}
}
} }
} }

View File

@@ -1,55 +1,142 @@
import Link from 'next/link' import { Form, Formik } from 'formik'
import React, { useRef } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { ButtonCommon, Inputcommon } from 'src/components/common' import { ButtonCommon, InputFiledInForm } from 'src/components/common'
import InputCommon from 'src/components/common/InputCommon/InputCommon' import ModalAuthenticate from 'src/components/common/ModalAuthenticate/ModalAuthenticate'
import { CheckOutForm } from 'src/utils/types.utils' import { useMessage } from 'src/components/contexts'
import { useModalCommon } from 'src/components/hooks'
import { useSetCustomerForOrder } from 'src/components/hooks/order'
import { ErrorCode } from 'src/domains/enums/ErrorCode'
import { CommonError } from 'src/domains/interfaces/CommonError'
import { LANGUAGE } from 'src/utils/language.utils'
import { CustomInputCommon } from 'src/utils/type.utils'
import * as Yup from 'yup'
import ChekoutNotePolicy from '../ChekoutNotePolicy/ChekoutNotePolicy'
import s from './CustomerInfoForm.module.scss' import s from './CustomerInfoForm.module.scss'
interface CustomerInfoFormProps { import ModalConfirmLogin from './ModalConfirmLogin/ModalConfirmLogin'
onConfirm?: (id: number, formInfo: CheckOutForm) => void interface Props {
id: number id: number
onConfirm: (id: number) => void
activeStep: number
} }
const CustomerInfoForm = ({ id, onConfirm }: CustomerInfoFormProps) => { const displayingErrorMessagesSchema = Yup.object().shape({
const nameRef = useRef<React.ElementRef<typeof InputCommon>>(null) firstName: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
const emailRef = useRef<React.ElementRef<typeof InputCommon>>(null) lastName: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
emailAddress: Yup.string().email(LANGUAGE.MESSAGE.INVALID_EMAIL).required(LANGUAGE.MESSAGE.REQUIRED),
})
const handleConfirmClick = () => { const CustomerInfoForm = ({ id, onConfirm, activeStep }: Props) => {
onConfirm && const firstNameRef = useRef<CustomInputCommon>(null)
onConfirm(id, { const emailRef = useRef<CustomInputCommon>(null)
name: nameRef?.current?.getValue().toString(), const { setCustomerForOrder, loading } = useSetCustomerForOrder()
email: emailRef.current?.getValue().toString(), const { showMessageError } = useMessage()
}) const [emailAddress, setEmailAddress] = useState<string>('')
const { visible: visibleModalConfirmLogin, closeModal: closeModalConfirmLogin, openModal: openModalConfirmLogin } = useModalCommon({ initialValue: false })
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
useEffect(() => {
setTimeout(() => {
firstNameRef.current?.focus()
}, 500);
}, [activeStep])
const handleSubmit = (values: { firstName: string, lastName: string, emailAddress: string }) => {
const { firstName, lastName, emailAddress } = values
setEmailAddress(emailAddress)
setCustomerForOrder({ firstName, lastName, emailAddress }, onSubmitCalBack)
}
const onSubmitCalBack = (isSuccess: boolean, error?: CommonError) => {
// TODO:
if (isSuccess) {
onConfirm(id)
} else {
if (error?.errorCode === ErrorCode.EmailAddressConflictError) {
// show modal common
openModalConfirmLogin()
} else if (error?.errorCode === ErrorCode.NoActiveOrderError) {
showMessageError("Your cart is empty! Please add items to the cart!")
} else {
showMessageError(error?.message)
}
}
}
const handleOpenModalLogin = () => {
closeModalConfirmLogin()
openModalAuthen()
}
const handleCloseModalConfirmLogin = () => {
closeModalConfirmLogin()
emailRef.current?.focus()
} }
return ( return (
<div className={s.warpper}> <section className={s.warpper}>
<div className={s.body}> <div className={s.body}>
<Inputcommon type="text" placeholder="Full Name" ref={nameRef} /> <Formik
<Inputcommon type="text" placeholder="Email Address" ref={emailRef} /> initialValues={{
firstName: '',
lastName: '',
emailAddress: '',
}}
validationSchema={displayingErrorMessagesSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isValid, submitForm }) => (
<Form className="u-form">
<div className="body">
<div className="line">
<InputFiledInForm
name="firstName"
placeholder="First name"
ref={firstNameRef}
error={
touched.firstName && errors.firstName
? errors.firstName.toString()
: ''
}
isShowIconSuccess={touched.firstName && !errors.firstName}
/>
<InputFiledInForm
name="lastName"
placeholder="Last name"
error={
touched.lastName && errors.lastName
? errors.lastName.toString()
: ''
}
isShowIconSuccess={touched.lastName && !errors.lastName}
/>
</div>
<InputFiledInForm
name="emailAddress"
placeholder="Email Address"
error={
touched.emailAddress && errors.emailAddress
? errors.emailAddress.toString()
: ''
}
ref={emailRef}
isShowIconSuccess={touched.emailAddress && !errors.emailAddress}
onEnter={isValid ? submitForm : undefined}
/>
</div>
<div className={s.bottom}>
<ChekoutNotePolicy />
<ButtonCommon HTMLType='submit' loading={loading} size="large">
Continue to Shipping
</ButtonCommon>
</div>
</Form>
)}
</Formik>
</div> </div>
<div className={s.bottom}> <ModalConfirmLogin visible={visibleModalConfirmLogin} closeModal={handleCloseModalConfirmLogin} handleOk={handleOpenModalLogin} email={emailAddress} />
<div className={s.note}> <ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} initialEmail={emailAddress} disableRedirect={true} />
By clicking continue you agree to Casper's{' '} </section>
{
<Link href="#">
<strong>terms and conditions</strong>
</Link>
}{' '}
and{' '}
{
<Link href="#">
<strong>privacy policy </strong>
</Link>
}
.
</div>
<div className={s.button}>
<ButtonCommon onClick={handleConfirmClick}>
Continue to Shipping
</ButtonCommon>
</div>
</div>
</div>
) )
} }

View File

@@ -0,0 +1,5 @@
.modalConfirmLogin {
min-width: 40rem;
text-align: center;
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { ModalConfirm } from 'src/components/common';
import { LANGUAGE } from 'src/utils/language.utils';
import s from './ModalConfirmLogin.module.scss'
interface Props {
visible: boolean
closeModal: () => void
handleOk: () => void
email: string
}
const ModalConfirmLogin = ({ visible, closeModal, handleOk, email }: Props) => {
return (
<div>
<ModalConfirm
visible={visible}
onClose={closeModal}
onOk={handleOk}
okText={LANGUAGE.BUTTON_LABEL.SIGNIN}
cancelText="Change email address"
>
<div className={s.modalConfirmLogin}>
<p> Account already exists for email {email} </p>
<p>Please signin to continue or use another email</p>
</div>
</ModalConfirm>
</div>
);
};
export default ModalConfirmLogin;

View File

@@ -1,19 +1,18 @@
import React from 'react' import React from 'react'
import { ButtonCommon, TabCommon, TabPane } from 'src/components/common' import { ButtonCommon, TabCommon, TabPane } from 'src/components/common'
import { CheckOutForm } from 'src/utils/types.utils'
import BankTransfer from '../BankTransfer/BankTransfer' import BankTransfer from '../BankTransfer/BankTransfer'
import Link from 'next/link' import ChekoutNotePolicy from '../ChekoutNotePolicy/ChekoutNotePolicy'
import s from './PaymentInfoForm.module.scss'
import CreditCardForm from '../CreditCardForm/CreditCardForm' import CreditCardForm from '../CreditCardForm/CreditCardForm'
import s from './PaymentInfoForm.module.scss'
interface PaymentInfoFormProps { interface PaymentInfoFormProps {
onConfirm?: (id: number, formInfo: CheckOutForm) => void onConfirm?: (id: number) => void
id: number id: number
} }
const PaymentInfoForm = ({onConfirm,id}: PaymentInfoFormProps) => { const PaymentInfoForm = ({onConfirm,id}: PaymentInfoFormProps) => {
const handleConfirmClick = () => { const handleConfirmClick = () => {
onConfirm && onConfirm(id,{}) onConfirm && onConfirm(id)
} }
return ( return (
<div className={s.wrapper}> <div className={s.wrapper}>
@@ -29,21 +28,7 @@ const PaymentInfoForm = ({onConfirm,id}: PaymentInfoFormProps) => {
</TabPane> </TabPane>
</TabCommon> </TabCommon>
<div className={s.bottom}> <div className={s.bottom}>
<div className={s.note}> <ChekoutNotePolicy/>
By clicking continue you agree to Casper's{' '}
{
<Link href="#">
<strong>terms and conditions</strong>
</Link>
}{' '}
and{' '}
{
<Link href="#">
<strong>privacy policy </strong>
</Link>
}
.
</div>
<div className={s.button}> <div className={s.button}>
<ButtonCommon onClick={handleConfirmClick}> <ButtonCommon onClick={handleConfirmClick}>
Submit Order Submit Order

View File

@@ -1,45 +1,20 @@
@import "../../../../../../styles/utilities"; @import "../../../../../../styles/utilities";
.warpper{ .warpper {
@apply u-form; @apply u-form;
@screen md { @screen md {
padding: 0 5.6rem; padding: 0 5.6rem;
} }
.bottom{ .bottom {
@apply flex flex-col items-start;
margin-top: 2.4rem; margin-top: 2.4rem;
@apply flex justify-between items-center; button {
.note{ margin-left: auto;
font-size: 1.2rem;
line-height: 2rem;
}
@screen sm-only {
@apply flex-col items-start;
.button {
padding-top: 2rem;
}
} }
} }
.line{ .line {
>div{ > div {
width: 50%; width: 50%;
} }
} }
.method{ }
width: 100%;
height: 5.6rem;
padding: 1.6rem;
border-radius: 0.8rem;
@apply flex justify-between items-center border border-solid border-line bg-gray;
.left{
@apply flex;
.name{
margin-left: 1.6rem;
color: var(--text-active);
}
}
.price{
font-weight: bold;
color: var(--text-active);
}
}
}

View File

@@ -1,96 +1,189 @@
import React, { useRef } from 'react' import { Form, Formik } from 'formik'
import { ButtonCommon, Inputcommon, SelectCommon } from 'src/components/common' import React, { useEffect, useRef } from 'react'
import s from './ShippingInfoForm.module.scss' import { ButtonCommon, InputFiledInForm, SelectFieldInForm } from 'src/components/common'
import Link from 'next/link' import { useMessage } from 'src/components/contexts'
import { useAvailableCountries, useSetOrderShippingAddress } from 'src/components/hooks/order'
import { LANGUAGE } from 'src/utils/language.utils'
import { CustomInputCommon } from 'src/utils/type.utils' import { CustomInputCommon } from 'src/utils/type.utils'
import { Shipping } from 'src/components/icons' import * as Yup from 'yup'
import { CheckOutForm } from 'src/utils/types.utils' import ChekoutNotePolicy from '../ChekoutNotePolicy/ChekoutNotePolicy'
import s from './ShippingInfoForm.module.scss'
interface ShippingInfoFormProps { interface ShippingInfoFormProps {
onConfirm?: (id:number,formInfo:CheckOutForm)=>void id: number
id:number activeStep: number
onConfirm: (id: number) => void
} }
const option = [
const displayingErrorMessagesSchema = Yup.object().shape({
streetLine1: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
city: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
province: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
postalCode: Yup.number().required(LANGUAGE.MESSAGE.REQUIRED),
countryCode: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
phoneNumber: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
})
const DEFAULT_COUNTRY_CODE = 'MY'
const DEFAULT_PROVINCE = 'Sabah'
// TODO: update data
const provinceOptions = [
{ {
name: 'Hồ Chí Minh', name: 'Hồ Chí Minh',
value: 'Hồ Chí Minh',
}, },
{ {
name: 'Hà Nội', name: 'Hà Nội',
value: 'Hà Nội',
},
{
name: 'Sabah',
value: 'Sabah',
}, },
] ]
const ShippingInfoForm = ({onConfirm,id}: ShippingInfoFormProps) => { const ShippingInfoForm = ({ onConfirm, id, activeStep }: ShippingInfoFormProps) => {
const addressRef = useRef<CustomInputCommon>(null) const addressRef = useRef<CustomInputCommon>(null)
const cityRef = useRef<CustomInputCommon>(null) const { setOrderShippingAddress, loading } = useSetOrderShippingAddress()
const stateRef = useRef<CustomInputCommon>(null) const { showMessageError } = useMessage()
const codeRef = useRef<CustomInputCommon>(null) const { countries } = useAvailableCountries()
const phoneRef = useRef<CustomInputCommon>(null)
const handleConfirmClick = () => { useEffect(() => {
onConfirm && onConfirm(id,{ setTimeout(() => {
address: addressRef?.current?.getValue().toString(), addressRef.current?.focus()
city: cityRef.current?.getValue().toString(), }, 500);
state: stateRef?.current?.getValue().toString(), }, [activeStep])
code: Number(codeRef.current?.getValue()),
phone: Number(phoneRef?.current?.getValue()), const handleSubmit = (values: any) => {
}) setOrderShippingAddress(values, onSubmitCalBack)
} }
const onSubmitCalBack = (isSuccess: boolean, msg?: string) => {
if (isSuccess) {
onConfirm(id)
} else {
showMessageError(msg)
}
}
return ( return (
<div className={s.warpper}> <div className={s.warpper}>
<div className={s.body}> <div className={s.body}>
<Inputcommon <Formik
type="text" initialValues={
placeholder="Street Address" {
ref={addressRef} streetLine1: '',
/> city: '',
<Inputcommon type="text" placeholder="City" ref={cityRef} /> province: DEFAULT_PROVINCE,
<div className={s.line}> postalCode: '',
<SelectCommon options={option} type="custom" size="large">State</SelectCommon> countryCode: DEFAULT_COUNTRY_CODE,
<Inputcommon type="text" placeholder="Zip Code" ref={codeRef} /> phoneNumber: '',
</div> }}
<Inputcommon validationSchema={displayingErrorMessagesSchema}
type="text" onSubmit={handleSubmit}
placeholder="Phone (delivery contact)" >
ref={phoneRef} {({ errors, touched, isValid, submitForm }) => (
/> <Form className="u-form">
<div className={s.method}> <div className="body">
<div className={s.left}> <div className={s.input}>
<div className={s.icon}> <InputFiledInForm
<Shipping/> name="streetLine1"
</div> placeholder="Address"
<div className={s.name}> ref={addressRef}
Standard Delivery Method error={
</div> touched.streetLine1 && errors.streetLine1
</div> ? errors.streetLine1.toString()
<div className={s.right}> : ''
<div className={s.price}> }
Free isShowIconSuccess={touched.streetLine1 && !errors.streetLine1}
</div> />
</div> </div>
</div> <div className="line">
</div> <div className={s.input}>
<div className={s.bottom}> <InputFiledInForm
<div className={s.note}> name="city"
By clicking continue you agree to Casper's{' '} placeholder="City"
{ error={
<Link href="#"> touched.city && errors.city
<strong>terms and conditions</strong> ? errors.city.toString()
</Link> : ''
}{' '} }
and{' '} isShowIconSuccess={touched.city && !errors.city}
{ />
<Link href="#"> </div>
<strong>privacy policy </strong>
</Link> <div className={s.input}>
} <SelectFieldInForm
. options={provinceOptions}
</div> name="province"
<div className={s.button}> placeholder="Province"
<ButtonCommon onClick={handleConfirmClick}> error={
Continue to Payment touched.province && errors.province
</ButtonCommon> ? errors.province.toString()
</div> : ''
}
/>
</div>
</div>
<div className="line">
<div className={s.input}>
<InputFiledInForm
name="postalCode"
placeholder="Postal Code"
error={
touched.postalCode && errors.postalCode
? errors.postalCode.toString()
: ''
}
isShowIconSuccess={touched.postalCode && !errors.postalCode}
/>
</div>
<div className={s.input}>
<SelectFieldInForm
options={countries || []}
keyNameOption={['name']}
keyValueOption="code"
name="countryCode"
placeholder="Country"
error={
touched.countryCode && errors.countryCode
? errors.countryCode.toString()
: ''
}
/>
</div>
</div>
<div className={s.inputPhoneNumber}>
<InputFiledInForm
name="phoneNumber"
placeholder="Phone number"
error={
touched.phoneNumber && errors.phoneNumber
? errors.phoneNumber.toString()
: ''
}
isShowIconSuccess={touched.phoneNumber && !errors.phoneNumber}
onEnter={isValid ? submitForm : undefined}
/>
</div>
<div className={s.bottom}>
<ChekoutNotePolicy />
<ButtonCommon HTMLType='submit' loading={loading} size="large">
Continue to Shipping method
</ButtonCommon>
</div>
</div>
</Form>
)}
</Formik>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,28 @@
.shippingMethod {
@apply relative;
.method {
@apply w-full flex justify-between items-center border border-solid border-line bg-gray cursor-pointer;
height: 5.6rem;
padding: 1.6rem;
border-radius: 0.8rem;
.left {
@apply flex;
.name {
margin-left: 1.6rem;
color: var(--text-active);
}
}
.price {
font-weight: bold;
color: var(--text-active);
}
}
.options {
margin-top: 0.8rem;
width: 100%;
background: var(--white);
border: 1px solid var(--border-line);
border-radius: 0.8rem;
}
}

View File

@@ -0,0 +1,75 @@
import { ShippingMethodQuote } from '@framework/schema'
import React, { memo, useState } from 'react'
import { useMessage } from 'src/components/contexts'
import { useEligibleShippingMethods, useSetOrderShippingMethod } from 'src/components/hooks/order'
import { Shipping } from 'src/components/icons'
import { CheckoutStep } from '../../CheckoutInfo'
import s from './ShippingMethod.module.scss'
import ShippingMethodItem from './ShippingMethodItem/ShippingMethodItem'
interface Props {
currency: string
onConfirm: (id: number) => void
}
const ShippingMethod = memo(({ currency, onConfirm }: Props) => {
const { eligibleShippingMethods } = useEligibleShippingMethods()
const { setOrderShippingMethod } = useSetOrderShippingMethod()
const [selectedValue, setSelectedValue] = useState<ShippingMethodQuote | undefined>(eligibleShippingMethods ? eligibleShippingMethods[0] : undefined)
const { showMessageError } = useMessage()
const onChange = (id: string) => {
const newValue = eligibleShippingMethods?.find(item => item.id === id)
if (newValue) {
setSelectedValue(newValue)
if (newValue?.id) {
setOrderShippingMethod(newValue?.id, onSubmitCalBack)
}
}
}
const onSubmitCalBack = (isSuccess: boolean, msg?: string) => {
if (isSuccess) {
onConfirm(CheckoutStep.ShippingMethodInfo)
} else {
showMessageError(msg)
}
}
return (
<div className={s.shippingMethod}>
<div className={s.method}>
<div className={s.left}>
<div className={s.icon}>
<Shipping />
</div>
<div className={s.name}>
{selectedValue?.name}
</div>
</div>
<div className={s.right}>
<div className={s.price}>
{selectedValue?.price ? `${selectedValue?.price / 100} ${currency}` : "Free"}
</div>
</div>
</div>
<div className={s.options}>
<ul>
{eligibleShippingMethods?.map(item => <ShippingMethodItem
key={item.id}
id={item.id}
name={item.name}
price={item.price}
currency={currency}
isActive={selectedValue?.id === item.id}
onSelect={onChange}
/>)}
</ul>
</div>
</div>
)
})
ShippingMethod.displayName = 'ShippingMethod'
export default ShippingMethod

View File

@@ -0,0 +1,25 @@
.shippingMethodItem {
@apply w-full flex justify-between items-center cursor-pointer transition-all duration-200;
padding: 1.6rem;
&:hover {
@apply bg-gray;
}
.left {
@apply flex;
.icon {
@apply transition-all duration-200;
opacity: 0;
&.show {
opacity: 1;
}
}
.name {
margin-left: 1.6rem;
color: var(--text-active);
}
}
.price {
font-weight: bold;
color: var(--text-active);
}
}

View File

@@ -0,0 +1,38 @@
import classNames from 'classnames'
import React, { memo } from 'react'
import { IconCheck } from 'src/components/icons'
import s from './ShippingMethodItem.module.scss'
interface Props {
id: string
name: string
price: number
currency: string
onSelect: (id: string) => void
isActive: boolean
}
const ShippingMethodItem = memo(({ id, name, price, currency, isActive, onSelect }: Props) => {
const handleSelect = () => {
onSelect(id)
}
return (
<li className={s.shippingMethodItem} key={id} onClick={handleSelect}>
<div className={s.left}>
<div className={classNames(s.icon, { [s.show]: isActive })}>
<IconCheck />
</div>
<div className={s.name}>
{name}
</div>
</div>
<div className={s.price}>
{price ? `${price / 100} ${currency}` : "Free"}
</div>
</li>
)
})
ShippingMethodItem.displayName = 'ShippingMethodItem'
export default ShippingMethodItem

View File

@@ -1,6 +1,7 @@
@import "../../../../styles/utilities"; @import "../../../../styles/utilities";
.warrper{ .warrper{
@apply flex w-full h-full absolute; @apply flex w-full;
min-height: 100%;
.right { .right {
display: none; display: none;
@screen lg { @screen lg {
@@ -46,10 +47,7 @@
color:var(--text-base); color:var(--text-base);
} }
} }
button{
margin-top: 2rem;
width: 100%;
}
} }
} }
} }

View File

@@ -1,5 +1,8 @@
import classNames from 'classnames' import classNames from 'classnames'
import React, { useState } from 'react' import React, { useState } from 'react'
import { MessageCommon } from 'src/components/common'
import { useMessage } from 'src/components/contexts'
import { useGetActiveOrderForCheckout } from 'src/components/hooks/order'
import IconHide from 'src/components/icons/IconHide' import IconHide from 'src/components/icons/IconHide'
import { CHECKOUT_BILL_DATA } from 'src/utils/demo-data' import { CHECKOUT_BILL_DATA } from 'src/utils/demo-data'
import { CheckoutBill, CheckoutInfo } from '..' import { CheckoutBill, CheckoutInfo } from '..'
@@ -7,26 +10,30 @@ import s from "./CheckoutPage.module.scss"
interface CheckoutPageProps { interface CheckoutPageProps {
} }
const CheckoutPage = ({}: CheckoutPageProps) => { const CheckoutPage = ({ }: CheckoutPageProps) => {
const { messages, removeMessage } = useMessage()
const [isShow, setIsShow] = useState(false) const [isShow, setIsShow] = useState(false)
const { order } = useGetActiveOrderForCheckout()
const onClose = () => { const onClose = () => {
setIsShow(false) setIsShow(false)
} }
const onViewCart =() => { const onViewCart = () => {
setIsShow(true) setIsShow(true)
} }
return ( return (
<div className={s.warrper}> <div className={s.warrper}>
<div className={s.left}><CheckoutInfo onViewCart = {onViewCart}/></div> <MessageCommon messages={messages} onRemove={removeMessage} />
<div className={s.right}><CheckoutBill data={CHECKOUT_BILL_DATA}/></div> <div className={s.left}><CheckoutInfo onViewCart={onViewCart} currency={order?.currency.code} /></div>
<div className={classNames({ [s.mobile] :true,[s.isShow]: isShow})}> <div className={s.right}><CheckoutBill data={order} /></div>
<div className={classNames({ [s.mobile]: true, [s.isShow]: isShow })}>
<div className={s.modal}> <div className={s.modal}>
<div className={s.content}> <div className={s.content}>
<div className={s.head}> <div className={s.head}>
<h3>Your Cart({CHECKOUT_BILL_DATA.length})</h3> <h3>Your Cart({CHECKOUT_BILL_DATA.length})</h3>
<div onClick={onClose}><IconHide/></div> <div onClick={onClose}><IconHide /></div>
</div> </div>
<CheckoutBill data={CHECKOUT_BILL_DATA}/> <CheckoutBill data={order} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -201,7 +201,7 @@
} }
} }
.line { .line {
@apply flex justify-between items-center; @apply flex justify-between items-start;
> div { > div {
flex: 1; flex: 1;
&:not(:last-child) { &:not(:last-child) {

View File

@@ -1,4 +1,4 @@
import { request } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { RequestDocument, Variables } from 'graphql-request/dist/types' import { RequestDocument, Variables } from 'graphql-request/dist/types'
import { LOCAL_STORAGE_KEY } from './constanst.utils' import { LOCAL_STORAGE_KEY } from './constanst.utils'
@@ -12,11 +12,15 @@ interface QueryOptions {
const fetcher = async <T>(options: QueryOptions): Promise<T> => { const fetcher = async <T>(options: QueryOptions): Promise<T> => {
const { query, variables } = options const { query, variables } = options
const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN) const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
const res = await request<T>( const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, {
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, credentials: 'include',
mode: 'cors',
headers: token ? { Authorization: 'Bearer ' + token } : {},
})
const res = await graphQLClient.request<T>(
query, query,
variables, variables,
token ? { Authorization: 'Bearer ' + token } : {}
) )
return res return res

View File

@@ -1,6 +1,6 @@
import { Collection } from '@commerce/types/collection'; import { Collection } from '@commerce/types/collection';
import { Facet } from "@commerce/types/facet"; import { Facet } from "@commerce/types/facet";
import { Product, ProductCard, ProductOption, ProductOptionValues } from "@commerce/types/product"; import { Product, ProductCard, ProductOptionValues } from "@commerce/types/product";
import { FacetValue, SearchResultSortParameter } from './../../framework/vendure/schema.d'; import { FacetValue, SearchResultSortParameter } from './../../framework/vendure/schema.d';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT, FACET, PRODUCT_SORT_OPTION_VALUE } from "./constanst.utils"; import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT, FACET, PRODUCT_SORT_OPTION_VALUE } from "./constanst.utils";
import { PromiseWithKey, SelectedOptions, SortOrder } from "./types.utils"; import { PromiseWithKey, SelectedOptions, SortOrder } from "./types.utils";

View File

@@ -5,12 +5,16 @@ export const LANGUAGE = {
CONFIRM:'Confirm', CONFIRM:'Confirm',
ADD_TO_CARD: 'Add to Cart', ADD_TO_CARD: 'Add to Cart',
PREORDER: 'Pre-Order Now', PREORDER: 'Pre-Order Now',
SIGNIN :'Sign In' SIGNIN :'Sign In',
CANCEL: 'Cancel',
}, },
PLACE_HOLDER: { PLACE_HOLDER: {
SEARCH: 'Search', SEARCH: 'Search',
}, },
MESSAGE: { MESSAGE: {
ERROR: 'Something went wrong! Please try again!' ERROR: 'Something went wrong! Please try again!',
INVALID_EMAIL: 'Your email was wrong',
REQUIRED: 'Required',
} }
} }

View File

@@ -1,7 +1,6 @@
import { rawRequest } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { RequestDocument, Variables } from 'graphql-request/dist/types' import { RequestDocument, Variables } from 'graphql-request/dist/types'
import { LOCAL_STORAGE_KEY } from './constanst.utils' import { LOCAL_STORAGE_KEY } from './constanst.utils'
interface QueryOptions { interface QueryOptions {
query: RequestDocument query: RequestDocument
variables?: Variables variables?: Variables
@@ -16,15 +15,20 @@ const rawFetcher = <T>({
}: QueryOptions): Promise<{ data: T; headers: any }> => { }: QueryOptions): Promise<{ data: T; headers: any }> => {
onLoad(true) onLoad(true)
const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN) const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
return rawRequest<T>(
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, {
credentials: 'include',
mode: 'cors',
headers: token ? { Authorization: 'Bearer ' + token } : {},
})
return graphQLClient.rawRequest<T>(
query as string, query as string,
variables, variables,
token ? { Authorization: 'Bearer ' + token } : {} )
) .then(({ data, headers }) => {
.then(({ data, headers }) => { return { data, headers }
return { data, headers } })
})
.finally(() => onLoad(false)) .finally(() => onLoad(false))
} }

View File

@@ -33,18 +33,6 @@ export interface BlogProps {
imageSrc: string | null, imageSrc: string | null,
} }
export interface CheckOutForm {
name?: string
email?: string
address?: string
city?: string
state?: string
code?: number
phone?: number
method?: string
shipping_fee?: number
}
export type MouseAndTouchEvent = MouseEvent | TouchEvent export type MouseAndTouchEvent = MouseEvent | TouchEvent
export enum SortOrder { export enum SortOrder {
@@ -72,4 +60,18 @@ export type PromiseWithKey = {
keyResult?: string, keyResult?: string,
} }
export type SelectedOptions = Record<string, string | null> // ref https://www.vendure.io/docs/typescript-api/orders/order-state/
export type OrderState = | 'Created'
| 'AddingItems'
| 'ArrangingPayment'
| 'PaymentAuthorized'
| 'PaymentSettled'
| 'PartiallyShipped'
| 'Shipped'
| 'PartiallyDelivered'
| 'Delivered'
| 'Modifying'
| 'ArrangingAdditionalPayment'
| 'Cancelled'
export type SelectedOptions = Record<string, string | null>