diff --git a/framework/commerce/types/product.ts b/framework/commerce/types/product.ts index e1a8da200..ddf51c518 100644 --- a/framework/commerce/types/product.ts +++ b/framework/commerce/types/product.ts @@ -43,8 +43,12 @@ export type Product = { slug?: string path?: string images: ProductImage[] - price: ProductPrice + price: number + currencyCode: CurrencyCode options: ProductOption[] + facetValueIds?: string[] + collectionIds?: string[] + collection?: string, } export type ProductCard = { diff --git a/framework/vendure/api/operations/get-all-facets.ts b/framework/vendure/api/operations/get-all-facets.ts index c4b002744..0bde04090 100644 --- a/framework/vendure/api/operations/get-all-facets.ts +++ b/framework/vendure/api/operations/get-all-facets.ts @@ -1,10 +1,10 @@ import { OperationContext } from '@commerce/api/operations' import { Facet } from '@commerce/types/facet' import { Provider, VendureConfig } from '../' -import { GetAllFacetsQuery } from '../../schema' +import { FacetFilterParameter, FacetSortParameter, GetAllFacetsQuery } from '../../schema' import { getAllFacetsQuery } from '../../utils/queries/get-all-facets-query' -export type FacetVariables = { first?: number } +export type FacetVariables = { first?: number, filter?: FacetFilterParameter, sort?: FacetSortParameter } export default function getAllFacetsOperation({ commerce, @@ -27,9 +27,10 @@ export default function getAllFacetsOperation({ } = {}): Promise<{ facets: Facet[] | any[] }> { const config = commerce.getConfig(cfg) const variables = { - input: { + options: { take: vars.first, - groupByFacet: true, + filter: vars.filter, + sort: vars.sort, }, } const { data } = await config.fetch(query, { diff --git a/framework/vendure/api/operations/get-all-products.ts b/framework/vendure/api/operations/get-all-products.ts index 1f558a7cb..5fb458f67 100644 --- a/framework/vendure/api/operations/get-all-products.ts +++ b/framework/vendure/api/operations/get-all-products.ts @@ -14,7 +14,7 @@ export default function getAllProductsOperation({ variables?: ProductVariables config?: Partial preview?: boolean - }): Promise<{ products: Product[] }> + }): Promise<{ products: Product[], totalItems: number }> async function getAllProducts({ query = getAllProductsQuery, @@ -25,7 +25,7 @@ export default function getAllProductsOperation({ variables?: ProductVariables config?: Partial preview?: boolean - } = {}): Promise<{ products: Product[] | any[] }> { + } = {}): Promise<{ products: Product[] | any[], totalItems: number }> { const config = commerce.getConfig(cfg) const variables = { input: { @@ -40,6 +40,7 @@ export default function getAllProductsOperation({ return { products: data.search.items.map((item) => normalizeSearchResult(item)), + totalItems: data.search.totalItems as number, } } diff --git a/framework/vendure/api/operations/get-product.ts b/framework/vendure/api/operations/get-product.ts index 4ab9ed2d9..0aa761ab0 100644 --- a/framework/vendure/api/operations/get-product.ts +++ b/framework/vendure/api/operations/get-product.ts @@ -2,7 +2,7 @@ import { Product } from '@commerce/types/product' import { OperationContext } from '@commerce/api/operations' import { Provider, VendureConfig } from '../' import { GetProductQuery } from '../../schema' -import { getProductQuery } from '../../utils/queries/get-product-query' +import { getProductQuery, getProductDetailQuery } from '../../utils/queries/get-product-query' export default function getProductOperation({ commerce, @@ -16,10 +16,8 @@ export default function getProductOperation({ variables: { slug: string } config?: Partial preview?: boolean - }): Promise { + }): Promise { const config = commerce.getConfig(cfg) - - const locale = config.locale const { data } = await config.fetch(query, { variables }) const product = data.product @@ -28,7 +26,6 @@ export default function getProductOperation({ return product.optionGroups.find((og) => og.id === id)!.name } return { - product: { id: product.id, name: product.name, description: product.description, @@ -49,20 +46,19 @@ export default function getProductOperation({ values: [{ label: o.name }], })), })), - price: { - value: product.variants[0].priceWithTax / 100, - currencyCode: product.variants[0].currencyCode, - }, + price: product.variants[0].priceWithTax / 100, + currencyCode: product.variants[0].currencyCode, options: product.optionGroups.map((og) => ({ id: og.id, displayName: og.name, values: og.options.map((o) => ({ label: o.name })), })), - } as Product, - } + facetValueIds: product.facetValues.map(item=> item.id), + collectionIds: product.collections.map(item => item.id) + } as Product } - return {} + return null } return getProduct diff --git a/framework/vendure/schema.d.ts b/framework/vendure/schema.d.ts index a09e8421c..3feb0e6ed 100644 --- a/framework/vendure/schema.d.ts +++ b/framework/vendure/schema.d.ts @@ -1,4 +1,7 @@ import { FacetValue, UpdateAddressInput } from './schema.d'; +import { ResetPassword } from './schema.d'; +import { requestPasswordReset } from '@framework/schema'; +import { FacetValue } from './schema.d'; export type Maybe = T | null export type Exact = { [K in keyof T]: T[K] @@ -3139,6 +3142,36 @@ export type LoginMutation = { __typename?: 'Mutation' } & { >) } +export type ResetPasswordMutation = { __typename?: 'Mutation' } & { + resetPassword: + | ({ __typename: 'CurrentUser' } & Pick) + | ({ __typename: 'PasswordResetTokenInvalidError' } & Pick< + PasswordResetTokenInvalidError, + 'errorCode' | 'message' + >) + | ({ __typename: 'PasswordResetTokenExpiredError' } & Pick< + PasswordResetTokenExpiredError, + 'errorCode' | 'message' + >) + | ({ __typename: 'NativeAuthStrategyError' } & Pick< + NativeAuthStrategyError, + 'errorCode' | 'message' + >) +} + +export type SignupMutation = { __typename?: 'Mutation' } & { + registerCustomerAccount: + | ({ __typename: 'Success' } & Pick) + | ({ __typename: 'MissingPasswordError' } & Pick< + MissingPasswordError, + 'errorCode' | 'message' + >) + | ({ __typename: 'NativeAuthStrategyError' } & Pick< + NativeAuthStrategyError, + 'errorCode' | 'message' + >) +} + export type VerifyCustomerAccountVariables = Exact<{ token: Scalars['String'] password?: Maybe @@ -3192,8 +3225,9 @@ export type SignupMutationVariables = Exact<{ input: RegisterCustomerInput }> -export type SignupMutation = { __typename?: 'Mutation' } & { - registerCustomerAccount: + +export type RequestPasswordReset = { __typename?: 'Mutation' } & { + requestPasswordReset: | ({ __typename: 'Success' } & Pick) | ({ __typename: 'MissingPasswordError' } & Pick< MissingPasswordError, @@ -3205,16 +3239,30 @@ export type SignupMutation = { __typename?: 'Mutation' } & { >) } + + export type ActiveCustomerQueryVariables = Exact<{ [key: string]: never }> export type ActiveCustomerQuery = { __typename?: 'Query' } & { activeCustomer?: Maybe< { __typename?: 'Customer' } & Pick< Customer, - 'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber' | 'orders' + 'id' | 'firstName' | 'lastName' | 'emailAddress' | 'addresses' | 'phoneNumber'| 'favorites' | 'orders' > > } +export type FavoriteList = PaginatedList & { + items: [Favorite!]! + totalItems: Int! +} + +type Favorite = Node & { + id: ID! + createdAt: DateTime! + updatedAt: DateTime! + product: Product + customer: Customer! +} export type GetAllProductPathsQueryVariables = Exact<{ first?: Maybe @@ -3232,7 +3280,8 @@ export type GetAllProductsQueryVariables = Exact<{ export type GetAllProductsQuery = { __typename?: 'Query' } & { search: { __typename?: 'SearchResponse' } & { - items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment> + items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>, + 'totalItems' } } @@ -3241,8 +3290,9 @@ export type GetAllFacetsQuery = { __typename?: 'Query' } & { items: Array< { __typename?: 'Facet' } & Pick< Facet, - 'id' | 'name' | 'code' - > & { + 'id' | 'name' | 'code' | 'values' + > + & { parent?: Maybe<{ __typename?: 'Facet' } & Pick> children?: Maybe< Array<{ __typename?: 'Facet' } & Pick> @@ -3314,7 +3364,7 @@ export type GetProductQuery = { __typename?: 'Query' } & { variants: Array< { __typename?: 'ProductVariant' } & Pick< ProductVariant, - 'id' | 'priceWithTax' | 'currencyCode' + 'id' | 'priceWithTax' | 'currencyCode' | 'price' > & { options: Array< { __typename?: 'ProductOption' } & Pick< @@ -3349,6 +3399,18 @@ export type GetProductQuery = { __typename?: 'Query' } & { > } > + facetValues: Array< + { __typename?: 'FacetValue' } & Pick< + FacetValue, + 'id' + > + > + collections: Array< + { __typename?: 'Collection' } & Pick< + Collection, + 'id' + > + > } > } diff --git a/framework/vendure/utils/mutations/request-password-reset-mutation.ts b/framework/vendure/utils/mutations/request-password-reset-mutation.ts new file mode 100644 index 000000000..474d8f33f --- /dev/null +++ b/framework/vendure/utils/mutations/request-password-reset-mutation.ts @@ -0,0 +1,14 @@ +export const requestPasswordReset = /* GraphQL */ ` +mutation RequestPasswordReset($emailAddress: String!) { + requestPasswordReset(emailAddress: $emailAddress) { + __typename + ...on Success{ + success + } + ...on ErrorResult{ + errorCode + message + } + } +} +` \ No newline at end of file diff --git a/framework/vendure/utils/mutations/reset-password-mutation.ts b/framework/vendure/utils/mutations/reset-password-mutation.ts new file mode 100644 index 000000000..8ff4058ed --- /dev/null +++ b/framework/vendure/utils/mutations/reset-password-mutation.ts @@ -0,0 +1,15 @@ +export const resetPasswordMutation = /* GraphQL */ ` +mutation resetPassword($token: String!,$password: String!){ + resetPassword(token: $token,password: $password){ + __typename + ...on CurrentUser{ + id + identifier + } + ...on ErrorResult{ + errorCode + message + } + } +} +` \ No newline at end of file diff --git a/framework/vendure/utils/mutations/toggle-wishlist-mutation.tsx b/framework/vendure/utils/mutations/toggle-wishlist-mutation.tsx new file mode 100644 index 000000000..d3dcb7c18 --- /dev/null +++ b/framework/vendure/utils/mutations/toggle-wishlist-mutation.tsx @@ -0,0 +1,9 @@ +export const toggleWishlistMutation = /* GraphQL */ ` + mutation toggleFavorite($productId:ID!){ + toggleFavorite(productId:$productId){ + items{ + id + } + } + } +` diff --git a/framework/vendure/utils/queries/active-customer-query.ts b/framework/vendure/utils/queries/active-customer-query.ts index b1598d8de..fb2579ff5 100644 --- a/framework/vendure/utils/queries/active-customer-query.ts +++ b/framework/vendure/utils/queries/active-customer-query.ts @@ -1,17 +1,35 @@ +import { searchResultFragment } from '../fragments/search-result-fragment' + export const activeCustomerQuery = /* GraphQL */ ` - query activeCustomer { - activeCustomer { - id - firstName - lastName - emailAddress - phoneNumber - addresses{ - streetLine1 - city - province - postalCode +query activeCustomer { + activeCustomer { + id + firstName + lastName + emailAddress + favorites{ + items{ + product{ + id + name + slug + assets{ + source + preview + } + variants{ + price + } + } } } + phoneNumber + addresses{ + streetLine1 + city + province + postalCode + } } +} ` diff --git a/framework/vendure/utils/queries/get-all-products-query.ts b/framework/vendure/utils/queries/get-all-products-query.ts index 1b44b2017..007c6594d 100644 --- a/framework/vendure/utils/queries/get-all-products-query.ts +++ b/framework/vendure/utils/queries/get-all-products-query.ts @@ -3,6 +3,7 @@ import { searchResultFragment } from '../fragments/search-result-fragment' export const getAllProductsQuery = /* GraphQL */ ` query getAllProducts($input: SearchInput!) { search(input: $input) { + totalItems items { ...SearchResult } diff --git a/framework/vendure/utils/queries/get-collections-query.ts b/framework/vendure/utils/queries/get-collections-query.ts index 79e00a292..f07a85249 100644 --- a/framework/vendure/utils/queries/get-collections-query.ts +++ b/framework/vendure/utils/queries/get-collections-query.ts @@ -24,7 +24,7 @@ export const getCollectionsNameQuery = /* GraphQL */ ` collections{ items{ name - link:slug + slug } } } diff --git a/framework/vendure/utils/queries/get-product-query.ts b/framework/vendure/utils/queries/get-product-query.ts index b2c502da9..6db960a96 100644 --- a/framework/vendure/utils/queries/get-product-query.ts +++ b/framework/vendure/utils/queries/get-product-query.ts @@ -36,6 +36,28 @@ export const getProductQuery = /* GraphQL */ ` name } } + facetValues { + id + } + collections { + id + } } } ` +export const getProductDetailQuery = /* GraphQL */ ` + query GetProductDetail($slug: String! = "hand-trowel") { + product(slug: $slug) { + name + description + variants { + price + priceWithTax + } + assets { + preview + name + } + } +} +` \ No newline at end of file diff --git a/package.json b/package.json index 84a77cf71..8474be667 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "body-scroll-lock": "^3.1.5", "classnames": "^2.3.1", "cookie": "^0.4.1", + "dns": "^0.2.2", "email-validator": "^2.0.4", "eslint": "^7.32.0", "eslint-config-next": "^11.1.2", @@ -35,6 +36,7 @@ "lodash.debounce": "^4.0.8", "lodash.random": "^3.2.0", "lodash.throttle": "^4.1.1", + "net": "^1.0.2", "next": "^11.0.0", "next-seo": "^4.26.0", "next-themes": "^0.0.14", diff --git a/pages/forgot-password.tsx b/pages/forgot-password.tsx new file mode 100644 index 000000000..8d4b1e570 --- /dev/null +++ b/pages/forgot-password.tsx @@ -0,0 +1,10 @@ +import { FormForgot, Layout } from 'src/components/common' + +export default function NotFound() { + return ( +
+ +
+ ) +} +NotFound.Layout = Layout diff --git a/pages/index.tsx b/pages/index.tsx index cac464a43..010094de4 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -62,27 +62,34 @@ export async function getStaticProps({ const freshFacetId = getFreshFacetId(facets) if (freshFacetId) { freshProductvariables.facetValueIds = [freshFacetId] + const freshProductsPromise = commerce.getAllProducts({ + variables: freshProductvariables, + config, + preview, + }) + promisesWithKey.push({ key: 'freshProducts', promise: freshProductsPromise, keyResult: 'products' }) + } else { + props.freshProducts = [] } - const freshProductsPromise = commerce.getAllProducts({ - variables: freshProductvariables, - config, - preview, - }) - promisesWithKey.push({ key: 'freshProducts', promise: freshProductsPromise, keyResult: 'products' }) // featured products const allFeaturedFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_FEATURED) const allDiscountFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_DISCOUNT) const facetValueIdsForFeaturedProducts = [...allFeaturedFacetIds, ...allDiscountFacetIds] - const featuredProductsPromise = commerce.getAllProducts({ - variables: { - facetValueIds: facetValueIdsForFeaturedProducts - }, - config, - preview, - }) - promisesWithKey.push({ key: 'featuredProducts', promise: featuredProductsPromise, keyResult: 'products' }) + + if (facetValueIdsForFeaturedProducts.length > 0) { + const featuredProductsPromise = commerce.getAllProducts({ + variables: { + facetValueIds: facetValueIdsForFeaturedProducts + }, + config, + preview, + }) + promisesWithKey.push({ key: 'featuredProducts', promise: featuredProductsPromise, keyResult: 'products' }) + } else { + props.featuredProducts = [] + } // collection const collectionsPromise = commerce.getAllCollections({ diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index a8e925df9..2da14a995 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -1,18 +1,114 @@ +import { Product } from '@framework/schema' +import commerce from '@lib/api/commerce' +import { GetStaticPathsContext, GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { Layout, RecipeDetail, RecommendedRecipes, RelevantBlogPosts } from 'src/components/common' import { ProductInfoDetail, ReleventProducts, ViewedProducts } from 'src/components/modules/product-detail' +import { MAX_PRODUCT_CAROUSEL, REVALIDATE_TIME } from 'src/utils/constanst.utils' import { BLOGS_DATA_TEST, INGREDIENT_DATA_TEST, RECIPE_DATA_TEST } from 'src/utils/demo-data' +import { getAllPromies } from 'src/utils/funtion.utils' +import { PromiseWithKey } from 'src/utils/types.utils' -export default function Slug() { +export default function Slug({ product, relevantProducts, collections }: InferGetStaticPropsType) { return <> - + - + - + } +export async function getStaticProps({ + params, + locale, + locales, + preview, +}: GetStaticPropsContext<{ slug: string }>) { + const config = { locale, locales } + let promisesWithKey = [] as PromiseWithKey[] + let props = {} as any + + const product = await commerce.getProduct({ + variables: { slug: params!.slug }, + config, + preview, + }) + props.product = product + + + if (!product) { + throw new Error(`Product with slug '${params!.slug}' not found`) + } + + // relevant product (filter by product detail's facetIds) + const relevantFacetIds = product.facetValueIds + if (relevantFacetIds && relevantFacetIds.length > 0) { + const relevantProductsPromise = commerce.getAllProducts({ + variables: { + first: MAX_PRODUCT_CAROUSEL, + facetValueIds: relevantFacetIds, + }, + config, + preview, + }) + promisesWithKey.push({ key: 'relevantProducts', promise: relevantProductsPromise, keyResult: 'products' }) + } else { + props.relevantProducts = [] + } + + + // collection + const collectionsPromise = commerce.getAllCollections({ + variables: {}, + config, + preview, + }) + promisesWithKey.push({ key: 'collections', promise: collectionsPromise, keyResult: 'collections' }) + + + try { + const promises = getAllPromies(promisesWithKey) + const rs = await Promise.all(promises) + + promisesWithKey.map((item, index) => { + props[item.key] = item.keyResult ? rs[index][item.keyResult] : rs[index] + return null + }) + + if (props.relevantProducts.length > 0) { + const relevantProducts = props.relevantProducts.filter((item: Product) => item.id !== product.id) + props.relevantProducts = relevantProducts + } + + return { + props, + revalidate: REVALIDATE_TIME, + } + } catch (err) { + console.log('err: ', err) + } +} + + +export async function getStaticPaths({ locales }: GetStaticPathsContext) { + const { products } = await commerce.getAllProductPaths() + + return { + paths: locales + ? locales.reduce((arr, locale) => { + // Add a product path for every locale + products.forEach((product: any) => { + arr.push(`/${locale}/product${product.path}`) + }) + return arr + }, []) + : products.map((product: any) => `/product${product.path}`), + fallback: 'blocking', + } +} + + Slug.Layout = Layout diff --git a/pages/products.tsx b/pages/products.tsx index 4f9c4eb66..4a37ab097 100644 --- a/pages/products.tsx +++ b/pages/products.tsx @@ -1,19 +1,97 @@ +import { ProductCard } from '@commerce/types/product'; +import { Collection, Facet } from '@framework/schema'; +import commerce from '@lib/api/commerce'; +import { GetStaticPropsContext } from 'next'; import { Layout } from 'src/components/common'; import { ViewedProducts } from 'src/components/modules/product-detail'; import ProductListFilter from 'src/components/modules/product-list/ProductListFilter/ProductListFilter'; -import RecipeListBanner from 'src/components/modules/recipes-list/RecipeListBanner/RecipeListBanner'; -import RecipesList from 'src/components/modules/recipes-list/RecipesList/RecipesList'; +import { CODE_FACET_BRAND, CODE_FACET_FEATURED, DEFAULT_PAGE_SIZE, REVALIDATE_TIME } from 'src/utils/constanst.utils'; +import { getAllPromies } from 'src/utils/funtion.utils'; +import { PromiseWithKey, SortOrder } from 'src/utils/types.utils'; import ProductListBanner from '../src/components/modules/product-list/ProductListBanner/ProductListBanner'; +interface Props { + facets: Facet[], + collections: Collection[], + productsResult: { products: ProductCard[], totalItems: number }, -export default function Products() { +} + +export default function Products({ facets, collections, productsResult }: Props) { return ( <> - - + + ) } +export async function getStaticProps({ + preview, + locale, + locales, +}: GetStaticPropsContext) { + const config = { locale, locales } + let promisesWithKey = [] as PromiseWithKey[] + let props = {} as any + + const facetsPromise = commerce.getAllFacets({ + variables: { + sort: { + code: SortOrder.Asc + }, + filter: { + code: { + in: [CODE_FACET_FEATURED, CODE_FACET_BRAND] + } + } + }, + config, + preview, + }) + + promisesWithKey.push({ key: 'facets', promise: facetsPromise, keyResult: 'facets' }) + + // collection + const collectionsPromise = commerce.getAllCollections({ + variables: {}, + config, + preview, + }) + promisesWithKey.push({ key: 'collections', promise: collectionsPromise, keyResult: 'collections' }) + + // products + const productsPromise = commerce.getAllProducts({ + variables: { + first: DEFAULT_PAGE_SIZE, + }, + config, + preview, + }) + promisesWithKey.push({ key: 'productsResult', promise: productsPromise }) + + + try { + const promises = getAllPromies(promisesWithKey) + const rs = await Promise.all(promises) + + promisesWithKey.map((item, index) => { + props[item.key] = item.keyResult ? rs[index][item.keyResult] : rs[index] + return null + }) + + return { + props, + revalidate: REVALIDATE_TIME, + } + } catch (err) { + + } +} + Products.Layout = Layout diff --git a/pages/reset-password.tsx b/pages/reset-password.tsx new file mode 100644 index 000000000..bc8905da3 --- /dev/null +++ b/pages/reset-password.tsx @@ -0,0 +1,10 @@ +import { FormResetPassword, Layout } from 'src/components/common' + +export default function NotFound() { + return ( +
+ +
+ ) +} +NotFound.Layout = Layout diff --git a/pages/test.tsx b/pages/test.tsx index 6244c3dd6..9a4db4421 100644 --- a/pages/test.tsx +++ b/pages/test.tsx @@ -1,17 +1,19 @@ import commerce from '@lib/api/commerce'; import { GetStaticPropsContext } from 'next'; -import { Layout } from 'src/components/common'; +import { ProductCard } from '@commerce/types/product'; +import { Layout, ListProductCardSkeleton } from 'src/components/common'; interface Props { - products: any + productDetail: ProductCard[], } -export default function Home({ products }: Props) { +export default function Home({ productDetail }: Props) { return ( <> -

- TOTAL: {products?.length} -

- {JSON.stringify(products[0])} + {/* */} + {/* */} + + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ab qui magnam debitis ex laborum laboriosam suscipit! Totam excepturi eum libero. + ) } @@ -22,28 +24,9 @@ export async function getServerSideProps({ locale, locales, }: GetStaticPropsContext) { - const config = { locale, locales } - const productsPromise = commerce.getAllProducts({ - // const productsPromise = commerce.getAllFacets({ - variables: { - first: 70, - // filter: { - // name: { - // contains: 'ca' - // } - // } - }, - config, - preview, - // Saleor provider only - ...({ featured: true } as any), - }) - - const { products } = await productsPromise - return { - props: { products }, + props: {}, } } diff --git a/src/components/common/EmptyCommon/EmptyCommon.module.scss b/src/components/common/EmptyCommon/EmptyCommon.module.scss index a31ba4374..4014faeea 100644 --- a/src/components/common/EmptyCommon/EmptyCommon.module.scss +++ b/src/components/common/EmptyCommon/EmptyCommon.module.scss @@ -4,13 +4,16 @@ padding: 1.6rem; margin: auto; .imgWrap { - min-width: 10rem; + min-height: 10rem; text-align: center; + img { + min-height: 10rem; + } } .description { color: var(--disabled); text-align: center; - margin-top: .8rem; + margin-top: 0.8rem; } } diff --git a/src/components/common/ForgotPassword/FormForgot/FormForgot.module.scss b/src/components/common/ForgotPassword/FormForgot/FormForgot.module.scss new file mode 100644 index 000000000..57b39c56c --- /dev/null +++ b/src/components/common/ForgotPassword/FormForgot/FormForgot.module.scss @@ -0,0 +1,22 @@ +@import '../../../../styles/utilities'; +.formAuthen{ + width: 50%; + margin: 0 auto; + padding: 4rem 0 ; + .title{ + @apply font-heading heading-3; + padding: 0 1.6rem 0 0.8rem; + margin-bottom: 2rem; + } + .bottom { + @apply flex justify-between items-center; + margin: 4rem auto; + .remembered { + @apply font-bold cursor-pointer; + color: var(--primary); + } + } + .socialAuthen{ + margin-bottom: 3rem; + } +} diff --git a/src/components/common/ForgotPassword/FormForgot/FormForgot.tsx b/src/components/common/ForgotPassword/FormForgot/FormForgot.tsx new file mode 100644 index 000000000..834c65919 --- /dev/null +++ b/src/components/common/ForgotPassword/FormForgot/FormForgot.tsx @@ -0,0 +1,89 @@ +import { Form, Formik } from 'formik'; +import React, { useRef } from 'react'; +import { ButtonCommon, InputFiledInForm } from 'src/components/common'; +import { useModalCommon } from 'src/components/hooks'; +import useRequestPasswordReset from 'src/components/hooks/auth/useRequestPasswordReset'; +import { CustomInputCommon } from 'src/utils/type.utils'; +import * as Yup from 'yup'; +import ModalAuthenticate from '../../ModalAuthenticate/ModalAuthenticate'; +import { default as s, default as styles } from './FormForgot.module.scss'; +import { useMessage } from 'src/components/contexts' +import { LANGUAGE } from 'src/utils/language.utils' + +interface Props { + +} +const DisplayingErrorMessagesSchema = Yup.object().shape({ + email: Yup.string().email('Your email was wrong').required('Required') +}) + +const FormForgot = ({ }: Props) => { + const {requestPassword} = useRequestPasswordReset(); + const { showMessageSuccess, showMessageError } = useMessage(); + + const emailRef = useRef(null); + + const { visible: visibleModalAuthen,closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false }); + + const onForgot = (values: { email: string }) => { + requestPassword({email: values.email},onForgotPasswordCallBack); + } + + const onForgotPasswordCallBack = (isSuccess: boolean, message?: string) => { + if (isSuccess) { + showMessageSuccess("Request forgot password successfully. Please verify your email to login.") + } else { + showMessageError(message || LANGUAGE.MESSAGE.ERROR) + } + } + + return ( +
+
+
+
Forgot Password
+ + {({ errors, touched, isValid, submitForm }) => ( +
+
+ +
+
+
+ I Remembered My Password? +
+ + Reset Password + +
+
+ )} +
+
+ +
+
+ ) + + +} + + +export default FormForgot; \ No newline at end of file diff --git a/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.module.scss b/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.module.scss new file mode 100644 index 000000000..faf1b7f06 --- /dev/null +++ b/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.module.scss @@ -0,0 +1,27 @@ +@import '../../../../styles/utilities'; +.formAuthen{ + width: 50%; + margin: 0 auto; + padding: 4rem 0 ; + .title{ + @apply font-heading heading-3; + padding: 0 1.6rem 0 0.8rem; + margin-bottom: 2rem; + } + .passwordNote { + @apply text-center caption text-label; + margin-top: 0.8rem; + } + .bottom { + @apply flex justify-center items-center; + margin: 4rem auto; + .remembered { + @apply font-bold cursor-pointer; + color: var(--primary); + } + } + .confirmPassword{ + margin-top: 2rem; + } + +} diff --git a/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.tsx b/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.tsx new file mode 100644 index 000000000..ad41396ab --- /dev/null +++ b/src/components/common/ForgotPassword/FormResetPassword/FormResetPassword.tsx @@ -0,0 +1,108 @@ +import { Form, Formik } from 'formik'; +import React, { useRef } from 'react'; +import { ButtonCommon, InputPasswordFiledInForm } from 'src/components/common'; +import { useMessage } from 'src/components/contexts'; +import useRequestPasswordReset from 'src/components/hooks/auth/useRequestPasswordReset'; +import { LANGUAGE } from 'src/utils/language.utils'; +import { CustomInputCommon } from 'src/utils/type.utils'; +import * as Yup from 'yup'; +import { useRouter } from 'next/router' +import { default as s, default as styles } from './FormResetPassword.module.scss'; +import { useResetPassword } from 'src/components/hooks/auth'; + +interface Props { + +} +const DisplayingErrorMessagesSchema = Yup.object().shape({ + password: Yup.string() + .matches( + /^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])((?=.*[0-9!@#$%^&*()\-_=+{};:,<.>]){1}).*$/, + 'Must contain 8 characters with at least 1 uppercase and 1 lowercase letter and either 1 number or 1 special character.' + ) + .max(30, 'Password is too long') + .required('Required'), + confirmPassword: Yup.string() + .label('Password Confirm') + .required() + .oneOf([Yup.ref('password')], 'Passwords does not match'), +}) + +const FormResetPassword = ({ }: Props) => { + const router = useRouter(); + + const {resetPassword} = useResetPassword(); + + const { showMessageSuccess, showMessageError } = useMessage(); + + const onReset = (values: {password: string }) => { + const { token } = router.query; + resetPassword({token:token,password: values.password},onResetPasswordCallBack); + } + + const onResetPasswordCallBack = (isSuccess: boolean, message?: string) => { + if (isSuccess) { + showMessageSuccess("Reset password successfully. Please to login.") + } else { + showMessageError(message || LANGUAGE.MESSAGE.ERROR) + } + } + + return ( +
+
+
+
Reset Password
+ + {({ errors, touched, isValid, submitForm }) => ( +
+
+ +
+
+ +
+ +
+ Must contain 8 characters with at least 1 uppercase and 1 + lowercase letter and either 1 number or 1 special character. +
+
+ + Change Password + +
+
+ )} +
+
+
+
+ ) +} + + +export default FormResetPassword; \ No newline at end of file diff --git a/src/components/common/Header/Header.tsx b/src/components/common/Header/Header.tsx index f3a514970..092bcc8e7 100644 --- a/src/components/common/Header/Header.tsx +++ b/src/components/common/Header/Header.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames' import React, { memo, useEffect, useRef, useState } from 'react' +import { useProductFilter } from 'src/components/contexts' import { useModalCommon } from 'src/components/hooks' import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate' import ModalCreateUserInfo from '../ModalCreateUserInfo/ModalCreateUserInfo' @@ -9,12 +10,12 @@ import HeaderSubMenu from './components/HeaderSubMenu/HeaderSubMenu' import HeaderSubMenuMobile from './components/HeaderSubMenuMobile/HeaderSubMenuMobile' import s from './Header.module.scss' interface props { - toggleFilter: () => void, - visibleFilter: boolean + } -const Header = memo(({ toggleFilter, visibleFilter }: props) => { +const Header = memo(({ }: props) => { const headeFullRef = useRef(null) + const { toggleProductFilter: toggleFilter } = useProductFilter() const [isFullHeader, setIsFullHeader] = useState(true) const [isModeAuthenRegister, setIsModeAuthenRegister] = useState(false) const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false }) @@ -63,7 +64,6 @@ const Header = memo(({ toggleFilter, visibleFilter }: props) => {
void openModalRegister: () => void openModalInfo: () => void @@ -38,7 +37,6 @@ const HeaderMenu = memo( ({ isFull, isStickyHeader, - visibleFilter, openModalLogin, openModalRegister, openModalInfo, @@ -60,6 +58,10 @@ const HeaderMenu = memo( onClick: openModalRegister, name: 'Create account', }, + { + link: '/forgot-password', + name: 'Forgot Password', + }, ], [openModalLogin, openModalRegister] ) @@ -90,6 +92,7 @@ const HeaderMenu = memo( ], [logout] ) + return (
-
+
)} +
-
-
-
- -
+
+
- + { + (!initialQueryFlag && loading && !productSearchResult) && + } +
diff --git a/src/components/modules/product-list/ProductListFilter/ProductSort/ProductSort.tsx b/src/components/modules/product-list/ProductListFilter/ProductSort/ProductSort.tsx new file mode 100644 index 000000000..a10cec443 --- /dev/null +++ b/src/components/modules/product-list/ProductListFilter/ProductSort/ProductSort.tsx @@ -0,0 +1,40 @@ +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import { SelectCommon } from 'src/components/common'; +import { OPTIONS_SORT_PRODUCT, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'; + +const ProductSort = () => { + const router = useRouter() + const [sortValue, setSortValue] = useState(); + + useEffect(() => { + const rs = router.query[QUERY_KEY.SORTBY] as string + if (rs) { + setSortValue(rs) + } + }, [router.query]) + + const onSortChange = (value: string) => { + setSortValue(value) + router.push({ + pathname: ROUTE.PRODUCTS, + query: { + ...router.query, + [QUERY_KEY.SORTBY]: value + } + }, + undefined, { shallow: true } + ) + } + + return ( + + ); +}; + +export default ProductSort; \ No newline at end of file diff --git a/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.module.scss b/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.module.scss new file mode 100644 index 000000000..a27b395c3 --- /dev/null +++ b/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.module.scss @@ -0,0 +1,12 @@ + +.productsMenuNavigationTablet { + @apply hidden; + @screen md { + @apply block; + padding-right: 2.4rem; + } + @screen xl { + @apply block; + width: 25%; + } +} diff --git a/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.tsx b/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.tsx new file mode 100644 index 000000000..d31609639 --- /dev/null +++ b/src/components/modules/product-list/ProductListFilter/ProductsMenuNavigationTablet/ProductsMenuNavigationTablet.tsx @@ -0,0 +1,32 @@ +import { Collection, Facet } from '@framework/schema' +import React from 'react' +import MenuNavigation from 'src/components/common/MenuNavigation/MenuNavigation' +import { QUERY_KEY } from 'src/utils/constanst.utils' +import s from './ProductsMenuNavigationTablet.module.scss' + +interface Props { + facets: Facet[] + collections: Collection[] + +} + +const ProductsMenuNavigationTablet = ({ facets, collections }: Props) => { + return ( +
+ + { + facets.map(item => ) + } +
+ ) +} + +export default ProductsMenuNavigationTablet diff --git a/src/components/modules/recipes-list/RecipesList/RecipesList.tsx b/src/components/modules/recipes-list/RecipesList/RecipesList.tsx index f636157fa..e91599a2c 100644 --- a/src/components/modules/recipes-list/RecipesList/RecipesList.tsx +++ b/src/components/modules/recipes-list/RecipesList/RecipesList.tsx @@ -189,13 +189,13 @@ const RecipesList = ({ data =recipe}:Props) => {
- +
- +
diff --git a/src/styles/_base.scss b/src/styles/_base.scss index 1eab49e38..e41b6763f 100644 --- a/src/styles/_base.scss +++ b/src/styles/_base.scss @@ -4,7 +4,7 @@ :root { --primary: #5b9a74; --primary-light: #e3f2e9; - --primary-lightest: #effaf4; + --primary-lightest: #F1F8F4; --info-dark: #00317a; --info: #3468B7; diff --git a/src/utils/constanst.utils.ts b/src/utils/constanst.utils.ts index 1db198178..7d0cf9ba5 100644 --- a/src/utils/constanst.utils.ts +++ b/src/utils/constanst.utils.ts @@ -1,5 +1,7 @@ import DefaultImg from '../../public/assets/images/default_img.jpg' +export const REVALIDATE_TIME = 60 +export const MAX_PRODUCT_CAROUSEL = 20 export const BLUR_DATA_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mN8fBIAApUBruKYvzsAAAAASUVORK5CYII=' export const DEFAULT_IMG = DefaultImg @@ -45,13 +47,23 @@ export const LOCAL_STORAGE_KEY = { TOKEN: 'token' } +export const QUERY_SPLIT_SEPERATOR = ',' export const QUERY_KEY = { TAB: 'tab', CATEGORY: 'category', BRAND: 'brand', - FEATURED: 'feature', + FEATURED: 'featured', SORTBY: 'sortby', - RECIPES: 'recipes' + RECIPES: 'recipes', + PAGE: 'page', +} + +export const PRODUCT_SORT_OPTION_VALUE = { + NAME_ASC: 'name_asc', + NAME_DESC: 'name_desc', + PRICE_ASC: 'price_asc', + PRICE_DESC: 'price_desc', + } export enum ProductFeature { @@ -113,10 +125,31 @@ export const BRAND = [ export const CODE_FACET_FEATURED = 'featured' export const CODE_FACET_DISCOUNT = 'discount' +export const CODE_FACET_BRAND = 'brand' export const CODE_FACET_FEATURED_VARIANT = { FRESH: 'fresh', } +export const OPTIONS_SORT_PRODUCT = [ + { + name: 'By Name (A-Z)', + value: PRODUCT_SORT_OPTION_VALUE.NAME_ASC, + }, + { + name: 'By Name (Z-A)', + value: PRODUCT_SORT_OPTION_VALUE.NAME_DESC, + }, + { + name: 'Price (Low to High)', + value: PRODUCT_SORT_OPTION_VALUE.PRICE_ASC, + }, + { + name: 'Price (High to Low)', + value: PRODUCT_SORT_OPTION_VALUE.PRICE_DESC, + }, +]; + + export const FEATURED = [ { name: 'Best Sellers', diff --git a/src/utils/funtion.utils.ts b/src/utils/funtion.utils.ts index 853b77108..408d9d0fd 100644 --- a/src/utils/funtion.utils.ts +++ b/src/utils/funtion.utils.ts @@ -1,12 +1,60 @@ import { Facet } from "@commerce/types/facet"; -import { FacetValue } from './../../framework/vendure/schema.d'; -import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT } from "./constanst.utils"; -import { PromiseWithKey } from "./types.utils"; +import { Collection, FacetValue, SearchResultSortParameter } from './../../framework/vendure/schema.d'; +import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT, PRODUCT_SORT_OPTION_VALUE } from "./constanst.utils"; +import { PromiseWithKey, SortOrder } from "./types.utils"; export function isMobile() { return window.innerWidth < 768 } +export function getPageFromQuery(pageQuery: string) { + let page = 0 + try { + page = +pageQuery + if (isNaN(page)) { + page = 0 + } + } catch (err) { + page = 0 + } + return page +} + + +export function getProductSortParamFromQuery(query: string) { + let rs = {} as SearchResultSortParameter + switch (query) { + case PRODUCT_SORT_OPTION_VALUE.NAME_ASC: + rs = { + name: SortOrder.Asc + } + break; + + case PRODUCT_SORT_OPTION_VALUE.NAME_DESC: + rs = { + name: SortOrder.Desc + } + break; + + case PRODUCT_SORT_OPTION_VALUE.PRICE_ASC: + rs = { + price: SortOrder.Asc + } + break; + + case PRODUCT_SORT_OPTION_VALUE.PRICE_DESC: + rs = { + price: SortOrder.Desc + } + break; + + default: + break; + } + + return rs +} + export function removeItem(arr: Array, value: T): Array { const index = arr.indexOf(value); if (index > -1) { @@ -58,6 +106,25 @@ export function getFacetNamesFromIds(facets: FacetValue[], ids?: string[]): stri return names.join(", ") } -export function getAllPromies (promies: PromiseWithKey[]) { - return promies.map(item => item.promise) +export function getFacetIdsFromCodes(facets: FacetValue[], codes?: string[]): string[] { + if (!codes || codes?.length === 0) { + return [] + } + + const facetItems = facets.filter((item: FacetValue) => codes.includes(item.code)) + const ids = facetItems.map((item: FacetValue) => item.id) + return ids } + +export const getCategoryNameFromCollectionId = (colelctions: Collection[], collectionId?: string ) => { + if (!collectionId) { + return '' + } + + const collection = colelctions.find(item => item.id === collectionId) + return collection?.name || '' +} + +export function getAllPromies(promies: PromiseWithKey[]) { + return promies.map(item => item.promise) +} \ No newline at end of file diff --git a/src/utils/types.utils.ts b/src/utils/types.utils.ts index ca21b605f..59243b80f 100644 --- a/src/utils/types.utils.ts +++ b/src/utils/types.utils.ts @@ -34,18 +34,23 @@ export interface BlogProps { export interface CheckOutForm { name?: string - email?:string + email?: string address?: string - city?:string - state?:string - code?:number - phone?:number - method?:string - shipping_fee?:number + city?: string + state?: string + code?: number + phone?: number + method?: string + shipping_fee?: number } export type MouseAndTouchEvent = MouseEvent | TouchEvent +export enum SortOrder { + Asc = 'ASC', + Desc = 'DESC', +} + export type filterContextType = { visible: boolean; open: () => void;