mirror of
https://github.com/LaconicNetwork/laconic.com.git
synced 2026-09-07 19:24:06 +00:00
Blog page (#15)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { flatten } from 'lodash'
|
||||
|
||||
import cms from './cms'
|
||||
import { BlogPostRecord } from './cms/generated'
|
||||
import { getPagination, PaginationData } from './utils'
|
||||
|
||||
const DEFAULT_PAGE_STEP = 9
|
||||
|
||||
export type GetBlogPostsParams = {
|
||||
page?: number
|
||||
step?: number
|
||||
filterIds?: string[]
|
||||
filterSlugs?: string[]
|
||||
category?: string
|
||||
search?: string
|
||||
}
|
||||
|
||||
export const getBlogPosts = async ({
|
||||
page = 1,
|
||||
step = DEFAULT_PAGE_STEP,
|
||||
filterIds,
|
||||
filterSlugs,
|
||||
category,
|
||||
search = ''
|
||||
}: GetBlogPostsParams) => {
|
||||
const { allBlogPosts, _allBlogPostsMeta } = await cms().GetBlogPosts({
|
||||
skip: (page - 1) * step,
|
||||
step,
|
||||
filterIds,
|
||||
filterSlugs,
|
||||
category,
|
||||
search
|
||||
})
|
||||
|
||||
const pagination = getPagination(page, _allBlogPostsMeta.count, step)
|
||||
|
||||
return {
|
||||
pagination,
|
||||
data: allBlogPosts
|
||||
}
|
||||
}
|
||||
|
||||
export const getBlogPostsCategories = async () => {
|
||||
const { allCategories } = await cms().GetBlogPostsCategories()
|
||||
|
||||
// Push dummy category
|
||||
allCategories.push({
|
||||
id: '0',
|
||||
slug: 'none',
|
||||
title: 'No Category'
|
||||
})
|
||||
|
||||
return allCategories
|
||||
}
|
||||
|
||||
export type GetBlogPostsSlugsParams = {
|
||||
page?: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
export const getBlogPostsSlugs = async ({
|
||||
page = 1,
|
||||
step = DEFAULT_PAGE_STEP
|
||||
}: GetBlogPostsSlugsParams) => {
|
||||
const { allBlogPosts, _allBlogPostsMeta } = await cms().BlogPostsSlug({
|
||||
skip: (page - 1) * step,
|
||||
step
|
||||
})
|
||||
|
||||
const pagination = getPagination(page, _allBlogPostsMeta.count, step)
|
||||
|
||||
return {
|
||||
pagination,
|
||||
data: allBlogPosts
|
||||
}
|
||||
}
|
||||
|
||||
export const getAllBlogPostsSlugs = async () => {
|
||||
const postsChunks: Pick<BlogPostRecord, 'slug'>[][] = []
|
||||
let nextPage: number | null = 1
|
||||
|
||||
while (nextPage) {
|
||||
const {
|
||||
pagination,
|
||||
data
|
||||
}: { pagination: PaginationData; data: Pick<BlogPostRecord, 'slug'>[] } =
|
||||
await getBlogPostsSlugs({
|
||||
page: nextPage,
|
||||
step: 100
|
||||
})
|
||||
|
||||
postsChunks.push(data)
|
||||
|
||||
nextPage = pagination.nextPage
|
||||
}
|
||||
|
||||
return flatten(postsChunks)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fragment Author on AuthorRecord {
|
||||
name
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
fragment BlogPost on BlogPostRecord {
|
||||
_seoMetaTags {
|
||||
...SEOTags
|
||||
}
|
||||
title
|
||||
date
|
||||
category {
|
||||
title
|
||||
slug
|
||||
}
|
||||
author {
|
||||
...Author
|
||||
}
|
||||
slug
|
||||
content {
|
||||
value
|
||||
}
|
||||
image {
|
||||
...Image
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fragment Category on CategoryRecord {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fragment Image on FileField {
|
||||
url
|
||||
alt
|
||||
height
|
||||
width
|
||||
title
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fragment SEOTags on Tag {
|
||||
content
|
||||
tag
|
||||
attributes
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { getSdk } from './generated'
|
||||
|
||||
const cms = (preview?: boolean) => {
|
||||
const endpoint = 'https://graphql.datocms.com/'
|
||||
const previewEndpoint = 'https://graphql.datocms.com/preview'
|
||||
|
||||
return getSdk(
|
||||
new GraphQLClient(preview ? previewEndpoint : endpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.CMS_ACCESS_TOKEN}`
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export default cms
|
||||
@@ -0,0 +1,40 @@
|
||||
query GetBlogPostsCategories {
|
||||
allCategories {
|
||||
...Category
|
||||
}
|
||||
}
|
||||
|
||||
query GetBlogPosts(
|
||||
$skip: IntType!
|
||||
$step: IntType!
|
||||
$filterIds: [ItemId] = []
|
||||
$filterSlugs: [String] = []
|
||||
$category: [ItemId]
|
||||
$search: String!
|
||||
) {
|
||||
_allBlogPostsMeta(
|
||||
filter: {
|
||||
title: { isBlank: false }
|
||||
id: { notIn: $filterIds }
|
||||
slug: { notIn: $filterSlugs }
|
||||
category: { anyIn: $category }
|
||||
OR: [{ title: { matches: { pattern: $search } } }]
|
||||
}
|
||||
) {
|
||||
count
|
||||
}
|
||||
allBlogPosts(
|
||||
filter: {
|
||||
title: { isBlank: false }
|
||||
id: { notIn: $filterIds }
|
||||
slug: { notIn: $filterSlugs }
|
||||
category: { anyIn: $category }
|
||||
OR: [{ title: { matches: { pattern: $search } } }]
|
||||
}
|
||||
first: $step
|
||||
orderBy: date_DESC
|
||||
skip: $skip
|
||||
) {
|
||||
...BlogPost
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query BlogPostsSlug($skip: IntType!, $step: IntType!) {
|
||||
_allBlogPostsMeta {
|
||||
count
|
||||
}
|
||||
allBlogPosts(first: $step, skip: $skip, orderBy: date_DESC) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
|
||||
query SingleBlogPost($slug: String!) {
|
||||
blogPost(filter: { slug: { eq: $slug }, title: { isBlank: false } }) {
|
||||
...BlogPost
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@import '~/css/helpers';
|
||||
|
||||
.structured {
|
||||
max-width: tovw(856px, 'default', 856px);
|
||||
|
||||
p {
|
||||
font-family: var(--font-tt-hoves);
|
||||
font-size: tovw(22px, 'default', 15px);
|
||||
line-height: 160%;
|
||||
letter-spacing: -0.01em;
|
||||
margin: tovw(44px, 'default', 40px) 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { StructuredText } from 'react-datocms'
|
||||
|
||||
import Marked from '~/components/common/marked'
|
||||
import { Container } from '~/components/layout/container'
|
||||
|
||||
import s from './blog.module.scss'
|
||||
|
||||
export const renderBlock = ({ record }) => {
|
||||
switch (record.__typename) {
|
||||
case 'ImageRecord':
|
||||
return (
|
||||
<div className="my-12">
|
||||
<img
|
||||
src={record.image?.url ?? '/4040404'}
|
||||
width={record.image?.width ?? 0}
|
||||
height={record.image?.height ?? 0}
|
||||
alt={record.image?.alt ?? record.image?.title ?? 'blog image'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
case 'CalloutRecord':
|
||||
return <Marked>{record.content ?? ''}</Marked>
|
||||
// case 'ShareLinkRecord':
|
||||
// return (
|
||||
// <div className="flex justify-center mt-20 space-x-4">
|
||||
// <SocialLink variant="logo-only" tag="/" type="twitter" />
|
||||
// <SocialLink variant="logo-only" tag="/" type="telegram" />
|
||||
// <SocialLink variant="logo-only" tag="/" type="facebook" />
|
||||
// <SocialLink variant="logo-only" type="copy-link" />
|
||||
// </div>
|
||||
// )
|
||||
// case 'CtaRecord':
|
||||
// return (
|
||||
// <SectionCTAs
|
||||
// className="flex flex-col items-center justify-center mt-20 space-y-2 sm:space-y-0 sm:space-x-2 sm:flex-row"
|
||||
// ctas={
|
||||
// record?.links?.map((link, i) => {
|
||||
// return {
|
||||
// href: link?.href ?? '',
|
||||
// as: 'a',
|
||||
// children: link?.label ?? '',
|
||||
// variant: i === 0 ? 'primary' : 'tertiary',
|
||||
// size: 'lg'
|
||||
// }
|
||||
// }) ?? []
|
||||
// }
|
||||
// />
|
||||
// )
|
||||
// case 'TableRecord':
|
||||
// return (
|
||||
// <ViewportWidthBox>
|
||||
// <Container size="sm">
|
||||
// <div className={s.table}>
|
||||
// <Marked>{record.markdownTable ?? ''}</Marked>
|
||||
// </div>
|
||||
// </Container>
|
||||
// </ViewportWidthBox>
|
||||
// )
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const CustomStructuredText = ({ data }) => {
|
||||
return (
|
||||
<Container className={s.structured}>
|
||||
<StructuredText data={data} renderBlock={renderBlock} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomStructuredText
|
||||
@@ -0,0 +1,74 @@
|
||||
import { render } from 'datocms-structured-text-to-plain-text'
|
||||
import { BlogPostFragment } from 'lib/cms/generated'
|
||||
|
||||
export const getPlainTextContent = (post: BlogPostFragment) => {
|
||||
const text = render(post.content)
|
||||
return text ?? ''
|
||||
}
|
||||
|
||||
export const getDescription = (post: BlogPostFragment) => {
|
||||
let text = getPlainTextContent(post)
|
||||
const charLimit = 200
|
||||
text = text.substring(0, charLimit)
|
||||
let words = text.trim().split(/\s+/)
|
||||
words = words.splice(0, words.length - 1)
|
||||
const description = words.join(' ') + '...'
|
||||
return description
|
||||
}
|
||||
|
||||
export const getSeoTags = (post: BlogPostFragment) => {
|
||||
const description = getDescription(post)
|
||||
|
||||
const filteredMetaTags = post._seoMetaTags.filter((t) => {
|
||||
if (
|
||||
t.attributes?.name === 'description' ||
|
||||
t.attributes?.name === 'twitter:description' ||
|
||||
t.attributes?.property === 'og:description' ||
|
||||
t.tag === 'title'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return [
|
||||
...filteredMetaTags,
|
||||
{
|
||||
content: post.title + ' | Akash Network',
|
||||
tag: 'title'
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
content: description,
|
||||
name: 'description'
|
||||
},
|
||||
content: null,
|
||||
tag: 'meta'
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
content: description,
|
||||
property: 'og:description'
|
||||
},
|
||||
content: null,
|
||||
tag: 'meta'
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
content: description,
|
||||
name: 'twitter:description'
|
||||
},
|
||||
content: null,
|
||||
tag: 'meta'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export type BlogPostWithMeta = Omit<BlogPostFragment, 'content'> & {
|
||||
plainTextContent: string
|
||||
numId: number
|
||||
}
|
||||
|
||||
export type SingleBlogPost = BlogPostFragment & {
|
||||
description: string
|
||||
}
|
||||
@@ -39,3 +39,60 @@ export const formatStatCount = (count = 0, withAbbr = false, decimals = 2) => {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const range = (start: number, stop?: number, step?: number) => {
|
||||
if (typeof stop == 'undefined') {
|
||||
// one param defined
|
||||
stop = start
|
||||
start = 0
|
||||
}
|
||||
|
||||
if (typeof step == 'undefined') {
|
||||
step = 1
|
||||
}
|
||||
|
||||
if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = []
|
||||
for (let i = start; step > 0 ? i < stop : i > stop; i += step) {
|
||||
result.push(i)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getHash(input: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
hash = (hash << 5) - hash + input.charCodeAt(i)
|
||||
hash |= 0 // to 32bit integer
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
export const formatDate = (date: string) => {
|
||||
const [year, month, day] = date.split('-')
|
||||
return `${month}.${day}.${year}`
|
||||
}
|
||||
|
||||
export type PaginationData = {
|
||||
page: number
|
||||
nextPage: number | null
|
||||
totalPages: number
|
||||
step: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export const getPagination = (
|
||||
page: number,
|
||||
total: number,
|
||||
step: number
|
||||
): PaginationData => ({
|
||||
page: page,
|
||||
nextPage: page < Math.ceil(total / step) ? page + 1 : null,
|
||||
totalPages: Math.ceil(total / step),
|
||||
step: step,
|
||||
total
|
||||
})
|
||||
|
||||
@@ -60,3 +60,12 @@ export const makeQuery = (
|
||||
if (replace) return router.replace(url, url, { scroll: false, ...opts })
|
||||
else return router.push(url, url, { scroll: false, ...opts })
|
||||
}
|
||||
|
||||
export const getQueryParams = (params: QueryParams) => {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.keys(params).forEach((key) => {
|
||||
const value = params[key]
|
||||
if (value !== null) searchParams.append(key, value)
|
||||
})
|
||||
return searchParams
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user