mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 12:24:18 +00:00
fix conflict
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
"features": {
|
||||
"cart": true,
|
||||
"search": true,
|
||||
"wishlist": false,
|
||||
"customerAuth": false,
|
||||
"customCheckout": false
|
||||
"wishlist": true,
|
||||
"customerAuth": true,
|
||||
"customCheckout": true
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { GetAllFacetsOperation } from './../types/facet';
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginOperation } from '../types/login'
|
||||
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
|
||||
@@ -9,6 +10,7 @@ import type {
|
||||
GetProductOperation,
|
||||
} from '../types/product'
|
||||
import type { APIProvider, CommerceAPI } from '.'
|
||||
import { GetAllCollectionsOperation } from '@commerce/types/collection';
|
||||
|
||||
const noop = () => {
|
||||
throw new Error('Not implemented')
|
||||
@@ -23,6 +25,9 @@ export const OPERATIONS = [
|
||||
'getAllProductPaths',
|
||||
'getAllProducts',
|
||||
'getProduct',
|
||||
'getAllFacets',
|
||||
'getAllCollections',
|
||||
|
||||
] as const
|
||||
|
||||
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
|
||||
@@ -154,8 +159,43 @@ export type Operations<P extends APIProvider> = {
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
}
|
||||
|
||||
getAllFacets: {
|
||||
<T extends GetAllFacetsOperation>(opts: {
|
||||
variables?: T['variables']
|
||||
config?: P['config']
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
<T extends GetAllFacetsOperation>(
|
||||
opts: {
|
||||
variables?: T['variables']
|
||||
config?: P['config']
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
}
|
||||
|
||||
getAllCollections: {
|
||||
<T extends GetAllCollectionsOperation>(opts: {
|
||||
variables?: T['variables']
|
||||
config?: P['config']
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
<T extends GetAllCollectionsOperation>(
|
||||
opts: {
|
||||
variables?: T['variables']
|
||||
config?: P['config']
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type APIOperations<P extends APIProvider> = {
|
||||
[K in keyof Operations<P>]?: (ctx: OperationContext<P>) => Operations<P>[K]
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ Adding a commerce provider means adding a new folder in `framework` with a folde
|
||||
- useSearch
|
||||
- getProduct
|
||||
- getAllProducts
|
||||
- getAllFacets
|
||||
- getAllCollections
|
||||
- `wishlist`
|
||||
- useWishlist
|
||||
- useAddItem
|
||||
|
54
framework/commerce/types/collection.ts
Normal file
54
framework/commerce/types/collection.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Asset } from '../../vendure/schema.d';
|
||||
|
||||
export type Collection = {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
description: string
|
||||
featuredAsse: Asset
|
||||
asset: Asset[]
|
||||
}
|
||||
|
||||
export type SearchCollectionsBody = {
|
||||
search?: string
|
||||
sort?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type CollectionTypes = {
|
||||
collection: Collection
|
||||
searchBody: SearchCollectionsBody
|
||||
}
|
||||
|
||||
export type SearchCollectionsHook<T extends CollectionTypes = CollectionTypes> = {
|
||||
data: {
|
||||
collections: T['collection'][]
|
||||
found: boolean
|
||||
}
|
||||
body: T['searchBody']
|
||||
input: T['searchBody']
|
||||
fetcherInput: T['searchBody']
|
||||
}
|
||||
|
||||
export type CollectionsSchema<T extends CollectionTypes = CollectionTypes> = {
|
||||
endpoint: {
|
||||
options: {}
|
||||
handlers: {
|
||||
getCollections: SearchCollectionsHook<T>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type GetAllCollectionsOperation<T extends CollectionTypes = CollectionTypes> = {
|
||||
data: { collections: T['collection'][] }
|
||||
variables: {
|
||||
ids?: string[]
|
||||
first?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type GetCollectionOperation<T extends CollectionTypes = CollectionTypes> = {
|
||||
data: { collection?: T['collection'] }
|
||||
variables: { code: string; } | { code?: never; }
|
||||
}
|
52
framework/commerce/types/facet.ts
Normal file
52
framework/commerce/types/facet.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FacetValue } from './../../vendure/schema.d';
|
||||
|
||||
export type Facet = {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
values: FacetValue[]
|
||||
}
|
||||
|
||||
export type SearchFacetsBody = {
|
||||
search?: string
|
||||
sort?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type FacetTypes = {
|
||||
facet: Facet
|
||||
searchBody: SearchFacetsBody
|
||||
}
|
||||
|
||||
export type SearchFacetsHook<T extends FacetTypes = FacetTypes> = {
|
||||
data: {
|
||||
facets: T['facet'][]
|
||||
found: boolean
|
||||
}
|
||||
body: T['searchBody']
|
||||
input: T['searchBody']
|
||||
fetcherInput: T['searchBody']
|
||||
}
|
||||
|
||||
export type FacetsSchema<T extends FacetTypes = FacetTypes> = {
|
||||
endpoint: {
|
||||
options: {}
|
||||
handlers: {
|
||||
getFacets: SearchFacetsHook<T>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type GetAllFacetsOperation<T extends FacetTypes = FacetTypes> = {
|
||||
data: { facets: T['facet'][] }
|
||||
variables: {
|
||||
ids?: string[]
|
||||
first?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type GetFacetOperation<T extends FacetTypes = FacetTypes> = {
|
||||
data: { facet?: T['facet'] }
|
||||
variables: { code: string; } | { code?: never; }
|
||||
}
|
@@ -1,3 +1,6 @@
|
||||
import { CurrencyCode } from './../../vendure/schema.d';
|
||||
import { FacetValueFilterInput, LogicalOperator, SearchResultSortParameter } from "@framework/schema"
|
||||
|
||||
export type ProductImage = {
|
||||
url: string
|
||||
alt?: string
|
||||
@@ -40,9 +43,28 @@ export type Product = {
|
||||
slug?: string
|
||||
path?: string
|
||||
images: ProductImage[]
|
||||
variants: ProductVariant[]
|
||||
price: ProductPrice
|
||||
price: number
|
||||
currencyCode: CurrencyCode
|
||||
options: ProductOption[]
|
||||
facetValueIds?: string[]
|
||||
collectionIds?: string[]
|
||||
collection?: string,
|
||||
}
|
||||
|
||||
export type ProductCard = {
|
||||
id: string
|
||||
name: string
|
||||
slug?: string
|
||||
imageSrc: string
|
||||
price: number
|
||||
currencyCode: CurrencyCode
|
||||
oldPrice?: number
|
||||
discount?: number
|
||||
weight?: number
|
||||
facetValueIds?: string[],
|
||||
collectionIds?: string[],
|
||||
collection?: string,
|
||||
isNotSell?: boolean
|
||||
}
|
||||
|
||||
export type SearchProductsBody = {
|
||||
@@ -79,17 +101,24 @@ export type ProductsSchema<T extends ProductTypes = ProductTypes> = {
|
||||
|
||||
export type GetAllProductPathsOperation<
|
||||
T extends ProductTypes = ProductTypes
|
||||
> = {
|
||||
data: { products: Pick<T['product'], 'path'>[] }
|
||||
variables: { first?: number }
|
||||
}
|
||||
> = {
|
||||
data: { products: Pick<T['product'], 'path'>[] }
|
||||
variables: { first?: number }
|
||||
}
|
||||
|
||||
export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
|
||||
data: { products: T['product'][] }
|
||||
variables: {
|
||||
relevance?: 'featured' | 'best_selling' | 'newest'
|
||||
ids?: string[]
|
||||
first?: number
|
||||
term?: String
|
||||
facetValueIds?: string[]
|
||||
facetValueOperator?: LogicalOperator
|
||||
facetValueFilters?: FacetValueFilterInput[]
|
||||
collectionId?: string
|
||||
collectionSlug?: string
|
||||
groupByProduct?: Boolean
|
||||
take?: number
|
||||
skip?: number
|
||||
sort?: SearchResultSortParameter
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import type { APIProvider, CommerceAPIConfig } from '@commerce/api'
|
||||
import type { CommerceAPIConfig } from '@commerce/api'
|
||||
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
|
||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||
|
||||
import login from './operations/login'
|
||||
import getAllFacets from './operations/get-all-facets'
|
||||
import getAllCollections from './operations/get-all-collection'
|
||||
import getAllPages from './operations/get-all-pages'
|
||||
import getPage from './operations/get-page'
|
||||
import getSiteInfo from './operations/get-site-info'
|
||||
import getCustomerWishlist from './operations/get-customer-wishlist'
|
||||
import getAllProductPaths from './operations/get-all-product-paths'
|
||||
import getAllProducts from './operations/get-all-products'
|
||||
import getCustomerWishlist from './operations/get-customer-wishlist'
|
||||
import getPage from './operations/get-page'
|
||||
import getProduct from './operations/get-product'
|
||||
import getSiteInfo from './operations/get-site-info'
|
||||
import login from './operations/login'
|
||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||
|
||||
|
||||
export interface VendureConfig extends CommerceAPIConfig {}
|
||||
|
||||
@@ -40,6 +42,8 @@ const operations = {
|
||||
getAllProductPaths,
|
||||
getAllProducts,
|
||||
getProduct,
|
||||
getAllFacets,
|
||||
getAllCollections,
|
||||
}
|
||||
|
||||
export const provider = { config, operations }
|
||||
|
45
framework/vendure/api/operations/get-all-collection.ts
Normal file
45
framework/vendure/api/operations/get-all-collection.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { OperationContext } from '@commerce/api/operations'
|
||||
import { Collection } from '@commerce/types/collection'
|
||||
import { Provider, VendureConfig } from '..'
|
||||
import { GetAllCollectionsQuery } from '../../schema'
|
||||
import { getAllCollectionsQuery } from '../../utils/queries/get-all-collections-query'
|
||||
|
||||
export type CollectionVariables = { first?: number }
|
||||
|
||||
export default function getAllCollectionsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllCollections(opts?: {
|
||||
variables?: CollectionVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<{ collections: Collection[] }>
|
||||
|
||||
async function getAllCollections({
|
||||
query = getAllCollectionsQuery,
|
||||
variables: { ...vars } = {},
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: CollectionVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<{ collections: Collection[] | any[] }> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
const variables = {
|
||||
input: {
|
||||
take: vars.first,
|
||||
groupByCollection: true,
|
||||
},
|
||||
}
|
||||
const { data } = await config.fetch<GetAllCollectionsQuery>(query, {
|
||||
variables,
|
||||
})
|
||||
|
||||
return {
|
||||
collections: data.collections.items,
|
||||
}
|
||||
}
|
||||
|
||||
return getAllCollections
|
||||
}
|
46
framework/vendure/api/operations/get-all-facets.ts
Normal file
46
framework/vendure/api/operations/get-all-facets.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { OperationContext } from '@commerce/api/operations'
|
||||
import { Facet } from '@commerce/types/facet'
|
||||
import { Provider, VendureConfig } from '../'
|
||||
import { FacetFilterParameter, FacetSortParameter, GetAllFacetsQuery } from '../../schema'
|
||||
import { getAllFacetsQuery } from '../../utils/queries/get-all-facets-query'
|
||||
|
||||
export type FacetVariables = { first?: number, filter?: FacetFilterParameter, sort?: FacetSortParameter }
|
||||
|
||||
export default function getAllFacetsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllFacets(opts?: {
|
||||
variables?: FacetVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<{ facets: Facet[] }>
|
||||
|
||||
async function getAllFacets({
|
||||
query = getAllFacetsQuery,
|
||||
variables: { ...vars } = {},
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: FacetVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<{ facets: Facet[] | any[] }> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
const variables = {
|
||||
options: {
|
||||
take: vars.first,
|
||||
filter: vars.filter,
|
||||
sort: vars.sort,
|
||||
},
|
||||
}
|
||||
const { data } = await config.fetch<GetAllFacetsQuery>(query, {
|
||||
variables,
|
||||
})
|
||||
|
||||
return {
|
||||
facets: data.facets.items,
|
||||
}
|
||||
}
|
||||
|
||||
return getAllFacets
|
||||
}
|
@@ -5,7 +5,7 @@ import { normalizeSearchResult } from '../../utils/normalize'
|
||||
import { getAllProductsQuery } from '../../utils/queries/get-all-products-query'
|
||||
import { OperationContext } from '@commerce/api/operations'
|
||||
|
||||
export type ProductVariables = { first?: number }
|
||||
export type ProductVariables = { first?: number, facetValueIds?: string[] }
|
||||
|
||||
export default function getAllProductsOperation({
|
||||
commerce,
|
||||
@@ -14,7 +14,7 @@ export default function getAllProductsOperation({
|
||||
variables?: ProductVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<{ products: Product[] }>
|
||||
}): Promise<{ products: Product[], totalItems: number }>
|
||||
|
||||
async function getAllProducts({
|
||||
query = getAllProductsQuery,
|
||||
@@ -25,11 +25,12 @@ export default function getAllProductsOperation({
|
||||
variables?: ProductVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
} = {}): Promise<{ products: Product[] | any[], totalItems: number }> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
const variables = {
|
||||
input: {
|
||||
take: vars.first,
|
||||
facetValueIds: vars.facetValueIds,
|
||||
groupByProduct: true,
|
||||
},
|
||||
}
|
||||
@@ -39,6 +40,7 @@ export default function getAllProductsOperation({
|
||||
|
||||
return {
|
||||
products: data.search.items.map((item) => normalizeSearchResult(item)),
|
||||
totalItems: data.search.totalItems as number,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<Product | {} | any> {
|
||||
}): Promise<Product | null> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
|
||||
const locale = config.locale
|
||||
const { data } = await config.fetch<GetProductQuery>(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
|
||||
|
85
framework/vendure/schema.d.ts
vendored
85
framework/vendure/schema.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import { ResetPassword } from './schema.d';
|
||||
import { requestPasswordReset } from '@framework/schema';
|
||||
import { FacetValue } from './schema.d';
|
||||
export type Maybe<T> = T | null
|
||||
export type Exact<T extends { [key: string]: unknown }> = {
|
||||
[K in keyof T]: T[K]
|
||||
@@ -95,6 +96,10 @@ export type QueryProductsArgs = {
|
||||
options?: Maybe<ProductListOptions>
|
||||
}
|
||||
|
||||
export type QueryFacetsArgs = {
|
||||
options?: Maybe<FacetListOptions>
|
||||
}
|
||||
|
||||
export type QuerySearchArgs = {
|
||||
input: SearchInput
|
||||
}
|
||||
@@ -2729,6 +2734,13 @@ export type ProductListOptions = {
|
||||
filter?: Maybe<ProductFilterParameter>
|
||||
}
|
||||
|
||||
export type FacetListOptions = {
|
||||
skip?: Maybe<Scalars['Int']>
|
||||
take?: Maybe<Scalars['Int']>
|
||||
sort?: Maybe<FacetSortParameter>
|
||||
filter?: Maybe<FacetFilterParameter>
|
||||
}
|
||||
|
||||
export type UpdateOrderItemsResult =
|
||||
| Order
|
||||
| OrderModificationError
|
||||
@@ -2886,6 +2898,23 @@ export type ProductVariantSortParameter = {
|
||||
discountPrice?: Maybe<SortOrder>
|
||||
}
|
||||
|
||||
|
||||
export type FacetFilterParameter = {
|
||||
createdAt?: Maybe<DateOperators>
|
||||
updatedAt?: Maybe<DateOperators>
|
||||
languageCode?: Maybe<StringOperators>
|
||||
name?: Maybe<StringOperators>
|
||||
code?: Maybe<StringOperators>
|
||||
}
|
||||
|
||||
export type FacetSortParameter = {
|
||||
id?: Maybe<SortOrder>
|
||||
createdAt?: Maybe<SortOrder>
|
||||
updatedAt?: Maybe<SortOrder>
|
||||
name?: Maybe<SortOrder>
|
||||
code?: Maybe<SortOrder>
|
||||
}
|
||||
|
||||
export type CustomerFilterParameter = {
|
||||
createdAt?: Maybe<DateOperators>
|
||||
updatedAt?: Maybe<DateOperators>
|
||||
@@ -3010,7 +3039,9 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
|
||||
|
||||
export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
|
||||
SearchResult,
|
||||
'productId' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
|
||||
'productId' | 'sku' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
|
||||
| 'productAsset' | 'price' | 'priceWithTax' | 'currencyCode'
|
||||
| 'collectionIds' | 'facetValueIds' | 'collectionIds'
|
||||
> & {
|
||||
productAsset?: Maybe<
|
||||
{ __typename?: 'SearchResultAsset' } & Pick<
|
||||
@@ -3223,7 +3254,43 @@ export type GetAllProductsQueryVariables = Exact<{
|
||||
|
||||
export type GetAllProductsQuery = { __typename?: 'Query' } & {
|
||||
search: { __typename?: 'SearchResponse' } & {
|
||||
items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>
|
||||
items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>,
|
||||
'totalItems'
|
||||
}
|
||||
}
|
||||
|
||||
export type GetAllFacetsQuery = { __typename?: 'Query' } & {
|
||||
facets: { __typename?: 'FacetList' } & {
|
||||
items: Array<
|
||||
{ __typename?: 'Facet' } & Pick<
|
||||
Facet,
|
||||
'id' | 'name' | 'code' | 'values'
|
||||
>
|
||||
& {
|
||||
parent?: Maybe<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
|
||||
children?: Maybe<
|
||||
Array<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
|
||||
>
|
||||
}
|
||||
>,
|
||||
'totalItems'
|
||||
}
|
||||
}
|
||||
|
||||
export type GetAllCollectionsQuery = { __typename?: 'Query' } & {
|
||||
collections: { __typename?: 'CollectionList' } & {
|
||||
items: Array<
|
||||
{ __typename?: 'Collection' } & Pick<
|
||||
Collection,
|
||||
'id' | 'name' | 'slug'
|
||||
> & {
|
||||
parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
|
||||
children?: Maybe<
|
||||
Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
|
||||
>
|
||||
}
|
||||
>,
|
||||
'totalItems'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3271,7 +3338,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
|
||||
variants: Array<
|
||||
{ __typename?: 'ProductVariant' } & Pick<
|
||||
ProductVariant,
|
||||
'id' | 'priceWithTax' | 'currencyCode'
|
||||
'id' | 'priceWithTax' | 'currencyCode' | 'price'
|
||||
> & {
|
||||
options: Array<
|
||||
{ __typename?: 'ProductOption' } & Pick<
|
||||
@@ -3306,6 +3373,18 @@ export type GetProductQuery = { __typename?: 'Query' } & {
|
||||
>
|
||||
}
|
||||
>
|
||||
facetValues: Array<
|
||||
{ __typename?: 'FacetValue' } & Pick<
|
||||
FacetValue,
|
||||
'id'
|
||||
>
|
||||
>
|
||||
collections: Array<
|
||||
{ __typename?: 'Collection' } & Pick<
|
||||
Collection,
|
||||
'id'
|
||||
>
|
||||
>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
@@ -19,6 +19,8 @@ export const searchResultFragment = /* GraphQL */ `
|
||||
min
|
||||
max
|
||||
}
|
||||
}
|
||||
},
|
||||
facetValueIds,
|
||||
collectionIds,
|
||||
}
|
||||
`
|
||||
|
@@ -1,22 +1,23 @@
|
||||
import { Product } from '@commerce/types/product'
|
||||
import { Cart } from '@commerce/types/cart'
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import { CartFragment, SearchResultFragment } from '../schema'
|
||||
|
||||
export function normalizeSearchResult(item: SearchResultFragment): Product {
|
||||
export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
|
||||
return {
|
||||
id: item.productId,
|
||||
name: item.productName,
|
||||
description: item.description,
|
||||
slug: item.slug,
|
||||
path: item.slug,
|
||||
images: [{ url: item.productAsset?.preview + '?w=800&mode=crop' || '' }],
|
||||
variants: [],
|
||||
price: {
|
||||
value: (item.priceWithTax as any).min / 100,
|
||||
currencyCode: item.currencyCode,
|
||||
},
|
||||
options: [],
|
||||
sku: item.sku,
|
||||
imageSrc: item.productAsset?.preview ? item.productAsset?.preview + '?w=800&mode=crop' : '',
|
||||
price: (item.priceWithTax as any).min / 100,
|
||||
currencyCode: item.currencyCode,
|
||||
facetValueIds: item.facetValueIds,
|
||||
collectionIds: item.collectionIds,
|
||||
|
||||
// TODO:
|
||||
// oldPrice: item.price
|
||||
// discount
|
||||
// isNotSell
|
||||
// weight
|
||||
}
|
||||
}
|
||||
|
||||
|
12
framework/vendure/utils/queries/get-all-collections-query.ts
Normal file
12
framework/vendure/utils/queries/get-all-collections-query.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const getAllCollectionsQuery = /* GraphQL */ `
|
||||
query collections ($options: CollectionListOptions) {
|
||||
collections (options: $options){
|
||||
totalItems,
|
||||
items {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
17
framework/vendure/utils/queries/get-all-facets-query.ts
Normal file
17
framework/vendure/utils/queries/get-all-facets-query.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const getAllFacetsQuery = /* GraphQL */ `
|
||||
query facets ($options: FacetListOptions) {
|
||||
facets (options: $options){
|
||||
totalItems,
|
||||
items {
|
||||
id
|
||||
name
|
||||
code
|
||||
values {
|
||||
id
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@@ -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
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export const getCollectionsNameQuery = /* GraphQL */ `
|
||||
collections{
|
||||
items{
|
||||
name
|
||||
link:slug
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@@ -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",
|
||||
|
107
pages/index.tsx
107
pages/index.tsx
@@ -1,19 +1,37 @@
|
||||
import { ProductCard } from '@commerce/types/product';
|
||||
import { ProductVariables } from '@framework/api/operations/get-all-products';
|
||||
import { Collection, FacetValue } from '@framework/schema';
|
||||
import commerce from '@lib/api/commerce';
|
||||
import { GetStaticPropsContext } from 'next';
|
||||
import { Layout } from 'src/components/common';
|
||||
import { FeaturedProductsCarousel, HomeBanner, HomeCategories, HomeCollection, HomeCTA, HomeFeature, HomeRecipe, HomeSubscribe, HomeVideo } from 'src/components/modules/home';
|
||||
import { FeaturedProductsCarousel, FreshProducts, HomeBanner, HomeCategories, HomeCollection, HomeCTA, HomeFeature, HomeRecipe, HomeSubscribe, HomeVideo } from 'src/components/modules/home';
|
||||
import HomeSpice from 'src/components/modules/home/HomeSpice/HomeSpice';
|
||||
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED } from 'src/utils/constanst.utils';
|
||||
import { getAllFacetValueIdsByParentCode, getAllFacetValuesForFeatuedProducts, getAllPromies, getFreshFacetId } from 'src/utils/funtion.utils';
|
||||
import { PromiseWithKey } from 'src/utils/types.utils';
|
||||
|
||||
export default function Home() {
|
||||
interface Props {
|
||||
featuredAndDiscountFacetsValue: FacetValue[],
|
||||
freshProducts: ProductCard[],
|
||||
featuredProducts: ProductCard[],
|
||||
collections: Collection[]
|
||||
|
||||
}
|
||||
export default function Home({ featuredAndDiscountFacetsValue,
|
||||
freshProducts, featuredProducts,
|
||||
collections }: Props) {
|
||||
return (
|
||||
<>
|
||||
<HomeBanner />
|
||||
<HomeFeature />
|
||||
<HomeCategories />
|
||||
<FreshProducts data={freshProducts} collections={collections} />
|
||||
<HomeCollection />
|
||||
<HomeVideo />
|
||||
<HomeSpice/>
|
||||
<FeaturedProductsCarousel/>
|
||||
<HomeSpice />
|
||||
<FeaturedProductsCarousel data={featuredProducts} featuredFacetsValue={featuredAndDiscountFacetsValue} />
|
||||
<HomeCTA />
|
||||
<HomeRecipe />
|
||||
<HomeRecipe />
|
||||
<HomeSubscribe />
|
||||
|
||||
{/* // todo: uncomment
|
||||
@@ -22,4 +40,83 @@ export default function Home() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export async function getStaticProps({
|
||||
preview,
|
||||
locale,
|
||||
locales,
|
||||
}: GetStaticPropsContext) {
|
||||
const config = { locale, locales }
|
||||
let promisesWithKey = [] as PromiseWithKey[]
|
||||
let props = {} as any
|
||||
|
||||
const { facets } = await commerce.getAllFacets({
|
||||
variables: {},
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
props.featuredAndDiscountFacetsValue = getAllFacetValuesForFeatuedProducts(facets)
|
||||
|
||||
// fresh products
|
||||
const freshProductvariables: ProductVariables = {}
|
||||
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 = []
|
||||
}
|
||||
|
||||
|
||||
// featured products
|
||||
const allFeaturedFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_FEATURED)
|
||||
const allDiscountFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_DISCOUNT)
|
||||
const facetValueIdsForFeaturedProducts = [...allFeaturedFacetIds, ...allDiscountFacetIds]
|
||||
|
||||
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({
|
||||
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
|
||||
})
|
||||
|
||||
return {
|
||||
props,
|
||||
revalidate: 60,
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Home.Layout = Layout
|
||||
|
@@ -1,17 +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({ product, relevantProducts, collections }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
|
||||
export default function Slug() {
|
||||
return <>
|
||||
<ProductInfoDetail />
|
||||
<ProductInfoDetail productDetail={product} collections={collections}/>
|
||||
<RecipeDetail ingredients={INGREDIENT_DATA_TEST} />
|
||||
<RecommendedRecipes data={RECIPE_DATA_TEST} />
|
||||
<ReleventProducts />
|
||||
<ReleventProducts data={relevantProducts} collections={collections}/>
|
||||
<ViewedProducts />
|
||||
<RelevantBlogPosts data={BLOGS_DATA_TEST} title="relevent blog posts"/>
|
||||
<RelevantBlogPosts data={BLOGS_DATA_TEST} title="relevent blog posts" />
|
||||
</>
|
||||
}
|
||||
|
||||
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<string[]>((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
|
||||
|
@@ -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 (
|
||||
<>
|
||||
<ProductListBanner />
|
||||
<ProductListFilter/>
|
||||
<ViewedProducts/>
|
||||
<ProductListFilter
|
||||
collections={collections}
|
||||
facets={facets}
|
||||
products={productsResult.products}
|
||||
total={productsResult.totalItems} />
|
||||
<ViewedProducts />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@@ -1,18 +1,34 @@
|
||||
import { Layout } from 'src/components/common'
|
||||
import { useMessage } from 'src/components/contexts'
|
||||
|
||||
export default function Test() {
|
||||
const { showMessageError } = useMessage()
|
||||
|
||||
const handleClick = () => {
|
||||
showMessageError("Create account successfully")
|
||||
}
|
||||
import commerce from '@lib/api/commerce';
|
||||
import { GetStaticPropsContext } from 'next';
|
||||
import { ProductCard } from '@commerce/types/product';
|
||||
import { Layout, ListProductCardSkeleton } from 'src/components/common';
|
||||
|
||||
interface Props {
|
||||
productDetail: ProductCard[],
|
||||
}
|
||||
export default function Home({ productDetail }: Props) {
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleClick}>Click me</button>
|
||||
{/* <ListProductCardSkeleton /> */}
|
||||
{/* <ListProductCardSkeleton count={1} /> */}
|
||||
<ListProductCardSkeleton count={10} />
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ab qui magnam debitis ex laborum laboriosam suscipit! Totam excepturi eum libero.
|
||||
<ListProductCardSkeleton count={10} isWrap/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Test.Layout = Layout
|
||||
|
||||
export async function getServerSideProps({
|
||||
preview,
|
||||
locale,
|
||||
locales,
|
||||
}: GetStaticPropsContext) {
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Home.Layout = Layout
|
||||
|
BIN
public/assets/images/default_img.jpg
Normal file
BIN
public/assets/images/default_img.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,9 @@
|
||||
width: 24rem;
|
||||
height: 24rem;
|
||||
}
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.right{
|
||||
margin-left: 1.2rem;
|
||||
|
@@ -1,39 +1,55 @@
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import { Facet, FacetValue } from '@framework/schema'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { FeaturedProductProps } from 'src/utils/types.utils'
|
||||
import s from './FeaturedProductCard.module.scss'
|
||||
import { LANGUAGE } from '../../../utils/language.utils'
|
||||
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { ImgWithLink } from '..'
|
||||
export interface FeaturedProductCardProps extends FeaturedProductProps {
|
||||
buttonText?: string
|
||||
import { LANGUAGE } from '../../../utils/language.utils'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
|
||||
import s from './FeaturedProductCard.module.scss'
|
||||
export interface FeaturedProductCardProps extends ProductCard {
|
||||
buttonText?: string,
|
||||
subText?: string,
|
||||
}
|
||||
|
||||
const FeaturedProductCard = ({
|
||||
imageSrc,
|
||||
title,
|
||||
subTitle,
|
||||
name,
|
||||
slug,
|
||||
price,
|
||||
originPrice,
|
||||
subText,
|
||||
currencyCode,
|
||||
buttonText = LANGUAGE.BUTTON_LABEL.BUY_NOW,
|
||||
}: FeaturedProductCardProps) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className={s.featuredProductCardWarpper}>
|
||||
<div className={s.left}>
|
||||
<ImgWithLink src={imageSrc} alt={title}/>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<ImgWithLink src={imageSrc} alt={name} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={s.right}>
|
||||
<div className={s.rightTop}>
|
||||
<div className={s.title}>{title}</div>
|
||||
<div className={s.subTitle}>{subTitle}</div>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.title}>{name}</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={s.subTitle}>{subText}</div>
|
||||
<div className={s.priceWrapper}>
|
||||
<div className={s.price}>{price} </div>
|
||||
<div className={s.originPrice}>{originPrice} </div>
|
||||
<div className={s.price}>{price} {currencyCode}</div>
|
||||
{/* TODO: */}
|
||||
{/* <div className={s.originPrice}>{originPrice} </div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.buttonWarpper}>
|
||||
<div className={s.icon}>
|
||||
<ButtonIconBuy size='default'/>
|
||||
<ButtonIconBuy size='default' />
|
||||
</div>
|
||||
<div className={s.button}>
|
||||
<ButtonCommon>{buttonText}</ButtonCommon>
|
||||
|
@@ -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<HTMLDivElement>(null)
|
||||
const { toggleProductFilter: toggleFilter } = useProductFilter()
|
||||
const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
|
||||
const [isModeAuthenRegister, setIsModeAuthenRegister] = useState<boolean>(false)
|
||||
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
|
||||
@@ -63,7 +64,6 @@ const Header = memo(({ toggleFilter, visibleFilter }: props) => {
|
||||
<div className={s.menu}>
|
||||
<HeaderMenu
|
||||
isFull={isFullHeader}
|
||||
visibleFilter={visibleFilter}
|
||||
toggleFilter={toggleFilter}
|
||||
openModalLogin={openModalLogin}
|
||||
openModalRegister = {openModalRegister}
|
||||
|
@@ -50,10 +50,6 @@
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 1.2rem;
|
||||
@apply hidden;
|
||||
&.isShow {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
@screen md {
|
||||
display: none;
|
||||
|
@@ -27,7 +27,6 @@ interface Props {
|
||||
children?: any
|
||||
isFull?: boolean
|
||||
isStickyHeader?: boolean
|
||||
visibleFilter?: boolean
|
||||
openModalLogin: () => void
|
||||
openModalRegister: () => void
|
||||
openModalInfo: () => void
|
||||
@@ -38,7 +37,6 @@ const HeaderMenu = memo(
|
||||
({
|
||||
isFull,
|
||||
isStickyHeader,
|
||||
visibleFilter,
|
||||
openModalLogin,
|
||||
openModalRegister,
|
||||
openModalInfo,
|
||||
@@ -94,6 +92,7 @@ const HeaderMenu = memo(
|
||||
],
|
||||
[logout]
|
||||
)
|
||||
|
||||
return (
|
||||
<section
|
||||
className={classNames({
|
||||
@@ -109,12 +108,7 @@ const HeaderMenu = memo(
|
||||
{FILTER_PAGE.includes(router.pathname) && (
|
||||
<button className={s.iconFilter} onClick={toggleFilter}>
|
||||
<IconFilter />
|
||||
<div
|
||||
className={classNames({
|
||||
[s.dot]: true,
|
||||
[s.isShow]: visibleFilter,
|
||||
})}
|
||||
></div>
|
||||
<div className={s.dot}></div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
@@ -42,7 +42,7 @@ const HeaderSubMenu = memo(() => {
|
||||
<ul className={s.menu}>
|
||||
{/* todo: handle active item */}
|
||||
<li>
|
||||
<MenuDropdown options={collections?.items ?? []} align="left">Categories</MenuDropdown>
|
||||
<MenuDropdown options={collections || []} align="left">Categories</MenuDropdown>
|
||||
</li>
|
||||
{
|
||||
MENU.map(item => <li key={item.name}
|
||||
|
@@ -68,4 +68,5 @@ const HeaderSubMenuMobile = memo(({ }: Props) => {
|
||||
)
|
||||
})
|
||||
|
||||
HeaderSubMenuMobile.displayName = 'HeaderSubMenuMobile'
|
||||
export default HeaderSubMenuMobile
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import s from './ImgWithLink.module.scss'
|
||||
import Image from 'next/image'
|
||||
import { BLUR_DATA_IMG } from 'src/utils/constanst.utils'
|
||||
import { BLUR_DATA_IMG, DEFAULT_IMG } from 'src/utils/constanst.utils'
|
||||
|
||||
export interface ImgWithLinkProps {
|
||||
src: string,
|
||||
@@ -12,7 +12,7 @@ export interface ImgWithLinkProps {
|
||||
const ImgWithLink = ({ src, alt, blurDataURL = BLUR_DATA_IMG }: ImgWithLinkProps) => {
|
||||
return (
|
||||
<div className={s.imgWithLink}>
|
||||
<Image src={src} alt={alt}
|
||||
<Image src={src || DEFAULT_IMG.src} alt={alt}
|
||||
layout="fill"
|
||||
className={s.imgWithLink}
|
||||
placeholder="blur"
|
||||
|
@@ -2,6 +2,7 @@ import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import s from './LabelCommon.module.scss'
|
||||
interface LabelCommonProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode
|
||||
size?: 'default' | 'large'
|
||||
shape?: 'half' | 'round' | 'default'
|
||||
type?: 'default' | 'discount' | 'waiting' | 'delivering' | 'delivered'
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { CommerceProvider } from '@framework'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
import { CartDrawerProvider, MessageProvider } from 'src/components/contexts'
|
||||
import { CartDrawerProvider, MessageProvider, ProductFilterProvider } from 'src/components/contexts'
|
||||
import LayoutContent from './LayoutContent/LayoutContent'
|
||||
interface Props {
|
||||
className?: string
|
||||
@@ -13,9 +13,11 @@ const Layout: FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<CommerceProvider locale={locale}>
|
||||
<CartDrawerProvider>
|
||||
<MessageProvider>
|
||||
<LayoutContent>{children}</LayoutContent>
|
||||
</MessageProvider>
|
||||
<ProductFilterProvider>
|
||||
<MessageProvider>
|
||||
<LayoutContent>{children}</LayoutContent>
|
||||
</MessageProvider>
|
||||
</ProductFilterProvider>
|
||||
</CartDrawerProvider>
|
||||
</CommerceProvider>
|
||||
)
|
||||
|
@@ -17,7 +17,7 @@
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
@screen xl {
|
||||
@screen md {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
import { useMessage } from 'src/components/contexts'
|
||||
import { useModalCommon } from 'src/components/hooks'
|
||||
import { BRAND, CATEGORY, FEATURED, FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { CartDrawer, Footer, MessageCommon, ScrollToTop } from '../..'
|
||||
import Header from '../../Header/Header'
|
||||
import MenuNavigationProductList from '../../MenuNavigationProductList/MenuNavigationProductList'
|
||||
@@ -14,23 +14,13 @@ interface Props {
|
||||
}
|
||||
|
||||
const LayoutContent: FC<Props> = ({ children }) => {
|
||||
const { pathname } = useRouter()
|
||||
const { visible: visibleFilter, openModal: openFilter, closeModal: closeFilter } = useModalCommon({ initialValue: false })
|
||||
const router = useRouter()
|
||||
const {messages, removeMessage} = useMessage()
|
||||
|
||||
const toggleFilter = () => {
|
||||
if (visibleFilter) {
|
||||
closeFilter()
|
||||
} else {
|
||||
openFilter()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={s.mainLayout}>
|
||||
<Header toggleFilter={toggleFilter} visibleFilter={visibleFilter} />
|
||||
<Header/>
|
||||
{
|
||||
router.pathname === ROUTE.ACCOUNT ?
|
||||
<section className={s.wrapperWithBg}>
|
||||
@@ -38,10 +28,9 @@ const LayoutContent: FC<Props> = ({ children }) => {
|
||||
</section> :
|
||||
<main>{children}</main>
|
||||
}
|
||||
<div className={s.filter}><MenuNavigationProductList categories={CATEGORY} brands={BRAND} featured={FEATURED} visible={visibleFilter} onClose={closeFilter} /> </div>
|
||||
<ScrollToTop visibilityHeight={1500} />
|
||||
{
|
||||
FILTER_PAGE.includes(pathname) && (<div className={s.filter}><MenuNavigationProductList categories={CATEGORY} brands={BRAND} featured={FEATURED} visible={visibleFilter} onClose={closeFilter} /> </div>)
|
||||
FILTER_PAGE.includes(router.pathname) && (<div className={s.filter}><MenuNavigationProductList /> </div>)
|
||||
}
|
||||
<Footer />
|
||||
</div>
|
||||
|
@@ -0,0 +1,12 @@
|
||||
.listProductCardSkeleton {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
overflow: unset;
|
||||
> div {
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import classNames from 'classnames'
|
||||
import { ProductCardSkeleton } from '..'
|
||||
import s from './ListProductCardSkeleton.module.scss'
|
||||
|
||||
type Props = {
|
||||
count?: number
|
||||
isWrap?: boolean
|
||||
}
|
||||
const ListProductCardSkeleton = ({ count = 5, isWrap }: Props) => {
|
||||
|
||||
return (
|
||||
<div className={classNames(s.listProductCardSkeleton, { [s.wrap]: isWrap })}>
|
||||
{
|
||||
Array.from(Array(count).keys()).map(item => <ProductCardSkeleton key={item} />)
|
||||
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListProductCardSkeleton
|
@@ -1,9 +1,8 @@
|
||||
@import "../../../styles/utilities";
|
||||
.menuFilterWrapper{
|
||||
@apply spacing-horizontal;
|
||||
|
||||
.menuFilterWrapper{
|
||||
.menuFilterHeading{
|
||||
@apply sub-headline font-bold ;
|
||||
@apply sub-headline font-bold;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 0.8rem 0;
|
||||
@@ -19,21 +18,5 @@
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-line);
|
||||
}
|
||||
|
||||
li{
|
||||
margin: 1rem 0;
|
||||
padding:0;
|
||||
div{
|
||||
padding: 0.8rem 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
background-color: var(--gray);
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
color:white;
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,44 +1,34 @@
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react';
|
||||
import s from './MenuFilter.module.scss';
|
||||
import MenuFilterItem from './MenuFilterItem/MenuFilterItem';
|
||||
|
||||
import s from './MenuFilter.module.scss'
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading?:string,
|
||||
categories:{name:string,link:string}[],
|
||||
type:string,
|
||||
onChangeValue?: (value: Object) => void
|
||||
heading?: string,
|
||||
categories: { name: string, slug?: string, code?: string }[],
|
||||
type: string,
|
||||
onChange: (value: string, type: string, isSellect?: boolean) => void
|
||||
isSingleSelect?: boolean
|
||||
singleSelectedValue?: string
|
||||
}
|
||||
|
||||
const MenuFilter = ({heading,categories,type,onChangeValue}:Props)=> {
|
||||
const [active, setActive] = useState<string>('');
|
||||
|
||||
function handleClick(link:string){
|
||||
setActive(link);
|
||||
|
||||
if(active === link){
|
||||
setActive('');
|
||||
}
|
||||
const MenuFilter = ({ heading, categories, type, onChange, singleSelectedValue, isSingleSelect }: Props) => {
|
||||
function handleChange(value: string, isSellect: boolean) {
|
||||
onChange(value, type, isSellect)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
let href = active?.split("=");
|
||||
const linkValue = href[1];
|
||||
|
||||
onChangeValue && onChangeValue({[type]:linkValue});
|
||||
},[active])
|
||||
|
||||
return (
|
||||
<section className={s.menuFilterWrapper}>
|
||||
<h2 className={s.menuFilterHeading}>{heading}</h2>
|
||||
<ul className={s.menuFilterList}>
|
||||
{
|
||||
categories.map(item => <li key={item.name}>
|
||||
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
|
||||
{item.name}
|
||||
</div>
|
||||
</li>)
|
||||
categories.map(item => <MenuFilterItem
|
||||
key={item.slug || item.code}
|
||||
name={item.name}
|
||||
type={type}
|
||||
value={item.slug || item.code || ''}
|
||||
onChange={handleChange}
|
||||
isActive={isSingleSelect && (item.slug || item.code) === singleSelectedValue}
|
||||
/>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
@@ -0,0 +1,15 @@
|
||||
.menuFilterItem {
|
||||
margin: 1rem 0;
|
||||
padding: 0;
|
||||
div {
|
||||
padding: 0.8rem 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
background-color: var(--gray);
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
color: var(--white);
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import classNames from 'classnames';
|
||||
import { route } from 'next/dist/server/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import s from './MenuFilterItem.module.scss';
|
||||
|
||||
interface Props {
|
||||
name: string,
|
||||
value: string,
|
||||
type: string,
|
||||
isActive?: boolean
|
||||
onChange: (value: string, isSellect: boolean) => void
|
||||
}
|
||||
|
||||
const MenuFilterItem = ({ name, value, type, isActive, onChange }: Props) => {
|
||||
const router = useRouter()
|
||||
const [isSelected, setIsSelected] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
setIsSelected(isActive)
|
||||
}, [isActive])
|
||||
|
||||
useEffect(() => {
|
||||
const rs = (router.query[type] || []).includes(value)
|
||||
setIsSelected(rs)
|
||||
}, [type, router.query, value])
|
||||
|
||||
function handleClick() {
|
||||
onChange(value, !isSelected)
|
||||
setIsSelected(!isSelected)
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={s.menuFilterItem}>
|
||||
<div onClick={handleClick} className={classNames({ [s.active]: isSelected })}>
|
||||
{name}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuFilterItem
|
@@ -3,31 +3,13 @@
|
||||
@apply hidden;
|
||||
@screen md {
|
||||
@apply block;
|
||||
}
|
||||
.menuNavigationHeading{
|
||||
@screen md {
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
}
|
||||
.menuNavigationList{
|
||||
@screen md {
|
||||
li{
|
||||
margin: 0.8rem 0;
|
||||
a{
|
||||
display:block;
|
||||
width:100%;
|
||||
color:var(--text-base);
|
||||
&:hover {
|
||||
@apply text-primary;
|
||||
}
|
||||
&.active {
|
||||
@apply text-primary;
|
||||
}
|
||||
}
|
||||
.menuNavigationHeading{
|
||||
@screen md {
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +1,27 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import s from './MenuNavigation.module.scss'
|
||||
import MenuNavigationItem from './MenuNavigationItem/MenuNavigationItem'
|
||||
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading:string,
|
||||
categories:{name:string,link:string}[]
|
||||
heading: string,
|
||||
queryKey: string,
|
||||
categories: { name: string, slug?: string, code?: string }[]
|
||||
isSingleSelect?: boolean
|
||||
}
|
||||
|
||||
const MenuNavigation = ({heading,categories}:Props)=> {
|
||||
const router = useRouter()
|
||||
|
||||
const MenuNavigation = ({ heading, queryKey, categories, isSingleSelect }: Props) => {
|
||||
return (
|
||||
<section className={s.menuNavigationWrapper}>
|
||||
<h2 className={s.menuNavigationHeading}>{heading}({categories.length})</h2>
|
||||
<ul className={s.menuNavigationList}>
|
||||
{
|
||||
categories.map(item => <li key={item.name}
|
||||
>
|
||||
<Link href={item.link}>
|
||||
<a className={classNames({ [s.active]: router.asPath === item.link})}>
|
||||
{item.name}
|
||||
</a>
|
||||
</Link>
|
||||
</li>)
|
||||
categories.map(item => <MenuNavigationItem
|
||||
key={item.name}
|
||||
name={item.name}
|
||||
value={item.slug || item.code || ''}
|
||||
queryKey={queryKey}
|
||||
isSingleSelect={isSingleSelect}
|
||||
/>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
@@ -0,0 +1,15 @@
|
||||
.menuNavigationItem {
|
||||
@screen md {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0.8rem 0;
|
||||
color: var(--text-base);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
@apply text-active;
|
||||
}
|
||||
&.active {
|
||||
@apply text-primary;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { QUERY_KEY, QUERY_SPLIT_SEPERATOR, ROUTE } from 'src/utils/constanst.utils'
|
||||
import s from './MenuNavigationItem.module.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
value: string
|
||||
queryKey: string
|
||||
isSingleSelect?: boolean
|
||||
|
||||
}
|
||||
|
||||
const MenuNavigationItem = ({ name, value, queryKey, isSingleSelect }: Props) => {
|
||||
const router = useRouter()
|
||||
const [isActive, setIsActive] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setIsActive(false)
|
||||
}
|
||||
|
||||
const queryString = router.query[queryKey] as string || ''
|
||||
setIsActive(queryString.split(QUERY_SPLIT_SEPERATOR).includes(value))
|
||||
}, [router.query, queryKey, value])
|
||||
|
||||
const handleClick = () => {
|
||||
const queryString = router.query[queryKey] as string || ''
|
||||
const prevQuery = queryString.split(QUERY_SPLIT_SEPERATOR)
|
||||
|
||||
let newQuery = ''
|
||||
if (isSingleSelect) {
|
||||
newQuery = isActive ? '' : value
|
||||
} else {
|
||||
if (isActive) {
|
||||
newQuery = prevQuery.filter(item => item !== value).join(QUERY_SPLIT_SEPERATOR)
|
||||
} else {
|
||||
newQuery = [...prevQuery, value].join(QUERY_SPLIT_SEPERATOR)
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
...router.query,
|
||||
[queryKey]: newQuery
|
||||
}
|
||||
|
||||
if (queryKey === QUERY_KEY.CATEGORY) {
|
||||
query[QUERY_KEY.PAGE] = "0"
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
query
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
}
|
||||
|
||||
return (<li className={classNames(s.menuNavigationItem, { [s.active]: isActive })}
|
||||
onClick={handleClick}>
|
||||
{name}
|
||||
</li>)
|
||||
}
|
||||
|
||||
export default MenuNavigationItem
|
@@ -1,13 +1,5 @@
|
||||
@import "../../../styles/utilities";
|
||||
.menuNavigationProductListDesktop{
|
||||
@screen sm {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
@screen xl {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
.menuNavigationProductListMobile{
|
||||
@apply relative transition-all duration-100;
|
||||
&.isShow{
|
||||
@@ -37,7 +29,7 @@
|
||||
transform: translateY(0%)
|
||||
}
|
||||
.content{
|
||||
@apply absolute w-full h-full;
|
||||
@apply absolute w-full h-full spacing-horizontal custom-scroll;
|
||||
margin-top: 3rem;
|
||||
padding-top: 10rem ;
|
||||
padding-bottom: 10rem;
|
||||
@@ -46,6 +38,7 @@
|
||||
height: 96%;
|
||||
bottom: 0;
|
||||
border-radius: 2.4rem 2.4rem 0 0;
|
||||
|
||||
.head{
|
||||
@apply flex justify-between fixed;
|
||||
top:0;
|
||||
@@ -57,12 +50,11 @@
|
||||
background-color: white;
|
||||
z-index: 10000;
|
||||
h3{
|
||||
@apply heading-3 font-bold;
|
||||
color:var(--text-base);
|
||||
@apply heading-3 font-heading;
|
||||
}
|
||||
}
|
||||
.foot{
|
||||
@apply fixed;
|
||||
@apply fixed text-center;
|
||||
bottom: 0;
|
||||
left:0;
|
||||
width: 100%;
|
||||
@@ -70,7 +62,7 @@
|
||||
padding: 0 1rem 3rem 1rem;
|
||||
|
||||
}
|
||||
button{
|
||||
button {
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -1,56 +1,155 @@
|
||||
import React, { useState } from 'react';
|
||||
import {ButtonCommon} from 'src/components/common';
|
||||
import { QueryFacetsArgs } from '@framework/schema';
|
||||
import classNames from 'classnames';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ButtonCommon } from 'src/components/common';
|
||||
import { useProductFilter } from 'src/components/contexts';
|
||||
import { useGetAllCollection } from 'src/components/hooks/collection';
|
||||
import { useFacets } from 'src/components/hooks/facets';
|
||||
import IconHide from 'src/components/icons/IconHide';
|
||||
import { CODE_FACET_BRAND, CODE_FACET_FEATURED, OPTIONS_SORT_PRODUCT, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
|
||||
import { LANGUAGE } from 'src/utils/language.utils';
|
||||
import { SortOrder } from 'src/utils/types.utils';
|
||||
import MenuFilter from '../MenuFilter/MenuFilter';
|
||||
import SkeletonParagraph from '../SkeletonCommon/SkeletonParagraph/SkeletonParagraph';
|
||||
import s from './MenuNavigationProductList.module.scss';
|
||||
import MenuSort from './MenuSort/MenuSort';
|
||||
import {LANGUAGE} from 'src/utils/language.utils';
|
||||
import classNames from 'classnames'
|
||||
import MenuFilter from '../MenuFilter/MenuFilter';
|
||||
import MenuNavigation from '../MenuNavigation/MenuNavigation';
|
||||
import IconHide from 'src/components/icons/IconHide';
|
||||
|
||||
interface Props{
|
||||
categories:{name:string,link:string}[],
|
||||
brands:{name:string,link:string}[],
|
||||
featured:{name:string,link:string}[],
|
||||
visible: boolean,
|
||||
onClose: () => void
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
const MenuNavigationProductList = ({categories,brands,featured,visible,onClose}:Props)=>{
|
||||
|
||||
const [dataSort,setDataSort] = useState({});
|
||||
|
||||
function handleValue(value:Object){
|
||||
setDataSort({...dataSort,...value});
|
||||
const FACET_QUERY = {
|
||||
options: {
|
||||
sort: {
|
||||
code: SortOrder.Asc
|
||||
},
|
||||
filter: {
|
||||
code: {
|
||||
in: [CODE_FACET_FEATURED, CODE_FACET_BRAND]
|
||||
}
|
||||
}
|
||||
}
|
||||
function filter(){
|
||||
// console.log(dataSort)
|
||||
} as QueryFacetsArgs
|
||||
|
||||
const MenuNavigationProductList = ({}: Props) => {
|
||||
const router = useRouter()
|
||||
const { productFilterVisible: visible, closeProductFilter: onClose } = useProductFilter()
|
||||
const { facets, loading: facetsLoading } = useFacets(FACET_QUERY)
|
||||
const { collections, loading: collectionLoading } = useGetAllCollection()
|
||||
const [brandQuery, setBrandQuery] = useState<string[]>([])
|
||||
const [featuredQuery, setFeaturedQuery] = useState<string[]>([])
|
||||
const [categoryQuery, setCategoryQuery] = useState<string>()
|
||||
const [sortValue, setSortValue] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const rs = router.query[QUERY_KEY.SORTBY] as string
|
||||
if (rs) {
|
||||
setSortValue(rs)
|
||||
}
|
||||
}, [router.query])
|
||||
|
||||
useEffect(() => {
|
||||
const rs = router.query[QUERY_KEY.CATEGORY] as string
|
||||
if (rs) {
|
||||
setCategoryQuery(rs)
|
||||
}
|
||||
}, [router.query])
|
||||
|
||||
function onSubmit() {
|
||||
let newURL = `${ROUTE.PRODUCTS}?`
|
||||
|
||||
if (categoryQuery) {
|
||||
newURL += `&${QUERY_KEY.CATEGORY}=${categoryQuery}`
|
||||
}
|
||||
|
||||
if (brandQuery.length > 0) {
|
||||
newURL += `&${QUERY_KEY.BRAND}=${brandQuery.join(",")}`
|
||||
}
|
||||
|
||||
if (featuredQuery.length > 0) {
|
||||
newURL += `&${QUERY_KEY.FEATURED}=${featuredQuery.join(",")}`
|
||||
}
|
||||
|
||||
if (sortValue) {
|
||||
newURL += `&${QUERY_KEY.SORTBY}=${sortValue}`
|
||||
}
|
||||
router.push(newURL)
|
||||
onClose()
|
||||
}
|
||||
return(
|
||||
<>
|
||||
<div className={s.menuNavigationProductListDesktop}>
|
||||
<MenuNavigation categories={categories} heading="Categories"/>
|
||||
<MenuNavigation categories={brands} heading="Brands"/>
|
||||
<MenuNavigation categories={featured} heading="Featured"/>
|
||||
</div>
|
||||
<div className={classNames({ [s.menuNavigationProductListMobile] :true,[s.isShow]: visible})}>
|
||||
<div className={classNames({ [s.menuNavigationProductModal] :true,[s.animation]: visible})}>
|
||||
<div className={s.content}>
|
||||
<div className={s.head}>
|
||||
<h3>FILTER</h3>
|
||||
<div onClick={onClose}><IconHide/></div>
|
||||
</div>
|
||||
<MenuFilter categories={categories} heading="Categories" type="category" onChangeValue={handleValue}/>
|
||||
<MenuFilter categories={brands} heading="Brand" type="brand" onChangeValue={handleValue}/>
|
||||
<MenuFilter categories={featured} heading="Featured" type="featured" onChangeValue={handleValue}/>
|
||||
<MenuSort heading="SORT BY" type="sort" onChangeValue={handleValue}/>
|
||||
<div className={s.foot}>
|
||||
<ButtonCommon size="large" onClick={filter}>{LANGUAGE.BUTTON_LABEL.CONFIRM}</ButtonCommon>
|
||||
</div>
|
||||
|
||||
const onSortChange = (value: string) => {
|
||||
setSortValue(value)
|
||||
}
|
||||
|
||||
const onCategoryChange = (value: string, isSelect: boolean) => {
|
||||
if (isSelect) {
|
||||
setCategoryQuery(value)
|
||||
} else {
|
||||
setCategoryQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const onFilterOptionChange = (value: string, type: string, isSelect: boolean = true) => {
|
||||
if (type === QUERY_KEY.CATEGORY) {
|
||||
onCategoryChange(value, isSelect)
|
||||
} else {
|
||||
let rs = [...featuredQuery]
|
||||
let setDataFunction = setFeaturedQuery
|
||||
|
||||
if (type === CODE_FACET_BRAND) {
|
||||
rs = [...brandQuery]
|
||||
setDataFunction = setBrandQuery
|
||||
}
|
||||
|
||||
if (isSelect) {
|
||||
rs.push(value)
|
||||
} else {
|
||||
rs = rs.filter(item => item !== value)
|
||||
}
|
||||
setDataFunction(rs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={classNames({ [s.menuNavigationProductListMobile]: true, [s.isShow]: visible })}>
|
||||
<div className={classNames({ [s.menuNavigationProductModal]: true, [s.animation]: visible })}>
|
||||
<div className={s.content}>
|
||||
<div className={s.head}>
|
||||
<h3>FILTER</h3>
|
||||
<div onClick={onClose}><IconHide /></div>
|
||||
</div>
|
||||
|
||||
{collectionLoading && <SkeletonParagraph rows={5} />}
|
||||
<MenuFilter categories={collections}
|
||||
heading="Categories"
|
||||
type={QUERY_KEY.CATEGORY}
|
||||
onChange={onFilterOptionChange}
|
||||
singleSelectedValue={categoryQuery}
|
||||
isSingleSelect={true}
|
||||
/>
|
||||
|
||||
{facetsLoading && <>
|
||||
<SkeletonParagraph rows={5} />
|
||||
<SkeletonParagraph rows={5} />
|
||||
</>}
|
||||
{
|
||||
facets?.map(item => <MenuFilter
|
||||
key={item.id}
|
||||
type={item.code}
|
||||
categories={item.values}
|
||||
heading={item.name}
|
||||
onChange={onFilterOptionChange}
|
||||
/>)
|
||||
}
|
||||
<MenuSort heading="SORT BY" onChange={onSortChange} value={sortValue} options={OPTIONS_SORT_PRODUCT} />
|
||||
<div className={s.foot}>
|
||||
<ButtonCommon size="large" onClick={onSubmit}>{LANGUAGE.BUTTON_LABEL.CONFIRM}</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,40 +1,12 @@
|
||||
@import "../../../../styles/utilities";
|
||||
.menuSortWrapper{
|
||||
@apply spacing-horizontal;
|
||||
|
||||
.menuSortWrapper{
|
||||
.menuSortHeading{
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
@apply heading-3 font-heading;
|
||||
margin: 0.8rem 0;
|
||||
}
|
||||
.menuSortList{
|
||||
padding-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
li{
|
||||
div{
|
||||
height: 4.8rem;
|
||||
line-height: 4.8rem;
|
||||
padding: 0 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
@apply font-bold relative;
|
||||
color:var(--text-active);
|
||||
background-color: var(--gray);
|
||||
&::after{
|
||||
@apply absolute;
|
||||
content:"";
|
||||
background-image: url('/assets/svg/check.svg');
|
||||
right: 1.6rem;
|
||||
top: calc(50% - 24px/2);
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,63 +1,30 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
|
||||
import { PRODUCT_SORT_OPTION_VALUE, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
|
||||
import s from './MenuSort.module.scss';
|
||||
import MenuSortItem from './MenuSortItem/MenuSortItem';
|
||||
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading:string,
|
||||
type:string,
|
||||
onChangeValue?: (value: Object) => void
|
||||
heading: string,
|
||||
options: {name: string, value: string}[]
|
||||
value?: string,
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const SORT = [
|
||||
{
|
||||
name: 'By Name',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=by-name`,
|
||||
},
|
||||
{
|
||||
name: 'Price(High to Low)',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=high-to-low`,
|
||||
},
|
||||
{
|
||||
name: 'Price (Low to High)',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=low-to-high`,
|
||||
},
|
||||
{
|
||||
name: 'On Sale',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=on-sale`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const MenuSort = ({heading,type,onChangeValue}:Props)=> {
|
||||
const [active, setActive] = useState<string>('');
|
||||
|
||||
function handleClick(link:string){
|
||||
setActive(link);
|
||||
|
||||
if(active === link){
|
||||
setActive('');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
let href = active?.split("=");
|
||||
const linkValue = href[1];
|
||||
|
||||
onChangeValue && onChangeValue({[type]:linkValue});
|
||||
},[active])
|
||||
|
||||
const MenuSort = ({ heading, value, onChange, options }: Props) => {
|
||||
return (
|
||||
<section className={classNames(s.menuSortWrapper)}>
|
||||
<h2 className={classNames(s.menuSortHeading)}>{heading}</h2>
|
||||
<ul className={s.menuSortList}>
|
||||
{
|
||||
SORT.map(item => <li key={item.name}>
|
||||
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
|
||||
{item.name}
|
||||
</div>
|
||||
</li>)
|
||||
options.map(item => <MenuSortItem
|
||||
key={item.value}
|
||||
name={item.name}
|
||||
value={item.value}
|
||||
currentValue={value}
|
||||
onChange={onChange}
|
||||
/>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
@@ -0,0 +1,26 @@
|
||||
.menuSortItem {
|
||||
div {
|
||||
height: 4.8rem;
|
||||
line-height: 4.8rem;
|
||||
padding: 0 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
@apply font-bold relative;
|
||||
color: var(--text-active);
|
||||
background-color: var(--primary-lightest);
|
||||
&::after {
|
||||
@apply absolute;
|
||||
content: "";
|
||||
background-image: url("./check.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
right: 1.6rem;
|
||||
top: calc(50% - 24px / 2);
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import classNames from 'classnames';
|
||||
import s from './MenuSortItem.module.scss';
|
||||
|
||||
interface Props {
|
||||
name: string,
|
||||
value: string,
|
||||
currentValue?: string,
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const MenuSortItem = ({ onChange, name, value, currentValue }: Props) => {
|
||||
const handleChange = () => {
|
||||
onChange(value)
|
||||
}
|
||||
return (
|
||||
<li className={s.menuSortItem}>
|
||||
<div onClick={handleChange} className={classNames({ [s.active]: value === currentValue })}>
|
||||
{name}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuSortItem
|
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.7104 1.20986C14.6175 1.11613 14.5069 1.04174 14.385 0.990969C14.2632 0.940201 14.1324 0.914062 14.0004 0.914062C13.8684 0.914062 13.7377 0.940201 13.6159 0.990969C13.494 1.04174 13.3834 1.11613 13.2904 1.20986L5.84044 8.66986L2.71044 5.52986C2.61392 5.43662 2.49998 5.36331 2.37512 5.3141C2.25026 5.2649 2.11694 5.24077 1.98276 5.24309C1.84858 5.24541 1.71617 5.27414 1.59309 5.32763C1.47001 5.38113 1.35868 5.45834 1.26544 5.55486C1.1722 5.65138 1.09889 5.76532 1.04968 5.89018C1.00048 6.01503 0.976347 6.14836 0.978669 6.28254C0.98099 6.41672 1.00972 6.54913 1.06321 6.67221C1.1167 6.79529 1.19392 6.90662 1.29044 6.99986L5.13044 10.8399C5.2234 10.9336 5.334 11.008 5.45586 11.0588C5.57772 11.1095 5.70843 11.1357 5.84044 11.1357C5.97245 11.1357 6.10316 11.1095 6.22502 11.0588C6.34687 11.008 6.45748 10.9336 6.55044 10.8399L14.7104 2.67986C14.8119 2.58622 14.893 2.47257 14.9484 2.34607C15.0038 2.21957 15.0324 2.08296 15.0324 1.94486C15.0324 1.80676 15.0038 1.67015 14.9484 1.54365C14.893 1.41715 14.8119 1.3035 14.7104 1.20986Z" fill="#5b9a74" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@@ -48,7 +48,7 @@ const FormRegister = ({ onSwitch, isHide }: Props) => {
|
||||
|
||||
const onSignupCallBack = (isSuccess: boolean, message?: string) => {
|
||||
if (isSuccess) {
|
||||
showMessageSuccess("Create account successfully. Please verify your email to login.")
|
||||
showMessageSuccess("Create account successfully. Please verify your email to login.", 15000)
|
||||
} else {
|
||||
showMessageError(message || LANGUAGE.MESSAGE.ERROR)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ const FormRegister = ({ onSwitch, isHide }: Props) => {
|
||||
validationSchema={DisplayingErrorMessagesSchema}
|
||||
onSubmit={onSignup}
|
||||
>
|
||||
{({ errors, touched }) => (
|
||||
{({ errors, touched, isValid, submitForm }) => (
|
||||
<Form className="u-form">
|
||||
<div className="body">
|
||||
<InputFiledInForm
|
||||
@@ -93,6 +93,7 @@ const FormRegister = ({ onSwitch, isHide }: Props) => {
|
||||
? errors.password.toString()
|
||||
: ''
|
||||
}
|
||||
onEnter={isValid ? submitForm : undefined}
|
||||
/>
|
||||
<div className={styles.passwordNote}>
|
||||
Must contain 8 characters with at least 1 uppercase and 1
|
||||
|
@@ -25,7 +25,7 @@ const ModalCreateUserInfo = ({ demoVisible: visible, demoCloseModal: closeModal
|
||||
<Inputcommon placeholder='Street Address' ref={firstInputRef} />
|
||||
<Inputcommon placeholder='City' />
|
||||
<div className={s.line}>
|
||||
<SelectCommon option={STATE_OPTIONS} type="custom" size="large" placeholder='State'/>
|
||||
<SelectCommon options={STATE_OPTIONS} type="custom" size="large" placeholder='State'/>
|
||||
<Inputcommon placeholder='Zip code' />
|
||||
</div>
|
||||
<Inputcommon placeholder='Phone (delivery contact)' />
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { IconBuy } from 'src/components/icons'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { ProductProps } from 'src/utils/types.utils'
|
||||
import { ImgWithLink } from '..'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
|
||||
@@ -11,16 +11,18 @@ import LabelCommon from '../LabelCommon/LabelCommon'
|
||||
import s from './ProductCard.module.scss'
|
||||
import ProductNotSell from './ProductNotSell/ProductNotSell'
|
||||
|
||||
export interface ProductCardProps extends ProductProps {
|
||||
export interface ProductCardProps extends ProductCard {
|
||||
buttonText?: string
|
||||
isSingleButton?: boolean,
|
||||
}
|
||||
|
||||
const ProductCard = ({
|
||||
category,
|
||||
const ProductCardComponent = ({
|
||||
collection,
|
||||
name,
|
||||
slug,
|
||||
weight,
|
||||
price,
|
||||
currencyCode,
|
||||
buttonText = 'Buy Now',
|
||||
imageSrc,
|
||||
isNotSell,
|
||||
@@ -35,24 +37,31 @@ const ProductCard = ({
|
||||
return (
|
||||
<div className={s.productCardWarpper}>
|
||||
<div className={s.cardTop}>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/test`}>
|
||||
<div className={s.productImage}>
|
||||
<ImgWithLink src={imageSrc} alt={name}/>
|
||||
</div>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.productImage}>
|
||||
<ImgWithLink src={imageSrc} alt={name}/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
{
|
||||
collection &&
|
||||
<div className={s.productLabel}>
|
||||
<LabelCommon shape="half">{category}</LabelCommon>
|
||||
<LabelCommon shape="half">{collection}</LabelCommon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={s.cardMid}>
|
||||
<div className={s.cardMidTop}>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/test`}>
|
||||
<div className={s.productname}>{name} </div>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.productname}>{name} </div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={s.productWeight}>{weight}</div>
|
||||
</div>
|
||||
<div className={s.cardMidBot}>
|
||||
<div className={s.productPrice}>{price}</div>
|
||||
<div className={s.productPrice}>{price} {currencyCode}</div>
|
||||
<div className={s.wishList}>
|
||||
<ItemWishList />
|
||||
</div>
|
||||
@@ -80,4 +89,4 @@ const ProductCard = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCard
|
||||
export default ProductCardComponent
|
||||
|
@@ -0,0 +1,15 @@
|
||||
.productCardSkeleton {
|
||||
max-width: 22.4rem;
|
||||
min-width: 20rem;
|
||||
margin: 0 0.8rem;
|
||||
> div {
|
||||
width: 100% !important;
|
||||
}
|
||||
.content {
|
||||
margin-top: 1.6rem;
|
||||
> div {
|
||||
margin: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import SkeletonImage from "../SkeletonCommon/SkeletonImage/SkeletonImage"
|
||||
import SkeletonParagraph from "../SkeletonCommon/SkeletonParagraph/SkeletonParagraph"
|
||||
import s from './ProductCardSkeleton.module.scss'
|
||||
|
||||
const ProductCardSkeleton = ({ }) => {
|
||||
|
||||
return (
|
||||
<div className={s.productCardSkeleton}>
|
||||
<SkeletonImage />
|
||||
<div className={s.content}>
|
||||
<SkeletonParagraph rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCardSkeleton
|
@@ -1,11 +1,28 @@
|
||||
.wrapper{
|
||||
.list{
|
||||
// max-width: 109.4rem;
|
||||
@apply flex flex-wrap justify-around;
|
||||
.wrapper {
|
||||
margin: 4rem auto;
|
||||
.list {
|
||||
@apply grid grid-cols-2;
|
||||
@screen md {
|
||||
@apply grid-cols-3;
|
||||
}
|
||||
@screen lg {
|
||||
@apply grid-cols-4;
|
||||
}
|
||||
@screen xl {
|
||||
@apply grid-cols-6;
|
||||
}
|
||||
.empty {
|
||||
button {
|
||||
margin-top: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.pagination{
|
||||
.pagination {
|
||||
padding-top: 4.8rem;
|
||||
// max-width: 109.4rem;
|
||||
@apply flex justify-center items-center ;
|
||||
@apply flex justify-center items-center;
|
||||
&.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1,50 @@
|
||||
import React, { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { DEFAULT_PAGE_SIZE, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { ButtonCommon, EmptyCommon } from '..'
|
||||
import PaginationCommon from '../PaginationCommon/PaginationCommon'
|
||||
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard'
|
||||
import s from "./ProductList.module.scss"
|
||||
|
||||
interface ProductListProps {
|
||||
data: ProductCardProps[]
|
||||
data: ProductCardProps[],
|
||||
total?: number,
|
||||
defaultCurrentPage?: number
|
||||
onPageChange?: (page: number) => void
|
||||
}
|
||||
|
||||
const ProductList = ({data}: ProductListProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const onPageChange = (page:number) => {
|
||||
setCurrentPage(page)
|
||||
const ProductList = ({ data, total = data.length, defaultCurrentPage, onPageChange }: ProductListProps) => {
|
||||
const router = useRouter()
|
||||
const handlePageChange = (page: number) => {
|
||||
onPageChange && onPageChange(page)
|
||||
}
|
||||
|
||||
const handleShowAllProduct = () => {
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.wrapper}>
|
||||
<div className={s.list}>
|
||||
{
|
||||
data.slice(currentPage*20,(currentPage+1)*20).map((product,index)=>{
|
||||
return <ProductCard {...product} key={index}/>
|
||||
data.map((product, index) => {
|
||||
return <ProductCard {...product} key={index} />
|
||||
})
|
||||
}
|
||||
{
|
||||
data.length === 0 && <div className={s.empty}>
|
||||
<EmptyCommon />
|
||||
<ButtonCommon onClick={handleShowAllProduct}>Show all products</ButtonCommon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={s.pagination}>
|
||||
<PaginationCommon total={data.length} pageSize={20} onChange={onPageChange}/>
|
||||
<div className={classNames(s.pagination, { [s.hide]: data.length === 0 })}>
|
||||
<PaginationCommon defaultCurrent={defaultCurrentPage} total={total} pageSize={DEFAULT_PAGE_SIZE} onChange={handlePageChange} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -1,24 +1,33 @@
|
||||
import s from './SelectCommon.module.scss'
|
||||
import classNames from 'classnames'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { IconVectorDown } from 'src/components/icons'
|
||||
import s from './SelectCommon.module.scss'
|
||||
import SelectOption from './SelectOption/SelectOption'
|
||||
|
||||
interface Props {
|
||||
placeholder? : string,
|
||||
value?: string,
|
||||
size?: 'base' | 'large',
|
||||
type?: 'default' | 'custom',
|
||||
option: {name: string, value: string}[],
|
||||
options: {name: string, value: string}[],
|
||||
onChange?: (value: string) => void,
|
||||
}
|
||||
|
||||
const SelectCommon = ({ type = 'default', size = 'base', option, placeholder, onChange}: Props) => {
|
||||
const [selectedName, setSelectedName] = useState(placeholder)
|
||||
const [selectedValue, setSelectedValue] = useState('')
|
||||
const SelectCommon = ({ value, type = 'default', size = 'base', options, placeholder, onChange}: Props) => {
|
||||
const [selectedName, setSelectedName] = useState<string>()
|
||||
const [selectedValue, setSelectedValue] = useState<string>('')
|
||||
|
||||
const changeSelectedName = (item:string, value: string) => {
|
||||
useEffect(() => {
|
||||
setSelectedValue(value || '')
|
||||
|
||||
const name = options.find(item => item.value === value)?.name
|
||||
setSelectedName(name)
|
||||
}, [value, options])
|
||||
|
||||
const changeSelectedName = (value: string) => {
|
||||
setSelectedValue(value)
|
||||
setSelectedName(item)
|
||||
const name = options.find(item => item.value === value)?.name
|
||||
setSelectedName(name)
|
||||
onChange && onChange(value)
|
||||
}
|
||||
return(
|
||||
@@ -33,7 +42,7 @@ const SelectCommon = ({ type = 'default', size = 'base', option, placeholder, on
|
||||
[s.selectTrigger] : true,
|
||||
|
||||
})}
|
||||
>{selectedName}<IconVectorDown /></div>
|
||||
>{selectedName || placeholder}<IconVectorDown /></div>
|
||||
|
||||
<div className={s.hoverWrapper}>
|
||||
<div className={classNames({
|
||||
@@ -43,8 +52,12 @@ const SelectCommon = ({ type = 'default', size = 'base', option, placeholder, on
|
||||
})}
|
||||
>
|
||||
{
|
||||
option.map(item =>
|
||||
<SelectOption key={item.value} itemName={item.name} value={item.value} onClick={changeSelectedName} size={size} selected={(selectedValue === item.value)} />
|
||||
options.map(item =>
|
||||
<SelectOption key={item.value}
|
||||
itemName={item.name}
|
||||
value={item.value}
|
||||
onChange={changeSelectedName}
|
||||
size={size} selected={(selectedValue === item.value)} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@@ -2,16 +2,16 @@ import s from './SelectOption.module.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props{
|
||||
onClick: (name: string, value: string) => void,
|
||||
onChange: (value: string) => void,
|
||||
itemName: string,
|
||||
size: 'base' | 'large',
|
||||
value: string,
|
||||
selected?: boolean,
|
||||
}
|
||||
|
||||
const SelectOption = ({onClick, itemName, size, value, selected} : Props) => {
|
||||
const changeName = () => {
|
||||
onClick(itemName, value)
|
||||
const SelectOption = ({onChange, itemName, size, value, selected} : Props) => {
|
||||
const handleChange = () => {
|
||||
onChange(value)
|
||||
}
|
||||
return(
|
||||
<div className={classNames({
|
||||
@@ -19,7 +19,7 @@ const SelectOption = ({onClick, itemName, size, value, selected} : Props) => {
|
||||
[s[size]] : !!size,
|
||||
[s.isChoose] : selected ,
|
||||
})}
|
||||
onClick = {changeName}
|
||||
onClick = {handleChange}
|
||||
>{itemName}</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
@import '../../../../styles/utilities';
|
||||
|
||||
.skeletonImage {
|
||||
@apply relative;
|
||||
@apply relative overflow-hidden;
|
||||
background: #DDDBDD;
|
||||
|
||||
&.small {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
@import '../../../../styles/utilities';
|
||||
|
||||
.skeletonParagraph {
|
||||
@apply overflow-hidden;
|
||||
margin: 0 1.6rem;
|
||||
|
||||
.row {
|
||||
|
@@ -53,5 +53,6 @@ export { default as InputFiledInForm} from './InputFiledInForm/InputFiledInForm'
|
||||
export { default as MessageCommon} from './MessageCommon/MessageCommon'
|
||||
export { default as FormForgot} from './ForgotPassword/FormForgot/FormForgot'
|
||||
export { default as FormResetPassword} from './ForgotPassword/FormResetPassword/FormResetPassword'
|
||||
|
||||
export { default as ProductCardSkeleton} from './ProductCardSkeleton/ProductCardSkeleton'
|
||||
export { default as ListProductCardSkeleton} from './ListProductCardSkeleton/ListProductCardSkeleton'
|
||||
|
||||
|
@@ -1,43 +0,0 @@
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { filterContextType } from "src/utils/types.utils";
|
||||
|
||||
const contextDefaultValues: filterContextType = {
|
||||
visible: false,
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
const FilterContext = createContext<filterContextType>(contextDefaultValues);
|
||||
|
||||
export function useAuth() {
|
||||
return useContext(FilterContext);
|
||||
}
|
||||
|
||||
type FilterProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function FilterProvider({ children }: FilterProviderProps) {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const value = {
|
||||
visible,
|
||||
open,
|
||||
close,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<FilterContext.Provider value={value}>
|
||||
{children}
|
||||
</FilterContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type ProductFilterContextType = {
|
||||
productFilterVisible: boolean;
|
||||
toggleProductFilter: (visible?: boolean) => void;
|
||||
openProductFilter: () => void;
|
||||
closeProductFilter: () => void;
|
||||
};
|
||||
const DEFAULT_VALUE: ProductFilterContextType = {
|
||||
productFilterVisible: false,
|
||||
toggleProductFilter: () => { },
|
||||
openProductFilter: () => { },
|
||||
closeProductFilter: () => { },
|
||||
};
|
||||
|
||||
export const ProductFilterContext = createContext<ProductFilterContextType>(DEFAULT_VALUE)
|
||||
|
||||
export function useProductFilter() {
|
||||
return useContext(ProductFilterContext);
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { ProductFilterContext } from "./ProductFilterContext";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ProductFilterProvider({ children }: Props) {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const closeProductFilter = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const openProductFilter = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const toggleProductFilter = () => {
|
||||
setVisible(!visible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductFilterContext.Provider value={{productFilterVisible: visible, closeProductFilter, openProductFilter, toggleProductFilter}}>
|
||||
{children}
|
||||
</ProductFilterContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -3,3 +3,6 @@ export * from './CartDrawer/CartDrawerProvider'
|
||||
|
||||
export * from './Message/MessageContext'
|
||||
export * from './Message/MessageProvider'
|
||||
|
||||
export * from './ProductFilter/ProductFilterContext'
|
||||
export * from './ProductFilter/ProductFilterProvider'
|
||||
|
@@ -5,8 +5,8 @@ import useSWR from 'swr';
|
||||
|
||||
|
||||
const useGetAllCollection = () => {
|
||||
const { data, ...rest } = useSWR<GetCollectionsQuery>([getCollectionsNameQuery], gglFetcher)
|
||||
return { collections: data?.collections, ...rest }
|
||||
const { data, isValidating, ...rest } = useSWR<GetCollectionsQuery>([getCollectionsNameQuery], gglFetcher)
|
||||
return { collections: data?.collections.items || [], loading: isValidating, ...rest }
|
||||
}
|
||||
|
||||
export default useGetAllCollection;
|
3
src/components/hooks/facets/index.ts
Normal file
3
src/components/hooks/facets/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useFacets } from './useFacets'
|
||||
|
||||
|
11
src/components/hooks/facets/useFacets.tsx
Normal file
11
src/components/hooks/facets/useFacets.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { GetAllFacetsQuery, QueryFacetsArgs } from '@framework/schema'
|
||||
import { getAllFacetsQuery } from '@framework/utils/queries/get-all-facets-query'
|
||||
import gglFetcher from 'src/utils/gglFetcher'
|
||||
import useSWR from 'swr'
|
||||
|
||||
const useFacets = (options?: QueryFacetsArgs) => {
|
||||
const { data, isValidating, ...rest } = useSWR<GetAllFacetsQuery>([getAllFacetsQuery, options], gglFetcher)
|
||||
return { facets: data?.facets.items, totalItems: data?.facets.totalItems, loading: isValidating, ...rest }
|
||||
}
|
||||
|
||||
export default useFacets
|
4
src/components/hooks/product/index.ts
Normal file
4
src/components/hooks/product/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as useSearchProducts } from './useSearchProducts'
|
||||
export { default as useProductDetail } from './useProductDetail'
|
||||
|
||||
|
16
src/components/hooks/product/useProductDetail.tsx
Normal file
16
src/components/hooks/product/useProductDetail.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GetProductQuery } from '@framework/schema'
|
||||
import { getProductDetailQuery } from '@framework/utils/queries/get-product-query';
|
||||
import gglFetcher from 'src/utils/gglFetcher'
|
||||
import useSWR from 'swr'
|
||||
|
||||
|
||||
interface ProductDetail {
|
||||
slug: string
|
||||
}
|
||||
|
||||
const useProductDetail = () => {
|
||||
const { data, ...rest } = useSWR<GetProductQuery>([getProductDetailQuery],gglFetcher)
|
||||
return { productDetail: data?.product, ...rest }
|
||||
}
|
||||
|
||||
export default useProductDetail
|
13
src/components/hooks/product/useSearchProducts.tsx
Normal file
13
src/components/hooks/product/useSearchProducts.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { GetAllProductsQuery, QuerySearchArgs } from '@framework/schema'
|
||||
import { normalizeSearchResult } from '@framework/utils/normalize'
|
||||
import { getAllProductsQuery } from '@framework/utils/queries/get-all-products-query'
|
||||
import gglFetcher from 'src/utils/gglFetcher'
|
||||
import useSWR from 'swr'
|
||||
|
||||
const useSearchProducts = (options?: QuerySearchArgs) => {
|
||||
const { data, isValidating, ...rest } = useSWR<GetAllProductsQuery>([getAllProductsQuery, options], gglFetcher)
|
||||
|
||||
return { products: data?.search.items.map((item) => normalizeSearchResult(item)), totalItems: data?.search.totalItems, loading: isValidating, ...rest }
|
||||
}
|
||||
|
||||
export default useSearchProducts
|
@@ -43,7 +43,7 @@ const EditInfoModal = ({ accountInfo, visible = false, closeModal }: EditInfoMod
|
||||
|
||||
<div className="flex">
|
||||
<div className={s.inputState}>
|
||||
<SelectCommon type="custom" placeholder="State" option={states} />
|
||||
<SelectCommon type="custom" placeholder="State" options={states} />
|
||||
</div>
|
||||
|
||||
<div className={s.inputPostalCode}>
|
||||
|
@@ -46,7 +46,7 @@ const ShippingInfoForm = ({onConfirm,id}: ShippingInfoFormProps) => {
|
||||
/>
|
||||
<Inputcommon type="text" placeholder="City" ref={cityRef} />
|
||||
<div className={s.line}>
|
||||
<SelectCommon option={option} type="custom" size="large">State</SelectCommon>
|
||||
<SelectCommon options={option} type="custom" size="large">State</SelectCommon>
|
||||
<Inputcommon type="text" placeholder="Zip Code" ref={codeRef} />
|
||||
</div>
|
||||
<Inputcommon
|
||||
|
@@ -12,7 +12,7 @@ interface ColectionCarcouselProps extends CollectionHeadingProps {
|
||||
data: ProductCardProps[]
|
||||
itemKey: string
|
||||
viewAllLink?: string,
|
||||
category:string
|
||||
category?: string
|
||||
}
|
||||
|
||||
const ColectionCarcousel = ({
|
||||
@@ -21,7 +21,8 @@ const ColectionCarcousel = ({
|
||||
title,
|
||||
subtitle,
|
||||
type,
|
||||
category
|
||||
category,
|
||||
viewAllLink = ROUTE.PRODUCTS,
|
||||
}: ColectionCarcouselProps) => {
|
||||
return (
|
||||
<div className={s.colectionCarcoucelWarpper}>
|
||||
@@ -34,7 +35,7 @@ const ColectionCarcousel = ({
|
||||
></CollectionHeading>
|
||||
</div>
|
||||
<div className={s.right}>
|
||||
<ViewAllItem link={`${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=${category}`}/>
|
||||
<ViewAllItem link={category ? `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${category}` : viewAllLink} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.bot}>
|
||||
|
@@ -1,92 +1,89 @@
|
||||
import React from 'react'
|
||||
import { FacetValue } from '@framework/schema'
|
||||
import React, { useMemo } from 'react'
|
||||
import { ResponsiveType } from 'react-multi-carousel'
|
||||
import { CarouselCommon, FeaturedProductCard,HeadingCommon} from 'src/components/common'
|
||||
import { CarouselCommon, FeaturedProductCard, HeadingCommon } from 'src/components/common'
|
||||
import { FeaturedProductCardProps } from 'src/components/common/FeaturedProductCard/FeaturedProductCard'
|
||||
import { getFacetNamesFromIds } from 'src/utils/funtion.utils'
|
||||
import s from "./FeaturedProductsCarousel.module.scss"
|
||||
interface FeaturedProductsCarouselProps {
|
||||
title?: string
|
||||
data: FeaturedProductCardProps[]
|
||||
featuredFacetsValue: FacetValue[],
|
||||
}
|
||||
|
||||
const dataDemo:FeaturedProductCardProps[] = [{
|
||||
title: "Sale 25% Coffee Bean",
|
||||
subTitle: "50 first Orders within a day",
|
||||
originPrice: "$20.00",
|
||||
price: "$14.00",
|
||||
imageSrc: "https://user-images.githubusercontent.com/76099413/133043628-db7813f9-1bb7-4ee1-b028-dc4295563494.png"
|
||||
},{
|
||||
title: "Sale 20% Fruits",
|
||||
subTitle: "50 first Orders within a day",
|
||||
originPrice: "$20.00",
|
||||
price: "$14.00",
|
||||
imageSrc: "https://user-images.githubusercontent.com/76099413/133043630-07a353b9-573d-4c1d-b1de-2c932e3f14f7.png"
|
||||
},{
|
||||
title: "Sale 25% Coffee Bean",
|
||||
subTitle: "50 first Orders within a day",
|
||||
originPrice: "$20.00",
|
||||
price: "$14.00",
|
||||
imageSrc: "https://user-images.githubusercontent.com/76099413/133043633-954c105b-c703-4e5c-8f5f-7943ad633ff0.png"
|
||||
}]
|
||||
const RESPONSIVE: ResponsiveType = {
|
||||
hugeScreen: {
|
||||
breakpoint: { max: 9999, min: 1500 },
|
||||
items: 2.25,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
largeScreen: {
|
||||
breakpoint: { max: 1500, min: 1440 },
|
||||
items: 2.075,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
largeDesktop: {
|
||||
breakpoint: { max: 1440, min: 1280 },
|
||||
items: 1.75,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
desktop: {
|
||||
breakpoint: { max: 1280, min: 1148 },
|
||||
items: 1.5,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
smallDesktop: {
|
||||
breakpoint: { max: 1148, min: 1024 },
|
||||
items: 1.375,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
lap: {
|
||||
breakpoint: { max: 1024, min: 968 },
|
||||
items: 1.7,
|
||||
},
|
||||
tablet: {
|
||||
breakpoint: { max: 968, min: 768 },
|
||||
items: 1.075,
|
||||
},
|
||||
smallTablet: {
|
||||
breakpoint: { max: 768, min: 640 },
|
||||
items: 1.25,
|
||||
},
|
||||
largeMobile: {
|
||||
breakpoint: { max: 640, min: 400 },
|
||||
items: 1.275,
|
||||
},
|
||||
mobile: {
|
||||
breakpoint: { max: 400, min: 300 },
|
||||
items: 1.1,
|
||||
},
|
||||
smallMobile: {
|
||||
breakpoint: { max: 300, min: 0 },
|
||||
items: 1,
|
||||
},
|
||||
}
|
||||
const RESPONSIVE: ResponsiveType = {
|
||||
hugeScreen: {
|
||||
breakpoint: { max: 9999, min: 1500 },
|
||||
items: 2.25,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
largeScreen: {
|
||||
breakpoint: { max: 1500, min: 1440 },
|
||||
items: 2.075,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
largeDesktop: {
|
||||
breakpoint: { max: 1440, min: 1280 },
|
||||
items: 1.75,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
desktop: {
|
||||
breakpoint: { max: 1280, min: 1148 },
|
||||
items: 1.5,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
smallDesktop: {
|
||||
breakpoint: { max: 1148, min: 1024 },
|
||||
items: 1.375,
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
lap: {
|
||||
breakpoint: { max: 1024, min: 968 },
|
||||
items: 1.7,
|
||||
},
|
||||
tablet: {
|
||||
breakpoint: { max: 968, min: 768 },
|
||||
items: 1.075,
|
||||
},
|
||||
smallTablet: {
|
||||
breakpoint: { max: 768, min: 640 },
|
||||
items: 1.25,
|
||||
},
|
||||
largeMobile: {
|
||||
breakpoint: { max: 640, min: 400 },
|
||||
items: 1.275,
|
||||
},
|
||||
mobile: {
|
||||
breakpoint: { max: 400, min: 300 },
|
||||
items: 1.1,
|
||||
},
|
||||
smallMobile: {
|
||||
breakpoint: { max: 300, min: 0 },
|
||||
items: 1,
|
||||
},
|
||||
}
|
||||
|
||||
const FeaturedProductsCarousel = ({title="Featured Products"}: FeaturedProductsCarouselProps) => {
|
||||
return (
|
||||
<div className={s.warpper}>
|
||||
<div className={s.heading}>
|
||||
<HeadingCommon>{title}</HeadingCommon>
|
||||
</div>
|
||||
<CarouselCommon<FeaturedProductCardProps> data={dataDemo} Component={FeaturedProductCard} itemKey="featured-products" responsive={RESPONSIVE}/>
|
||||
</div>
|
||||
)
|
||||
const FeaturedProductsCarousel = ({ data, featuredFacetsValue }: FeaturedProductsCarouselProps) => {
|
||||
const featuredProducts = useMemo(() => {
|
||||
return data.map(item => {
|
||||
return {
|
||||
...item,
|
||||
subText: getFacetNamesFromIds(featuredFacetsValue, item.facetValueIds)
|
||||
}
|
||||
})
|
||||
}, [data, featuredFacetsValue])
|
||||
return (
|
||||
<div className={s.warpper}>
|
||||
<div className={s.heading}>
|
||||
<HeadingCommon>Featured Products</HeadingCommon>
|
||||
</div>
|
||||
<CarouselCommon<FeaturedProductCardProps>
|
||||
data={featuredProducts}
|
||||
defaultComponentProps={featuredFacetsValue}
|
||||
Component={FeaturedProductCard}
|
||||
itemKey="featured-products"
|
||||
responsive={RESPONSIVE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedProductsCarousel
|
||||
|
39
src/components/modules/home/FreshProducts/FreshProducts.tsx
Normal file
39
src/components/modules/home/FreshProducts/FreshProducts.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import { Collection } from '@framework/schema'
|
||||
import React, { useMemo } from 'react'
|
||||
import { OPTION_ALL, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { getCategoryNameFromCollectionId } from 'src/utils/funtion.utils'
|
||||
import { CollectionCarcousel } from '..'
|
||||
interface FreshProductsProps {
|
||||
data: ProductCard[]
|
||||
collections: Collection[]
|
||||
}
|
||||
|
||||
const FreshProducts = ({ data, collections }: FreshProductsProps) => {
|
||||
const dataWithCategory = useMemo(() => {
|
||||
return data.map(item => {
|
||||
return {
|
||||
...item,
|
||||
collection: getCategoryNameFromCollectionId(collections, item.collectionIds ? item.collectionIds[0] : undefined)
|
||||
}
|
||||
})
|
||||
}, [data, collections])
|
||||
|
||||
if (data.length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="w-full">
|
||||
<CollectionCarcousel
|
||||
type="highlight"
|
||||
data={dataWithCategory}
|
||||
itemKey="product-1"
|
||||
title="Fresh Products Today"
|
||||
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
|
||||
viewAllLink={`${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=${OPTION_ALL}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FreshProducts
|
@@ -95,14 +95,6 @@ const dataTest = [
|
||||
const HomeCollection = (props: HomeCollectionProps) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<CollectionCarcousel
|
||||
type="highlight"
|
||||
data={dataTest}
|
||||
itemKey="product-1"
|
||||
title="Fresh Products Today"
|
||||
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
|
||||
category={"veggie"}
|
||||
/>
|
||||
<CollectionCarcousel
|
||||
data={dataTest}
|
||||
itemKey="product-2"
|
||||
|
@@ -4,6 +4,7 @@ export { default as HomeCategories } from './HomeCategories/HomeCategories'
|
||||
export { default as HomeCTA } from './HomeCTA/HomeCTA'
|
||||
export { default as HomeSubscribe } from './HomeSubscribe/HomeSubscribe'
|
||||
export { default as HomeVideo } from './HomeVideo/HomeVideo'
|
||||
export { default as FreshProducts } from './FreshProducts/FreshProducts'
|
||||
export { default as HomeCollection } from './HomeCollection/HomeCollection'
|
||||
export { default as HomeRecipe } from './HomeRecipe/HomeRecipe'
|
||||
export { default as FeaturedProductsCarousel } from './FeaturedProductsCarousel/FeaturedProductsCarousel'
|
||||
|
@@ -1,20 +1,28 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react';
|
||||
import ProductImgs from './components/ProductImgs/ProductImgs'
|
||||
import ProductInfo from './components/ProductInfo/ProductInfo'
|
||||
import s from './ProductInfoDetail.module.scss'
|
||||
import { Product } from '@commerce/types/product'
|
||||
import { Collection } from '@framework/schema'
|
||||
import { getCategoryNameFromCollectionId } from 'src/utils/funtion.utils';
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
productDetail: Product,
|
||||
collections: Collection[]
|
||||
}
|
||||
|
||||
const ProductInfoDetail = ({ }: Props) => {
|
||||
const ProductInfoDetail = ({ productDetail, collections }: Props) => {
|
||||
const dataWithCategoryName = useMemo(() => {
|
||||
return {
|
||||
...productDetail,
|
||||
collection: getCategoryNameFromCollectionId(collections, productDetail.collectionIds ? productDetail.collectionIds[0] : undefined)
|
||||
}
|
||||
}, [productDetail, collections])
|
||||
return (
|
||||
<section className={s.productInfoDetail}>
|
||||
<ProductImgs/>
|
||||
<ProductInfo/>
|
||||
<ProductImgs productImage={productDetail.images}/>
|
||||
<ProductInfo productInfoDetail={dataWithCategoryName}/>
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductInfoDetail
|
||||
|
@@ -3,15 +3,15 @@ import { ImgWithLink } from 'src/components/common'
|
||||
import s from './ProductImgItem.module.scss'
|
||||
|
||||
export interface ProductImgItemProps {
|
||||
src: string
|
||||
url: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
|
||||
const ProductImgItem = ({ src, alt }: ProductImgItemProps) => {
|
||||
const ProductImgItem = ({ url, alt }: ProductImgItemProps) => {
|
||||
return (
|
||||
<section className={s.productImgItem}>
|
||||
<ImgWithLink src={src} alt={alt} />
|
||||
<ImgWithLink src={url} alt={alt} />
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
@@ -3,26 +3,12 @@ import { ResponsiveType } from 'react-multi-carousel'
|
||||
import { CarouselCommon } from 'src/components/common'
|
||||
import ProductImgItem, { ProductImgItemProps } from '../ProductImgItem/ProductImgItem'
|
||||
import s from './ProductImgs.module.scss'
|
||||
import { ProductImage } from '@commerce/types/product';
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any,
|
||||
productImage: ProductImage[]
|
||||
}
|
||||
|
||||
const DATA = [
|
||||
{
|
||||
src: 'https://user-images.githubusercontent.com/76729908/133026929-199799fc-bd75-4445-a24d-15c0e41796eb.png',
|
||||
alt: 'Meat',
|
||||
},
|
||||
{
|
||||
src: 'https://user-images.githubusercontent.com/76729908/130574371-3b75fa72-9552-4605-aba9-a4b31cd9dce7.png',
|
||||
alt: 'Broccoli',
|
||||
},
|
||||
{
|
||||
src: 'https://user-images.githubusercontent.com/76729908/130574371-3b75fa72-9552-4605-aba9-a4b31cd9dce7.png',
|
||||
alt: 'Broccoli',
|
||||
}
|
||||
]
|
||||
|
||||
const RESPONSIVE: ResponsiveType = {
|
||||
desktop: {
|
||||
@@ -31,11 +17,11 @@ const RESPONSIVE: ResponsiveType = {
|
||||
slidesToSlide: 1, // optional, default to 1.
|
||||
},
|
||||
}
|
||||
const ProductImgs = ({ }: Props) => {
|
||||
const ProductImgs = ({ productImage }: Props) => {
|
||||
return (
|
||||
<section className={s.productImgs}>
|
||||
<CarouselCommon<ProductImgItemProps>
|
||||
data={DATA}
|
||||
data={productImage}
|
||||
itemKey="product-detail-img"
|
||||
Component={ProductImgItem}
|
||||
responsive={RESPONSIVE}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { Product } from '@commerce/types/product'
|
||||
import React from 'react'
|
||||
import { ButtonCommon, LabelCommon, QuanittyInput } from 'src/components/common'
|
||||
import { IconBuy } from 'src/components/icons'
|
||||
@@ -5,25 +6,25 @@ import { LANGUAGE } from 'src/utils/language.utils'
|
||||
import s from './ProductInfo.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any,
|
||||
productInfoDetail: Product
|
||||
}
|
||||
|
||||
const ProductInfo = ({ }: Props) => {
|
||||
const ProductInfo = ({ productInfoDetail }: Props) => {
|
||||
console.log(productInfoDetail)
|
||||
return (
|
||||
<section className={s.productInfo}>
|
||||
<div className={s.info}>
|
||||
<LabelCommon shape='half'>SEAFOOD</LabelCommon>
|
||||
<h2 className={s.heading}>SeaPAk</h2>
|
||||
<LabelCommon shape='half'>{productInfoDetail.collection}</LabelCommon>
|
||||
<h2 className={s.heading}>{productInfoDetail.name}</h2>
|
||||
<div className={s.price}>
|
||||
<div className={s.old}>
|
||||
<span className={s.number}>Rp 32.000</span>
|
||||
<span className={s.number}>Rp {productInfoDetail.price}</span>
|
||||
<LabelCommon type='discount'>-15%</LabelCommon>
|
||||
</div>
|
||||
<div className={s.current}>Rp 27.500</div>
|
||||
<div className={s.current}>Rp {productInfoDetail.price}</div>
|
||||
</div>
|
||||
<div className={s.description}>
|
||||
In a large non-reactive dish, mix together the orange juice, soy sauce, olive oil, lemon juice, parsley
|
||||
{productInfoDetail.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.actions}>
|
||||
|
@@ -1,13 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ProductCard } from '@commerce/types/product';
|
||||
import { Collection } from '@framework/schema';
|
||||
import React, { useMemo } from 'react';
|
||||
import ListProductWithInfo from 'src/components/common/ListProductWithInfo/ListProductWithInfo';
|
||||
import { PRODUCT_DATA_TEST } from 'src/utils/demo-data';
|
||||
import { getCategoryNameFromCollectionId } from 'src/utils/funtion.utils';
|
||||
|
||||
const ReleventProducts = () => {
|
||||
interface Props {
|
||||
data: ProductCard[]
|
||||
collections: Collection[]
|
||||
|
||||
}
|
||||
|
||||
const ReleventProducts = ({ data, collections }: Props) => {
|
||||
const dataWithCategoryName = useMemo(() => {
|
||||
return data.map(item => {
|
||||
return {
|
||||
...item,
|
||||
collection: getCategoryNameFromCollectionId(collections, item.collectionIds ? item.collectionIds[0] : undefined)
|
||||
}
|
||||
})
|
||||
}, [data, collections])
|
||||
|
||||
if (data.length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ListProductWithInfo
|
||||
title="Relevant Products"
|
||||
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
|
||||
data={PRODUCT_DATA_TEST}
|
||||
data={dataWithCategoryName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@@ -13,72 +13,59 @@
|
||||
@screen md {
|
||||
@apply flex;
|
||||
}
|
||||
.categories{
|
||||
padding-right: 2.4rem;
|
||||
@apply hidden;
|
||||
@screen md {
|
||||
@apply block;
|
||||
}
|
||||
@screen xl{
|
||||
@apply block;
|
||||
width:25%;
|
||||
}
|
||||
}
|
||||
.list{
|
||||
@screen md {
|
||||
@apply flex justify-between flex-wrap w-full;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
@screen xl {
|
||||
width:75%;
|
||||
}
|
||||
.inner{
|
||||
@screen md {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
@apply w-full;
|
||||
.top {
|
||||
.left {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
.boxItem {
|
||||
@screen md {
|
||||
@apply flex justify-between flex-wrap;
|
||||
margin: 1rem 0;
|
||||
|
||||
.iconFilter {
|
||||
@apply relative;
|
||||
&:focus {
|
||||
outline: none;
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
.item {
|
||||
@screen md {
|
||||
width: calc(97% / 2);
|
||||
margin-top:1rem;
|
||||
}
|
||||
@screen lg{
|
||||
width: calc(97% / 3);
|
||||
margin-top:1rem;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--text-active);
|
||||
}
|
||||
.dot {
|
||||
@apply absolute;
|
||||
top: -0.08rem;
|
||||
right: -0.2rem;
|
||||
background-color: var(--negative);
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 1.2rem;
|
||||
}
|
||||
}
|
||||
@screen md {
|
||||
@apply flex justify-between flex-wrap w-full;
|
||||
margin: 1rem 0;
|
||||
.iconFilter {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.boxSelect{
|
||||
@apply w-auto;
|
||||
// padding: 2.5rem 0;
|
||||
display: none;
|
||||
|
||||
@screen xl {
|
||||
@screen md {
|
||||
@apply block;
|
||||
width: auto;
|
||||
padding:0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.categorySelectCate{
|
||||
@screen xl {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
label{
|
||||
|
||||
label {
|
||||
@apply font-bold topline ;
|
||||
color:var(--text-active);
|
||||
@screen xl {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.select{
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,12 +1,25 @@
|
||||
import React from 'react'
|
||||
import { HeadingCommon, ProductList, SelectCommon } from 'src/components/common'
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import { Collection, Facet, FacetValue, QuerySearchArgs } from '@framework/schema'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { HeadingCommon, ListProductCardSkeleton, ProductList } from 'src/components/common'
|
||||
import BreadcrumbCommon from 'src/components/common/BreadcrumbCommon/BreadcrumbCommon'
|
||||
import MenuNavigation from 'src/components/common/MenuNavigation/MenuNavigation'
|
||||
import { BRAND, CATEGORY, FEATURED} from 'src/utils/constanst.utils'
|
||||
import { PRODUCT_DATA_TEST_PAGE } from 'src/utils/demo-data'
|
||||
import { useProductFilter } from 'src/components/contexts'
|
||||
import { useSearchProducts } from 'src/components/hooks/product'
|
||||
import { IconFilter } from 'src/components/icons'
|
||||
import { DEFAULT_PAGE_SIZE, QUERY_KEY, QUERY_SPLIT_SEPERATOR, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { getFacetIdsFromCodes, getPageFromQuery, getProductSortParamFromQuery } from 'src/utils/funtion.utils'
|
||||
import s from './ProductListFilter.module.scss'
|
||||
import ProductsMenuNavigationTablet from './ProductsMenuNavigationTablet/ProductsMenuNavigationTablet'
|
||||
import ProductSort from './ProductSort/ProductSort'
|
||||
|
||||
interface ProductListFilterProps {}
|
||||
interface ProductListFilterProps {
|
||||
facets: Facet[]
|
||||
collections: Collection[]
|
||||
products: ProductCard[]
|
||||
total: number
|
||||
|
||||
}
|
||||
|
||||
const BREADCRUMB = [
|
||||
{
|
||||
@@ -14,49 +27,98 @@ const BREADCRUMB = [
|
||||
link: `#`,
|
||||
},
|
||||
]
|
||||
const OPTIONSLECT = [
|
||||
{
|
||||
name: 'Most Viewed',
|
||||
value: 'most-viewed',
|
||||
},
|
||||
{
|
||||
name: 'Lastest Products',
|
||||
value: 'lastest-products',
|
||||
},
|
||||
{
|
||||
name: 'Recent Products',
|
||||
value: 'recent-products',
|
||||
},
|
||||
]
|
||||
|
||||
const onModalClose = () => {
|
||||
|
||||
|
||||
const DEFAULT_SEARCH_ARGS = {
|
||||
groupByProduct: true, take: DEFAULT_PAGE_SIZE
|
||||
}
|
||||
|
||||
const ProductListFilter = (props: ProductListFilterProps) => {
|
||||
const ProductListFilter = ({ facets, collections, products, total }: ProductListFilterProps) => {
|
||||
const router = useRouter()
|
||||
const { openProductFilter } = useProductFilter()
|
||||
const [initialQueryFlag, setInitialQueryFlag] = useState<boolean>(true)
|
||||
const [optionQueryProduct, setOptionQueryProduct] = useState<QuerySearchArgs>({ input: DEFAULT_SEARCH_ARGS })
|
||||
const { products: productSearchResult, totalItems, loading } = useSearchProducts(optionQueryProduct)
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const page = getPageFromQuery(router.query[QUERY_KEY.PAGE] as string)
|
||||
setCurrentPage(page)
|
||||
}, [router.query])
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
query: {
|
||||
...router.query,
|
||||
[QUERY_KEY.PAGE]: page
|
||||
}
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const query = { input: { ...DEFAULT_SEARCH_ARGS } } as QuerySearchArgs
|
||||
|
||||
const page = getPageFromQuery(router.query[QUERY_KEY.PAGE] as string)
|
||||
query.input.skip = page * DEFAULT_PAGE_SIZE
|
||||
|
||||
|
||||
const sortQuery = router.query[QUERY_KEY.SORTBY] as string
|
||||
if (sortQuery) {
|
||||
query.input.sort = getProductSortParamFromQuery(sortQuery)
|
||||
}
|
||||
|
||||
// collections
|
||||
const categoryQuery = router.query[QUERY_KEY.CATEGORY] as string
|
||||
if (categoryQuery) {
|
||||
query.input.collectionSlug = categoryQuery
|
||||
}
|
||||
|
||||
// facets
|
||||
const facetsQuery = [router.query[QUERY_KEY.FEATURED] as string, router.query[QUERY_KEY.BRAND] as string].join(QUERY_SPLIT_SEPERATOR)
|
||||
if (facetsQuery) {
|
||||
const facetsValue = [] as FacetValue[]
|
||||
facets.map((item: Facet) => {
|
||||
facetsValue.push(...item.values)
|
||||
return null
|
||||
})
|
||||
|
||||
query.input.facetValueIds = getFacetIdsFromCodes(facetsValue, facetsQuery.split(QUERY_SPLIT_SEPERATOR))
|
||||
}
|
||||
|
||||
setOptionQueryProduct(query)
|
||||
setInitialQueryFlag(false)
|
||||
}, [router.query, facets])
|
||||
|
||||
return (
|
||||
<div className={s.warpper}>
|
||||
<div className={s.breadcrumb}>
|
||||
<BreadcrumbCommon crumbs={BREADCRUMB} />
|
||||
</div>
|
||||
<div className={s.main}>
|
||||
<div className={s.categories}>
|
||||
<MenuNavigation categories={CATEGORY} heading="Categories" />
|
||||
<MenuNavigation categories={BRAND} heading="Brands" />
|
||||
<MenuNavigation categories={FEATURED} heading="featured" />
|
||||
</div>
|
||||
|
||||
<ProductsMenuNavigationTablet facets={facets} collections={collections} />
|
||||
<div className={s.list}>
|
||||
<HeadingCommon align="left">SPECIAL RECIPES</HeadingCommon>
|
||||
<div className={s.top}>
|
||||
<div className={s.left}>
|
||||
<HeadingCommon align="left">SPECIAL RECIPES</HeadingCommon>
|
||||
<button className={s.iconFilter} onClick={openProductFilter}>
|
||||
<IconFilter />
|
||||
<div className={s.dot}></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={s.boxSelect}>
|
||||
<div className={s.categorySelectSort}>
|
||||
<div className={s.select}>
|
||||
<SelectCommon option={OPTIONSLECT} placeholder="Sort By" />
|
||||
</div>
|
||||
<div className={s.boxSelect}>
|
||||
<ProductSort />
|
||||
</div>
|
||||
</div>
|
||||
<ProductList data={PRODUCT_DATA_TEST_PAGE} />
|
||||
{
|
||||
(!initialQueryFlag && loading && !productSearchResult) && <ListProductCardSkeleton count={DEFAULT_PAGE_SIZE} isWrap />
|
||||
}
|
||||
<ProductList data={initialQueryFlag ? products : (productSearchResult || [])} total={totalItems !== undefined ? totalItems : total} onPageChange={onPageChange} defaultCurrentPage={currentPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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<string>();
|
||||
|
||||
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 (
|
||||
<SelectCommon
|
||||
options={OPTIONS_SORT_PRODUCT}
|
||||
placeholder="Sort By"
|
||||
value={sortValue}
|
||||
onChange={onSortChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductSort;
|
@@ -0,0 +1,12 @@
|
||||
|
||||
.productsMenuNavigationTablet {
|
||||
@apply hidden;
|
||||
@screen md {
|
||||
@apply block;
|
||||
padding-right: 2.4rem;
|
||||
}
|
||||
@screen xl {
|
||||
@apply block;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
@@ -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 (
|
||||
<div className={s.productsMenuNavigationTablet}>
|
||||
<MenuNavigation
|
||||
heading="Categories"
|
||||
categories={collections}
|
||||
queryKey={QUERY_KEY.CATEGORY}
|
||||
isSingleSelect={true} />
|
||||
{
|
||||
facets.map(item => <MenuNavigation
|
||||
key={item.id}
|
||||
queryKey={item.code}
|
||||
categories={item.values}
|
||||
heading={item.name} />)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductsMenuNavigationTablet
|
@@ -189,13 +189,13 @@ const RecipesList = ({ data =recipe}:Props) => {
|
||||
<div className={s.categorySelectCate}>
|
||||
<label htmlFor="">Categories</label>
|
||||
<div className={s.select}>
|
||||
<SelectCommon option={CATEGORYSELECT} placeholder="Categories"/>
|
||||
<SelectCommon options={CATEGORYSELECT} placeholder="Categories"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.categorySelectSort}>
|
||||
<label htmlFor="" >Sort By</label>
|
||||
<div className={s.select}>
|
||||
<SelectCommon option={OPTIONSLECT} placeholder="Sort By" />
|
||||
<SelectCommon options={OPTIONSLECT} placeholder="Sort By" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
:root {
|
||||
--primary: #5b9a74;
|
||||
--primary-light: #e3f2e9;
|
||||
--primary-lightest: #effaf4;
|
||||
--primary-lightest: #F1F8F4;
|
||||
|
||||
--info-dark: #00317a;
|
||||
--info: #3468B7;
|
||||
|
@@ -1,135 +1,177 @@
|
||||
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
|
||||
|
||||
export const SOCIAL_LINKS = {
|
||||
FB: 'FB',
|
||||
TWITTER: 'TWITTER',
|
||||
YOUTUBE: 'YOUTUBE',
|
||||
IG: 'IG',
|
||||
FB: 'FB',
|
||||
TWITTER: 'TWITTER',
|
||||
YOUTUBE: 'YOUTUBE',
|
||||
IG: 'IG',
|
||||
}
|
||||
|
||||
export const ROUTE = {
|
||||
HOME: '/',
|
||||
ABOUT: '/about',
|
||||
ACCOUNT: '/account',
|
||||
HOME: '/',
|
||||
ABOUT: '/about',
|
||||
ACCOUNT: '/account',
|
||||
|
||||
PRODUCTS: '/products',
|
||||
PRODUCT_DETAIL: '/product',
|
||||
|
||||
BLOGS: '/blogs',
|
||||
BLOG_DETAIL: '/blog',
|
||||
PRODUCTS: '/products',
|
||||
PRODUCT_DETAIL: '/product',
|
||||
|
||||
RECIPES: '/recipes',
|
||||
RECIPE_DETAIL: '/recipe',
|
||||
BLOGS: '/blogs',
|
||||
BLOG_DETAIL: '/blog',
|
||||
|
||||
NOTIFICATION: '/notifications',
|
||||
BUSSINESS: '/bussiness',
|
||||
CONTACT: '/contact',
|
||||
CHECKOUT: '/checkout',
|
||||
FAQ: '/faq',
|
||||
CUSTOMER_SERVICE: '/customer-service',
|
||||
TERM_CONDITION: '/term-condition',
|
||||
PRIVACY_POLICY: '/privacy-policy',
|
||||
FORGOT_PASSWORD: '/forgot-password'
|
||||
RECIPES: '/recipes',
|
||||
RECIPE_DETAIL: '/recipe',
|
||||
|
||||
NOTIFICATION: '/notifications',
|
||||
BUSSINESS: '/bussiness',
|
||||
CONTACT: '/contact',
|
||||
CHECKOUT: '/checkout',
|
||||
FAQ: '/faq',
|
||||
CUSTOMER_SERVICE: '/customer-service',
|
||||
TERM_CONDITION: '/term-condition',
|
||||
PRIVACY_POLICY: '/privacy-policy',
|
||||
FORGOT_PASSWORD: '/forgot-password'
|
||||
}
|
||||
|
||||
export const ACCOUNT_TAB = {
|
||||
CUSTOMER_INFO: '',
|
||||
ORDER: 'orders',
|
||||
FAVOURITE: 'wishlist',
|
||||
CUSTOMER_INFO: '',
|
||||
ORDER: 'orders',
|
||||
FAVOURITE: 'wishlist',
|
||||
}
|
||||
|
||||
export const LOCAL_STORAGE_KEY = {
|
||||
TOKEN: 'token'
|
||||
}
|
||||
|
||||
export const QUERY_SPLIT_SEPERATOR = ','
|
||||
export const QUERY_KEY = {
|
||||
TAB: 'tab',
|
||||
CATEGORY: 'category',
|
||||
BRAND: 'brand',
|
||||
FEATURED: 'feature',
|
||||
SORTBY:'sortby',
|
||||
RECIPES:'recipes'
|
||||
TAB: 'tab',
|
||||
CATEGORY: 'category',
|
||||
BRAND: 'brand',
|
||||
FEATURED: 'featured',
|
||||
SORTBY: 'sortby',
|
||||
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 {
|
||||
BestSellers = 'Best Sellers',
|
||||
Sales = 'Sales',
|
||||
NewItem = 'New Item',
|
||||
Viewed = 'Viewed',
|
||||
BestSellers = 'Best Sellers',
|
||||
Sales = 'Sales',
|
||||
NewItem = 'New Item',
|
||||
Viewed = 'Viewed',
|
||||
}
|
||||
|
||||
export const KEY = {
|
||||
ENTER: 'Enter',
|
||||
ENTER: 'Enter',
|
||||
}
|
||||
|
||||
export const OPTION_ALL = 'all';
|
||||
export const DEFAULT_PAGE_SIZE=20;
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
|
||||
export const CATEGORY = [
|
||||
{
|
||||
name: 'All',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${OPTION_ALL}`,
|
||||
},
|
||||
{
|
||||
name: 'Veggie',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=veggie`,
|
||||
},
|
||||
{
|
||||
name: 'Seafood',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=seafood`,
|
||||
},
|
||||
{
|
||||
name: 'Frozen',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=frozen`,
|
||||
},
|
||||
{
|
||||
name: 'Coffee Bean',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=coffee_bean`,
|
||||
},
|
||||
{
|
||||
name: 'Sauce',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=sauce`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BRAND = [
|
||||
{
|
||||
name: 'Maggi',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=maggi`,
|
||||
},
|
||||
{
|
||||
name: 'Chomilex',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chomilex`,
|
||||
},
|
||||
{
|
||||
name: 'Chinsu',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chinsu`,
|
||||
},
|
||||
]
|
||||
|
||||
export const FEATURED = [
|
||||
{
|
||||
name: 'Best Sellers',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=best_sellers`,
|
||||
},
|
||||
{
|
||||
name: 'Sales',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=sales`,
|
||||
},
|
||||
{
|
||||
name: 'New Item',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=new_item`,
|
||||
},
|
||||
{
|
||||
name: 'Viewed',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=viewed`,
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_BLOG_PAGE_SIZE=6;
|
||||
{
|
||||
name: 'All',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${OPTION_ALL}`,
|
||||
},
|
||||
{
|
||||
name: 'Veggie',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=veggie`,
|
||||
},
|
||||
{
|
||||
name: 'Seafood',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=seafood`,
|
||||
},
|
||||
{
|
||||
name: 'Frozen',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=frozen`,
|
||||
},
|
||||
{
|
||||
name: 'Coffee Bean',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=coffee_bean`,
|
||||
},
|
||||
{
|
||||
name: 'Sauce',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=sauce`,
|
||||
},
|
||||
]
|
||||
|
||||
export const FILTER_PAGE = [ROUTE.HOME,ROUTE.PRODUCTS]
|
||||
export const BRAND = [
|
||||
{
|
||||
name: 'Maggi',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=maggi`,
|
||||
},
|
||||
{
|
||||
name: 'Chomilex',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chomilex`,
|
||||
},
|
||||
{
|
||||
name: 'Chinsu',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chinsu`,
|
||||
},
|
||||
]
|
||||
|
||||
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',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=best_sellers`,
|
||||
},
|
||||
{
|
||||
name: 'Sales',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=sales`,
|
||||
},
|
||||
{
|
||||
name: 'New Item',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=new_item`,
|
||||
},
|
||||
{
|
||||
name: 'Viewed',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=viewed`,
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_BLOG_PAGE_SIZE = 6;
|
||||
|
||||
export const FILTER_PAGE = [ROUTE.HOME, ROUTE.PRODUCTS]
|
||||
|
||||
export const STATE_OPTIONS = [
|
||||
{
|
||||
@@ -141,3 +183,4 @@ export const STATE_OPTIONS = [
|
||||
value: 'Hà Nội',
|
||||
},
|
||||
]
|
||||
|
||||
|
@@ -1,11 +1,130 @@
|
||||
import { Facet } from "@commerce/types/facet";
|
||||
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
|
||||
return window.innerWidth < 768
|
||||
}
|
||||
|
||||
export function removeItem<T>(arr: Array<T>, value: T): Array<T> {
|
||||
const index = arr.indexOf(value);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
export function getPageFromQuery(pageQuery: string) {
|
||||
let page = 0
|
||||
try {
|
||||
page = +pageQuery
|
||||
if (isNaN(page)) {
|
||||
page = 0
|
||||
}
|
||||
return [...arr];
|
||||
} 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<T>(arr: Array<T>, value: T): Array<T> {
|
||||
const index = arr.indexOf(value);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
return [...arr];
|
||||
}
|
||||
|
||||
function findFacetByCode(code: string, facets?: Facet) {
|
||||
return facets?.values.find((item: FacetValue) => item.code === code)
|
||||
}
|
||||
|
||||
export function getFeaturedFacetId(facets: Facet[]) {
|
||||
const featuredFacet = facets.find((item: Facet) => item.code === CODE_FACET_FEATURED)
|
||||
return featuredFacet?.id
|
||||
}
|
||||
|
||||
export function getFreshFacetId(facets: Facet[]) {
|
||||
const featuredFacet = facets.find((item: Facet) => item.code === CODE_FACET_FEATURED)
|
||||
const freshFacetValue = findFacetByCode(CODE_FACET_FEATURED_VARIANT.FRESH, featuredFacet)
|
||||
|
||||
return freshFacetValue?.id
|
||||
}
|
||||
|
||||
export function getAllFacetValueIdsByParentCode(facets: Facet[], code: string) {
|
||||
const featuredFacet = facets.find((item: Facet) => item.code === code)
|
||||
const rs = featuredFacet?.values.map((item: FacetValue) => item.id)
|
||||
|
||||
return rs || []
|
||||
}
|
||||
|
||||
export function getAllFacetValuesForFeatuedProducts(facets: Facet[]) {
|
||||
const facetsRs = facets.filter((item: Facet) => item.code === CODE_FACET_FEATURED || item.code === CODE_FACET_DISCOUNT)
|
||||
let rs = [] as FacetValue[]
|
||||
facetsRs.map((item: Facet) => {
|
||||
rs = rs.concat(item.values)
|
||||
return null
|
||||
})
|
||||
return rs
|
||||
}
|
||||
|
||||
export function getFacetNamesFromIds(facets: FacetValue[], ids?: string[]): string {
|
||||
if (!ids || ids?.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const facetItems = facets.filter((item: FacetValue) => ids.includes(item.id))
|
||||
const names = facetItems.map((item: FacetValue) => item.name)
|
||||
return names.join(", ")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@@ -34,20 +34,31 @@ 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;
|
||||
close: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
export type PromiseWithKey = {
|
||||
key: string
|
||||
promise: PromiseLike<any>
|
||||
keyResult?: string,
|
||||
}
|
||||
|
Reference in New Issue
Block a user