feat: implement landing page

This commit is contained in:
Hossein Mehrabi 2023-08-22 03:46:17 +03:30
parent fed07d5324
commit a9c915195a
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
22 changed files with 799 additions and 172 deletions

View File

@ -8,19 +8,22 @@ export enum AuthorsDirection {
ROW = 'row',
}
const Authors = ({
authors,
email,
gap = 12,
flexDirection = AuthorsDirection.ROW,
}: {
export type AuthorsProps = React.ComponentProps<typeof AuthorsContainer> & {
authors: LPE.Author.Document[]
email: boolean
gap?: number
flexDirection?: AuthorsDirection
}
const Authors: React.FC<AuthorsProps> = ({
authors,
email,
gap = 12,
flexDirection = AuthorsDirection.ROW,
...props
}) => {
return authors?.length > 0 ? (
<AuthorsContainer gap={gap} flexDirection={flexDirection}>
<AuthorsContainer gap={gap} flexDirection={flexDirection} {...props}>
{authors.map((author, index) =>
index < authors.length - 1 ? (
<AuthorContainer gap={gap} key={author.name}>

View File

@ -11,13 +11,16 @@ export enum Size {
LARGE = 'large',
}
interface Props {
export type PodcastShowCardProps = React.ComponentProps<typeof Container> & {
show: LPE.Podcast.Show
}
export default function PodcastShowCard({ show }: Props) {
export default function PodcastShowCard({
show,
...props
}: PodcastShowCardProps) {
return (
<Container>
<Container {...props}>
<LogosCircleIcon width={73} height={73} />
<ShowData>
<Typography variant="h3">{show.title}</Typography>

View File

@ -1,25 +1,24 @@
import React, { FC } from 'react'
import {
ResponsiveImage,
ResponsiveImageProps,
} from '@/components/ResponsiveImage/ResponsiveImage'
import { LPE } from '@/types/lpe.types'
import Link from 'next/link'
import { FC } from 'react'
interface Props {
export type PostCardCoverProps = React.ComponentProps<typeof Link> & {
imageProps: ResponsiveImageProps
imageData: LPE.Image.Document
playIcon?: boolean
link: string
}
export const PostCardCover: FC<Props> = ({
link,
export const PostCardCover: FC<PostCardCoverProps> = ({
imageProps,
imageData,
playIcon,
...props
}) => {
return (
<Link href={link}>
<Link {...props}>
<ResponsiveImage {...imageProps} data={imageData} />
</Link>
)

View File

@ -4,14 +4,14 @@ import styled from '@emotion/styled'
import { LPE } from '@/types/lpe.types'
import PostType = LPE.PostType
interface Props {
export type Props = React.ComponentProps<typeof Container> & {
contentType: PostType
date: Date | null
}
export const PostCardLabel: FC<Props> = ({ contentType, date }) => {
export const PostCardLabel: FC<Props> = ({ contentType, date, ...props }) => {
return (
<Container>
<Container {...props}>
<Typography variant="body3" genericFontFamily="sans-serif">
{contentType.toUpperCase()}
</Typography>
@ -35,5 +35,4 @@ const Container = styled.div`
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 8px;
`

View File

@ -1,36 +1,45 @@
import { LPE } from '@/types/lpe.types'
import Link from 'next/link'
import { Typography } from '@acid-info/lsd-react'
import { LogosCircleIcon } from '../Icons/LogosCircleIcon'
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import Image from 'next/image'
import Link from 'next/link'
export interface PostCardShowDetailsProps {
export type PostCardShowDetailsProps = Partial<
React.ComponentProps<typeof CustomLink>
> & {
title: string
slug: string
episodeNumber: number
logo?: LPE.Image.Document
podcast: LPE.Podcast.Show
size?: 'small' | 'medium'
}
// TODO
export const PostCardShowDetails = (props: PostCardShowDetailsProps) => {
const { slug, episodeNumber, podcast } = props
export const PostCardShowDetails = ({
slug,
episodeNumber,
podcast,
size = 'medium',
...props
}: PostCardShowDetailsProps) => {
return (
<CustomLink href={`/podcasts/${slug}`}>
<CustomLink {...props} href={`/podcasts/${slug}`}>
<Container>
{podcast && (
<>
<Image
<Logo
src={podcast?.logo?.url}
width={38}
height={38}
width={size === 'medium' ? 38 : 28}
height={size === 'medium' ? 38 : 28}
alt={podcast.logo.alt}
/>
<PodcastInfo>
<Typography variant="body2">{podcast.title}</Typography>
{size !== 'small' && (
<Typography variant="body3">{episodeNumber} EP</Typography>
)}
</PodcastInfo>
</>
)}
@ -54,3 +63,7 @@ const PodcastInfo = styled.div`
const CustomLink = styled(Link)`
text-decoration: none;
`
const Logo = styled(Image)`
border-radius: 100%;
`

View File

@ -10,22 +10,18 @@ import { AuthorsDirection } from '../Authors/Authors'
import { ResponsiveImageProps } from '../ResponsiveImage/ResponsiveImage'
import { PostCardLabel } from './PostCard.Label'
import { PostCardCover } from '@/components/PostCard/PostCard.Cover'
import {
PostCardShowDetails,
PostCardShowDetailsProps,
} from '@/components/PostCard/PostCard.ShowDetails'
import { css } from '@emotion/react'
import { PostCardLabel } from './PostCard.Label'
export type PostAppearanceProps = {
imageProps?: ResponsiveImageProps
}
export enum PodcastType {
NETWORK_STATE = 'network-state',
HASHING_IT_OUT = 'hashing-it-out',
}
export type PostDataProps = {
slug: string
date: Date | null
@ -41,8 +37,8 @@ export type PostCardProps = CommonProps &
React.HTMLAttributes<HTMLDivElement> & {
appearance?: PostAppearanceProps
data: PostDataProps
isFeatured?: boolean
contentType: LPE.PostType
size?: 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large'
}
export const PostCard = (_props: PostCardProps) => {
@ -58,91 +54,252 @@ export const PostCard = (_props: PostCardProps) => {
tags = [],
podcastShowDetails,
},
size = 'small',
contentType,
isFeatured = false,
...props
} = _props
const link =
contentType === LPE.PostTypes.Article
? `/article/${slug}`
: `/podcasts/${slug}`
: `/podcasts/${podcastShowDetails?.slug}/${slug}`
return (
<Container {...props}>
{coverImage && (
const coverImageElement = coverImage && (
<PostCardCover
className="coverImage"
href={link}
imageProps={imageProps}
imageData={coverImage}
link={link}
/>
)}
)
<PostCardLabel contentType={contentType} date={date} />
const labelElement = (
<PostCardLabel className="label" contentType={contentType} date={date} />
)
<TitleLink href={link}>
<Title genericFontFamily="serif" component="h3">
const titleElement = (
<Link href={link} className="titleLink">
<Typography
className="title"
genericFontFamily="serif"
component="h3"
variant={
size === 'xxsmall'
? 'h6'
: size === 'xsmall'
? 'body3'
: size === 'small'
? 'h4'
: 'h2'
}
>
{title}
</Title>
</TitleLink>
</Typography>
</Link>
)
{subtitle && (
<Subtitle variant={'body1'} genericFontFamily="sans-serif">
const subtitleElement = subtitle && (
<Typography
className="subtitle"
variant={'body1'}
genericFontFamily="sans-serif"
>
{subtitle}
</Subtitle>
)}
</Typography>
)
{authors && authors.length > 0 && (
const authorsElement = authors && authors.length > 0 && (
<Authors
className="authors"
authors={authors}
email={false}
flexDirection={AuthorsDirection.ROW}
gap={8}
/>
)}
)
{podcastShowDetails && <PostCardShowDetails {...podcastShowDetails} />}
{tags.length > 0 && <Tags tags={tags} />}
const showElement = podcastShowDetails && (
<PostCardShowDetails
{...podcastShowDetails}
size={size === 'large' ? 'medium' : 'small'}
className="showDetails"
/>
)
const tagsElement = tags.length > 0 && <Tags className="tags" tags={tags} />
return (
<Container {...props} size={size}>
{size === 'large' ? (
<>
<div>
{labelElement}
{titleElement}
{subtitleElement}
{authorsElement}
{showElement}
{tagsElement}
</div>
<div>{coverImageElement}</div>
</>
) : (
<>
{coverImageElement}
{labelElement}
{titleElement}
{subtitleElement}
{authorsElement}
{showElement}
{tagsElement}
</>
)}
</Container>
)
}
const Container = styled.div`
PostCard.toData = (post: LPE.Post.Document, shows: LPE.Podcast.Show[] = []) => {
const show =
post.type === 'podcast'
? post.show || shows.find((show) => show.id === post.showId)
: undefined
return {
date:
post.type === 'podcast'
? post.publishedAt
? new Date(post.publishedAt)
: null
: post.modifiedAt
? new Date(post.modifiedAt)
: null,
slug: post.slug,
title: post.title,
authors: post.type === 'article' ? post.authors : [],
coverImage: post.coverImage,
subtitle: (post.type === 'article' && post.subtitle) || '',
tags: post.tags,
...(post.type === 'podcast' && show
? {
podcastShowDetails: {
episodeNumber: post.episodeNumber,
title: show.title,
slug: show.slug,
podcast: show,
},
}
: {}),
}
}
const Container = styled.div<Pick<PostCardProps, 'size'>>`
display: flex;
flex-direction: column;
position: 'relative';
gap: 16px;
`
const CustomTypography = styled(Typography)`
.label {
margin-bottom: -8px;
}
.titleLink {
text-decoration: none;
width: fit-content;
}
.title,
.subtitle {
text-overflow: ellipsis;
overflow: hidden;
word-break: break-word;
`
const PodcastAuthor = styled.div`
display: flex;
align-items: center;
gap: 12px;
`
const TitleLink = styled(Link)`
text-decoration: none;
width: fit-content;
`
const Subtitle = styled(CustomTypography)`
@media (min-width: 768px) {
}
@media (max-width: 768px) {
font-size: 14px;
line-height: 20px;
}
`
const Title = styled(CustomTypography)`
.title {
@media (max-width: 768px) {
font-size: 28px;
line-height: 36px;
}
}
.subtitle {
@media (max-width: 768px) {
font-size: 14px;
line-height: 20px;
}
}
${({ size }) =>
size === 'xxsmall' &&
css`
.label {
}
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-height: calc(2 * var(--lsd-h6-lineHeight));
}
.subtitle {
display: none;
}
.coverImage {
display: none;
}
.tags {
display: none;
}
.authors,
.showDetails {
flex-grow: 1;
display: flex;
}
`}
${({ size }) =>
size === 'xsmall' &&
css`
.label {
margin-bottom: -px;
}
.title {
}
.subtitle {
display: none;
}
.coverImage {
}
.tags {
margin-top: 8px;
}
.authors,
.showDetails {
}
`}
${({ size }) => size === 'small' && css``}
${({ size }) => size === 'medium' && css``}
${({ size }) =>
size === 'large' &&
css`
display: grid;
grid-template-columns: repeat(2, 1fr);
> * {
display: flex;
flex-direction: column;
position: 'relative';
gap: 16px;
}
`}
`

View File

@ -0,0 +1,112 @@
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import React, { useMemo } from 'react'
import { LPE } from '../../types/lpe.types'
import { chunkArray } from '../../utils/array.utils'
import { PostCard, PostCardProps } from '../PostCard'
export type PostsGridProps = Partial<React.ComponentProps<typeof Container>> & {
posts?: LPE.Post.Document[]
}
export const PostsGrid: React.FC<PostsGridProps> = ({
cols = 4,
size = 'small',
posts = [],
bordered = false,
...props
}) => {
const groups = useMemo(() => chunkArray(posts, cols), [posts, cols])
return (
<Container {...props} cols={cols} size={size} bordered={bordered}>
{groups.map((group, index) => (
<div className="row" key={index}>
{group.map((post) => (
<div key={post.id} className="post-card-wrapper">
<PostCard
size={size}
className="post-card"
contentType={post.type}
data={PostCard.toData(post)}
/>
</div>
))}
</div>
))}
</Container>
)
}
const Container = styled.div<{
cols: number
bordered: boolean
size: PostCardProps['size']
}>`
display: grid;
gap: 16px 0;
${(props) => css`
> .row {
display: grid;
grid-template-columns: repeat(${props.cols}, 1fr);
gap: 0 32px;
& > div {
padding: 24px 0;
border-top: ${props.bordered ? '1px' : '0'} solid
rgb(var(--lsd-border-primary));
}
}
`}
${(props) =>
props.size === 'xxsmall' &&
css`
> .row {
padding: 24px 0;
gap: 0 32px;
& > div {
border-top: 0;
padding: 0;
position: relative;
}
& > div:not(:last-child)::after {
content: ' ';
height: 100%;
width: 1px;
background: rgb(var(--lsd-border-primary));
position: absolute;
top: 0;
right: -16px;
display: ${props.bordered ? 'block' : 'none'};
}
}
`}
${(props) =>
props.size === 'xsmall' &&
css`
> .row {
gap: 0 16px;
& > div {
box-sizing: border-box;
border-top: 0;
}
& > div:last-child {
}
& > div:not(:last-child) {
}
}
`}
${(props) => props.size === 'small' && css``}
${(props) => props.size === 'medium' && css``}
${(props) => props.size === 'large' && css``}
`

View File

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

View File

@ -3,13 +3,17 @@ import styled from '@emotion/styled'
import Link from 'next/link'
import { useRouter } from 'next/router'
const Tags = ({ tags, className }: { tags: string[]; className?: string }) => {
export type TagsProps = React.ComponentProps<typeof TagsContainer> & {
tags: string[]
}
const Tags: React.FC<TagsProps> = ({ tags, className, ...props }) => {
const router = useRouter()
const { query } = router
const { topics } = query
return tags?.length > 0 ? (
<TagsContainer className={className}>
<TagsContainer className={className} {...props}>
{tags.map((tag, idx) => (
<Link key={`tag-${idx}`} href={`/search?topics=${tag}`}>
<Tag

View File

@ -0,0 +1,88 @@
import { Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import React, { useMemo } from 'react'
import { PostsGrid } from '../../components/PostsGrid'
import { useRecentPosts } from '../../queries/useRecentPosts.query'
import { LPE } from '../../types/lpe.types'
import { chunkArray } from '../../utils/array.utils'
import { PodcastShowsPreview } from '../PodcastShowsPreview'
export type HomePageProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
data: {
tags: string[]
shows: LPE.Podcast.Show[]
latest: LPE.Post.Document[]
highlighted: LPE.Post.Document[]
}
}
export const HomePage: React.FC<HomePageProps> = ({
data,
data: { highlighted = [], shows = [], tags = [], latest = [] },
...props
}) => {
const query = useRecentPosts({ initialData: latest, limit: 10 })
const [group1, group2] = useMemo(
() => [[query.posts.slice(0, 5)], chunkArray(query.posts.slice(5), 4, 2)],
[query.posts],
)
return (
<Root {...props}>
<PostsGrid posts={group1[0]} cols={5} bordered size="xxsmall" />
<PostsGrid
posts={highlighted.slice(0, 1)}
cols={1}
bordered
size="large"
/>
{group2.map((group, index) => (
<PostsGrid
bordered
key={index}
posts={group}
cols={index % 2 !== 0 ? 2 : 4}
size={index % 2 !== 0 ? 'medium' : 'small'}
/>
))}
{query.hasNextPage && (
<div className="load-more">
<Button
onClick={() => query.fetchNextPage()}
size="large"
disabled={query.isLoading}
>
<Typography variant="label1">
{query.isFetchingNextPage ? 'Loading...' : 'See more posts'}
</Typography>
</Button>
</div>
)}
<PodcastShowsPreview data={{ shows }} />
</Root>
)
}
const Root = styled('div')`
width: 100%;
display: flex;
flex-direction: column;
.load-more {
width: 100%;
text-align: center;
button {
width: 340px;
}
}
.podcasts {
margin-top: 40px;
}
`

View File

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

View File

@ -0,0 +1,175 @@
import { ArrowDownIcon, Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import clsx from 'clsx'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { PostsGrid } from '../../components/PostsGrid'
import { LPE } from '../../types/lpe.types'
export type PodcastShowsPreviewProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
data: {
shows: LPE.Podcast.Show[]
}
}
export const PodcastShowsPreview: React.FC<PodcastShowsPreviewProps> = ({
data,
data: { shows = [] },
...props
}) => {
return (
<Root {...props} className={clsx('podcasts', props.className)}>
<div className="podcasts__header">
<Typography variant="subtitle1">Podcasts</Typography>
<Link href="/podcasts">
<Button variant="outlined" size="small">
Go to all podcasts
</Button>
</Link>
</div>
<div
className={clsx(
'podcasts__shows',
shows.length > 1 && 'podcasts__shows--bordered',
)}
>
{shows.slice(0, 2).map((show) => (
<div key={show.id} className={clsx('podcasts__show')}>
<div className="podcasts__show-card">
<Image
width={56}
height={56}
alt={show.title}
src={show.logo.url}
className="podcasts__show-logo"
/>
<Typography variant="h3" className="podcasts__show-title">
{show.title}
</Typography>
{show.description && (
<Typography
variant="subtitle2"
className="podcasts__show-description"
dangerouslySetInnerHTML={{ __html: show.description }}
/>
)}
<Link
href={`/podcasts/${show.slug}`}
className="podcasts__show-link"
>
<Button
size="small"
variant="outlined"
icon={<ArrowDownIcon color="primary" />}
>
Podcast page
</Button>
</Link>
<Typography className="podcasts__show-hosts">
{show.hosts.length > 0 && (
<span>Hosted by: {show.hosts[0].name}</span>
)}
<span>{show.numberOfEpisodes} EP</span>
</Typography>
</div>
<div className="podcasts__show-episodes">
<PostsGrid
posts={(show.episodes || []).slice(0, 2)}
size="xsmall"
cols={2}
/>
<PostsGrid
posts={(show.episodes || []).slice(2, 4)}
size="xsmall"
cols={2}
/>
</div>
</div>
))}
</div>
</Root>
)
}
const Root = styled('div')`
& .podcasts {
&__header {
padding: 16px 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-top: 1px solid rgb(var(--lsd-border-primary));
border-bottom: 1px solid rgb(var(--lsd-border-primary));
}
&__shows {
display: grid;
grid-template-columns: repeat(2, 1fr);
padding-top: 24px;
& > div:first-child {
border-right: 1px solid rgb(var(--lsd-border-primary));
padding-right: 16px;
}
&--bordered {
& > div:last-child {
padding-left: 16px;
}
}
}
&__show-card {
margin-top: 24px;
align-items: flex-start;
}
&__show-logo {
width: 56px;
height: 56px;
border-radius: 100%;
}
&__show-title {
margin-top: 16px;
}
&__show-description {
margin-top: 8px;
}
&__show-link {
display: block;
margin-top: 24px;
text-decoration: none;
}
&__show-hosts {
display: block;
margin-top: 200px;
span:not(:last-child) {
&:after {
content: '•';
margin: 0 8px;
text-decoration: none;
display: inline-block;
}
}
}
&__show-episodes {
> div:not(:last-child) {
border-bottom: 1px solid rgb(var(--lsd-border-primary));
}
}
}
`

View File

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

View File

@ -4,6 +4,7 @@ import { uiConfigs } from '@/configs/ui.configs'
import { SearchBarProvider } from '@/context/searchbar.context'
import { DefaultLayout } from '@/layouts/DefaultLayout'
import { css, Global } from '@emotion/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { NextComponentType, NextPageContext } from 'next'
import type { AppProps } from 'next/app'
import Head from 'next/head'
@ -23,6 +24,8 @@ type AppLayoutProps<P = {}> = AppProps & {
Component: NextLayoutComponentType
}
const queryClient = new QueryClient()
export default function App({ Component, pageProps }: AppLayoutProps) {
const hydrated = useHydrated()
@ -94,11 +97,13 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
}
`}
/>
<QueryClientProvider client={queryClient}>
<ProgressBar />
<SearchBarProvider>
{getLayout(<Component {...pageProps} />)}
</SearchBarProvider>
{hydrated && <GlobalAudioPlayer />}
</QueryClientProvider>
</LSDThemeProvider>
)
}

View File

@ -7,11 +7,11 @@ export default async function handler(
res: NextApiResponse<any>,
) {
const {
query: { page = 1, limit = 10 },
query: { skip = 0, limit = 10 },
} = req
const response = await unbodyApi.getRecentPosts({
page: parseInt(page, 1),
skip: parseInt(skip, 0),
limit: parseInt(limit, 10),
})

View File

@ -1,34 +1,11 @@
import { FeaturedPost } from '@/components/FeaturedPost'
import { PostsList } from '@/components/PostList/PostList'
import { Section } from '@/components/Section/Section'
import { useSearchBarContext } from '@/context/searchbar.context'
import { PostListLayout } from '@/types/ui.types'
import { useEffect } from 'react'
import { GetStaticProps, NextPage } from 'next'
import SEO from '../components/SEO/SEO'
import { api } from '../services/api.service'
import { HomePage, HomePageProps } from '../containers/HomePage'
import unbodyApi from '../services/unbody/unbody.service'
import { LPE } from '../types/lpe.types'
type Props = {
posts: LPE.Article.Data[]
featured: LPE.Article.Data
error: string | null
tags: string[]
}
export default function Home({ posts, featured, tags }: Props) {
const { setTags } = useSearchBarContext()
useEffect(() => {
setTags(tags)
}, [setTags, tags])
useEffect(() => {
api
.getLatestEpisodes({ showSlug: 'hashing-it-out', page: 1, limit: 1 })
.then((res) => console.log(res))
}, [])
type PageProps = Pick<HomePageProps, 'data'>
const Page: NextPage<PageProps> = (props) => {
return (
<>
<SEO
@ -37,32 +14,36 @@ export default function Home({ posts, featured, tags }: Props) {
}
title={'Logos Press Engine'}
/>
{featured && (
<Section title={'Featured'}>
<FeaturedPost post={featured} />
</Section>
)}
<Section title={'Latest posts'}>
<PostsList posts={posts} layout={PostListLayout.XXXX_XX} />
</Section>
<HomePage data={props.data} />
</>
)
}
export const getStaticProps = async () => {
const {
data: { posts, highlighted },
errors,
} = await unbodyApi.getHomepagePosts()
export const getStaticProps: GetStaticProps<PageProps> = async () => {
const { data: tags = [] } = await unbodyApi.getTopics(true)
const { data: highlighted } = await unbodyApi.getHighlightedPosts()
const { data: latest = [] } = await unbodyApi.getRecentPosts({
skip: 0,
limit: 15,
})
const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
const { data: _shows = [] } = await unbodyApi.getPodcastShows({
populateEpisodes: true,
episodesLimit: 10,
})
const shows = [..._shows].sort((a, b) => (a.title > b.title ? -1 : 1))
return {
props: {
posts,
errors,
featured: highlighted,
tags: topics || [],
data: {
tags,
shows,
latest,
highlighted,
},
},
}
}
export default Page

View File

@ -1,12 +1,10 @@
import { SEO } from '@/components/SEO'
import PodcastsContainer from '@/containers/PodcastsContainer'
import PodcastsLayout from '@/layouts/PodcastsLayout/Podcasts.layout'
import unbodyApi from '@/services/unbody/unbody.service'
import { GetStaticPropsContext } from 'next'
import { ReactNode } from 'react'
import { LPE } from '../../types/lpe.types'
import PodcastsLayout from '@/layouts/PodcastsLayout/Podcasts.layout'
import PodcastsContainer from '@/containers/PodcastsContainer'
import TEMP_DATA from './podcasts-temp-data.json'
import unbodyApi from '@/services/unbody/unbody.service'
type PodcastsProps = {
shows: LPE.Podcast.Show[]

View File

@ -0,0 +1,54 @@
import { useInfiniteQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { api } from '../services/api.service'
import { LPE } from '../types/lpe.types'
export const useRecentPosts = ({
initialData = [],
limit = 10,
}: {
initialData?: LPE.Post.Document[]
limit?: number
}) => {
const query = useInfiniteQuery(
['latest-posts', initialData, limit],
async ({ pageParam }) => {
const firstPageLimit = initialData.length
const _limit = pageParam === 1 ? firstPageLimit : limit
const skip =
pageParam === 1 ? 0 : (pageParam - 2) * limit + firstPageLimit
return api.getRecentPosts({ skip, limit: _limit }).then((res) => ({
page: pageParam,
posts: res.data,
hasMore: res.data.length === _limit,
}))
},
{
initialData: {
pageParams: [1],
pages: [
{
page: 1,
hasMore: true,
posts: initialData,
},
],
},
getNextPageParam: (lastPage, all) =>
lastPage.hasMore ? lastPage.page + 1 : undefined,
getPreviousPageParam: (firstPage) =>
firstPage ? firstPage.page - 1 : undefined,
},
)
const posts = useMemo(
() => (query.data?.pages || []).flatMap((page) => page.posts),
[query.data],
)
return {
posts,
...query,
}
}

View File

@ -3,13 +3,13 @@ import { LPE } from '../types/lpe.types'
export class ApiService {
getRecentPosts = async ({
page = 1,
skip = 0,
limit = 10,
}: {
page?: number
skip?: number
limit?: number
}): Promise<ApiResponse<LPE.Post.Document[]>> =>
fetch(`/api/posts?page=${page}&limit=${limit}`)
fetch(`/api/posts?skip=${skip}&limit=${limit}`)
.then((res) => res.json())
.catch((e) => {
console.error(e)

View File

@ -25,7 +25,8 @@ export const PodcastShowDataType: UnbodyDataTypeConfig<
if (!original) return data as any
const description = data.content.find(
(block) => block.labels.length === 0 && block.type === 'text',
(block) =>
block.labels.length === 0 && block.type === 'text' && block.order > 2,
)
const image = data.content.find(

View File

@ -59,7 +59,22 @@ export class UnbodyService {
} = { posts: [] }
constructor(private apiKey: string, private projectId: string) {
const cache = new InMemoryCache({})
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
Get: {
merge(existing = {}, incoming) {
return {
...existing,
...incoming,
}
},
},
},
},
},
})
this.client = new ApolloClient({
uri: 'https://graphql.unbody.io',
@ -382,7 +397,7 @@ export class UnbodyService {
]),
...this.helpers.args.page(1, 50),
imageBlocks: true,
textBlocks: false,
textBlocks: true,
mentions: true,
},
})
@ -647,17 +662,16 @@ export class UnbodyService {
})
getRecentPosts = async ({
page = 1,
limit = 10,
skip = 0,
}: {
page?: number
limit?: number
skip?: number
}) =>
this.handleRequest(async () => {
const { posts } = await this.loadInitialData()
const startIndex = (page - 1) * limit
return posts.slice(startIndex, startIndex + limit)
return posts.slice(skip, skip + limit)
}, [])
getHighlightedPosts = async () =>

18
src/utils/array.utils.ts Normal file
View File

@ -0,0 +1,18 @@
export const chunkArray = <T>(arr: T[], ...pattern: number[]): T[][] => {
const result: T[][] = []
let index = 0
let iteration = 0
while (index < arr.length) {
const take = pattern[iteration % pattern.length]
const elements = arr.slice(index, index + take)
result.push(elements)
iteration++
index += elements.length
}
return result
}