feat: get-blog-detail and relevant blogs

This commit is contained in:
Quangnhankie
2021-10-20 10:11:07 +07:00
parent abea83cdd6
commit 7ea734d0ad
16 changed files with 459 additions and 98 deletions

View File

@@ -11,8 +11,12 @@ import type {
} from '../types/product'
import type {
GetAllBlogsOperation,
GetFeaturedOperation
GetFeaturedOperation,
GetAllBlogPathsOperation,
GetBlogDetailOperation,
GetRelevantBlogsOperation
} from '../types/blogs'
import type { APIProvider, CommerceAPI } from '.'
import { GetAllCollectionsOperation } from '@commerce/types/collection';
@@ -32,7 +36,10 @@ export const OPERATIONS = [
'getAllFacets',
'getAllCollections',
'getAllBlogs',
'getFeaturedBlog'
'getFeaturedBlog',
'getAllBlogPaths',
'getBlogDetail',
'getRelevantBlogs'
] as const
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
@@ -133,6 +140,21 @@ export type Operations<P extends APIProvider> = {
): Promise<T['data']>
}
getAllBlogPaths: {
<T extends GetAllBlogPathsOperation>(opts: {
variables?: T['variables']
config?: P['config']
}): Promise<T['data']>
<T extends GetAllBlogPathsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
} & OperationOptions
): Promise<T['data']>
}
getAllProducts: {
<T extends GetAllProductsOperation>(opts: {
variables?: T['variables']
@@ -166,6 +188,22 @@ export type Operations<P extends APIProvider> = {
): Promise<T['data']>
}
getRelevantBlogs: {
<T extends GetRelevantBlogsOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetRelevantBlogsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getFeaturedBlog: {
<T extends GetAllBlogsOperation>(opts: {
variables?: T['variables']
@@ -182,6 +220,21 @@ export type Operations<P extends APIProvider> = {
): Promise<T['data']>
}
getBlogDetail: {
<T extends GetBlogDetailOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetBlogDetailOperation>(
opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getProduct: {

View File

@@ -1,4 +1,4 @@
import { SearchResultSortParameter } from "@framework/schema";
import { Asset, BlogTranslation, Maybe, Product } from './../../vendure/schema.d';
export type BlogList = Node &{
@@ -15,13 +15,27 @@ export type BlogsType = {
totalItems: number
}
export type GetAllBlogsOperation<T extends BlogsType = BlogsType> = {
data: { items: T['items'][] }
data: { items: T['items'][], totalItems: number }
variables: {
productId: number,
take?: number
skip?: number
}
}
export type GetRelevantBlogsOperation<T extends BlogsType = BlogsType> = {
data: { items: T['items'][], totalItems: number }
variables: {
take?: number
skip?: number
}
}
export type GetBlogDetailOperation<T extends BlogsType = BlogsType> = {
data: T['items'],
variables: {
slug?: string
}
}
export type GetFeaturedOperation<T extends BlogsType = BlogsType> = {
data: { items: T['items'][] }
@@ -29,4 +43,10 @@ export type GetFeaturedOperation<T extends BlogsType = BlogsType> = {
take?: number
skip?: number
}
}
}
export type GetAllBlogPathsOperation<
T extends BlogsType = BlogsType
> = {
data: { blogs: Pick<T['items'], 'translations'>[] }
variables: { first?: number }
}

View File

@@ -1,3 +1,4 @@
import { BlogsType } from './blogs';
import { CurrencyCode } from './../../vendure/schema.d';
import { FacetValueFilterInput, LogicalOperator, SearchResultSortParameter } from "@framework/schema"
@@ -110,6 +111,8 @@ export type GetAllProductPathsOperation<
variables: { first?: number }
}
export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
data: { products: T['product'][] }
variables: {

View File

@@ -1,18 +1,21 @@
import type { CommerceAPIConfig } from '@commerce/api'
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
import getAllFacets from './operations/get-all-facets'
import getAllBlogs from './operations/get-all-blogs'
import getAllCollections from './operations/get-all-collection'
import getAllFacets from './operations/get-all-facets'
import getAllPages from './operations/get-all-pages'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getBlogDetail from './operations/get-blog-detail'
import getCustomerWishlist from './operations/get-customer-wishlist'
import getFeaturedBlog from './operations/get-featured-blog'
import getPage from './operations/get-page'
import getProduct from './operations/get-product'
import getSiteInfo from './operations/get-site-info'
import getAllBlogPaths from './operations/get-all-blog-paths'
import getRelevantBlogs from './operations/get-relevant-blogs'
import login from './operations/login'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import getAllBlogs from './operations/get-all-blogs'
import getFeaturedBlog from './operations/get-featured-blog'
export interface VendureConfig extends CommerceAPIConfig {}
@@ -46,7 +49,10 @@ const operations = {
getAllFacets,
getAllCollections,
getAllBlogs,
getFeaturedBlog
getFeaturedBlog,
getBlogDetail,
getAllBlogPaths,
getRelevantBlogs
}
export const provider = { config, operations }

View File

@@ -0,0 +1,53 @@
import { BlogList } from './../../schema.d';
import { OperationContext,OperationOptions } from '@commerce/api/operations';
import { BigcommerceConfig } from '../../../bigcommerce/api';
import type { GetAllBlogPathsQuery,BlogTranslation } from '../../schema';
import { getAllBlogPathsQuery } from '../../utils/queries/get-all-blog-paths-query';
import { Provider } from '../index';
import { GetAllBlogPathsOperation } from './../../../commerce/types/blogs';
export type GetAllBlogPathsResult = {
blogs: Array<{ node: { path: string } }>
}
export default function getAllBlogPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllBlogPaths<
T extends GetAllBlogPathsOperation
>(opts?: {
variables?: T['variables']
config?: BigcommerceConfig
}): Promise<T['data']>
async function getAllBlogPaths<T extends GetAllBlogPathsOperation>(
opts: {
variables?: T['variables']
config?: BigcommerceConfig
} & OperationOptions
): Promise<T['data']>
async function getAllBlogPaths<T extends GetAllBlogPathsOperation>({
query = getAllBlogPathsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: T['variables']
config?: BigcommerceConfig
} = {}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
const { data } = await config.fetch<GetAllBlogPathsQuery>(query, {
variables,
})
const blogs = data.blogs.items;
return {
blogs: blogs?.map(val=>val.translations.map((p:BlogTranslation) => ({ path: `/${p.slug}` })))
}
}
return getAllBlogPaths
}

View File

@@ -0,0 +1,55 @@
import { OperationContext } from '@commerce/api/operations'
import { Provider, VendureConfig } from '..'
import { GetBlogQuery,BlogList } from '../../schema'
import { getBlogDetailQuery } from '../../utils/queries/get-blog-detail'
export type BlogVariables = {
slug?: string,
}
export default function getBlogDetailOperation({
commerce,
}: OperationContext<Provider>) {
async function getBlogDetail(opts?: {
variables?: BlogVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ blogDetail: BlogList}>
async function getBlogDetail({
query = getBlogDetailQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: BlogVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ blogDetail: BlogList | any }> {
const config = commerce.getConfig(cfg)
const variables = {
slug: vars.slug
}
const { data } = await config.fetch<GetBlogQuery>(query, {
variables,
})
return {
blogDetail: {
id:data?.blog?.id,
title: data?.blog?.translations[0].title,
imageSrc: data?.blog?.featuredAsset?.preview ?? null,
slug: data?.blog?.translations[0]?.slug,
description: data?.blog?.translations[0]?.description,
isPublish: data?.blog?.isPublish,
isFeatured: data?.blog?.isFeatured,
authorName: data?.blog?.authorName,
authorAvatarAsset : data?.blog?.authorAvatarAsset?.preview,
createdAt: data?.blog?.createdAt,
relevantProducts: data?.blog?.relevantProducts.map(val=>val.id)
}
}
}
return getBlogDetail
}

View File

@@ -0,0 +1,54 @@
import { OperationContext } from '@commerce/api/operations'
import { Provider, VendureConfig } from '..'
import { BlogList,GetRelevantBlogsQuery } from '../../schema'
import { getRelevantBlogsQuery } from '../../utils/queries/get-relevant-blogs'
export type BlogVariables = {
productId?: number,
}
export default function getRelevantBlogsOperation({
commerce,
}: OperationContext<Provider>) {
async function getRelevantBlogs(opts?: {
variables?: BlogVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ relevantBlogs: GetRelevantBlogsQuery[]}>
async function getRelevantBlogs({
query = getRelevantBlogsQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: BlogVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ relevantBlogs: GetRelevantBlogsQuery[] | any[] }> {
const config = commerce.getConfig(cfg)
const variables = {
productId: vars.productId,
}
const { data } = await config.fetch<GetRelevantBlogsQuery>(query, {
variables,
})
return {
relevantBlogs: data?.relevantBlogs?.items?.map((val:BlogList)=>({
id: val.id,
title: val.translations[0]?.title,
imageSrc: val.featuredAsset?.preview ?? null,
slug: val.translations[0]?.slug,
description: val.translations[0]?.description,
isPublish: val.isPublish,
isFeatured: val.isFeatured,
authorName: val.authorName,
authorAvatarAsset : val.authorAvatarAsset?.preview,
createdAt: val.createdAt
})),
}
}
return getRelevantBlogs
}

View File

@@ -2337,10 +2337,19 @@ export type BlogList = Node &{
translations: Array<BlogTranslation>
authorName: Scalars['String']
authorAvatarAsset:Asset
relevantProducts: Product
relevantProducts: Product[]
isFeatured: Boolean
}
export type GetBlogQuery = { __typename?: 'Query' } & {
blog?: Maybe<
{ __typename?: 'Blog' } & BlogList
>
}
export type BlogTranslation = {
__typename?: 'BlogTranslation'
id: Scalars['ID']
@@ -2359,6 +2368,13 @@ export type GetAllBlogsQuery = PaginatedList & {
'totalItems'
}
}
export type GetRelevantBlogsQuery = PaginatedList & {
relevantBlogs: { __typename?: 'BlogList' } & {
items: Array<{ __typename?: 'Blog' } & BlogList!>,
}
}
export type GetFeaturedBlogQuery = PaginatedList & {
id:string,
featuredBlogs: { __typename?: 'BlogList' } & {
@@ -2367,6 +2383,7 @@ export type GetFeaturedBlogQuery = PaginatedList & {
}
}
export type QueryBlogs = {
excludeBlogIds:Array,
options: BlogListOptions
@@ -3339,6 +3356,12 @@ export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
items: Array<{ __typename?: 'Product' } & Pick<Product, 'slug'>>
}
}
export type GetAllBlogPathsQuery = { __typename?: 'Query' } & {
blogs: { __typename?: 'BlogList' } & {
items: Array<{ __typename?: 'Blog' } & Pick<BlogList,'slug','translations'>>
}
}
export type GetAllProductsQueryVariables = Exact<{
input: SearchInput

View File

@@ -0,0 +1,11 @@
export const getAllBlogPathsQuery = /* GraphQL */ `
query getAllBlogPaths($excludeBlogIds: [ID]! = [],$options:BlogListOptions){
blogs(excludeBlogIds: $excludeBlogIds,options: $options){
items{
translations {
slug
}
}
}
}
`

View File

@@ -0,0 +1,26 @@
export const getBlogDetailQuery = /* GraphQL */ `
query getBlog($slug: String ){
blog(slug: $slug){
id
isPublish
isFeatured
authorName
createdAt
authorAvatarAsset{
preview
}
featuredAsset {
preview
}
translations {
title
slug
description
content
}
relevantProducts{
id
}
}
}
`

View File

@@ -0,0 +1,25 @@
export const getRelevantBlogsQuery = /* GraphQL */ `
query relevantBlogs($productId: ID!){
relevantBlogs(productId:$productId){
items {
id
isPublish
isFeatured
authorName
createdAt
authorAvatarAsset{
preview
}
featuredAsset {
preview
}
translations {
title
slug
description
content
}
}
}
}
`

View File

@@ -2,16 +2,111 @@ import { Layout, RelevantBlogPosts } from 'src/components/common';
import BlogContent from 'src/components/modules/blog-detail/BlogContent/BlogContent';
import BlogDetailImg from 'src/components/modules/blog-detail/BlogDetailImg/BlogDetailImg';
import { BLOGS_DATA_TEST } from 'src/utils/demo-data'
import { GetStaticPropsContext,GetStaticPathsContext } from 'next';
import { PromiseWithKey } from 'src/utils/types.utils';
import { getAllPromies } from 'src/utils/funtion.utils';
import commerce from '@lib/api/commerce';
import { BlogCardProps } from 'src/components/common/CardBlog/CardBlog';
import { REVALIDATE_TIME } from 'src/utils/constanst.utils'
interface Props {
blog:{blogDetail?: BlogCardProps},
relevantBlogs:{blogDetail?:BlogCardProps[]}
}
export default function BlogDetailPage({blog,relevantBlogs}:Props) {
export default function BlogDetailPage() {
let date = new Date(blog?.blogDetail?.createdAt ?? '' );
let fullDate = date.toLocaleString('en-us', { month: 'long' }) + " " + date.getDate()+","+date.getFullYear();
return (
<>
<BlogDetailImg/>
<BlogContent/>
<RelevantBlogPosts data={BLOGS_DATA_TEST} title="You will like also" bgcolor="cream"/>
<BlogDetailImg imgSrc={blog?.blogDetail?.imageSrc ?? ''} />
<BlogContent
title={blog?.blogDetail?.title}
content={blog?.blogDetail?.description}
imgAuthor={blog?.blogDetail?.authorAvatarAsset}
authorName={blog?.blogDetail?.authorName}
date={fullDate}
/>
{relevantBlogs.relevantBlogs?.length> 0 && <RelevantBlogPosts data={relevantBlogs.relevantBlogs} title="You will like also" bgcolor="cream"/>}
</>
)
}
export async function getStaticProps({
params,
locale,
locales,
preview,
}: GetStaticPropsContext<{ slug: string }> ) {
const config = { locale, locales }
let promisesWithKey = [] as PromiseWithKey[]
let props = {} as any
// Blog detail
const blogDetailPromise = await commerce.getBlogDetail({
variables: { slug: params!.slug },
config,
preview,
})
props.blog = blogDetailPromise;
if (!blogDetailPromise) {
throw new Error(`Blog with slug '${params!.slug}' not found`)
}
// Relevant Blogs
const relevantProductId = blogDetailPromise.blogDetail.relevantProducts?.[0];
if (relevantProductId && blogDetailPromise.blogDetail.relevantProducts.length > 0) {
const relevantBlogs = commerce.getRelevantBlogs({
variables: { productId: relevantProductId },
config,
preview,
})
promisesWithKey.push({ key: 'relevantBlogs', promise: relevantBlogs})
}else {
props.relevantBlogs = [];
}
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
})
console.log(props.relevantBlogs);
return {
props,
revalidate: REVALIDATE_TIME,
}
} catch (err) {
}
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
const { blogs } = await commerce.getAllBlogPaths()
return {
paths: locales
? locales.reduce<string[]>((arr, locale) => {
blogs.forEach((blog: any) => {
arr.push(`/${locale}/blog/${blog.slug}`)
})
return arr
}, [])
: blogs.map((product: any) => `/blog/${product.path}`),
fallback: 'blocking',
}
}
BlogDetailPage.Layout = Layout

View File

@@ -5,7 +5,9 @@
.authorImage {
width:3.2rem;
height:3.2rem;
border-radius:3.2rem;
img{
border-radius:3.2rem !important;
}
div{
min-width:3.2rem !important;
}

View File

@@ -14,40 +14,7 @@ interface RelevantProps {
bgcolor?: "default" | "cream"
}
const recipe:BlogCardProps[] = [
{
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
slug: 'have-a-nice-lunch',
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185783-8100ef4e-7a72-4dc1-bb12-2ca46b56b393.png",
},{
title: "9 Ways to Make an Aloe Vera Mask at Home",
slug: 'have-a-nice-lunch',
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185911-df505d10-fdcd-4312-add3-7c62ad8af71e.png",
},{
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
slug: 'have-a-nice-lunch',
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185959-7ad75580-ca6d-4684-83d9-3f64500bbc97.png",
},{
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
slug: 'have-a-nice-lunch',
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185783-8100ef4e-7a72-4dc1-bb12-2ca46b56b393.png",
},{
title: "9 Ways to Make an Aloe Vera Mask at Home",
slug: 'have-a-nice-lunch',
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185911-df505d10-fdcd-4312-add3-7c62ad8af71e.png",
},{
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
slug: 'have-a-nice-lunch',
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
imageSrc: "https://user-images.githubusercontent.com/46085455/133185959-7ad75580-ca6d-4684-83d9-3f64500bbc97.png",
}]
const RelevantBlogPosts = ({ data = recipe, itemKey="detail-relevant", title="Relevant Blog Posts", bgcolor = "default" }: RelevantProps) => {
const RelevantBlogPosts = ({ data , itemKey="detail-relevant", title="Relevant Blog Posts", bgcolor = "default" }: RelevantProps) => {
return (
<div className={classNames({
[s.blogPostWarpper] : true,
@@ -63,7 +30,7 @@ const recipe:BlogCardProps[] = [
</div>
</div>
<div className={s.bot}>
<BlogPostCarousel data={data} itemKey={itemKey} />
<BlogPostCarousel data={data} itemKey={itemKey} />}
</div>
</div>
)

View File

@@ -9,57 +9,24 @@ import Link from 'next/link';
interface BlogContentProps {
className?: string
children?: any,
title?: string,
content?: string,
imgAuthor?: string,
date?: string,
authorName?: string,
}
const BlogContent = ({}:BlogContentProps) => {
const BlogContent = ({title,date='',content,imgAuthor='',authorName='' }:BlogContentProps) => {
return (
<>
<div className={s.blogContentWrapper}>
<DateTime date="APRIL 30, 2021"/>
<h1>The Best Sesame Soy Broccoli Salad</h1>
<DateTime date={date}/>
<h1>{title}</h1>
<div className={s.author}>
<Author image={imageAuthor.src} name="Alessandro Del Piero" />
<Author image={imgAuthor} name={authorName} />
</div>
<section className={s.content}>
<p> When youre trying to eat healthier but want something more substantial than a leafy green salad, broccoli salad is there for you. I love the crunch and heft of broccoli, especially when its cut up into bite size spoonable pieces.
<br/>
<br/>
Some people arent into raw broccoli, but I love it! I always go for the raw broccoli on those vegetable platters that seem to be at every potluck/party you go to.
<br/>
<br/>
This is a simple broccoli salad: you have the bulk of it, raw broccoli; crunchy red onions for a bit of acidity and raw crunch, craisins for sweetness, almonds for a nutty counter point; and a sweet and tangy soy-rice vinegar-sesame dressing.
</p>
<br/>
<br/>
<h2 className={s.heading2}>What is broccoli salad</h2>
<br/>
<p> When youre trying to eat healthier but want something more substantial than a leafy green salad, broccoli salad is there for you. I love the crunch and heft of broccoli, especially when its cut up into bite size spoonable pieces.
<br/>
<br/>
Some people arent into raw broccoli, but I love it! I always go for the raw broccoli on those vegetable platters that seem to be at every potluck/party you go to.
<br/>
<br/>
This is a simple broccoli salad: you have the bulk of it, raw broccoli; crunchy red onions for a bit of acidity and raw crunch, craisins for sweetness, almonds for a nutty counter point; and a sweet and tangy soy-rice vinegar-sesame dressing.
</p>
<br/>
<br/>
<h2 className={s.heading2}>What about broccoli stems?</h2>
<br/>
<p>
You can eat broccoli stems. In fact, they are delicious. Just use a peeler to peel off the outsides and then trim the stalks into small 1/4-1/2 cubes.
</p>
<br/>
<figure>
<ImgWithLink src="https://user-images.githubusercontent.com/89437339/133046625-bdf9cc0d-6f22-43e5-a49d-d4d34df19cf2.jpg" alt="blog-detail" />
</figure>
{content}
</section>
<div className={s.boxShare}>

View File

@@ -5,7 +5,8 @@ import BreadcrumbCommon from 'src/components/common/BreadcrumbCommon/BreadcrumbC
import s from './BlogDetailImg.module.scss';
interface Props {
className?: string
children?: any
children?: any,
imgSrc?:string
}
const CRUMBS =[
@@ -14,14 +15,14 @@ const CRUMBS =[
link:"/blog"
}
]
const BlogDetailImg = ({}:Props ) => {
const BlogDetailImg = ({imgSrc = ''}:Props ) => {
return (
<>
<div className={s.beadcrumb}>
<BreadcrumbCommon crumbs={CRUMBS} />
</div>
<div className={s.image}>
<ImgWithLink src="https://user-images.githubusercontent.com/89437339/133044532-8b5f9442-841b-4187-84b4-d6cc66676b52.png" alt="Ảnh đại diện"/>
<ImgWithLink src={imgSrc} alt="avatar"/>
</div>
</>
)