Merge pull request #107 from acid-info/static-pages

WIP: Static pages
This commit is contained in:
amir houieh 2023-08-26 14:30:34 +02:00 committed by GitHub
commit cbea3dce0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 374 additions and 9 deletions

View File

@ -18,7 +18,7 @@ export const RenderArticleBlock = ({
}: {
block: LPE.Article.ContentBlock
activeId: string | null
headingElementsRef: HeadingElementsRef
headingElementsRef?: HeadingElementsRef
hide?: boolean
}) => {
switch (block.type) {
@ -70,7 +70,9 @@ export const RenderArticleBlock = ({
variant="body1"
component={block.tagName as any}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
className={`${extractClassFromFirstTag(
block.html,
)} ${block.classNames.join(' ')}`}
id={extractIdFromFirstTag(block.html) || `p-${block.order}`}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>
@ -82,7 +84,9 @@ export const RenderArticleBlock = ({
variant="body1"
component={block.tagName as any}
genericFontFamily="sans-serif"
className={extractClassFromFirstTag(block.html) || ''}
className={`${extractClassFromFirstTag(
block.html,
)} ${block.classNames.join(' ')}`}
id={extractIdFromFirstTag(block.html) || `p-${block.order}`}
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
/>

View File

@ -11,7 +11,7 @@ import { LPE } from '../../types/lpe.types'
type Props = PropsWithChildren<{
block: LPE.Article.TextBlock
headingElementsRef: HeadingElementsRef
headingElementsRef?: HeadingElementsRef
typographyProps?: TypographyProps
}>
export const ArticleHeading = ({
@ -24,7 +24,9 @@ export const ArticleHeading = ({
extractIdFromFirstTag(block.html) || `${block.tagName}-${block.order}`
const refProp = {
ref: (ref: any) => {
if (headingElementsRef) {
headingElementsRef.current[id] = ref
}
},
}

View File

@ -48,7 +48,7 @@ const ArticleHeader = ({
<ArticleSubtitle
variant="body1"
genericFontFamily="sans-serif"
component="div"
component="p"
>
{subtitle}
</ArticleSubtitle>

View File

@ -0,0 +1,62 @@
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import React from 'react'
import { LPE } from '../../types/lpe.types'
import { RenderArticleBlock } from '@/components/Article/Article.Block'
export type StaticPageProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
data: {
page: LPE.StaticPage.Document
}
}
export const StaticPage: React.FC<StaticPageProps> = ({
data,
data: { page },
...props
}) => {
const titleBlock = data.page.content.find((block) => {
return (
block.type === LPE.Post.ContentBlockTypes.Text &&
block.classNames &&
block.classNames.includes('title')
)
}) as LPE.Post.TextBlock | undefined
return (
<Root {...props}>
<article>
{titleBlock && (
<Typography variant={'h1'} genericFontFamily={'serif'}>
{titleBlock.text}
</Typography>
)}
{data.page.content.map((block, idx) => (
<RenderArticleBlock block={block} activeId={null} key={idx} />
))}
</article>
</Root>
)
}
const Root = styled.div`
display: flex;
justify-content: center;
#p-2 {
font-size: var(--lsd-body1-fontSize) !important;
line-height: var(--lsd-body1-lineHeight) !important;
}
article {
width: 700px;
> * {
margin-bottom: 1rem;
}
}
.title {
display: none;
}
`

View File

@ -0,0 +1 @@
export * from './StaticPage'

87
src/pages/[slug].tsx Normal file
View File

@ -0,0 +1,87 @@
import { CustomNextPage, GetStaticPaths, GetStaticProps } from 'next'
import Error from 'next/error'
import SEO from '../components/SEO/SEO'
import { StaticPage, StaticPageProps } from '../containers/StaticPage'
import unbodyApi from '../services/unbody/unbody.service'
import NotFoundPage from './404'
type PageProps = Partial<Pick<StaticPageProps, 'data'>> & {
error?: string
notFound?: boolean
}
const Page: CustomNextPage<PageProps> = ({
data,
error,
notFound,
...props
}) => {
if (!data) {
if (notFound) {
return <Error statusCode={400} />
}
return <Error statusCode={500} />
}
return (
<>
<SEO
description={
data.page.subtitle ||
'Logos online publishing and blogging platform for writers and readers.'
}
title={`${data.page.title} - Logos Press Engine`}
/>
<StaticPage data={data} />
</>
)
}
export const getStaticPaths: GetStaticPaths = async () => {
const { data } = await unbodyApi.getStaticPages()
return {
paths: data.map((page) => ({
params: {
slug: page.slug,
},
})),
fallback: true,
}
}
export const getStaticProps: GetStaticProps<PageProps> = async (ctx) => {
const { data, errors } = await unbodyApi.getStaticPage({
slug: ctx.params!.slug as string,
})
if (!data) {
if (errors && typeof errors === 'string' && errors.includes('not found')) {
return {
notFound: true,
props: {
notFound: true,
},
}
}
console.error(errors)
return {
props: {
error: 'Something went wrong!',
},
notFound: false,
}
}
return {
props: {
data: {
page: data,
},
},
notFound: false,
}
}
export default Page

View File

@ -0,0 +1,42 @@
import { LPE } from '../../../types/lpe.types'
import { UnbodyResGoogleDocData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const StaticPageDataType: UnbodyDataTypeConfig<
UnbodyResGoogleDocData,
LPE.StaticPage.Document
> = {
key: 'StaticPageDocument',
objectType: 'GoogleDoc',
classes: ['static-page', 'document'],
isMatch: (helpers, data) => data.pathString.includes('/Static pages/'),
transform: async (helpers, data) => {
const textBlock = helpers.dataTypes.get({
objectType: 'TextBlock',
})
const imageBlock = helpers.dataTypes.get({
objectType: 'ImageBlock',
})
const blocks =
await helpers.dataTypes.transformMany<LPE.Article.ContentBlock>(
[...textBlock, ...imageBlock],
[...(data.blocks || [])].sort((a, b) => a.order - b.order),
data,
)
return {
id: data._additional.id,
slug: data.slug,
title: data.title,
subtitle: data.subtitle || '',
summary: data.summary || '',
createdAt: data.createdAt || null,
modifiedAt: data.modifiedAt || null,
content: blocks,
type: 'static_page',
}
},
}

View File

@ -5,6 +5,7 @@ import { ArticleTextBlockDataType } from './ArticleTextBlock.dataType'
import { ImageBlockDataType } from './ImageBlock.dataType'
import { PodcastEpisodeDataType } from './PodcastEpisodeDocument.dataType'
import { PodcastShowDataType } from './PodcastShowDocument.dataType'
import { StaticPageDataType } from './StaticPageDocument.dataType'
import { TextBlockDataType } from './TextBlock.dataType'
import { UnbodyDataTypes } from './UnbodyDataTypes'
@ -17,4 +18,5 @@ export const unbodyDataTypes = new UnbodyDataTypes([
ArticleSearchResultItemDataType,
PodcastShowDataType,
PodcastEpisodeDataType,
StaticPageDataType,
])

View File

@ -49,6 +49,7 @@ export const UnbodyDataTypeKeys = {
ArticleSearchResultItem: 'ArticleSearchResultItem',
PodcastShowDocument: 'PodcastShowDocument',
PodcastEpisodeDocument: 'PodcastEpisodeDocument',
StaticPageDocument: 'StaticPageDocument',
} as const
export type UnbodyDataTypeKey =
@ -61,6 +62,7 @@ export const UnbodyDataTypeClasses = {
Episode: 'episode',
Document: 'document',
Search: 'search',
StaticPage: 'static-page',
} as const
export type UnbodyDataTypeClass =

View File

@ -36,6 +36,10 @@ const podcastEpisodeDocument = unbodyDataTypes.get({
classes: ['podcast', 'document', 'episode'],
objectType: 'GoogleDoc',
})!
const staticPageDocument = unbodyDataTypes.get({
classes: ['static-page', 'document'],
objectType: 'GoogleDoc',
})!
const sortPosts = (a: LPE.Post.Document, b: LPE.Post.Document) =>
(a.type === 'podcast' ? new Date(a.publishedAt) : new Date(a.modifiedAt!)) >
@ -52,7 +56,8 @@ export class UnbodyService {
initialData: {
posts: LPE.Post.Document[]
} = { posts: [] }
staticPages: LPE.StaticPage.Document[]
} = { posts: [], staticPages: [] }
constructor(private apiKey: string, private projectId: string) {
const cache = new InMemoryCache({
@ -103,6 +108,7 @@ export class UnbodyService {
private _loadInitialData = async () => {
const articles: LPE.Article.Data[] = await this.fetchAllArticles()
const episodes: LPE.Podcast.Document[] = await this.fetchAllEpisodes()
const staticPages = await this.fetchAllStaticPages()
const posts: LPE.Post.Document[] = [...articles, ...episodes].sort(
sortPosts,
@ -110,9 +116,10 @@ export class UnbodyService {
this.initialData = {
posts,
staticPages,
}
this.initialDataPromise.resolve({ posts })
this.initialDataPromise.resolve({ posts, staticPages })
}
loadInitialData = async (forced: boolean = false) => {
@ -123,6 +130,31 @@ export class UnbodyService {
return this.initialDataPromise.promise
}
private fetchAllStaticPages = async () => {
const result: LPE.StaticPage.Document[] = []
let skip = 0
const limit = 50
while (true) {
const { data } = await this.findStaticPages({
skip,
limit,
parseContent: false,
textBlocks: false,
includeDrafts: false,
toc: false,
})
result.push(...data)
if (data.length === 0) break
skip += 50
}
return result
}
private fetchAllEpisodes = async () => {
const result: LPE.Podcast.Document[] = []
@ -227,6 +259,112 @@ export class UnbodyService {
return GoogleDoc?.[0]?.meta?.count || 0
}, 0)
findStaticPages = ({
skip = 0,
limit = 10,
slug,
toc = false,
filter,
nearObject,
textBlocks = false,
includeDrafts = false,
}: {
slug?: string
skip?: number
limit?: number
toc?: boolean
filter?: GetPostsQueryVariables['filter']
nearObject?: string
textBlocks?: boolean
parseContent?: boolean
includeDrafts?: boolean
}) =>
this.handleRequest<LPE.StaticPage.Document[]>(async () => {
const {
data: {
Get: { GoogleDoc: docs },
},
} = await this.client.query({
query: GetPostsDocument,
variables: {
toc,
textBlocks,
mentions: true,
imageBlocks: true,
...(nearObject
? {
nearObject: {
id: nearObject,
},
}
: {}),
...this.helpers.args.page(skip, limit),
filter: {
operator: 'And',
operands: [
this.helpers.args.wherePath([
'Static pages',
includeDrafts ? 'published|draft' : 'published',
]),
...(slug
? [
{
path: ['slug'],
operator: 'Equal',
valueString: slug,
} as GetObjectsGoogleDocWhereInpObj,
]
: []),
...(nearObject
? [
{
path: ['id'],
operator: 'NotEqual',
valueString: nearObject,
} as GetObjectsGoogleDocWhereInpObj,
]
: []),
...(filter ? [filter] : []),
],
},
},
})
return unbodyDataTypes.transformMany<LPE.StaticPage.Document>(
staticPageDocument,
docs,
undefined,
)
}, [])
getStaticPages = () =>
this.handleRequest<LPE.StaticPage.Document[]>(async () => {
const { staticPages } = await this.loadInitialData()
return staticPages
}, [])
getStaticPage = ({
slug,
includeDrafts = false,
}: {
slug: string
includeDrafts?: boolean
}) =>
this.handleRequest<LPE.StaticPage.Document | null>(async () => {
const { data } = await this.findStaticPages({
limit: 1,
slug,
includeDrafts,
textBlocks: true,
parseContent: true,
})
if (data.length === 0) throw 'Static page not found!'
return data[0]
}, null)
getArticles = ({
skip = 0,
limit = 10,

View File

@ -52,7 +52,6 @@ export namespace LPE {
} as const
export type ContentBlockType = DictValues<typeof ContentBlockTypes>
export const ContentBlockLabels = {
Title: 'title',
Subtitle: 'subtitle',
@ -92,6 +91,32 @@ export namespace LPE {
export type Document = LPE.Article.Data | LPE.Podcast.Document
}
export namespace StaticPage {
export type TextBlock = Post.TextBlock<Metadata>
export type ImageBlock = Post.ImageBlock<Metadata>
export type ContentBlock = Post.ContentBlock<Metadata>
export type Footnote = Post.Footnote
export type Footnotes = Post.Footnotes
export const ContentBlockLabels = Post.ContentBlockLabels
export type ContentBlockLabel = Post.ContentBlockLabel
export type Metadata = {
id: string
slug: string
title: string
summary: string
subtitle: string
createdAt: string | null
modifiedAt: string | null
type: 'static_page'
}
export type Document = Metadata & {
content: Array<Article.ContentBlock>
}
}
export namespace Article {
export type Toc = Post.Toc
export type TocItem = Post.TocItem