Merge branch 'release-stable' of https://github.com/KieIO/grocery-vercel-commerce into feature/m4-get-blog-detail

This commit is contained in:
Quangnhankie
2021-10-21 09:50:00 +07:00
25 changed files with 590 additions and 332 deletions

View File

@@ -306,6 +306,49 @@ export type MutationResetPasswordArgs = {
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 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 Address = Node & {
updateCustomerAddress:
| {
@@ -3115,7 +3158,7 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
> & { product: { __typename?: 'Product' } & Pick<Product, 'slug'> }
}
>
}
}
export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
SearchResult,
@@ -3132,7 +3175,7 @@ export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
priceWithTax:
| ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
| ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>)
}
}
export type AddItemToOrderMutationVariables = Exact<{
variantId: Scalars['ID']
@@ -3314,7 +3357,7 @@ export type ActiveCustomerQuery = { __typename?: 'Query' } & {
{ __typename?: 'Customer' } & Pick<
Customer,
Favorite,
'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber'| 'orders'
'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber' | 'orders'
>
>
}
@@ -3497,7 +3540,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
collections: Array<
{ __typename?: 'Collection' } & Pick<
Collection,
'id'|"name"
'id' | "name"
>
>
}

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

@@ -4,6 +4,7 @@
"scripts": {
"dev": "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",
"start": "PORT=3005 next start",
"analyze": "BUNDLE_ANALYZE=both yarn build",

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,11 +17,11 @@ interface Props {
onSwitch: () => void
}
const DisplayingErrorMessagesSchema = Yup.object().shape({
email: Yup.string().email('Your email was wrong').required('Required'),
const displayingErrorMessagesSchema = Yup.object().shape({
email: Yup.string().email(LANGUAGE.MESSAGE.INVALID_EMAIL).required(LANGUAGE.MESSAGE.REQUIRED),
password: Yup.string()
.max(30, 'Password is too long')
.required('Required'),
.required(LANGUAGE.MESSAGE.REQUIRED),
})
const FormLogin = ({ onSwitch, isHide }: Props) => {
@@ -56,7 +56,7 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
password: '',
email: '',
}}
validationSchema={DisplayingErrorMessagesSchema}
validationSchema={displayingErrorMessagesSchema}
onSubmit={onLogin}
>

View File

@@ -6,7 +6,7 @@ export type MessageContextType = {
removeMessage: (id: number) => void
showMessageSuccess: (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
}
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 { LANGUAGE } from 'src/utils/language.utils'
import { MessageContext } from './MessageContext'
type Props = {
@@ -33,8 +34,8 @@ export function MessageProvider({ children }: Props) {
createNewMessage(content, timeout, 'info')
}
const showMessageError = (content: string, timeout?: number) => {
createNewMessage(content, timeout, 'error')
const showMessageError = (content?: string, timeout?: number) => {
createNewMessage(content || LANGUAGE.MESSAGE.ERROR, timeout, 'error')
}
const showMessageWarning = (content: string, timeout?: number) => {

View File

@@ -1,4 +1,3 @@
import { Cart } from '@commerce/types/cart'
import { ActiveOrderQuery } from '@framework/schema'
import { cartFragment } from '@framework/utils/fragments/cart-fragment'
import { normalizeCart } from '@framework/utils/normalize'

View File

@@ -0,0 +1,2 @@
export { default as useSetCustomerForOrder } from './useSetCustomerForOrder'

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 { useGetActiveOrder } from '../cart'
const useSetCustomerForOrder = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrder()
const setCustomerForOrder = (input: CreateCustomerInput,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<SetCustomerForOrderMutation>({
query: setCustomerForOrderMutation,
variables: { input },
})
.then(({ data }) => {
if (data.setCustomerForOrder.__typename === 'ActiveOrderCustomerFragment') {
fCallBack(true)
mutate()
} else {
fCallBack(false, data.setCustomerForOrder.message)
}
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, setCustomerForOrder, error }
}
export default useSetCustomerForOrder

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import { Logo } from 'src/components/common'
import { ButtonCommon, Logo } from 'src/components/common'
import CheckoutCollapse from 'src/components/common/CheckoutCollapse/CheckoutCollapse'
import { useAddProductToCart, useGetActiveOrder } from 'src/components/hooks/cart'
import { removeItem } from 'src/utils/funtion.utils'
import { CheckOutForm } from 'src/utils/types.utils'
import s from './CheckoutInfo.module.scss'
@@ -8,38 +9,38 @@ import CustomerInfoForm from './components/CustomerInfoForm/CustomerInfoForm'
import PaymentInfoForm from './components/PaymentInfoForm/PaymentInfoForm'
import ShippingInfoForm from './components/ShippingInfoForm/ShippingInfoForm'
interface CheckoutInfoProps {
onViewCart:()=>void
onViewCart: () => void
}
const CheckoutInfo = ({onViewCart}: CheckoutInfoProps) => {
const CheckoutInfo = ({ onViewCart }: CheckoutInfoProps) => {
const [active, setActive] = useState(1)
const [done, setDone] = useState<number[]>([])
const [info, setInfo] = useState<CheckOutForm>({})
const onEdit = (id:number) => {
const onEdit = (id: number) => {
setActive(id)
setDone(removeItem<number>(done,id))
setDone(removeItem<number>(done, id))
}
const onConfirm = (id:number,formInfo:CheckOutForm) => {
if(id+1>formList.length){
console.log({...info,...formInfo})
}else{
if(done.length>0){
for (let i = id+1; i <= formList.length; i++) {
if(!done.includes(i)){
const onConfirm = (id: number, formInfo: CheckOutForm) => {
if (id + 1 > formList.length) {
console.log({ ...info, ...formInfo })
} else {
if (done.length > 0) {
for (let i = id + 1; i <= formList.length; i++) {
if (!done.includes(i)) {
setActive(i)
}
}
}else{
setActive(id+1)
} else {
setActive(id + 1)
}
setDone([...done,id])
setDone([...done, id])
}
setInfo({...info,...formInfo})
setInfo({ ...info, ...formInfo })
}
const getNote = (id:number) => {
const getNote = (id: number) => {
switch (id) {
case 1:
return `${info.name}, ${info.email}`
@@ -54,21 +55,37 @@ const CheckoutInfo = ({onViewCart}: CheckoutInfoProps) => {
{
id: 1,
title: 'Customer Information',
form: <CustomerInfoForm onConfirm={onConfirm} id={1}/>,
form: <CustomerInfoForm onConfirm={onConfirm} id={1} />,
},
{
id: 2,
title: 'Shipping Information',
form: <ShippingInfoForm onConfirm={onConfirm} id={2}/>,
form: <ShippingInfoForm onConfirm={onConfirm} id={2} />,
},
{
id: 3,
title: 'Payment Information',
form: <PaymentInfoForm onConfirm={onConfirm} id={3}/>,
form: <PaymentInfoForm onConfirm={onConfirm} id={3} />,
},
]
// TODO: remove
const { addProduct } = useAddProductToCart()
const createOrder = () => {
addProduct({ variantId: "63", quantity: 1 }, handleAddToCartCallback)
}
const handleAddToCartCallback = (isSuccess: boolean, message?: string) => {
// console.log("after create order: ", isSuccess, message)
}
const {order} = useGetActiveOrder()
return (
<div className={s.warpper}>
<ButtonCommon onClick={createOrder}>test create order</ButtonCommon>
<ButtonCommon onClick={createOrder}>test get active order</ButtonCommon>
<div className={s.title}>
<Logo />
<div className={s.viewCart} onClick={onViewCart}>View cart</div>

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

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

View File

@@ -1,55 +1,117 @@
import Link from 'next/link'
import { Form, Formik } from 'formik'
import React, { useRef } from 'react'
import { ButtonCommon, Inputcommon } from 'src/components/common'
import InputCommon from 'src/components/common/InputCommon/InputCommon'
import { CheckOutForm } from 'src/utils/types.utils'
import { ButtonCommon, InputFiledInForm } from 'src/components/common'
import { useMessage } from 'src/components/contexts'
import { useSetCustomerForOrder } from 'src/components/hooks/order'
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'
interface CustomerInfoFormProps {
onConfirm?: (id: number, formInfo: CheckOutForm) => void
id: number
interface Props {
isHide: boolean
onSwitch: () => void
}
const CustomerInfoForm = ({ id, onConfirm }: CustomerInfoFormProps) => {
const nameRef = useRef<React.ElementRef<typeof InputCommon>>(null)
const emailRef = useRef<React.ElementRef<typeof InputCommon>>(null)
const displayingErrorMessagesSchema = Yup.object().shape({
firstName: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
lastName: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
emailAddress: Yup.string().email(LANGUAGE.MESSAGE.INVALID_EMAIL).required(LANGUAGE.MESSAGE.REQUIRED),
})
const CustomerInfoForm = ({ onSwitch, isHide }: Props) => {
const firstNameRef = useRef<CustomInputCommon>(null)
const emailRef = useRef<CustomInputCommon>(null)
const { setCustomerForOrder, loading } = useSetCustomerForOrder()
const { showMessageError } = useMessage()
const handleSubmit = (values: { firstName: string, lastName: string, emailAddress: string }) => {
console.log('on submit: ', values)
const { firstName, lastName, emailAddress } = values
setCustomerForOrder({ firstName, lastName, emailAddress }, onSubmitCalBack)
// onConfirm &&
// onConfirm(id, {
// name: nameRef?.current?.getValue().toString(),
// email: emailRef.current?.getValue().toString(),
// })
}
const onSubmitCalBack = (isSuccess: boolean, msg?: string) => {
// TODO:
console.log("result: ", isSuccess, msg)
if (isSuccess) {
} else {
console.log("error here")
showMessageError(msg)
}
const handleConfirmClick = () => {
onConfirm &&
onConfirm(id, {
name: nameRef?.current?.getValue().toString(),
email: emailRef.current?.getValue().toString(),
})
}
return (
<div className={s.warpper}>
<section className={s.warpper}>
<div className={s.body}>
<Inputcommon type="text" placeholder="Full Name" ref={nameRef} />
<Inputcommon type="text" placeholder="Email Address" ref={emailRef} />
<Formik
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"
ref={emailRef}
error={
touched.emailAddress && errors.emailAddress
? errors.emailAddress.toString()
: ''
}
isShowIconSuccess={touched.emailAddress && !errors.emailAddress}
onEnter={isValid ? submitForm : undefined}
/>
</div>
<div className={s.bottom}>
<div className={s.note}>
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}>
<ButtonCommon onClick={handleConfirmClick}>
<ChekoutNotePolicy />
<ButtonCommon HTMLType='submit' loading={loading} size="large">
Continue to Shipping
</ButtonCommon>
</div>
</Form>
)}
</Formik>
</div>
</div>
</section>
)
}

View File

@@ -14,9 +14,11 @@ interface ShippingInfoFormProps {
const option = [
{
name: 'Hồ Chí Minh',
value: 'Hồ Chí Minh',
},
{
name: 'Hà Nội',
value: 'Hà Nội',
},
]

View File

@@ -1,5 +1,7 @@
import classNames from 'classnames'
import React, { useState } from 'react'
import { MessageCommon } from 'src/components/common'
import { useMessage } from 'src/components/contexts'
import IconHide from 'src/components/icons/IconHide'
import { CHECKOUT_BILL_DATA } from 'src/utils/demo-data'
import { CheckoutBill, CheckoutInfo } from '..'
@@ -8,7 +10,9 @@ interface CheckoutPageProps {
}
const CheckoutPage = ({}: CheckoutPageProps) => {
const { messages, removeMessage } = useMessage()
const [isShow, setIsShow] = useState(false)
const onClose = () => {
setIsShow(false)
}
@@ -17,6 +21,7 @@ const CheckoutPage = ({}: CheckoutPageProps) => {
}
return (
<div className={s.warrper}>
<MessageCommon messages={messages} onRemove={removeMessage} />
<div className={s.left}><CheckoutInfo onViewCart = {onViewCart}/></div>
<div className={s.right}><CheckoutBill data={CHECKOUT_BILL_DATA}/></div>
<div className={classNames({ [s.mobile] :true,[s.isShow]: isShow})}>

View File

@@ -201,7 +201,7 @@
}
}
.line {
@apply flex justify-between items-center;
@apply flex justify-between items-start;
> div {
flex: 1;
&: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 { LOCAL_STORAGE_KEY } from './constanst.utils'
@@ -12,11 +12,15 @@ interface QueryOptions {
const fetcher = async <T>(options: QueryOptions): Promise<T> => {
const { query, variables } = options
const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
const res = await request<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 } : {},
})
const res = await graphQLClient.request<T>(
query,
variables,
token ? { Authorization: 'Bearer ' + token } : {}
)
return res

View File

@@ -11,6 +11,9 @@ export const LANGUAGE = {
SEARCH: 'Search',
},
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 { LOCAL_STORAGE_KEY } from './constanst.utils'
interface QueryOptions {
query: RequestDocument
variables?: Variables
@@ -16,11 +15,16 @@ const rawFetcher = <T>({
}: QueryOptions): Promise<{ data: T; headers: any }> => {
onLoad(true)
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,
variables,
token ? { Authorization: 'Bearer ' + token } : {}
)
.then(({ data, headers }) => {
return { data, headers }

View File

@@ -72,4 +72,18 @@ export type PromiseWithKey = {
keyResult?: string,
}
// 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>