mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 14:48:08 +00:00
feat: implement landing page
This commit is contained in:
parent
fed07d5324
commit
a9c915195a
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
`
|
||||
|
@ -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>
|
||||
<Typography variant="body3">{episodeNumber} EP</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%;
|
||||
`
|
||||
|
@ -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}`
|
||||
|
||||
const coverImageElement = coverImage && (
|
||||
<PostCardCover
|
||||
className="coverImage"
|
||||
href={link}
|
||||
imageProps={imageProps}
|
||||
imageData={coverImage}
|
||||
/>
|
||||
)
|
||||
|
||||
const labelElement = (
|
||||
<PostCardLabel className="label" contentType={contentType} date={date} />
|
||||
)
|
||||
|
||||
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}
|
||||
</Typography>
|
||||
</Link>
|
||||
)
|
||||
|
||||
const subtitleElement = subtitle && (
|
||||
<Typography
|
||||
className="subtitle"
|
||||
variant={'body1'}
|
||||
genericFontFamily="sans-serif"
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)
|
||||
|
||||
const authorsElement = authors && authors.length > 0 && (
|
||||
<Authors
|
||||
className="authors"
|
||||
authors={authors}
|
||||
email={false}
|
||||
flexDirection={AuthorsDirection.ROW}
|
||||
gap={8}
|
||||
/>
|
||||
)
|
||||
|
||||
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}>
|
||||
{coverImage && (
|
||||
<PostCardCover
|
||||
imageProps={imageProps}
|
||||
imageData={coverImage}
|
||||
link={link}
|
||||
/>
|
||||
<Container {...props} size={size}>
|
||||
{size === 'large' ? (
|
||||
<>
|
||||
<div>
|
||||
{labelElement}
|
||||
{titleElement}
|
||||
{subtitleElement}
|
||||
{authorsElement}
|
||||
{showElement}
|
||||
{tagsElement}
|
||||
</div>
|
||||
<div>{coverImageElement}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{coverImageElement}
|
||||
{labelElement}
|
||||
{titleElement}
|
||||
{subtitleElement}
|
||||
{authorsElement}
|
||||
{showElement}
|
||||
{tagsElement}
|
||||
</>
|
||||
)}
|
||||
|
||||
<PostCardLabel contentType={contentType} date={date} />
|
||||
|
||||
<TitleLink href={link}>
|
||||
<Title genericFontFamily="serif" component="h3">
|
||||
{title}
|
||||
</Title>
|
||||
</TitleLink>
|
||||
|
||||
{subtitle && (
|
||||
<Subtitle variant={'body1'} genericFontFamily="sans-serif">
|
||||
{subtitle}
|
||||
</Subtitle>
|
||||
)}
|
||||
|
||||
{authors && authors.length > 0 && (
|
||||
<Authors
|
||||
authors={authors}
|
||||
email={false}
|
||||
flexDirection={AuthorsDirection.ROW}
|
||||
gap={8}
|
||||
/>
|
||||
)}
|
||||
|
||||
{podcastShowDetails && <PostCardShowDetails {...podcastShowDetails} />}
|
||||
{tags.length > 0 && <Tags tags={tags} />}
|
||||
</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)`
|
||||
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) {
|
||||
.label {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const Title = styled(CustomTypography)`
|
||||
@media (max-width: 768px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
.titleLink {
|
||||
text-decoration: none;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
112
src/components/PostsGrid/PostsGrid.tsx
Normal file
112
src/components/PostsGrid/PostsGrid.tsx
Normal 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``}
|
||||
`
|
1
src/components/PostsGrid/index.ts
Normal file
1
src/components/PostsGrid/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './PostsGrid'
|
@ -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
|
||||
|
88
src/containers/HomePage/HomePage.tsx
Normal file
88
src/containers/HomePage/HomePage.tsx
Normal 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;
|
||||
}
|
||||
`
|
1
src/containers/HomePage/index.ts
Normal file
1
src/containers/HomePage/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './HomePage'
|
175
src/containers/PodcastShowsPreview/PodcastShowsPreview.tsx
Normal file
175
src/containers/PodcastShowsPreview/PodcastShowsPreview.tsx
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
1
src/containers/PodcastShowsPreview/index.ts
Normal file
1
src/containers/PodcastShowsPreview/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './PodcastShowsPreview'
|
@ -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) {
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<ProgressBar />
|
||||
<SearchBarProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SearchBarProvider>
|
||||
{hydrated && <GlobalAudioPlayer />}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ProgressBar />
|
||||
<SearchBarProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SearchBarProvider>
|
||||
{hydrated && <GlobalAudioPlayer />}
|
||||
</QueryClientProvider>
|
||||
</LSDThemeProvider>
|
||||
)
|
||||
}
|
||||
|
@ -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),
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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[]
|
||||
|
54
src/queries/useRecentPosts.query.ts
Normal file
54
src/queries/useRecentPosts.query.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
18
src/utils/array.utils.ts
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user