mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-21 21:58:07 +00:00
feat: implement initial strapi integration
This commit is contained in:
parent
e4ef86af4d
commit
7189c9982f
@ -2,10 +2,9 @@ module.exports = {
|
||||
client: {
|
||||
includes: ['src/**/*.{ts,tsx}'],
|
||||
service: {
|
||||
name: 'unbody-graphql',
|
||||
name: 'strapi-graphql',
|
||||
localSchemaFile: [
|
||||
'./src/lib/unbody/unbody.graphql',
|
||||
'./src/lib/unbody/unbody.extend.graphql',
|
||||
'./src/lib/strapi/strapi.graphql',
|
||||
], // how to configure to multiple schemas?
|
||||
},
|
||||
},
|
||||
|
15
codegen.ts
15
codegen.ts
@ -1,8 +1,7 @@
|
||||
import { CodegenConfig } from '@graphql-codegen/cli'
|
||||
|
||||
const graphqlEndpoint = 'https://graphql.unbody.io'
|
||||
const projectId = process.env.UNBODY_PROJECT_ID || ''
|
||||
const authorization = process.env.UNBODY_API_KEY || ''
|
||||
const graphqlEndpoint = process.env.STRAPI_GRAPHQL_URL || ''
|
||||
const token = process.env.STRAPI_API_KEY || ''
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
@ -10,21 +9,19 @@ const config: CodegenConfig = {
|
||||
{
|
||||
[graphqlEndpoint]: {
|
||||
headers: {
|
||||
authorization,
|
||||
'x-project-id': projectId,
|
||||
authorization: token,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
},
|
||||
'src/lib/unbody/unbody.extend.graphql',
|
||||
],
|
||||
documents: ['src/**/*.{ts,tsx}'],
|
||||
documents: ['src/services/strapi/*.{ts,tsx}'],
|
||||
generates: {
|
||||
'src/lib/unbody/unbody.graphql': {
|
||||
'src/lib/strapi/strapi.graphql': {
|
||||
plugins: ['schema-ast'],
|
||||
},
|
||||
'src/lib/unbody/unbody.generated.ts': {
|
||||
'src/lib/strapi/strapi.generated.ts': {
|
||||
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
|
||||
presetConfig: {
|
||||
fragmentMasking: false,
|
||||
|
@ -3,12 +3,11 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
domains: [
|
||||
'images.cdn.unbody.io',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'image.simplecastcdn.com',
|
||||
'img.youtube.com',
|
||||
],
|
||||
// loader: 'imgix',
|
||||
// path: 'https://images.cdn.unbody.io',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,7 @@
|
||||
"next": "13.3.0",
|
||||
"next-query-params": "^4.2.3",
|
||||
"nextjs-progressbar": "^0.0.16",
|
||||
"node-html-parser": "^6.1.12",
|
||||
"odoo-await": "^3.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
@ -59,6 +60,7 @@
|
||||
"react-player": "^2.12.0",
|
||||
"react-quick-pinch-zoom": "^4.9.0",
|
||||
"react-use": "^17.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"typescript": "5.0.4",
|
||||
"use-query-params": "^2.2.1",
|
||||
"yup": "^1.3.2"
|
||||
|
@ -39,6 +39,17 @@ export const RenderArticleBlock = ({
|
||||
case 'h6': {
|
||||
return <ArticleHeading block={block} />
|
||||
}
|
||||
case 'blockquote':
|
||||
return (
|
||||
<Quote mode="indented-line" genericFontFamily="serif">
|
||||
<Paragraph
|
||||
variant="body1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: extractInnerHtml(block.html),
|
||||
}}
|
||||
/>
|
||||
</Quote>
|
||||
)
|
||||
case 'p': {
|
||||
const isIframe = block.embed && block.labels.includes('embed')
|
||||
if (block.embed && isIframe) {
|
||||
@ -56,22 +67,6 @@ export const RenderArticleBlock = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
block.classNames.includes('subtitle') &&
|
||||
block.classNames.includes('u-with-margin-left')
|
||||
) {
|
||||
return (
|
||||
<Quote mode="indented-line" genericFontFamily="serif">
|
||||
<Paragraph
|
||||
variant="body1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: extractInnerHtml(block.html),
|
||||
}}
|
||||
/>
|
||||
</Quote>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
variant="body1"
|
||||
|
@ -12,7 +12,7 @@ type Props = {
|
||||
export const ArticleImageBlockWrapper = ({ image, order }: Props) => {
|
||||
return (
|
||||
<Container id={`i-${order}`}>
|
||||
<LightBox caption={image.alt}>
|
||||
<LightBox caption={image.caption || ''}>
|
||||
<ResponsiveImage data={image} />
|
||||
</LightBox>
|
||||
</Container>
|
||||
|
@ -15,6 +15,7 @@ import ArticleSummary from './Article.Summary'
|
||||
export type ArticleHeaderProps = LPE.Article.Data
|
||||
|
||||
const ArticleHeader = ({
|
||||
title,
|
||||
summary,
|
||||
subtitle,
|
||||
authors,
|
||||
@ -33,19 +34,15 @@ const ArticleHeader = ({
|
||||
date={modifiedAt ? new Date(modifiedAt) : null}
|
||||
readingLength={readingTime}
|
||||
/>
|
||||
<ArticleTitle
|
||||
block={
|
||||
content.find((block) =>
|
||||
block.labels.includes(LPE.Article.ContentBlockLabels.Title),
|
||||
) as LPE.Article.TextBlock
|
||||
}
|
||||
typographyProps={{
|
||||
variant: 'h1',
|
||||
genericFontFamily: 'serif',
|
||||
component: 'h1',
|
||||
<span
|
||||
id="title-anchor"
|
||||
ref={(ref) => {
|
||||
headingElementsRef.current['h-0'] = ref as HTMLHeadingElement
|
||||
}}
|
||||
headingElementsRef={headingElementsRef}
|
||||
/>
|
||||
></span>
|
||||
<Typography variant="h1" component="h1" genericFontFamily="serif">
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<ArticleSubtitle
|
||||
variant="body1"
|
||||
@ -55,7 +52,10 @@ const ArticleHeader = ({
|
||||
{subtitle}
|
||||
</ArticleSubtitle>
|
||||
)}
|
||||
<TagsAndSocial tags={tags} className={'articleTags'} />
|
||||
<TagsAndSocial
|
||||
tags={tags.map((tag) => tag.name)}
|
||||
className={'articleTags'}
|
||||
/>
|
||||
<AuthorsContainer>
|
||||
<Authors authors={authors} />
|
||||
</AuthorsContainer>
|
||||
@ -65,7 +65,9 @@ const ArticleHeader = ({
|
||||
order={ArticleBlocksOrders.cover}
|
||||
/>
|
||||
)}
|
||||
<ArticleSummary summary={summary} showLabel={false} />
|
||||
{summary && summary.length > 0 && (
|
||||
<ArticleSummary summary={summary} showLabel={false} />
|
||||
)}
|
||||
</ArticleHeaderContainer>
|
||||
)
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import { LPE } from '../../types/lpe.types'
|
||||
import EpisodeBlocks from './Episode.Blocks'
|
||||
|
||||
const EpisodeTranscript = ({ episode }: { episode: LPE.Podcast.Document }) => {
|
||||
if (episode.content.length === 0) return <></>
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title variant={'h5'} genericFontFamily={'serif'}>
|
||||
|
@ -24,9 +24,8 @@ const EpisodeCredits = ({
|
||||
component="p"
|
||||
variant="label1"
|
||||
id={credit.id.replace('#', '')}
|
||||
>
|
||||
{credit.text}
|
||||
</Typography>
|
||||
dangerouslySetInnerHTML={{ __html: credit.html }}
|
||||
/>
|
||||
</Credit>
|
||||
))}
|
||||
</Credits>
|
||||
|
@ -26,7 +26,7 @@ const EpisodeFooter = ({ episode, relatedEpisodes }: Props) => {
|
||||
<EpisodeFooterContainer>
|
||||
{!!episode?.credits && <EpisodeCredits credits={episode.credits} />}
|
||||
{!!footnotes && <EpisodeFootnotes footnotes={footnotes} />}
|
||||
{!!relatedEpisodes && (
|
||||
{!!relatedEpisodes && relatedEpisodes.length > 0 && (
|
||||
<RelatedEpisodes
|
||||
podcastSlug={episode.show?.slug as string}
|
||||
relatedEpisodes={relatedEpisodes}
|
||||
|
@ -54,7 +54,7 @@ const EpisodeHeader = ({
|
||||
</Show>
|
||||
</CustomLink>
|
||||
)}
|
||||
<TagsAndSocial tags={tags} />
|
||||
<TagsAndSocial tags={tags.map((tag) => tag.name)} />
|
||||
{channels && <EpisodeChannels channels={channels} />}
|
||||
{description && (
|
||||
<ArticleSummary summary={description} showLabel={false} />
|
||||
|
@ -5,7 +5,7 @@ export type EpisodeState = {
|
||||
title: string
|
||||
podcast: string
|
||||
url: string
|
||||
coverImage: LPE.Post.ImageBlock | null
|
||||
coverImage: LPE.Image.Document | null
|
||||
path: string
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ PostCard.toData = (post: LPE.Post.Document, shows: LPE.Podcast.Show[] = []) => {
|
||||
authors: post.type === 'article' ? post.authors : [],
|
||||
coverImage: post.coverImage,
|
||||
subtitle: (post.type === 'article' && post.subtitle) || '',
|
||||
tags: post.tags,
|
||||
tags: post.tags.map((tag) => tag.name),
|
||||
...(post.type === 'podcast' && show
|
||||
? {
|
||||
podcastShowDetails: {
|
||||
|
@ -67,7 +67,7 @@ export const PostsList = (props: Props) => {
|
||||
title: post.title,
|
||||
subtitle: post.subtitle,
|
||||
coverImage: post.coverImage,
|
||||
tags: post.tags,
|
||||
tags: post.tags.map((tag) => tag.name),
|
||||
}}
|
||||
contentType={PostTypes.Article}
|
||||
/>
|
||||
|
@ -41,6 +41,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<Container>
|
||||
<div>
|
||||
<PostsGrid
|
||||
shows={shows}
|
||||
posts={highlighted.slice(0, 1)}
|
||||
pattern={[{ cols: 1, size: 'large' }]}
|
||||
breakpoints={[
|
||||
@ -56,6 +57,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
/>
|
||||
<Section title="Latest posts">
|
||||
<PostsGrid
|
||||
shows={shows}
|
||||
pattern={[{ cols: 4, size: 'small' }]}
|
||||
breakpoints={[
|
||||
{
|
||||
@ -103,11 +105,11 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
</div>
|
||||
<Grid xs={{ cols: 1 }} sm={{ cols: 4 }}>
|
||||
{tags.map((tag) => (
|
||||
<GridItem key={tag.value} cols={1}>
|
||||
<GridItem key={tag.name} cols={1}>
|
||||
<TagCard
|
||||
href={`/search?topic=${tag.value}`}
|
||||
name={formatTagText(tag.value)}
|
||||
count={tag.count}
|
||||
href={`/search?topic=${tag.name}`}
|
||||
name={formatTagText(tag.name)}
|
||||
count={tag.postsCount}
|
||||
/>
|
||||
</GridItem>
|
||||
))}
|
||||
|
@ -18,22 +18,12 @@ export const StaticPage: React.FC<StaticPageProps> = ({
|
||||
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>
|
||||
)}
|
||||
<Typography variant={'h1'} genericFontFamily={'serif'}>
|
||||
{page.title}
|
||||
</Typography>
|
||||
{data.page.content.map((block, idx) => (
|
||||
<RenderArticleBlock block={block} activeId={null} key={idx} />
|
||||
))}
|
||||
|
87
src/lib/TransformPipeline/TransformPipeline.ts
Normal file
87
src/lib/TransformPipeline/TransformPipeline.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Helpers, Transformer } from './types'
|
||||
|
||||
export class TransformPipeline {
|
||||
private transformers: Transformer[] = []
|
||||
private helpers: Helpers
|
||||
|
||||
constructor(transformers: Transformer<any>[]) {
|
||||
this.transformers = transformers
|
||||
|
||||
this.helpers = {
|
||||
transformers: this,
|
||||
}
|
||||
}
|
||||
|
||||
getOne = ({
|
||||
key,
|
||||
classes,
|
||||
objectType,
|
||||
}: {
|
||||
key?: string
|
||||
classes?: string | string[]
|
||||
objectType?: string
|
||||
}) => {
|
||||
let transformers = this.transformers
|
||||
|
||||
if (key) {
|
||||
return transformers.find((doc) => doc.key === key)
|
||||
}
|
||||
|
||||
return this.get({ classes, objectType })[0]
|
||||
}
|
||||
|
||||
get = ({
|
||||
classes: _classes,
|
||||
objectType,
|
||||
}: {
|
||||
classes?: string | string[]
|
||||
objectType?: string
|
||||
}) => {
|
||||
let transformers = this.transformers
|
||||
|
||||
if (objectType)
|
||||
transformers = transformers.filter(
|
||||
(dataType) => dataType.objectType === objectType,
|
||||
)
|
||||
|
||||
const classes = !_classes
|
||||
? []
|
||||
: Array.isArray(_classes)
|
||||
? _classes
|
||||
: [_classes]
|
||||
if (classes.length > 0)
|
||||
transformers = transformers.filter((dataType) =>
|
||||
classes.every((cls) => dataType.classes.includes(cls)),
|
||||
)
|
||||
|
||||
return transformers
|
||||
}
|
||||
|
||||
transform = async <O = any, T = any>(
|
||||
pipeline: Transformer[],
|
||||
data: T,
|
||||
root?: any,
|
||||
context?: any,
|
||||
): Promise<O> => {
|
||||
let obj = data
|
||||
|
||||
for (const dataType of pipeline) {
|
||||
if (dataType.isMatch(this.helpers, obj, data, root, context)) {
|
||||
obj = await dataType.transform(this.helpers, obj, data, root, context)
|
||||
}
|
||||
}
|
||||
|
||||
return obj as O | Promise<O>
|
||||
}
|
||||
|
||||
transformMany = async <O = any, T = any>(
|
||||
pipeline: Transformer[],
|
||||
data: T[],
|
||||
root?: any,
|
||||
context?: any,
|
||||
): Promise<O[]> => {
|
||||
return Promise.all(
|
||||
data.map((d) => this.transform<O, T>(pipeline, d, root, context)),
|
||||
)
|
||||
}
|
||||
}
|
27
src/lib/TransformPipeline/types.ts
Normal file
27
src/lib/TransformPipeline/types.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TransformPipeline } from './TransformPipeline'
|
||||
|
||||
export type Transformer<D = any, T = any, O = any, R = any, C = any> = {
|
||||
key: string
|
||||
classes: string[]
|
||||
objectType: string
|
||||
|
||||
isMatch: (
|
||||
helpers: Helpers,
|
||||
object: D,
|
||||
original: O,
|
||||
root: R | undefined,
|
||||
context: C,
|
||||
) => boolean
|
||||
|
||||
transform: (
|
||||
helpers: Helpers,
|
||||
object: D,
|
||||
original: O,
|
||||
root: R | undefined,
|
||||
context: C,
|
||||
) => T | Promise<T>
|
||||
}
|
||||
|
||||
export type Helpers = {
|
||||
transformers: TransformPipeline
|
||||
}
|
3129
src/lib/strapi/strapi.generated.ts
Normal file
3129
src/lib/strapi/strapi.generated.ts
Normal file
File diff suppressed because it is too large
Load Diff
1085
src/lib/strapi/strapi.graphql
Normal file
1085
src/lib/strapi/strapi.graphql
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ 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 { strapiApi } from '../services/strapi'
|
||||
|
||||
type PageProps = Partial<Pick<StaticPageProps, 'data'>> & {
|
||||
error?: string
|
||||
@ -37,7 +37,8 @@ const Page: CustomNextPage<PageProps> = ({
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const { data } = await unbodyApi.getStaticPages()
|
||||
const { data } = await strapiApi.getStaticPages({})
|
||||
|
||||
return {
|
||||
paths: data.map((page) => ({
|
||||
params: {
|
||||
@ -59,17 +60,18 @@ export const getStaticProps: GetStaticProps<PageProps> = async (ctx) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data, errors } = await unbodyApi.getStaticPage({
|
||||
const { data, errors } = await strapiApi.getStaticPages({
|
||||
parseContent: true,
|
||||
slug: slug as string,
|
||||
...(id
|
||||
? {
|
||||
id,
|
||||
includeDrafts: true,
|
||||
published: false,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
if (!data || data.length === 0) {
|
||||
if (errors && typeof errors === 'string' && errors.includes('not found')) {
|
||||
return {
|
||||
notFound: true,
|
||||
@ -93,7 +95,7 @@ export const getStaticProps: GetStaticProps<PageProps> = async (ctx) => {
|
||||
return {
|
||||
props: {
|
||||
data: {
|
||||
page: data,
|
||||
page: data[0],
|
||||
},
|
||||
},
|
||||
notFound: false,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import unbodyApi from '../../../../../services/unbody/unbody.service'
|
||||
import { strapiApi } from '../../../../../services/strapi'
|
||||
import { parseInt } from '../../../../../utils/data.utils'
|
||||
|
||||
export default async function handler(
|
||||
@ -10,7 +10,8 @@ export default async function handler(
|
||||
query: { skip = 0, limit = 10, showSlug },
|
||||
} = req
|
||||
|
||||
const response = await unbodyApi.getLatestEpisodes({
|
||||
const response = await strapiApi.getLatestEpisodes({
|
||||
highlighted: 'exclude',
|
||||
skip: parseInt(skip, 0),
|
||||
limit: parseInt(limit, 10),
|
||||
showSlug: Array.isArray(showSlug) ? showSlug[0] : showSlug,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import unbodyApi from '../../../services/unbody/unbody.service'
|
||||
import { strapiApi } from '../../../services/strapi'
|
||||
import { parseInt } from '../../../utils/data.utils'
|
||||
|
||||
export default async function handler(
|
||||
@ -10,9 +10,10 @@ export default async function handler(
|
||||
query: { skip = 0, limit = 10 },
|
||||
} = req
|
||||
|
||||
const response = await unbodyApi.getRecentPosts({
|
||||
skip: parseInt(skip, 0),
|
||||
limit: parseInt(limit, 10),
|
||||
const response = await strapiApi.getRecentPosts({
|
||||
skip: parseInt(skip as string, 0),
|
||||
limit: parseInt(limit as string, 10),
|
||||
highlighted: 'exclude',
|
||||
})
|
||||
|
||||
res.status(200).json(response)
|
||||
|
@ -1,7 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import unbodyApi from '../../../services/unbody/unbody.service'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { parseInt } from '../../../utils/data.utils'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@ -61,31 +59,39 @@ export default async function handler(
|
||||
}
|
||||
|
||||
if (postTypes.length > 0) {
|
||||
const response = await unbodyApi.searchPosts({
|
||||
tags,
|
||||
query: Array.isArray(q) ? q.join(' ').trim() : q.trim(),
|
||||
// const response = await unbodyApi.searchPosts({
|
||||
// tags,
|
||||
// query: Array.isArray(q) ? q.join(' ').trim() : q.trim(),
|
||||
|
||||
type: postTypes as LPE.PostType[],
|
||||
// type: postTypes as LPE.PostType[],
|
||||
|
||||
limit: parseInt(limit, 50),
|
||||
skip: parseInt(skip, 0),
|
||||
})
|
||||
// limit: parseInt(limit, 50),
|
||||
// skip: parseInt(skip, 0),
|
||||
// })
|
||||
|
||||
const response = {
|
||||
data: [],
|
||||
}
|
||||
|
||||
result.posts.push(...response.data)
|
||||
}
|
||||
|
||||
if (blockTypes.length > 0) {
|
||||
const response = await unbodyApi.searchPostBlocks({
|
||||
tags,
|
||||
query: Array.isArray(q) ? q.join(' ') : q,
|
||||
// const response = await unbodyApi.searchPostBlocks({
|
||||
// tags,
|
||||
// query: Array.isArray(q) ? q.join(' ') : q,
|
||||
|
||||
postType: postTypes as LPE.PostType[],
|
||||
type: blockTypes as LPE.Post.ContentBlockType[],
|
||||
// postType: postTypes as LPE.PostType[],
|
||||
// type: blockTypes as LPE.Post.ContentBlockType[],
|
||||
|
||||
method: 'hybrid',
|
||||
limit: parseInt(limit, 50),
|
||||
skip: parseInt(skip, 0),
|
||||
})
|
||||
// method: 'hybrid',
|
||||
// limit: parseInt(limit, 50),
|
||||
// skip: parseInt(skip, 0),
|
||||
// })
|
||||
|
||||
const response = {
|
||||
data: [],
|
||||
}
|
||||
|
||||
result.blocks.push(...response.data)
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import unbodyApi from '../../../../services/unbody/unbody.service'
|
||||
import { LPE } from '../../../../types/lpe.types'
|
||||
import { parseInt } from '../../../../utils/data.utils'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@ -27,26 +25,26 @@ export default async function handler(
|
||||
blocks: [],
|
||||
}
|
||||
|
||||
const query: Parameters<(typeof unbodyApi)['searchPostBlocks']>[0] = {
|
||||
tags,
|
||||
type: ['text', 'image'],
|
||||
postId: Array.isArray(id) ? id[0] : id,
|
||||
query: Array.isArray(q) ? q.join(' ') : q,
|
||||
method: 'nearText',
|
||||
certainty: 0.85,
|
||||
// const query: Parameters<(typeof unbodyApi)['searchPostBlocks']>[0] = {
|
||||
// tags,
|
||||
// type: ['text', 'image'],
|
||||
// postId: Array.isArray(id) ? id[0] : id,
|
||||
// query: Array.isArray(q) ? q.join(' ') : q,
|
||||
// method: 'nearText',
|
||||
// certainty: 0.85,
|
||||
|
||||
limit: parseInt(limit, 50),
|
||||
skip: parseInt(skip, 0),
|
||||
}
|
||||
// limit: parseInt(limit, 50),
|
||||
// skip: parseInt(skip, 0),
|
||||
// }
|
||||
|
||||
result.blocks = await unbodyApi
|
||||
.searchPostBlocks(query)
|
||||
.then((res) => res.data)
|
||||
// result.blocks = await unbodyApi
|
||||
// .searchPostBlocks(query)
|
||||
// .then((res) => res.data)
|
||||
|
||||
if (result.blocks.length === 0)
|
||||
result.blocks = await unbodyApi
|
||||
.searchPostBlocks({ ...query, method: 'hybrid' })
|
||||
.then((res) => res.data)
|
||||
// if (result.blocks.length === 0)
|
||||
// result.blocks = await unbodyApi
|
||||
// .searchPostBlocks({ ...query, method: 'hybrid' })
|
||||
// .then((res) => res.data)
|
||||
|
||||
res.status(200).json({
|
||||
data: result,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { SEO } from '@/components/SEO'
|
||||
import ArticleContainer from '@/containers/ArticleContainer'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import unbodyApi from '../../services/unbody/unbody.service'
|
||||
import { strapiApi } from '../../services/strapi'
|
||||
import { LPE } from '../../types/lpe.types'
|
||||
|
||||
type ArticleProps = {
|
||||
@ -24,7 +24,7 @@ const ArticlePage = ({ data, errors, why }: ArticleProps) => {
|
||||
pagePath={`/article/${data.data.slug}`}
|
||||
date={data.data.createdAt}
|
||||
tags={[
|
||||
...data.data.tags,
|
||||
...data.data.tags.map((tag) => tag.name),
|
||||
...data.data.authors.map((author) => author.name),
|
||||
]}
|
||||
contentType={LPE.PostTypes.Article}
|
||||
@ -35,17 +35,18 @@ const ArticlePage = ({ data, errors, why }: ArticleProps) => {
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const { data: posts, errors } = await unbodyApi.getArticles({
|
||||
const { data, errors } = await strapiApi.getPosts({
|
||||
skip: 0,
|
||||
limit: 50,
|
||||
includeDrafts: false,
|
||||
highlighted: 'include',
|
||||
parseContent: false,
|
||||
published: true,
|
||||
})
|
||||
|
||||
return {
|
||||
paths: errors
|
||||
? []
|
||||
: posts.map((post) => ({ params: { path: [post.slug] } })),
|
||||
: data.data.map((post) => ({ params: { path: [post.slug] } })),
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
@ -68,13 +69,15 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data, errors } = await unbodyApi.getArticle({
|
||||
const { data: res, errors } = await strapiApi.getPosts({
|
||||
parseContent: true,
|
||||
slug: slug as string,
|
||||
...(id ? { id, includeDrafts: true } : {}),
|
||||
highlighted: 'include',
|
||||
published: true,
|
||||
...(id ? { id, published: false } : {}),
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
if (!res?.data || res.data.length === 0) {
|
||||
return {
|
||||
notFound: true,
|
||||
props: { why: 'no article' },
|
||||
@ -82,22 +85,26 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: relatedArticles } = await unbodyApi.getRelatedArticles({
|
||||
id: data.id,
|
||||
const article = res.data[0]
|
||||
|
||||
const { data: relatedArticles } = await strapiApi.getRelatedPosts({
|
||||
id: article.id,
|
||||
type: 'article',
|
||||
})
|
||||
|
||||
const { data: articlesFromSameAuthors } =
|
||||
await unbodyApi.getArticlesFromSameAuthors({
|
||||
slug: slug as string,
|
||||
authors: data.authors.map((author) => author.name),
|
||||
await strapiApi.getPostsFromSameAuthors({
|
||||
type: 'article',
|
||||
excludeId: article.id,
|
||||
authors: article.authors.map((author) => author.id),
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
data: {
|
||||
data,
|
||||
relatedArticles,
|
||||
articlesFromSameAuthors,
|
||||
data: article,
|
||||
relatedArticles: relatedArticles,
|
||||
articlesFromSameAuthors: articlesFromSameAuthors.data,
|
||||
},
|
||||
error: JSON.stringify(errors),
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import { CustomNextPage, GetStaticProps } from 'next'
|
||||
import SEO from '../components/SEO/SEO'
|
||||
import { HomePage, HomePageProps } from '../containers/HomePage'
|
||||
import { DefaultLayout } from '../layouts/DefaultLayout'
|
||||
import unbodyApi from '../services/unbody/unbody.service'
|
||||
import { strapiApi } from '../services/strapi'
|
||||
|
||||
type PageProps = Pick<HomePageProps, 'data'>
|
||||
|
||||
@ -27,19 +27,26 @@ Page.getLayout = function getLayout(page: React.ReactNode) {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<PageProps> = async () => {
|
||||
const { data: tags = [] } = await unbodyApi.getTopics(true)
|
||||
const { data: highlighted } = await unbodyApi.getHighlightedPosts()
|
||||
const { data: latest } = await unbodyApi.getRecentPosts({
|
||||
skip: 0,
|
||||
limit: 12,
|
||||
// const { data: tags = [] } = await unbodyApi.getTopics(true)
|
||||
// const { data: highlighted } = await unbodyApi.getHighlightedPosts()
|
||||
// const { data: latest } = await unbodyApi.getRecentPosts({
|
||||
// skip: 0,
|
||||
// limit: 15,
|
||||
// })
|
||||
const { data: latest } = await strapiApi.getRecentPosts({
|
||||
highlighted: 'exclude',
|
||||
limit: 15,
|
||||
})
|
||||
|
||||
const { data: _shows = [] } = await unbodyApi.getPodcastShows({
|
||||
const { data: highlighted } = await strapiApi.getHighlightedPosts()
|
||||
const { data: _shows = [] } = await strapiApi.getPodcastShows({
|
||||
populateEpisodes: true,
|
||||
episodesLimit: 10,
|
||||
})
|
||||
|
||||
const shows = [..._shows].sort((a, b) => (a.title > b.title ? -1 : 1))
|
||||
const shows = [...(_shows ?? [])].sort((a, b) => (a.title > b.title ? -1 : 1))
|
||||
|
||||
const { data: tags = [] } = await strapiApi.getTopics()
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
@ -3,7 +3,7 @@ import EpisodeContainer from '@/containers/EpisodeContainer'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
|
||||
import unbodyApi from '@/services/unbody/unbody.service'
|
||||
import { strapiApi } from '../../../services/strapi'
|
||||
import { getPostLink } from '../../../utils/route.utils'
|
||||
|
||||
type EpisodeProps = {
|
||||
@ -29,7 +29,7 @@ const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
|
||||
postSlug: episode.slug as string,
|
||||
})}
|
||||
tags={[
|
||||
...episode.tags,
|
||||
...episode.tags.map((tag) => tag.name),
|
||||
...episode.authors.map((author) => author.name),
|
||||
]}
|
||||
contentType={LPE.PostTypes.Podcast}
|
||||
@ -40,7 +40,7 @@ const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: true })
|
||||
const { data } = await strapiApi.getPodcastShows({ populateEpisodes: true })
|
||||
|
||||
const paths = data.flatMap((show) => {
|
||||
return (
|
||||
@ -81,25 +81,36 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
}
|
||||
|
||||
// TODO : error handling
|
||||
const { data: episode, errors: episodeErros } =
|
||||
await unbodyApi.getPodcastEpisode({
|
||||
showSlug: showSlug as string,
|
||||
slug: epSlug as string,
|
||||
textBlocks: true,
|
||||
...(id
|
||||
? {
|
||||
id: id as string,
|
||||
includeDraft: true,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
const { data: episode, errors: episodeErros } = await strapiApi.getEpisode({
|
||||
showSlug: showSlug as string,
|
||||
slug: epSlug as string,
|
||||
published: true,
|
||||
...(id
|
||||
? {
|
||||
id: id as string,
|
||||
published: false,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
const { data: shows } = await strapiApi.getPodcastShows({})
|
||||
|
||||
// TODO : error handlings
|
||||
const { data: relatedEpisodes, errors: relatedEpisodesErros } =
|
||||
await unbodyApi.getRelatedEpisodes({
|
||||
showSlug: showSlug as string,
|
||||
id: episode?.id as string,
|
||||
})
|
||||
await strapiApi
|
||||
.getRelatedPosts({
|
||||
id: episode?.id as string,
|
||||
type: LPE.PostTypes.Podcast,
|
||||
})
|
||||
.then((data) => ({
|
||||
...data,
|
||||
data: data.data.map((post) => ({
|
||||
...post,
|
||||
show: shows.find(
|
||||
(show) => show.id === (post as LPE.Podcast.Document).showId,
|
||||
),
|
||||
})),
|
||||
}))
|
||||
|
||||
if (!episode) {
|
||||
return {
|
||||
@ -112,7 +123,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
return {
|
||||
props: {
|
||||
episode,
|
||||
relatedEpisodes,
|
||||
relatedEpisodes: relatedEpisodes,
|
||||
},
|
||||
revalidate: 10,
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { SEO } from '@/components/SEO'
|
||||
import PodcastShowContainer from '@/containers/PodcastShowContainer'
|
||||
import unbodyApi from '@/services/unbody/unbody.service'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ReactNode } from 'react'
|
||||
import { DefaultLayout } from '../../../layouts/DefaultLayout'
|
||||
import { strapiApi } from '../../../services/strapi'
|
||||
import { ApiPaginatedPayload } from '../../../types/data.types'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { getPostLink } from '../../../utils/route.utils'
|
||||
@ -48,7 +48,7 @@ const PodcastShowPage = ({
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: false })
|
||||
const { data } = await strapiApi.getPodcastShows({ populateEpisodes: false })
|
||||
|
||||
const paths = data.map((show) => {
|
||||
return {
|
||||
@ -76,30 +76,31 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
}
|
||||
|
||||
// TODO : error handling
|
||||
const { data: show, errors: podcastShowDataErrors } =
|
||||
await unbodyApi.getPodcastShow({
|
||||
showSlug: showSlug as string,
|
||||
const { data: shows, errors: podcastShowDataErrors } =
|
||||
await strapiApi.getPodcastShows({
|
||||
slug: showSlug as string,
|
||||
})
|
||||
|
||||
// TODO : error handling
|
||||
const { data: latestEpisodes, errors: latestEpisodesErros } =
|
||||
await unbodyApi.getLatestEpisodes({
|
||||
await strapiApi.getLatestEpisodes({
|
||||
showSlug: showSlug as string,
|
||||
limit: 8,
|
||||
})
|
||||
|
||||
// TODO : error handling
|
||||
const { data: highlightedEpisodes, errors: highlightedEpisodesErrors } =
|
||||
await unbodyApi.getHighlightedEpisodes({
|
||||
showSlug: showSlug as string,
|
||||
await strapiApi.getLatestEpisodes({
|
||||
highlighted: 'only',
|
||||
limit: 2,
|
||||
showSlug: showSlug as string,
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
show,
|
||||
show: shows[0],
|
||||
latestEpisodes,
|
||||
highlightedEpisodes,
|
||||
highlightedEpisodes: highlightedEpisodes.data,
|
||||
},
|
||||
revalidate: 10,
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { SEO } from '@/components/SEO'
|
||||
import PodcastsContainer from '@/containers/PodcastsContainer'
|
||||
import unbodyApi from '@/services/unbody/unbody.service'
|
||||
import { GetStaticPropsContext } from 'next'
|
||||
import { ReactNode } from 'react'
|
||||
import { DefaultLayout } from '../../layouts/DefaultLayout'
|
||||
import { strapiApi } from '../../services/strapi'
|
||||
import { LPE } from '../../types/lpe.types'
|
||||
import { getPostLink } from '../../utils/route.utils'
|
||||
|
||||
@ -43,18 +43,17 @@ const PodcastShowPage = ({
|
||||
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
// TODO : error handling
|
||||
const { data: podcastShows, errors: podcastShowsErrors } =
|
||||
await unbodyApi.getPodcastShows({
|
||||
await strapiApi.getPodcastShows({
|
||||
populateEpisodes: true,
|
||||
episodesLimit: 6,
|
||||
})
|
||||
|
||||
// TODO : error handling
|
||||
const { data: highlightedEpisodes, errors: highlightedEpisodesErrors } =
|
||||
await unbodyApi.getHighlightedEpisodes({})
|
||||
await strapiApi.getLatestEpisodes({ highlighted: 'only' })
|
||||
|
||||
const { data: latestEpisodes } = await unbodyApi.getPodcastEpisodes({
|
||||
const { data: latestEpisodes } = await strapiApi.getLatestEpisodes({
|
||||
limit: 10,
|
||||
populateShow: true,
|
||||
highlighted: 'exclude',
|
||||
})
|
||||
|
||||
@ -65,20 +64,11 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||
}
|
||||
}
|
||||
|
||||
const latestEps = podcastShows
|
||||
.flatMap((show) => show.episodes)
|
||||
.sort((a, b) => {
|
||||
const aDate = new Date(a!.publishedAt)
|
||||
const bDate = new Date(b!.publishedAt)
|
||||
return aDate > bDate ? -1 : 1
|
||||
})
|
||||
.filter((p) => p?.highlighted !== true)
|
||||
|
||||
return {
|
||||
props: {
|
||||
shows: podcastShows,
|
||||
highlightedEpisodes,
|
||||
latestEpisodes: latestEps,
|
||||
latestEpisodes: latestEpisodes.data,
|
||||
highlightedEpisodes: highlightedEpisodes.data,
|
||||
// errors,
|
||||
},
|
||||
revalidate: 10,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { CustomNextPage, GetServerSideProps } from 'next'
|
||||
import SEO from '../components/SEO/SEO'
|
||||
import unbodyApi from '../services/unbody/unbody.service'
|
||||
import { getPostLink } from '../utils/route.utils'
|
||||
|
||||
type PageProps = {}
|
||||
@ -20,10 +19,13 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
notFound: true,
|
||||
}
|
||||
|
||||
const { data, errors } = await unbodyApi.getDocById({
|
||||
id,
|
||||
includeDrafts: true,
|
||||
})
|
||||
// const { data, errors } = await unbodyApi.getDocById({
|
||||
// id,
|
||||
// includeDrafts: true,
|
||||
// })
|
||||
|
||||
const data = undefined as any
|
||||
const errors = '' as string
|
||||
|
||||
if (!data || errors) {
|
||||
return {
|
||||
|
@ -12,7 +12,6 @@ import { copyConfigs } from '../configs/copy.configs'
|
||||
import { GlobalSearchBox } from '../containers/GlobalSearchBox/GlobalSearchBox'
|
||||
import { DefaultLayout } from '../layouts/DefaultLayout'
|
||||
import { api } from '../services/api.service'
|
||||
import unbodyApi from '../services/unbody/unbody.service'
|
||||
|
||||
interface SearchPageProps {
|
||||
topics: string[]
|
||||
@ -118,11 +117,14 @@ SearchPage.getLayout = (page: ReactNode) => (
|
||||
)
|
||||
|
||||
export async function getStaticProps() {
|
||||
const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
|
||||
const { data: shows = [] } = await unbodyApi.getPodcastShows({
|
||||
populateEpisodes: true,
|
||||
episodesLimit: 10,
|
||||
})
|
||||
// const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
|
||||
// const { data: shows = [] } = await unbodyApi.getPodcastShows({
|
||||
// populateEpisodes: true,
|
||||
// episodesLimit: 10,
|
||||
// })
|
||||
|
||||
const topics = [] as any
|
||||
const shows = [] as any
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
@ -44,7 +44,8 @@ export const useRecentPosts = ({
|
||||
)
|
||||
|
||||
const posts = useMemo(
|
||||
() => (query.data?.pages || []).flatMap((page) => page.posts),
|
||||
() =>
|
||||
(query.data?.pages || []).flatMap((page) => page.posts).filter(Boolean),
|
||||
[query.data],
|
||||
)
|
||||
|
||||
|
1
src/services/strapi/index.ts
Normal file
1
src/services/strapi/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './strapi.service'
|
182
src/services/strapi/strapi.operators.ts
Normal file
182
src/services/strapi/strapi.operators.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
const POST_COMMON_ATTRIBUTES = gql`
|
||||
fragment PostCommonAttributes on Post {
|
||||
type
|
||||
title
|
||||
subtitle
|
||||
summary
|
||||
slug
|
||||
featured
|
||||
episode_number
|
||||
podcast_show {
|
||||
data {
|
||||
id
|
||||
}
|
||||
}
|
||||
cover_image {
|
||||
data {
|
||||
attributes {
|
||||
url
|
||||
width
|
||||
height
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
authors {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
email_address
|
||||
}
|
||||
}
|
||||
}
|
||||
publish_date
|
||||
publishedAt
|
||||
tags {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_PODCAST_SHOWS = gql`
|
||||
query GetPodcastShows(
|
||||
$filters: PodcastShowFiltersInput
|
||||
$pagination: PaginationArg
|
||||
$sort: [String]
|
||||
$publicationState: PublicationState
|
||||
) {
|
||||
podcastShows(
|
||||
filters: $filters
|
||||
pagination: $pagination
|
||||
sort: $sort
|
||||
publicationState: $publicationState
|
||||
) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
slug
|
||||
description
|
||||
hosts {
|
||||
data {
|
||||
attributes {
|
||||
name
|
||||
email_address
|
||||
}
|
||||
}
|
||||
}
|
||||
logo {
|
||||
data {
|
||||
attributes {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_POSTS_QUERY = gql`
|
||||
${POST_COMMON_ATTRIBUTES}
|
||||
|
||||
query GetPosts(
|
||||
$filters: PostFiltersInput
|
||||
$pagination: PaginationArg
|
||||
$sort: [String]
|
||||
$publicationState: PublicationState
|
||||
$withContent: Boolean = false
|
||||
) {
|
||||
posts(
|
||||
filters: $filters
|
||||
pagination: $pagination
|
||||
sort: $sort
|
||||
publicationState: $publicationState
|
||||
) {
|
||||
meta {
|
||||
pagination {
|
||||
total
|
||||
page
|
||||
pageSize
|
||||
pageCount
|
||||
}
|
||||
}
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...PostCommonAttributes
|
||||
body @include(if: $withContent)
|
||||
credits @include(if: $withContent)
|
||||
channel @include(if: $withContent) {
|
||||
channel
|
||||
link
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_RELATED_POSTS_QUERY = gql`
|
||||
${POST_COMMON_ATTRIBUTES}
|
||||
|
||||
query GetRelatedPosts($id: ID!, $type: String!) {
|
||||
post(id: $id) {
|
||||
data {
|
||||
attributes {
|
||||
related_posts(
|
||||
publicationState: LIVE
|
||||
filters: { type: { eq: $type } }
|
||||
) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...PostCommonAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_STATIC_PAGES = gql`
|
||||
query GetStaticPages(
|
||||
$filters: PageFiltersInput
|
||||
$pagination: PaginationArg
|
||||
$sort: [String]
|
||||
$publicationState: PublicationState
|
||||
$withContent: Boolean = false
|
||||
) {
|
||||
pages(
|
||||
sort: $sort
|
||||
filters: $filters
|
||||
pagination: $pagination
|
||||
publicationState: $publicationState
|
||||
) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
title
|
||||
subtitle
|
||||
description
|
||||
publishedAt
|
||||
body @include(if: $withContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
502
src/services/strapi/strapi.service.ts
Normal file
502
src/services/strapi/strapi.service.ts
Normal file
@ -0,0 +1,502 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
import axios, { Axios } from 'axios'
|
||||
import {
|
||||
Enum_Post_Type,
|
||||
GetPodcastShowsDocument,
|
||||
GetPostsDocument,
|
||||
GetRelatedPostsDocument,
|
||||
GetStaticPagesDocument,
|
||||
PodcastShowFiltersInput,
|
||||
PostFiltersInput,
|
||||
} from '../../lib/strapi/strapi.generated'
|
||||
import { ApiResponse } from '../../types/data.types'
|
||||
import { LPE } from '../../types/lpe.types'
|
||||
import { settle } from '../../utils/promise.utils'
|
||||
import { strapiTransformers } from './transformers/strapi.transformers'
|
||||
|
||||
export class StrapiService {
|
||||
client: ApolloClient<any> = null as any
|
||||
axios: Axios = null as any
|
||||
|
||||
constructor(apiUrl: string, graphqlUrl: string, apiKey: string) {
|
||||
this.axios = axios.create({
|
||||
baseURL: apiUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
const cache = new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
podcast_shows: {
|
||||
merge(existing = {}, incoming) {
|
||||
return {
|
||||
...existing,
|
||||
...incoming,
|
||||
}
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
merge(existing = {}, incoming) {
|
||||
return {
|
||||
...existing,
|
||||
...incoming,
|
||||
}
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
merge(existing = {}, incoming) {
|
||||
return {
|
||||
...existing,
|
||||
...incoming,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
this.client = new ApolloClient({
|
||||
cache,
|
||||
uri: graphqlUrl,
|
||||
ssrMode: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
handleResponse = <T>(
|
||||
data: T | null = null,
|
||||
errors: any = null,
|
||||
): ApiResponse<T> => {
|
||||
if (errors) console.error(errors)
|
||||
if (errors || !data) {
|
||||
return {
|
||||
data: data as any,
|
||||
errors: JSON.stringify(errors),
|
||||
}
|
||||
}
|
||||
return {
|
||||
data,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest = async <T = any>(
|
||||
handler: () => T | Promise<T>,
|
||||
defaultValue?: T,
|
||||
): Promise<ApiResponse<T>> => {
|
||||
const [res, err] = await settle<T>(handler)
|
||||
if (err) return this.handleResponse(defaultValue || null, err)
|
||||
return this.handleResponse(res)
|
||||
}
|
||||
|
||||
getStaticPages = async ({
|
||||
skip = 0,
|
||||
limit = 10,
|
||||
sort = ['publishedAt:desc'],
|
||||
filters = {},
|
||||
id,
|
||||
slug,
|
||||
published = true,
|
||||
parseContent = false,
|
||||
}: {
|
||||
skip?: number
|
||||
limit?: number
|
||||
sort?: string[]
|
||||
id?: string
|
||||
slug?: string
|
||||
filters?: PostFiltersInput
|
||||
published?: boolean
|
||||
parseContent?: boolean
|
||||
}) =>
|
||||
this.handleRequest<LPE.StaticPage.Document[]>(async () => {
|
||||
const result: LPE.StaticPage.Document[] = []
|
||||
|
||||
const {
|
||||
data: {
|
||||
pages: { data },
|
||||
},
|
||||
} = await this.client.query({
|
||||
query: GetStaticPagesDocument,
|
||||
variables: {
|
||||
pagination: {
|
||||
start: skip,
|
||||
limit: limit,
|
||||
},
|
||||
filters: {
|
||||
and: [
|
||||
...(filters ? [filters] : []),
|
||||
...(slug ? [{ slug: { eq: slug } }] : []),
|
||||
...(id ? [{ id: { eq: id } }] : []),
|
||||
],
|
||||
},
|
||||
sort,
|
||||
withContent: parseContent,
|
||||
publicationState: published ? 'LIVE' : 'PREVIEW',
|
||||
},
|
||||
})
|
||||
|
||||
result.push(
|
||||
...(await strapiTransformers.transformMany(
|
||||
strapiTransformers.get({}),
|
||||
data,
|
||||
undefined,
|
||||
undefined,
|
||||
)),
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
getPosts = async ({
|
||||
skip = 0,
|
||||
limit = 10,
|
||||
sort = ['publish_date:desc'],
|
||||
filters = {},
|
||||
id,
|
||||
slug,
|
||||
published = true,
|
||||
highlighted = 'include',
|
||||
parseContent = false,
|
||||
}: {
|
||||
skip?: number
|
||||
limit?: number
|
||||
sort?: string[]
|
||||
id?: string
|
||||
slug?: string
|
||||
filters?: PostFiltersInput
|
||||
published?: boolean
|
||||
highlighted?: 'only' | 'include' | 'exclude'
|
||||
parseContent?: boolean
|
||||
}) =>
|
||||
this.handleRequest(async () => {
|
||||
const result: LPE.Post.Document[] = []
|
||||
|
||||
const {
|
||||
data: {
|
||||
posts: { data, meta },
|
||||
},
|
||||
} = await this.client.query({
|
||||
query: GetPostsDocument,
|
||||
variables: {
|
||||
pagination: {
|
||||
start: skip,
|
||||
limit: limit,
|
||||
},
|
||||
filters: {
|
||||
and: [
|
||||
...(filters ? [filters] : []),
|
||||
...(slug ? [{ slug: { eq: slug } }] : []),
|
||||
...(id ? [{ id: { eq: id } }] : []),
|
||||
...(highlighted === 'only'
|
||||
? [
|
||||
{
|
||||
featured: {
|
||||
eq: true,
|
||||
},
|
||||
} as PostFiltersInput,
|
||||
]
|
||||
: highlighted === 'exclude'
|
||||
? [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
featured: {
|
||||
eq: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
featured: {
|
||||
null: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as PostFiltersInput,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
sort,
|
||||
withContent: parseContent,
|
||||
publicationState: published ? 'LIVE' : 'PREVIEW',
|
||||
},
|
||||
})
|
||||
|
||||
result.push(
|
||||
...(await strapiTransformers.transformMany(
|
||||
strapiTransformers.get({}),
|
||||
data,
|
||||
undefined,
|
||||
undefined,
|
||||
)),
|
||||
)
|
||||
|
||||
return {
|
||||
data: result,
|
||||
total: meta.pagination.total,
|
||||
hasMore: meta.pagination.page < meta.pagination.pageCount,
|
||||
}
|
||||
})
|
||||
|
||||
getPodcastShows = async ({
|
||||
skip = 0,
|
||||
limit = 10,
|
||||
sort = ['createdAt:desc'],
|
||||
filters = {},
|
||||
slug,
|
||||
published = true,
|
||||
populateEpisodes = false,
|
||||
episodesLimit = 10,
|
||||
}: {
|
||||
skip?: number
|
||||
limit?: number
|
||||
sort?: string[]
|
||||
slug?: string
|
||||
filters?: PodcastShowFiltersInput
|
||||
published?: boolean
|
||||
populateEpisodes?: boolean
|
||||
episodesLimit?: number
|
||||
}) =>
|
||||
this.handleRequest<LPE.Podcast.Show[]>(async () => {
|
||||
const result: LPE.Podcast.Show[] = []
|
||||
|
||||
const {
|
||||
data: {
|
||||
podcastShows: { data },
|
||||
},
|
||||
} = await this.client.query({
|
||||
query: GetPodcastShowsDocument,
|
||||
variables: {
|
||||
pagination: {
|
||||
start: skip,
|
||||
limit: limit,
|
||||
},
|
||||
filters: {
|
||||
and: [
|
||||
...(filters ? [filters] : []),
|
||||
...(slug ? [{ slug: { eq: slug } }] : []),
|
||||
],
|
||||
},
|
||||
sort,
|
||||
publicationState: published ? 'LIVE' : 'PREVIEW',
|
||||
},
|
||||
})
|
||||
|
||||
result.push(
|
||||
...(await strapiTransformers.transformMany(
|
||||
strapiTransformers.get({}),
|
||||
data,
|
||||
undefined,
|
||||
undefined,
|
||||
)),
|
||||
)
|
||||
|
||||
for (const show of result) {
|
||||
const { data } = await this.getPosts({
|
||||
filters: {
|
||||
podcast_show: {
|
||||
id: {
|
||||
eq: show.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
limit: episodesLimit,
|
||||
highlighted: 'include',
|
||||
parseContent: false,
|
||||
published: true,
|
||||
})
|
||||
|
||||
if (populateEpisodes)
|
||||
show.episodes = data.data as LPE.Podcast.Document[]
|
||||
|
||||
show.numberOfEpisodes = data.total
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
getRecentPosts = async ({
|
||||
skip = 0,
|
||||
limit = 10,
|
||||
highlighted = 'exclude',
|
||||
}: {
|
||||
limit?: number
|
||||
skip?: number
|
||||
highlighted?: 'only' | 'include' | 'exclude'
|
||||
}) =>
|
||||
this.handleRequest(async () => {
|
||||
const posts = await this.getPosts({
|
||||
limit,
|
||||
skip,
|
||||
highlighted,
|
||||
parseContent: false,
|
||||
published: true,
|
||||
})
|
||||
|
||||
return posts.data
|
||||
})
|
||||
|
||||
getHighlightedPosts = async () =>
|
||||
this.handleRequest(async () => {
|
||||
const posts = await this.getPosts({
|
||||
limit: 10,
|
||||
highlighted: 'only',
|
||||
parseContent: false,
|
||||
published: true,
|
||||
})
|
||||
|
||||
return posts.data.data
|
||||
})
|
||||
|
||||
getLatestEpisodes = async ({
|
||||
showSlug,
|
||||
limit = 10,
|
||||
skip = 0,
|
||||
highlighted = 'include',
|
||||
}: {
|
||||
showSlug?: string
|
||||
limit?: number
|
||||
skip?: number
|
||||
highlighted?: 'only' | 'include' | 'exclude'
|
||||
}) =>
|
||||
this.getPosts({
|
||||
limit,
|
||||
skip,
|
||||
highlighted,
|
||||
parseContent: false,
|
||||
published: true,
|
||||
filters: {
|
||||
...(showSlug
|
||||
? {
|
||||
podcast_show: {
|
||||
slug: {
|
||||
eq: showSlug,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
type: {
|
||||
eq: 'Episode' as Enum_Post_Type,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
getEpisode = async ({
|
||||
id,
|
||||
slug,
|
||||
showSlug,
|
||||
published = true,
|
||||
}: {
|
||||
showSlug: string
|
||||
slug?: string
|
||||
id?: string
|
||||
published?: boolean
|
||||
}) =>
|
||||
this.getPosts({
|
||||
limit: 1,
|
||||
highlighted: 'include',
|
||||
parseContent: true,
|
||||
published,
|
||||
id,
|
||||
slug,
|
||||
filters: {
|
||||
...(showSlug
|
||||
? {
|
||||
podcast_show: {
|
||||
slug: {
|
||||
eq: showSlug,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
type: {
|
||||
eq: 'Episode' as Enum_Post_Type,
|
||||
},
|
||||
},
|
||||
}).then((res) => ({
|
||||
...res,
|
||||
data: res.data.data[0] as LPE.Podcast.Document,
|
||||
}))
|
||||
|
||||
getPostsFromSameAuthors = async ({
|
||||
type,
|
||||
authors,
|
||||
excludeId,
|
||||
}: {
|
||||
authors: string[]
|
||||
excludeId?: string
|
||||
type: LPE.PostType
|
||||
}) =>
|
||||
this.getPosts({
|
||||
limit: 10,
|
||||
highlighted: 'include',
|
||||
parseContent: false,
|
||||
filters: {
|
||||
and: [
|
||||
{
|
||||
authors: {
|
||||
id: {
|
||||
in: authors,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: {
|
||||
eq: type === 'article' ? 'Article' : 'Podcast',
|
||||
},
|
||||
},
|
||||
...(excludeId
|
||||
? [
|
||||
{
|
||||
id: {
|
||||
ne: excludeId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
getRelatedPosts = async ({ id, type }: { id: string; type: LPE.PostType }) =>
|
||||
this.handleRequest<LPE.Post.Document[]>(async () => {
|
||||
const { data } = await this.client.query({
|
||||
query: GetRelatedPostsDocument,
|
||||
variables: {
|
||||
id,
|
||||
type:
|
||||
type === 'article'
|
||||
? ('Article' as Enum_Post_Type)
|
||||
: ('Episode' as Enum_Post_Type),
|
||||
},
|
||||
})
|
||||
|
||||
return strapiTransformers.transformMany(
|
||||
strapiTransformers.get({}),
|
||||
data.post.data.attributes.related_posts.data,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
getTopics = async () =>
|
||||
this.handleRequest<LPE.Tag.Document[]>(async () => {
|
||||
const { data } = await this.axios.get('/tags/getAll')
|
||||
return data.map((tag: any) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
postsCount: tag.posts?.count ?? 0,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
export const strapiApi = new StrapiService(
|
||||
process.env.STRAPI_API_URL || '',
|
||||
process.env.STRAPI_GRAPHQL_URL || '',
|
||||
process.env.STRAPI_API_KEY || '',
|
||||
)
|
10
src/services/strapi/strapi.types.ts
Normal file
10
src/services/strapi/strapi.types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {
|
||||
GetPodcastShowsQuery,
|
||||
GetPostsQuery,
|
||||
GetStaticPagesQuery,
|
||||
} from '../../lib/strapi/strapi.generated'
|
||||
|
||||
export type StrapiPostData = GetPostsQuery['posts']['data'][number]
|
||||
export type StrapiPodcastShowData =
|
||||
GetPodcastShowsQuery['podcastShows']['data'][number]
|
||||
export type StrapiStaticPageData = GetStaticPagesQuery['pages']['data'][number]
|
104
src/services/strapi/transformers/Episode.transformer.ts
Normal file
104
src/services/strapi/transformers/Episode.transformer.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { Transformer } from '../../../lib/TransformPipeline/types'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { settle } from '../../../utils/promise.utils'
|
||||
import { simplecastApi } from '../../simplecast.service'
|
||||
import { StrapiPostData } from '../strapi.types'
|
||||
import { transformStrapiHtmlContent } from './utils'
|
||||
|
||||
export const episodeTransformer: Transformer<
|
||||
LPE.Podcast.Document,
|
||||
LPE.Podcast.Document,
|
||||
StrapiPostData,
|
||||
undefined,
|
||||
undefined
|
||||
> = {
|
||||
key: 'EpisodeTransformer',
|
||||
classes: ['episode'],
|
||||
objectType: 'Post',
|
||||
isMatch: (helpers, object) => object.type === LPE.PostTypes.Podcast,
|
||||
transform: async (helpers, data, original, root, ctx) => {
|
||||
return {
|
||||
...data,
|
||||
credits: transformStrapiHtmlContent({
|
||||
html: original.attributes.credits || '',
|
||||
}).blocks as LPE.Post.TextBlock[],
|
||||
channels: original.attributes.channel
|
||||
? await transformChannels(original.attributes.channel)
|
||||
: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const transformChannels = async (
|
||||
channels: StrapiPostData['attributes']['channel'] = [],
|
||||
) => {
|
||||
const transformed: LPE.Podcast.Content['channels'] = []
|
||||
|
||||
for (const channel of channels) {
|
||||
const { channel: name, link } = channel
|
||||
|
||||
switch (name) {
|
||||
case 'Apple_Podcasts':
|
||||
transformed.push({
|
||||
name: LPE.Podcast.ChannelNames.ApplePodcasts,
|
||||
url: link,
|
||||
})
|
||||
break
|
||||
|
||||
case 'Google_Podcasts': {
|
||||
transformed.push({
|
||||
name: LPE.Podcast.ChannelNames.GooglePodcasts,
|
||||
url: link,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'Spotify': {
|
||||
transformed.push({
|
||||
name: LPE.Podcast.ChannelNames.Spotify,
|
||||
url: link,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'Youtube': {
|
||||
transformed.push({
|
||||
name: LPE.Podcast.ChannelNames.Youtube,
|
||||
url: link,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'Simplecast': {
|
||||
const episodeId = simplecastApi.extractEpisodeIdFromUrl(link)
|
||||
|
||||
if (!episodeId) {
|
||||
console.error('invalid Simplecast player url!')
|
||||
continue
|
||||
}
|
||||
|
||||
const [res, err] = await settle(() =>
|
||||
simplecastApi.getEpisode({ id: episodeId }),
|
||||
)
|
||||
|
||||
if (err) {
|
||||
console.error('failed to fetch Simplecast episode ', link)
|
||||
console.error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
transformed.push({
|
||||
name: LPE.Podcast.ChannelNames.Simplecast,
|
||||
url: link,
|
||||
data: {
|
||||
duration: res.duration,
|
||||
audioFileUrl: res.ad_free_audio_file_url ?? res.audio_file?.url,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transformed
|
||||
}
|
39
src/services/strapi/transformers/PodcastShow.transformer.ts
Normal file
39
src/services/strapi/transformers/PodcastShow.transformer.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Transformer } from '../../../lib/TransformPipeline/types'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { getPostLink } from '../../../utils/route.utils'
|
||||
import { StrapiPodcastShowData } from '../strapi.types'
|
||||
import { transformStrapiHtmlContent, transformStrapiImageData } from './utils'
|
||||
|
||||
export const podcastShowTransformer: Transformer<
|
||||
StrapiPodcastShowData,
|
||||
LPE.Podcast.Show,
|
||||
StrapiPodcastShowData,
|
||||
undefined,
|
||||
undefined
|
||||
> = {
|
||||
key: 'PodcastShowTransformer',
|
||||
classes: ['podcast'],
|
||||
objectType: 'PodcastShow',
|
||||
isMatch: (helpers, object) => object.__typename === 'PodcastShowEntity',
|
||||
transform: (helpers, data, original, root, ctx) => {
|
||||
const { id, attributes } = data
|
||||
|
||||
return {
|
||||
id,
|
||||
slug: attributes.slug,
|
||||
title: attributes.name,
|
||||
numberOfEpisodes: 0,
|
||||
url: getPostLink('podcast', { showSlug: attributes.slug }),
|
||||
description: attributes.description,
|
||||
descriptionText: transformStrapiHtmlContent({
|
||||
html: attributes.description || '',
|
||||
}).text,
|
||||
hosts: attributes.hosts.data.map((host) => ({
|
||||
id: '',
|
||||
name: host.attributes.name,
|
||||
emailAddress: host.attributes.email_address,
|
||||
})),
|
||||
logo: transformStrapiImageData(attributes.logo),
|
||||
}
|
||||
},
|
||||
}
|
109
src/services/strapi/transformers/Post.transformer.ts
Normal file
109
src/services/strapi/transformers/Post.transformer.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Transformer } from '../../../lib/TransformPipeline/types'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { calcReadingTime } from '../../../utils/string.utils'
|
||||
import { StrapiPostData } from '../strapi.types'
|
||||
import { transformStrapiHtmlContent, transformStrapiImageData } from './utils'
|
||||
|
||||
export const postTransformer: Transformer<
|
||||
StrapiPostData,
|
||||
LPE.Post.Document,
|
||||
StrapiPostData,
|
||||
undefined,
|
||||
undefined
|
||||
> = {
|
||||
key: 'PostTransformer',
|
||||
classes: ['post'],
|
||||
objectType: 'Post',
|
||||
isMatch: (helpers, object) => object.__typename === 'PostEntity',
|
||||
transform: (helpers, data, original, root, ctx) => {
|
||||
const { id, attributes } = data
|
||||
|
||||
const type = attributes.type
|
||||
const title = attributes.title
|
||||
const subtitle = attributes.subtitle
|
||||
const slug = attributes.slug
|
||||
const publishedAt = attributes.publish_date
|
||||
const isHighlighted = attributes.featured
|
||||
const isDraft = !attributes.publishedAt
|
||||
const coverImage: LPE.Post.Document['coverImage'] =
|
||||
transformStrapiImageData(attributes.cover_image)
|
||||
const tags: LPE.Tag.Document[] = attributes.tags.data.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.attributes.name,
|
||||
}))
|
||||
|
||||
const authors = attributes.authors.data.map((author) => ({
|
||||
id: author.id,
|
||||
name: author.attributes.name,
|
||||
emailAddress: author.attributes.email_address,
|
||||
}))
|
||||
|
||||
const summary = transformStrapiHtmlContent({
|
||||
html: attributes.summary || '',
|
||||
}).text
|
||||
|
||||
const {
|
||||
blocks: content,
|
||||
toc,
|
||||
text,
|
||||
} = transformStrapiHtmlContent({
|
||||
html: attributes.body || '',
|
||||
})
|
||||
|
||||
// add the title as the first toc item
|
||||
{
|
||||
toc.unshift({
|
||||
href: '#title-anchor',
|
||||
blockIndex: -1,
|
||||
level: 0,
|
||||
tag: 'h1',
|
||||
title,
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'Article') {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
slug,
|
||||
modifiedAt: publishedAt,
|
||||
createdAt: publishedAt,
|
||||
coverImage,
|
||||
tags,
|
||||
content,
|
||||
summary,
|
||||
readingTime: calcReadingTime(text),
|
||||
toc,
|
||||
type: 'article',
|
||||
authors,
|
||||
highlighted: isHighlighted,
|
||||
isDraft,
|
||||
} as LPE.Article.Data
|
||||
} else {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
slug,
|
||||
publishedAt: publishedAt,
|
||||
createdAt: publishedAt,
|
||||
coverImage,
|
||||
tags,
|
||||
channels: [],
|
||||
description: summary,
|
||||
type: 'podcast',
|
||||
isDraft,
|
||||
highlighted: isHighlighted,
|
||||
authors,
|
||||
content,
|
||||
episodeNumber: attributes.episode_number,
|
||||
showId: attributes.podcast_show.data?.id || null,
|
||||
modifiedAt: publishedAt,
|
||||
// will be filled in later
|
||||
credits: [],
|
||||
transcription: [],
|
||||
} as LPE.Podcast.Document
|
||||
}
|
||||
},
|
||||
}
|
42
src/services/strapi/transformers/StaticPage.transformer.ts
Normal file
42
src/services/strapi/transformers/StaticPage.transformer.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Transformer } from '../../../lib/TransformPipeline/types'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { StrapiStaticPageData } from '../strapi.types'
|
||||
import { transformStrapiHtmlContent } from './utils'
|
||||
|
||||
export const staticPageTransformer: Transformer<
|
||||
StrapiStaticPageData,
|
||||
LPE.StaticPage.Document,
|
||||
StrapiStaticPageData,
|
||||
undefined,
|
||||
undefined
|
||||
> = {
|
||||
key: 'StaticPageTransformer',
|
||||
classes: ['static_page'],
|
||||
objectType: 'StaticPage',
|
||||
isMatch: (helpers, object) => object.__typename === 'PageEntity',
|
||||
transform: (helpers, data, original, root, ctx) => {
|
||||
const { id, attributes } = data
|
||||
|
||||
const title = attributes.title
|
||||
const subtitle = attributes.subtitle
|
||||
const slug = attributes.slug
|
||||
const description = attributes.description
|
||||
|
||||
const { blocks: content } = transformStrapiHtmlContent({
|
||||
html: attributes.body || '',
|
||||
})
|
||||
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
title,
|
||||
subtitle,
|
||||
content,
|
||||
summary: description,
|
||||
createdAt: attributes.publishedAt,
|
||||
modifiedAt: attributes.publishedAt,
|
||||
isDraft: !attributes.publishedAt,
|
||||
type: 'static_page',
|
||||
}
|
||||
},
|
||||
}
|
12
src/services/strapi/transformers/strapi.transformers.ts
Normal file
12
src/services/strapi/transformers/strapi.transformers.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TransformPipeline } from '../../../lib/TransformPipeline/TransformPipeline'
|
||||
import { episodeTransformer } from './Episode.transformer'
|
||||
import { podcastShowTransformer } from './PodcastShow.transformer'
|
||||
import { postTransformer } from './Post.transformer'
|
||||
import { staticPageTransformer } from './StaticPage.transformer'
|
||||
|
||||
export const strapiTransformers = new TransformPipeline([
|
||||
podcastShowTransformer,
|
||||
staticPageTransformer,
|
||||
postTransformer,
|
||||
episodeTransformer,
|
||||
])
|
187
src/services/strapi/transformers/utils.ts
Normal file
187
src/services/strapi/transformers/utils.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import * as htmlParser from 'node-html-parser'
|
||||
import slugify from 'slugify'
|
||||
import { UploadFileEntity } from '../../../lib/strapi/strapi.generated'
|
||||
import { LPE } from '../../../types/lpe.types'
|
||||
import { convertToIframe } from '../../../utils/string.utils'
|
||||
|
||||
let assetsBaseUrl = process.env.NEXT_PUBLIC_ASSETS_BASE_URL ?? ''
|
||||
if (assetsBaseUrl.endsWith('/')) assetsBaseUrl = assetsBaseUrl.slice(1)
|
||||
|
||||
export const transformStrapiImageUrl = (url: string): string =>
|
||||
assetsBaseUrl + url
|
||||
|
||||
export const transformStrapiImageData = (
|
||||
image:
|
||||
| Pick<UploadFileEntity, 'attributes'>
|
||||
| {
|
||||
data: {
|
||||
attributes: Partial<UploadFileEntity['attributes']>
|
||||
}
|
||||
},
|
||||
): LPE.Image.Document => {
|
||||
const attributes = 'data' in image ? image.data.attributes : image.attributes
|
||||
|
||||
return {
|
||||
height: attributes.height || 0,
|
||||
width: attributes.width || 0,
|
||||
caption: attributes.caption || '',
|
||||
alt: attributes.caption || attributes.alternativeText || '',
|
||||
url: attributes.url ? transformStrapiImageUrl(attributes.url) : '',
|
||||
}
|
||||
}
|
||||
|
||||
export const transformStrapiHtmlContent = ({
|
||||
html,
|
||||
}: {
|
||||
html: string
|
||||
}): {
|
||||
toc: LPE.Post.TocItem[]
|
||||
blocks: LPE.Post.ContentBlock[]
|
||||
html: string
|
||||
text: string
|
||||
} => {
|
||||
const toc: LPE.Post.TocItem[] = []
|
||||
const blocks: LPE.Post.ContentBlock[] = []
|
||||
|
||||
// split paragraphs with <br> into multiple paragraphs
|
||||
html = html.replaceAll(
|
||||
/<p(\s+[^>]*)?>(.*?)<br>(.*?)<\/p>/g,
|
||||
(match, p1, p2, p3) =>
|
||||
[p2, p3]
|
||||
.join('<br>')
|
||||
.split('<br>')
|
||||
.map((p) => `<p${p1 || ''}>${p}</p>`)
|
||||
.join(''),
|
||||
)
|
||||
|
||||
let root = htmlParser.parse(html, { parseNoneClosedTags: true })
|
||||
|
||||
let blockIndex = -1
|
||||
|
||||
for (const child of root.childNodes) {
|
||||
if (!(child instanceof htmlParser.HTMLElement)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const tagName = child.tagName.toLowerCase()
|
||||
const isFigure = tagName === 'figure'
|
||||
const isMedia = isFigure && !!child.querySelector('oembed')
|
||||
const isImage = isFigure && !isMedia && !!child.querySelector('img')
|
||||
const empty = child.text.length === 0
|
||||
|
||||
if (!isFigure && empty) continue
|
||||
|
||||
blockIndex++
|
||||
|
||||
if (isImage) {
|
||||
const image = child.querySelector('img')
|
||||
if (!image) {
|
||||
blockIndex--
|
||||
continue
|
||||
}
|
||||
|
||||
const caption = child.textContent || ''
|
||||
const alt = image.getAttribute('alt') || ''
|
||||
const url = image.getAttribute('src') || ''
|
||||
const width = parseInt(image.getAttribute('width') || '0', 10)
|
||||
const height = parseInt(image.getAttribute('height') || '0', 10)
|
||||
|
||||
blocks.push({
|
||||
id: '',
|
||||
caption,
|
||||
type: 'image',
|
||||
width,
|
||||
height,
|
||||
alt: alt || caption,
|
||||
labels: [],
|
||||
order: blockIndex,
|
||||
url: url.startsWith('/') ? transformStrapiImageUrl(url) : url,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (isMedia) {
|
||||
const labels: LPE.Post.ContentBlockLabel[] = ['embed']
|
||||
|
||||
const oembed = child.querySelector('oembed')
|
||||
const url = oembed?.getAttribute('url') || ''
|
||||
if (!url) {
|
||||
blockIndex--
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const youtube = html.match(
|
||||
/(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/[^ "]+/gi,
|
||||
)
|
||||
|
||||
const simplecast = html.match(
|
||||
/(https?\:\/\/)?((player\.)?simplecast\.com)\/[^ "]+/gi,
|
||||
)
|
||||
|
||||
const label = youtube?.[0]
|
||||
? LPE.Post.ContentBlockLabels.YoutubeEmbed
|
||||
: LPE.Post.ContentBlockLabels.SimplecastEmbed
|
||||
|
||||
const src = youtube?.[0] || simplecast?.[0]
|
||||
if (src && src.length > 0) {
|
||||
labels.push(label)
|
||||
labels.push(LPE.Post.ContentBlockLabels.Embed)
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
id: `p-${blockIndex}`,
|
||||
order: 0,
|
||||
type: 'text',
|
||||
classNames: [],
|
||||
footnotes: [],
|
||||
html: child.outerHTML,
|
||||
labels,
|
||||
tagName: 'p',
|
||||
text: url,
|
||||
embed: {
|
||||
src: url,
|
||||
html: convertToIframe(url),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const text = child.text || ''
|
||||
const isHeading = tagName.startsWith('h')
|
||||
const id =
|
||||
child.id ||
|
||||
(isHeading && slugify(text, { lower: true, trim: true })) ||
|
||||
`p-${blockIndex}`
|
||||
child.setAttribute('id', id)
|
||||
|
||||
if (isHeading) {
|
||||
toc.push({
|
||||
blockIndex,
|
||||
href: `#${id}`,
|
||||
level: parseInt(tagName[1], 10),
|
||||
tag: tagName,
|
||||
title: text.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
id: '',
|
||||
footnotes: [],
|
||||
html: child.outerHTML,
|
||||
labels: [],
|
||||
tagName,
|
||||
text,
|
||||
order: blockIndex,
|
||||
classNames: Array.from(child.classList.values()),
|
||||
type: 'text',
|
||||
} as LPE.Post.TextBlock)
|
||||
}
|
||||
|
||||
return {
|
||||
toc,
|
||||
blocks,
|
||||
text: root.text,
|
||||
html: root.innerHTML,
|
||||
}
|
||||
}
|
@ -65,6 +65,6 @@ export const ArticleDataType: UnbodyDataTypeConfig<
|
||||
highlighted: data.path.includes('highlighted'),
|
||||
isDraft: data.path.includes('draft'),
|
||||
type: LPE.PostTypes.Article,
|
||||
}
|
||||
} as any
|
||||
},
|
||||
}
|
||||
|
@ -1798,8 +1798,8 @@ unbodyApi.onChange(async (oldData, data, changes, firstLoad) => {
|
||||
...post.tags.map(
|
||||
(tag) =>
|
||||
({
|
||||
name: formatTagText(tag),
|
||||
domain: formatTagText(tag),
|
||||
name: formatTagText(tag.name),
|
||||
domain: formatTagText(tag.name),
|
||||
} as Category),
|
||||
),
|
||||
],
|
||||
|
@ -8,17 +8,27 @@ export namespace LPE {
|
||||
|
||||
export type PostType = DictValues<typeof PostTypes>
|
||||
|
||||
export namespace Tag {
|
||||
export type Document = {
|
||||
id: string
|
||||
name: string
|
||||
postsCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Image {
|
||||
export type Document = {
|
||||
url: string
|
||||
alt: string
|
||||
width: number
|
||||
height: number
|
||||
caption?: string
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Author {
|
||||
export type Document = {
|
||||
id: string
|
||||
name: string
|
||||
emailAddress?: string
|
||||
}
|
||||
@ -153,7 +163,7 @@ export namespace LPE {
|
||||
summary: string
|
||||
subtitle: string
|
||||
authors: Author.Document[]
|
||||
tags: string[]
|
||||
tags: Tag.Document[]
|
||||
highlighted?: boolean
|
||||
isDraft?: boolean
|
||||
|
||||
@ -220,7 +230,7 @@ export namespace LPE {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
tags: string[]
|
||||
tags: Tag.Document[]
|
||||
description: string
|
||||
authors: Author.Document[]
|
||||
publishedAt: string
|
||||
@ -229,7 +239,7 @@ export namespace LPE {
|
||||
showId?: string
|
||||
highlighted?: boolean
|
||||
isDraft?: boolean
|
||||
coverImage?: Post.ImageBlock
|
||||
coverImage?: Image.Document
|
||||
show?: Show
|
||||
type: typeof LPE.PostTypes.Podcast
|
||||
}
|
||||
|
81
yarn.lock
81
yarn.lock
@ -2070,6 +2070,11 @@ bl@^4.1.0:
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@ -2447,6 +2452,17 @@ css-in-js-utils@^3.1.0:
|
||||
dependencies:
|
||||
hyphenate-style-name "^1.0.3"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-to-react-native@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32"
|
||||
@ -2464,6 +2480,11 @@ css-tree@^1.1.2:
|
||||
mdn-data "2.0.14"
|
||||
source-map "^0.6.1"
|
||||
|
||||
css-what@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
||||
csstype@^3.0.2, csstype@^3.0.6:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
@ -2629,6 +2650,36 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
dot-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
||||
@ -2695,6 +2746,11 @@ enhanced-resolve@^5.12.0:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
entities@^4.2.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
@ -3493,6 +3549,11 @@ has@^1.0.3:
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
he@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
header-case@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063"
|
||||
@ -4376,6 +4437,14 @@ node-fetch@^2.6.1, node-fetch@^2.6.12:
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-html-parser@^6.1.12:
|
||||
version "6.1.12"
|
||||
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.12.tgz#6138f805d0ad7a6b5ef415bcd91bca07374bf575"
|
||||
integrity sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==
|
||||
dependencies:
|
||||
css-select "^5.1.0"
|
||||
he "1.2.0"
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||
@ -4410,6 +4479,13 @@ nprogress@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
|
||||
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
|
||||
|
||||
nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
nullthrows@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
|
||||
@ -5250,6 +5326,11 @@ slice-ansi@^5.0.0:
|
||||
ansi-styles "^6.0.0"
|
||||
is-fullwidth-code-point "^4.0.0"
|
||||
|
||||
slugify@^1.6.6:
|
||||
version "1.6.6"
|
||||
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
|
||||
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
|
||||
|
||||
snake-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user