feat: implement initial strapi integration
This commit is contained in:
parent
e4ef86af4d
commit
7189c9982f
|
@ -2,10 +2,9 @@ module.exports = {
|
||||||
client: {
|
client: {
|
||||||
includes: ['src/**/*.{ts,tsx}'],
|
includes: ['src/**/*.{ts,tsx}'],
|
||||||
service: {
|
service: {
|
||||||
name: 'unbody-graphql',
|
name: 'strapi-graphql',
|
||||||
localSchemaFile: [
|
localSchemaFile: [
|
||||||
'./src/lib/unbody/unbody.graphql',
|
'./src/lib/strapi/strapi.graphql',
|
||||||
'./src/lib/unbody/unbody.extend.graphql',
|
|
||||||
], // how to configure to multiple schemas?
|
], // how to configure to multiple schemas?
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
15
codegen.ts
15
codegen.ts
|
@ -1,8 +1,7 @@
|
||||||
import { CodegenConfig } from '@graphql-codegen/cli'
|
import { CodegenConfig } from '@graphql-codegen/cli'
|
||||||
|
|
||||||
const graphqlEndpoint = 'https://graphql.unbody.io'
|
const graphqlEndpoint = process.env.STRAPI_GRAPHQL_URL || ''
|
||||||
const projectId = process.env.UNBODY_PROJECT_ID || ''
|
const token = process.env.STRAPI_API_KEY || ''
|
||||||
const authorization = process.env.UNBODY_API_KEY || ''
|
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
const config: CodegenConfig = {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
@ -10,21 +9,19 @@ const config: CodegenConfig = {
|
||||||
{
|
{
|
||||||
[graphqlEndpoint]: {
|
[graphqlEndpoint]: {
|
||||||
headers: {
|
headers: {
|
||||||
authorization,
|
authorization: token,
|
||||||
'x-project-id': projectId,
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'src/lib/unbody/unbody.extend.graphql',
|
|
||||||
],
|
],
|
||||||
documents: ['src/**/*.{ts,tsx}'],
|
documents: ['src/services/strapi/*.{ts,tsx}'],
|
||||||
generates: {
|
generates: {
|
||||||
'src/lib/unbody/unbody.graphql': {
|
'src/lib/strapi/strapi.graphql': {
|
||||||
plugins: ['schema-ast'],
|
plugins: ['schema-ast'],
|
||||||
},
|
},
|
||||||
'src/lib/unbody/unbody.generated.ts': {
|
'src/lib/strapi/strapi.generated.ts': {
|
||||||
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
|
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
|
||||||
presetConfig: {
|
presetConfig: {
|
||||||
fragmentMasking: false,
|
fragmentMasking: false,
|
||||||
|
|
|
@ -3,12 +3,11 @@ const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
domains: [
|
domains: [
|
||||||
'images.cdn.unbody.io',
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
'image.simplecastcdn.com',
|
'image.simplecastcdn.com',
|
||||||
'img.youtube.com',
|
'img.youtube.com',
|
||||||
],
|
],
|
||||||
// loader: 'imgix',
|
|
||||||
// path: 'https://images.cdn.unbody.io',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"next": "13.3.0",
|
"next": "13.3.0",
|
||||||
"next-query-params": "^4.2.3",
|
"next-query-params": "^4.2.3",
|
||||||
"nextjs-progressbar": "^0.0.16",
|
"nextjs-progressbar": "^0.0.16",
|
||||||
|
"node-html-parser": "^6.1.12",
|
||||||
"odoo-await": "^3.4.1",
|
"odoo-await": "^3.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.12.0",
|
||||||
"react-quick-pinch-zoom": "^4.9.0",
|
"react-quick-pinch-zoom": "^4.9.0",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"use-query-params": "^2.2.1",
|
"use-query-params": "^2.2.1",
|
||||||
"yup": "^1.3.2"
|
"yup": "^1.3.2"
|
||||||
|
|
|
@ -39,6 +39,17 @@ export const RenderArticleBlock = ({
|
||||||
case 'h6': {
|
case 'h6': {
|
||||||
return <ArticleHeading block={block} />
|
return <ArticleHeading block={block} />
|
||||||
}
|
}
|
||||||
|
case 'blockquote':
|
||||||
|
return (
|
||||||
|
<Quote mode="indented-line" genericFontFamily="serif">
|
||||||
|
<Paragraph
|
||||||
|
variant="body1"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: extractInnerHtml(block.html),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Quote>
|
||||||
|
)
|
||||||
case 'p': {
|
case 'p': {
|
||||||
const isIframe = block.embed && block.labels.includes('embed')
|
const isIframe = block.embed && block.labels.includes('embed')
|
||||||
if (block.embed && isIframe) {
|
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 (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
variant="body1"
|
variant="body1"
|
||||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
||||||
export const ArticleImageBlockWrapper = ({ image, order }: Props) => {
|
export const ArticleImageBlockWrapper = ({ image, order }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Container id={`i-${order}`}>
|
<Container id={`i-${order}`}>
|
||||||
<LightBox caption={image.alt}>
|
<LightBox caption={image.caption || ''}>
|
||||||
<ResponsiveImage data={image} />
|
<ResponsiveImage data={image} />
|
||||||
</LightBox>
|
</LightBox>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import ArticleSummary from './Article.Summary'
|
||||||
export type ArticleHeaderProps = LPE.Article.Data
|
export type ArticleHeaderProps = LPE.Article.Data
|
||||||
|
|
||||||
const ArticleHeader = ({
|
const ArticleHeader = ({
|
||||||
|
title,
|
||||||
summary,
|
summary,
|
||||||
subtitle,
|
subtitle,
|
||||||
authors,
|
authors,
|
||||||
|
@ -33,19 +34,15 @@ const ArticleHeader = ({
|
||||||
date={modifiedAt ? new Date(modifiedAt) : null}
|
date={modifiedAt ? new Date(modifiedAt) : null}
|
||||||
readingLength={readingTime}
|
readingLength={readingTime}
|
||||||
/>
|
/>
|
||||||
<ArticleTitle
|
<span
|
||||||
block={
|
id="title-anchor"
|
||||||
content.find((block) =>
|
ref={(ref) => {
|
||||||
block.labels.includes(LPE.Article.ContentBlockLabels.Title),
|
headingElementsRef.current['h-0'] = ref as HTMLHeadingElement
|
||||||
) as LPE.Article.TextBlock
|
|
||||||
}
|
|
||||||
typographyProps={{
|
|
||||||
variant: 'h1',
|
|
||||||
genericFontFamily: 'serif',
|
|
||||||
component: 'h1',
|
|
||||||
}}
|
}}
|
||||||
headingElementsRef={headingElementsRef}
|
></span>
|
||||||
/>
|
<Typography variant="h1" component="h1" genericFontFamily="serif">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<ArticleSubtitle
|
<ArticleSubtitle
|
||||||
variant="body1"
|
variant="body1"
|
||||||
|
@ -55,7 +52,10 @@ const ArticleHeader = ({
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</ArticleSubtitle>
|
</ArticleSubtitle>
|
||||||
)}
|
)}
|
||||||
<TagsAndSocial tags={tags} className={'articleTags'} />
|
<TagsAndSocial
|
||||||
|
tags={tags.map((tag) => tag.name)}
|
||||||
|
className={'articleTags'}
|
||||||
|
/>
|
||||||
<AuthorsContainer>
|
<AuthorsContainer>
|
||||||
<Authors authors={authors} />
|
<Authors authors={authors} />
|
||||||
</AuthorsContainer>
|
</AuthorsContainer>
|
||||||
|
@ -65,7 +65,9 @@ const ArticleHeader = ({
|
||||||
order={ArticleBlocksOrders.cover}
|
order={ArticleBlocksOrders.cover}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ArticleSummary summary={summary} showLabel={false} />
|
{summary && summary.length > 0 && (
|
||||||
|
<ArticleSummary summary={summary} showLabel={false} />
|
||||||
|
)}
|
||||||
</ArticleHeaderContainer>
|
</ArticleHeaderContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { LPE } from '../../types/lpe.types'
|
||||||
import EpisodeBlocks from './Episode.Blocks'
|
import EpisodeBlocks from './Episode.Blocks'
|
||||||
|
|
||||||
const EpisodeTranscript = ({ episode }: { episode: LPE.Podcast.Document }) => {
|
const EpisodeTranscript = ({ episode }: { episode: LPE.Podcast.Document }) => {
|
||||||
|
if (episode.content.length === 0) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Title variant={'h5'} genericFontFamily={'serif'}>
|
<Title variant={'h5'} genericFontFamily={'serif'}>
|
||||||
|
|
|
@ -24,9 +24,8 @@ const EpisodeCredits = ({
|
||||||
component="p"
|
component="p"
|
||||||
variant="label1"
|
variant="label1"
|
||||||
id={credit.id.replace('#', '')}
|
id={credit.id.replace('#', '')}
|
||||||
>
|
dangerouslySetInnerHTML={{ __html: credit.html }}
|
||||||
{credit.text}
|
/>
|
||||||
</Typography>
|
|
||||||
</Credit>
|
</Credit>
|
||||||
))}
|
))}
|
||||||
</Credits>
|
</Credits>
|
||||||
|
|
|
@ -26,7 +26,7 @@ const EpisodeFooter = ({ episode, relatedEpisodes }: Props) => {
|
||||||
<EpisodeFooterContainer>
|
<EpisodeFooterContainer>
|
||||||
{!!episode?.credits && <EpisodeCredits credits={episode.credits} />}
|
{!!episode?.credits && <EpisodeCredits credits={episode.credits} />}
|
||||||
{!!footnotes && <EpisodeFootnotes footnotes={footnotes} />}
|
{!!footnotes && <EpisodeFootnotes footnotes={footnotes} />}
|
||||||
{!!relatedEpisodes && (
|
{!!relatedEpisodes && relatedEpisodes.length > 0 && (
|
||||||
<RelatedEpisodes
|
<RelatedEpisodes
|
||||||
podcastSlug={episode.show?.slug as string}
|
podcastSlug={episode.show?.slug as string}
|
||||||
relatedEpisodes={relatedEpisodes}
|
relatedEpisodes={relatedEpisodes}
|
||||||
|
|
|
@ -54,7 +54,7 @@ const EpisodeHeader = ({
|
||||||
</Show>
|
</Show>
|
||||||
</CustomLink>
|
</CustomLink>
|
||||||
)}
|
)}
|
||||||
<TagsAndSocial tags={tags} />
|
<TagsAndSocial tags={tags.map((tag) => tag.name)} />
|
||||||
{channels && <EpisodeChannels channels={channels} />}
|
{channels && <EpisodeChannels channels={channels} />}
|
||||||
{description && (
|
{description && (
|
||||||
<ArticleSummary summary={description} showLabel={false} />
|
<ArticleSummary summary={description} showLabel={false} />
|
||||||
|
|
|
@ -5,7 +5,7 @@ export type EpisodeState = {
|
||||||
title: string
|
title: string
|
||||||
podcast: string
|
podcast: string
|
||||||
url: string
|
url: string
|
||||||
coverImage: LPE.Post.ImageBlock | null
|
coverImage: LPE.Image.Document | null
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ PostCard.toData = (post: LPE.Post.Document, shows: LPE.Podcast.Show[] = []) => {
|
||||||
authors: post.type === 'article' ? post.authors : [],
|
authors: post.type === 'article' ? post.authors : [],
|
||||||
coverImage: post.coverImage,
|
coverImage: post.coverImage,
|
||||||
subtitle: (post.type === 'article' && post.subtitle) || '',
|
subtitle: (post.type === 'article' && post.subtitle) || '',
|
||||||
tags: post.tags,
|
tags: post.tags.map((tag) => tag.name),
|
||||||
...(post.type === 'podcast' && show
|
...(post.type === 'podcast' && show
|
||||||
? {
|
? {
|
||||||
podcastShowDetails: {
|
podcastShowDetails: {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export const PostsList = (props: Props) => {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
subtitle: post.subtitle,
|
subtitle: post.subtitle,
|
||||||
coverImage: post.coverImage,
|
coverImage: post.coverImage,
|
||||||
tags: post.tags,
|
tags: post.tags.map((tag) => tag.name),
|
||||||
}}
|
}}
|
||||||
contentType={PostTypes.Article}
|
contentType={PostTypes.Article}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||||
<Container>
|
<Container>
|
||||||
<div>
|
<div>
|
||||||
<PostsGrid
|
<PostsGrid
|
||||||
|
shows={shows}
|
||||||
posts={highlighted.slice(0, 1)}
|
posts={highlighted.slice(0, 1)}
|
||||||
pattern={[{ cols: 1, size: 'large' }]}
|
pattern={[{ cols: 1, size: 'large' }]}
|
||||||
breakpoints={[
|
breakpoints={[
|
||||||
|
@ -56,6 +57,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||||
/>
|
/>
|
||||||
<Section title="Latest posts">
|
<Section title="Latest posts">
|
||||||
<PostsGrid
|
<PostsGrid
|
||||||
|
shows={shows}
|
||||||
pattern={[{ cols: 4, size: 'small' }]}
|
pattern={[{ cols: 4, size: 'small' }]}
|
||||||
breakpoints={[
|
breakpoints={[
|
||||||
{
|
{
|
||||||
|
@ -103,11 +105,11 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<Grid xs={{ cols: 1 }} sm={{ cols: 4 }}>
|
<Grid xs={{ cols: 1 }} sm={{ cols: 4 }}>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<GridItem key={tag.value} cols={1}>
|
<GridItem key={tag.name} cols={1}>
|
||||||
<TagCard
|
<TagCard
|
||||||
href={`/search?topic=${tag.value}`}
|
href={`/search?topic=${tag.name}`}
|
||||||
name={formatTagText(tag.value)}
|
name={formatTagText(tag.name)}
|
||||||
count={tag.count}
|
count={tag.postsCount}
|
||||||
/>
|
/>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -18,22 +18,12 @@ export const StaticPage: React.FC<StaticPageProps> = ({
|
||||||
data: { page },
|
data: { page },
|
||||||
...props
|
...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 (
|
return (
|
||||||
<Root {...props}>
|
<Root {...props}>
|
||||||
<article>
|
<article>
|
||||||
{titleBlock && (
|
<Typography variant={'h1'} genericFontFamily={'serif'}>
|
||||||
<Typography variant={'h1'} genericFontFamily={'serif'}>
|
{page.title}
|
||||||
{titleBlock.text}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{data.page.content.map((block, idx) => (
|
{data.page.content.map((block, idx) => (
|
||||||
<RenderArticleBlock block={block} activeId={null} key={idx} />
|
<RenderArticleBlock block={block} activeId={null} key={idx} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
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 Error from 'next/error'
|
||||||
import SEO from '../components/SEO/SEO'
|
import SEO from '../components/SEO/SEO'
|
||||||
import { StaticPage, StaticPageProps } from '../containers/StaticPage'
|
import { StaticPage, StaticPageProps } from '../containers/StaticPage'
|
||||||
import unbodyApi from '../services/unbody/unbody.service'
|
import { strapiApi } from '../services/strapi'
|
||||||
|
|
||||||
type PageProps = Partial<Pick<StaticPageProps, 'data'>> & {
|
type PageProps = Partial<Pick<StaticPageProps, 'data'>> & {
|
||||||
error?: string
|
error?: string
|
||||||
|
@ -37,7 +37,8 @@ const Page: CustomNextPage<PageProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const { data } = await unbodyApi.getStaticPages()
|
const { data } = await strapiApi.getStaticPages({})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: data.map((page) => ({
|
paths: data.map((page) => ({
|
||||||
params: {
|
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,
|
slug: slug as string,
|
||||||
...(id
|
...(id
|
||||||
? {
|
? {
|
||||||
id,
|
id,
|
||||||
includeDrafts: true,
|
published: false,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!data) {
|
if (!data || data.length === 0) {
|
||||||
if (errors && typeof errors === 'string' && errors.includes('not found')) {
|
if (errors && typeof errors === 'string' && errors.includes('not found')) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
@ -93,7 +95,7 @@ export const getStaticProps: GetStaticProps<PageProps> = async (ctx) => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
page: data,
|
page: data[0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notFound: false,
|
notFound: false,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import unbodyApi from '../../../../../services/unbody/unbody.service'
|
import { strapiApi } from '../../../../../services/strapi'
|
||||||
import { parseInt } from '../../../../../utils/data.utils'
|
import { parseInt } from '../../../../../utils/data.utils'
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
|
@ -10,7 +10,8 @@ export default async function handler(
|
||||||
query: { skip = 0, limit = 10, showSlug },
|
query: { skip = 0, limit = 10, showSlug },
|
||||||
} = req
|
} = req
|
||||||
|
|
||||||
const response = await unbodyApi.getLatestEpisodes({
|
const response = await strapiApi.getLatestEpisodes({
|
||||||
|
highlighted: 'exclude',
|
||||||
skip: parseInt(skip, 0),
|
skip: parseInt(skip, 0),
|
||||||
limit: parseInt(limit, 10),
|
limit: parseInt(limit, 10),
|
||||||
showSlug: Array.isArray(showSlug) ? showSlug[0] : showSlug,
|
showSlug: Array.isArray(showSlug) ? showSlug[0] : showSlug,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import unbodyApi from '../../../services/unbody/unbody.service'
|
import { strapiApi } from '../../../services/strapi'
|
||||||
import { parseInt } from '../../../utils/data.utils'
|
import { parseInt } from '../../../utils/data.utils'
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
|
@ -10,9 +10,10 @@ export default async function handler(
|
||||||
query: { skip = 0, limit = 10 },
|
query: { skip = 0, limit = 10 },
|
||||||
} = req
|
} = req
|
||||||
|
|
||||||
const response = await unbodyApi.getRecentPosts({
|
const response = await strapiApi.getRecentPosts({
|
||||||
skip: parseInt(skip, 0),
|
skip: parseInt(skip as string, 0),
|
||||||
limit: parseInt(limit, 10),
|
limit: parseInt(limit as string, 10),
|
||||||
|
highlighted: 'exclude',
|
||||||
})
|
})
|
||||||
|
|
||||||
res.status(200).json(response)
|
res.status(200).json(response)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import unbodyApi from '../../../services/unbody/unbody.service'
|
|
||||||
import { LPE } from '../../../types/lpe.types'
|
import { LPE } from '../../../types/lpe.types'
|
||||||
import { parseInt } from '../../../utils/data.utils'
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
|
@ -61,31 +59,39 @@ export default async function handler(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postTypes.length > 0) {
|
if (postTypes.length > 0) {
|
||||||
const response = await unbodyApi.searchPosts({
|
// const response = await unbodyApi.searchPosts({
|
||||||
tags,
|
// tags,
|
||||||
query: Array.isArray(q) ? q.join(' ').trim() : q.trim(),
|
// query: Array.isArray(q) ? q.join(' ').trim() : q.trim(),
|
||||||
|
|
||||||
type: postTypes as LPE.PostType[],
|
// type: postTypes as LPE.PostType[],
|
||||||
|
|
||||||
limit: parseInt(limit, 50),
|
// limit: parseInt(limit, 50),
|
||||||
skip: parseInt(skip, 0),
|
// skip: parseInt(skip, 0),
|
||||||
})
|
// })
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
|
|
||||||
result.posts.push(...response.data)
|
result.posts.push(...response.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockTypes.length > 0) {
|
if (blockTypes.length > 0) {
|
||||||
const response = await unbodyApi.searchPostBlocks({
|
// const response = await unbodyApi.searchPostBlocks({
|
||||||
tags,
|
// tags,
|
||||||
query: Array.isArray(q) ? q.join(' ') : q,
|
// query: Array.isArray(q) ? q.join(' ') : q,
|
||||||
|
|
||||||
postType: postTypes as LPE.PostType[],
|
// postType: postTypes as LPE.PostType[],
|
||||||
type: blockTypes as LPE.Post.ContentBlockType[],
|
// type: blockTypes as LPE.Post.ContentBlockType[],
|
||||||
|
|
||||||
method: 'hybrid',
|
// method: 'hybrid',
|
||||||
limit: parseInt(limit, 50),
|
// limit: parseInt(limit, 50),
|
||||||
skip: parseInt(skip, 0),
|
// skip: parseInt(skip, 0),
|
||||||
})
|
// })
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
|
|
||||||
result.blocks.push(...response.data)
|
result.blocks.push(...response.data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import unbodyApi from '../../../../services/unbody/unbody.service'
|
|
||||||
import { LPE } from '../../../../types/lpe.types'
|
import { LPE } from '../../../../types/lpe.types'
|
||||||
import { parseInt } from '../../../../utils/data.utils'
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
|
@ -27,26 +25,26 @@ export default async function handler(
|
||||||
blocks: [],
|
blocks: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const query: Parameters<(typeof unbodyApi)['searchPostBlocks']>[0] = {
|
// const query: Parameters<(typeof unbodyApi)['searchPostBlocks']>[0] = {
|
||||||
tags,
|
// tags,
|
||||||
type: ['text', 'image'],
|
// type: ['text', 'image'],
|
||||||
postId: Array.isArray(id) ? id[0] : id,
|
// postId: Array.isArray(id) ? id[0] : id,
|
||||||
query: Array.isArray(q) ? q.join(' ') : q,
|
// query: Array.isArray(q) ? q.join(' ') : q,
|
||||||
method: 'nearText',
|
// method: 'nearText',
|
||||||
certainty: 0.85,
|
// certainty: 0.85,
|
||||||
|
|
||||||
limit: parseInt(limit, 50),
|
// limit: parseInt(limit, 50),
|
||||||
skip: parseInt(skip, 0),
|
// skip: parseInt(skip, 0),
|
||||||
}
|
// }
|
||||||
|
|
||||||
result.blocks = await unbodyApi
|
// result.blocks = await unbodyApi
|
||||||
.searchPostBlocks(query)
|
// .searchPostBlocks(query)
|
||||||
.then((res) => res.data)
|
// .then((res) => res.data)
|
||||||
|
|
||||||
if (result.blocks.length === 0)
|
// if (result.blocks.length === 0)
|
||||||
result.blocks = await unbodyApi
|
// result.blocks = await unbodyApi
|
||||||
.searchPostBlocks({ ...query, method: 'hybrid' })
|
// .searchPostBlocks({ ...query, method: 'hybrid' })
|
||||||
.then((res) => res.data)
|
// .then((res) => res.data)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
data: result,
|
data: result,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SEO } from '@/components/SEO'
|
import { SEO } from '@/components/SEO'
|
||||||
import ArticleContainer from '@/containers/ArticleContainer'
|
import ArticleContainer from '@/containers/ArticleContainer'
|
||||||
import { GetStaticPropsContext } from 'next'
|
import { GetStaticPropsContext } from 'next'
|
||||||
import unbodyApi from '../../services/unbody/unbody.service'
|
import { strapiApi } from '../../services/strapi'
|
||||||
import { LPE } from '../../types/lpe.types'
|
import { LPE } from '../../types/lpe.types'
|
||||||
|
|
||||||
type ArticleProps = {
|
type ArticleProps = {
|
||||||
|
@ -24,7 +24,7 @@ const ArticlePage = ({ data, errors, why }: ArticleProps) => {
|
||||||
pagePath={`/article/${data.data.slug}`}
|
pagePath={`/article/${data.data.slug}`}
|
||||||
date={data.data.createdAt}
|
date={data.data.createdAt}
|
||||||
tags={[
|
tags={[
|
||||||
...data.data.tags,
|
...data.data.tags.map((tag) => tag.name),
|
||||||
...data.data.authors.map((author) => author.name),
|
...data.data.authors.map((author) => author.name),
|
||||||
]}
|
]}
|
||||||
contentType={LPE.PostTypes.Article}
|
contentType={LPE.PostTypes.Article}
|
||||||
|
@ -35,17 +35,18 @@ const ArticlePage = ({ data, errors, why }: ArticleProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const { data: posts, errors } = await unbodyApi.getArticles({
|
const { data, errors } = await strapiApi.getPosts({
|
||||||
skip: 0,
|
skip: 0,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
includeDrafts: false,
|
|
||||||
highlighted: 'include',
|
highlighted: 'include',
|
||||||
|
parseContent: false,
|
||||||
|
published: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: errors
|
paths: errors
|
||||||
? []
|
? []
|
||||||
: posts.map((post) => ({ params: { path: [post.slug] } })),
|
: data.data.map((post) => ({ params: { path: [post.slug] } })),
|
||||||
fallback: true,
|
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,
|
parseContent: true,
|
||||||
slug: slug as string,
|
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 {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
props: { why: 'no article' },
|
props: { why: 'no article' },
|
||||||
|
@ -82,22 +85,26 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: relatedArticles } = await unbodyApi.getRelatedArticles({
|
const article = res.data[0]
|
||||||
id: data.id,
|
|
||||||
|
const { data: relatedArticles } = await strapiApi.getRelatedPosts({
|
||||||
|
id: article.id,
|
||||||
|
type: 'article',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: articlesFromSameAuthors } =
|
const { data: articlesFromSameAuthors } =
|
||||||
await unbodyApi.getArticlesFromSameAuthors({
|
await strapiApi.getPostsFromSameAuthors({
|
||||||
slug: slug as string,
|
type: 'article',
|
||||||
authors: data.authors.map((author) => author.name),
|
excludeId: article.id,
|
||||||
|
authors: article.authors.map((author) => author.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
data,
|
data: article,
|
||||||
relatedArticles,
|
relatedArticles: relatedArticles,
|
||||||
articlesFromSameAuthors,
|
articlesFromSameAuthors: articlesFromSameAuthors.data,
|
||||||
},
|
},
|
||||||
error: JSON.stringify(errors),
|
error: JSON.stringify(errors),
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { CustomNextPage, GetStaticProps } from 'next'
|
||||||
import SEO from '../components/SEO/SEO'
|
import SEO from '../components/SEO/SEO'
|
||||||
import { HomePage, HomePageProps } from '../containers/HomePage'
|
import { HomePage, HomePageProps } from '../containers/HomePage'
|
||||||
import { DefaultLayout } from '../layouts/DefaultLayout'
|
import { DefaultLayout } from '../layouts/DefaultLayout'
|
||||||
import unbodyApi from '../services/unbody/unbody.service'
|
import { strapiApi } from '../services/strapi'
|
||||||
|
|
||||||
type PageProps = Pick<HomePageProps, 'data'>
|
type PageProps = Pick<HomePageProps, 'data'>
|
||||||
|
|
||||||
|
@ -27,19 +27,26 @@ Page.getLayout = function getLayout(page: React.ReactNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<PageProps> = async () => {
|
export const getStaticProps: GetStaticProps<PageProps> = async () => {
|
||||||
const { data: tags = [] } = await unbodyApi.getTopics(true)
|
// const { data: tags = [] } = await unbodyApi.getTopics(true)
|
||||||
const { data: highlighted } = await unbodyApi.getHighlightedPosts()
|
// const { data: highlighted } = await unbodyApi.getHighlightedPosts()
|
||||||
const { data: latest } = await unbodyApi.getRecentPosts({
|
// const { data: latest } = await unbodyApi.getRecentPosts({
|
||||||
skip: 0,
|
// skip: 0,
|
||||||
limit: 12,
|
// 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,
|
populateEpisodes: true,
|
||||||
episodesLimit: 10,
|
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 {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import EpisodeContainer from '@/containers/EpisodeContainer'
|
||||||
import { GetStaticPropsContext } from 'next'
|
import { GetStaticPropsContext } from 'next'
|
||||||
import { LPE } from '../../../types/lpe.types'
|
import { LPE } from '../../../types/lpe.types'
|
||||||
|
|
||||||
import unbodyApi from '@/services/unbody/unbody.service'
|
import { strapiApi } from '../../../services/strapi'
|
||||||
import { getPostLink } from '../../../utils/route.utils'
|
import { getPostLink } from '../../../utils/route.utils'
|
||||||
|
|
||||||
type EpisodeProps = {
|
type EpisodeProps = {
|
||||||
|
@ -29,7 +29,7 @@ const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
|
||||||
postSlug: episode.slug as string,
|
postSlug: episode.slug as string,
|
||||||
})}
|
})}
|
||||||
tags={[
|
tags={[
|
||||||
...episode.tags,
|
...episode.tags.map((tag) => tag.name),
|
||||||
...episode.authors.map((author) => author.name),
|
...episode.authors.map((author) => author.name),
|
||||||
]}
|
]}
|
||||||
contentType={LPE.PostTypes.Podcast}
|
contentType={LPE.PostTypes.Podcast}
|
||||||
|
@ -40,7 +40,7 @@ const EpisodePage = ({ episode, relatedEpisodes, errors }: EpisodeProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: true })
|
const { data } = await strapiApi.getPodcastShows({ populateEpisodes: true })
|
||||||
|
|
||||||
const paths = data.flatMap((show) => {
|
const paths = data.flatMap((show) => {
|
||||||
return (
|
return (
|
||||||
|
@ -81,25 +81,36 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: episode, errors: episodeErros } =
|
const { data: episode, errors: episodeErros } = await strapiApi.getEpisode({
|
||||||
await unbodyApi.getPodcastEpisode({
|
showSlug: showSlug as string,
|
||||||
showSlug: showSlug as string,
|
slug: epSlug as string,
|
||||||
slug: epSlug as string,
|
published: true,
|
||||||
textBlocks: true,
|
...(id
|
||||||
...(id
|
? {
|
||||||
? {
|
id: id as string,
|
||||||
id: id as string,
|
published: false,
|
||||||
includeDraft: true,
|
}
|
||||||
}
|
: {}),
|
||||||
: {}),
|
})
|
||||||
})
|
|
||||||
|
const { data: shows } = await strapiApi.getPodcastShows({})
|
||||||
|
|
||||||
// TODO : error handlings
|
// TODO : error handlings
|
||||||
const { data: relatedEpisodes, errors: relatedEpisodesErros } =
|
const { data: relatedEpisodes, errors: relatedEpisodesErros } =
|
||||||
await unbodyApi.getRelatedEpisodes({
|
await strapiApi
|
||||||
showSlug: showSlug as string,
|
.getRelatedPosts({
|
||||||
id: episode?.id as string,
|
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) {
|
if (!episode) {
|
||||||
return {
|
return {
|
||||||
|
@ -112,7 +123,7 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
episode,
|
episode,
|
||||||
relatedEpisodes,
|
relatedEpisodes: relatedEpisodes,
|
||||||
},
|
},
|
||||||
revalidate: 10,
|
revalidate: 10,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { SEO } from '@/components/SEO'
|
import { SEO } from '@/components/SEO'
|
||||||
import PodcastShowContainer from '@/containers/PodcastShowContainer'
|
import PodcastShowContainer from '@/containers/PodcastShowContainer'
|
||||||
import unbodyApi from '@/services/unbody/unbody.service'
|
|
||||||
import { GetStaticPropsContext } from 'next'
|
import { GetStaticPropsContext } from 'next'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { DefaultLayout } from '../../../layouts/DefaultLayout'
|
import { DefaultLayout } from '../../../layouts/DefaultLayout'
|
||||||
|
import { strapiApi } from '../../../services/strapi'
|
||||||
import { ApiPaginatedPayload } from '../../../types/data.types'
|
import { ApiPaginatedPayload } from '../../../types/data.types'
|
||||||
import { LPE } from '../../../types/lpe.types'
|
import { LPE } from '../../../types/lpe.types'
|
||||||
import { getPostLink } from '../../../utils/route.utils'
|
import { getPostLink } from '../../../utils/route.utils'
|
||||||
|
@ -48,7 +48,7 @@ const PodcastShowPage = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const { data } = await unbodyApi.getPodcastShows({ populateEpisodes: false })
|
const { data } = await strapiApi.getPodcastShows({ populateEpisodes: false })
|
||||||
|
|
||||||
const paths = data.map((show) => {
|
const paths = data.map((show) => {
|
||||||
return {
|
return {
|
||||||
|
@ -76,30 +76,31 @@ export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: show, errors: podcastShowDataErrors } =
|
const { data: shows, errors: podcastShowDataErrors } =
|
||||||
await unbodyApi.getPodcastShow({
|
await strapiApi.getPodcastShows({
|
||||||
showSlug: showSlug as string,
|
slug: showSlug as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: latestEpisodes, errors: latestEpisodesErros } =
|
const { data: latestEpisodes, errors: latestEpisodesErros } =
|
||||||
await unbodyApi.getLatestEpisodes({
|
await strapiApi.getLatestEpisodes({
|
||||||
showSlug: showSlug as string,
|
showSlug: showSlug as string,
|
||||||
limit: 8,
|
limit: 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: highlightedEpisodes, errors: highlightedEpisodesErrors } =
|
const { data: highlightedEpisodes, errors: highlightedEpisodesErrors } =
|
||||||
await unbodyApi.getHighlightedEpisodes({
|
await strapiApi.getLatestEpisodes({
|
||||||
showSlug: showSlug as string,
|
highlighted: 'only',
|
||||||
limit: 2,
|
limit: 2,
|
||||||
|
showSlug: showSlug as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
show,
|
show: shows[0],
|
||||||
latestEpisodes,
|
latestEpisodes,
|
||||||
highlightedEpisodes,
|
highlightedEpisodes: highlightedEpisodes.data,
|
||||||
},
|
},
|
||||||
revalidate: 10,
|
revalidate: 10,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { SEO } from '@/components/SEO'
|
import { SEO } from '@/components/SEO'
|
||||||
import PodcastsContainer from '@/containers/PodcastsContainer'
|
import PodcastsContainer from '@/containers/PodcastsContainer'
|
||||||
import unbodyApi from '@/services/unbody/unbody.service'
|
|
||||||
import { GetStaticPropsContext } from 'next'
|
import { GetStaticPropsContext } from 'next'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { DefaultLayout } from '../../layouts/DefaultLayout'
|
import { DefaultLayout } from '../../layouts/DefaultLayout'
|
||||||
|
import { strapiApi } from '../../services/strapi'
|
||||||
import { LPE } from '../../types/lpe.types'
|
import { LPE } from '../../types/lpe.types'
|
||||||
import { getPostLink } from '../../utils/route.utils'
|
import { getPostLink } from '../../utils/route.utils'
|
||||||
|
|
||||||
|
@ -43,18 +43,17 @@ const PodcastShowPage = ({
|
||||||
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: podcastShows, errors: podcastShowsErrors } =
|
const { data: podcastShows, errors: podcastShowsErrors } =
|
||||||
await unbodyApi.getPodcastShows({
|
await strapiApi.getPodcastShows({
|
||||||
populateEpisodes: true,
|
populateEpisodes: true,
|
||||||
episodesLimit: 6,
|
episodesLimit: 6,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO : error handling
|
// TODO : error handling
|
||||||
const { data: highlightedEpisodes, errors: highlightedEpisodesErrors } =
|
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,
|
limit: 10,
|
||||||
populateShow: true,
|
|
||||||
highlighted: 'exclude',
|
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 {
|
return {
|
||||||
props: {
|
props: {
|
||||||
shows: podcastShows,
|
shows: podcastShows,
|
||||||
highlightedEpisodes,
|
latestEpisodes: latestEpisodes.data,
|
||||||
latestEpisodes: latestEps,
|
highlightedEpisodes: highlightedEpisodes.data,
|
||||||
// errors,
|
// errors,
|
||||||
},
|
},
|
||||||
revalidate: 10,
|
revalidate: 10,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { CustomNextPage, GetServerSideProps } from 'next'
|
import { CustomNextPage, GetServerSideProps } from 'next'
|
||||||
import SEO from '../components/SEO/SEO'
|
import SEO from '../components/SEO/SEO'
|
||||||
import unbodyApi from '../services/unbody/unbody.service'
|
|
||||||
import { getPostLink } from '../utils/route.utils'
|
import { getPostLink } from '../utils/route.utils'
|
||||||
|
|
||||||
type PageProps = {}
|
type PageProps = {}
|
||||||
|
@ -20,10 +19,13 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, errors } = await unbodyApi.getDocById({
|
// const { data, errors } = await unbodyApi.getDocById({
|
||||||
id,
|
// id,
|
||||||
includeDrafts: true,
|
// includeDrafts: true,
|
||||||
})
|
// })
|
||||||
|
|
||||||
|
const data = undefined as any
|
||||||
|
const errors = '' as string
|
||||||
|
|
||||||
if (!data || errors) {
|
if (!data || errors) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { copyConfigs } from '../configs/copy.configs'
|
||||||
import { GlobalSearchBox } from '../containers/GlobalSearchBox/GlobalSearchBox'
|
import { GlobalSearchBox } from '../containers/GlobalSearchBox/GlobalSearchBox'
|
||||||
import { DefaultLayout } from '../layouts/DefaultLayout'
|
import { DefaultLayout } from '../layouts/DefaultLayout'
|
||||||
import { api } from '../services/api.service'
|
import { api } from '../services/api.service'
|
||||||
import unbodyApi from '../services/unbody/unbody.service'
|
|
||||||
|
|
||||||
interface SearchPageProps {
|
interface SearchPageProps {
|
||||||
topics: string[]
|
topics: string[]
|
||||||
|
@ -118,11 +117,14 @@ SearchPage.getLayout = (page: ReactNode) => (
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
|
// const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
|
||||||
const { data: shows = [] } = await unbodyApi.getPodcastShows({
|
// const { data: shows = [] } = await unbodyApi.getPodcastShows({
|
||||||
populateEpisodes: true,
|
// populateEpisodes: true,
|
||||||
episodesLimit: 10,
|
// episodesLimit: 10,
|
||||||
})
|
// })
|
||||||
|
|
||||||
|
const topics = [] as any
|
||||||
|
const shows = [] as any
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -44,7 +44,8 @@ export const useRecentPosts = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const posts = useMemo(
|
const posts = useMemo(
|
||||||
() => (query.data?.pages || []).flatMap((page) => page.posts),
|
() =>
|
||||||
|
(query.data?.pages || []).flatMap((page) => page.posts).filter(Boolean),
|
||||||
[query.data],
|
[query.data],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './strapi.service'
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -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 || '',
|
||||||
|
)
|
|
@ -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]
|
|
@ -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
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -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',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -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,
|
||||||
|
])
|
|
@ -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'),
|
highlighted: data.path.includes('highlighted'),
|
||||||
isDraft: data.path.includes('draft'),
|
isDraft: data.path.includes('draft'),
|
||||||
type: LPE.PostTypes.Article,
|
type: LPE.PostTypes.Article,
|
||||||
}
|
} as any
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1798,8 +1798,8 @@ unbodyApi.onChange(async (oldData, data, changes, firstLoad) => {
|
||||||
...post.tags.map(
|
...post.tags.map(
|
||||||
(tag) =>
|
(tag) =>
|
||||||
({
|
({
|
||||||
name: formatTagText(tag),
|
name: formatTagText(tag.name),
|
||||||
domain: formatTagText(tag),
|
domain: formatTagText(tag.name),
|
||||||
} as Category),
|
} as Category),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,17 +8,27 @@ export namespace LPE {
|
||||||
|
|
||||||
export type PostType = DictValues<typeof PostTypes>
|
export type PostType = DictValues<typeof PostTypes>
|
||||||
|
|
||||||
|
export namespace Tag {
|
||||||
|
export type Document = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
postsCount?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export namespace Image {
|
export namespace Image {
|
||||||
export type Document = {
|
export type Document = {
|
||||||
url: string
|
url: string
|
||||||
alt: string
|
alt: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
caption?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Author {
|
export namespace Author {
|
||||||
export type Document = {
|
export type Document = {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
emailAddress?: string
|
emailAddress?: string
|
||||||
}
|
}
|
||||||
|
@ -153,7 +163,7 @@ export namespace LPE {
|
||||||
summary: string
|
summary: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
authors: Author.Document[]
|
authors: Author.Document[]
|
||||||
tags: string[]
|
tags: Tag.Document[]
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
isDraft?: boolean
|
isDraft?: boolean
|
||||||
|
|
||||||
|
@ -220,7 +230,7 @@ export namespace LPE {
|
||||||
id: string
|
id: string
|
||||||
slug: string
|
slug: string
|
||||||
title: string
|
title: string
|
||||||
tags: string[]
|
tags: Tag.Document[]
|
||||||
description: string
|
description: string
|
||||||
authors: Author.Document[]
|
authors: Author.Document[]
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
|
@ -229,7 +239,7 @@ export namespace LPE {
|
||||||
showId?: string
|
showId?: string
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
isDraft?: boolean
|
isDraft?: boolean
|
||||||
coverImage?: Post.ImageBlock
|
coverImage?: Image.Document
|
||||||
show?: Show
|
show?: Show
|
||||||
type: typeof LPE.PostTypes.Podcast
|
type: typeof LPE.PostTypes.Podcast
|
||||||
}
|
}
|
||||||
|
|
81
yarn.lock
81
yarn.lock
|
@ -2070,6 +2070,11 @@ bl@^4.1.0:
|
||||||
inherits "^2.0.4"
|
inherits "^2.0.4"
|
||||||
readable-stream "^3.4.0"
|
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:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
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:
|
dependencies:
|
||||||
hyphenate-style-name "^1.0.3"
|
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:
|
css-to-react-native@^3.0.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32"
|
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"
|
mdn-data "2.0.14"
|
||||||
source-map "^0.6.1"
|
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:
|
csstype@^3.0.2, csstype@^3.0.6:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
|
@ -2629,6 +2650,36 @@ doctrine@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
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:
|
dot-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
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"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.2.0"
|
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:
|
error-ex@^1.3.1:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||||
|
@ -3493,6 +3549,11 @@ has@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
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:
|
header-case@^2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063"
|
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:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
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:
|
node-int64@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
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"
|
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
|
||||||
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
|
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:
|
nullthrows@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
|
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"
|
ansi-styles "^6.0.0"
|
||||||
is-fullwidth-code-point "^4.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:
|
snake-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
|
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
|
||||||
|
|
Loading…
Reference in New Issue