feat: add the browser all section to the landing page

This commit is contained in:
Hossein Mehrabi 2023-12-27 06:17:59 +03:30
parent 9db3c686ae
commit 73c3f3e904
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
11 changed files with 361 additions and 108 deletions

View File

@ -7,11 +7,9 @@ import { uiConfigs } from '../../configs/ui.configs'
import { useNavbarState } from '../../states/navbarState'
import { lsdUtils } from '../../utils/lsd.utils'
export type HeroProps = Partial<React.ComponentProps<typeof Container>> & {
tags?: string[]
}
export type HeroProps = Partial<React.ComponentProps<typeof Container>> & {}
export const Hero: React.FC<HeroProps> = ({ tags = [], ...props }) => {
export const Hero: React.FC<HeroProps> = ({ ...props }) => {
const ref = useRef<HTMLElement>(null)
const scroll = useWindowScroll()
const navbarState = useNavbarState()

View File

@ -1,73 +1,117 @@
import { Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import clsx from 'clsx'
import React from 'react'
import { lsdUtils } from '../../utils/lsd.utils'
export type SectionProps = Partial<
React.ComponentProps<typeof SectionContainer>
> & {
export type SectionProps = Partial<React.ComponentProps<typeof Root>> & {
title?: React.ReactNode
subtitle?: string | React.ReactNode
bordered?: boolean
size?: 'small' | 'large'
}
export const Section = ({
title,
subtitle,
bordered = true,
size = 'small',
className,
children,
...props
}: SectionProps) => {
return (
<SectionContainer bordered={bordered} {...props}>
<Root
className={clsx(
className,
'section',
`section--${size}`,
bordered && `section--bordered`,
)}
{...props}
>
{(title || subtitle) && (
<Container>
<Typography component="h2" variant="subtitle2">
<div className="section__header">
<Typography
component="h2"
variant="subtitle2"
className="section__title"
>
{title}
</Typography>
{subtitle && (
<>
<Typography variant="body2"></Typography>
{typeof subtitle === 'string' ? (
<Typography variant="body2">subtitle</Typography>
<Typography variant="body2" className="section__subtitle">
subtitle
</Typography>
) : (
subtitle
)}
</>
)}
</Container>
</div>
)}
{children}
</SectionContainer>
<div className="section__content">{children}</div>
</Root>
)
}
const SectionContainer = styled.section<{
bordered?: boolean
}>`
const Root = styled.section`
width: 100%;
box-sizing: border-box;
border-top: ${(props) =>
props.bordered ? '1px solid rgb(var(--lsd-border-primary))' : 'none'};
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
margin-top: var(--lsd-spacing-16);
&.section--small {
&.section--bordered {
border-top: 1px solid rgb(var(--lsd-border-primary));
}
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
margin-top: var(--lsd-spacing-16);
}
.section__header {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: var(--lsd-spacing-24) 0;
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
padding: var(--lsd-spacing-16) 0;
& > .section__title {
${lsdUtils.typography('subtitle3')}
}
}
}
}
`
const Container = styled.div`
display: flex;
align-items: center;
gap: 8px;
width: 100%;
&.section--large {
& > .section__header {
padding-bottom: var(--lsd-spacing-24);
padding: var(--lsd-spacing-24) 0;
& > .section__title {
${lsdUtils.typography('h2')}
}
}
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
padding: var(--lsd-spacing-16) 0;
&.section--bordered {
& > .section__header {
border-bottom: 1px solid rgb(var(--lsd-border-primary));
}
}
& > h2 {
${lsdUtils.typography('subtitle3')}
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
& > .section__header {
padding-bottom: var(--lsd-spacing-16);
& > .section__title {
${lsdUtils.typography('h3')}
}
}
}
}
`

View File

@ -0,0 +1,74 @@
import { Button, FolderIcon, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import Link from 'next/link'
import React from 'react'
export type TagCardProps = React.ComponentProps<typeof Root> & {
name: string
count?: number
}
export const TagCard: React.FC<TagCardProps> = ({ name, count, ...props }) => {
return (
<Root {...props}>
<CustomButton icon={<FolderIcon color="primary" />}>
<Info>
<Typography component="p" variant="label1">
{name}
</Typography>
{count && (
<Typography component="span" variant="subtitle3">
{count} posts
</Typography>
)}
</Info>
</CustomButton>
</Root>
)
}
const Root = styled(Link)`
text-decoration: none;
`
const CustomButton = styled(Button)`
width: 100%;
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: flex-end;
padding: var(--lsd-spacing-16) !important;
.lsd-button {
overflow: hidden;
}
.lsd-button__text {
flex: 1 1 auto;
}
&:hover {
.lsd-button__text {
text-decoration: none !important;
}
}
`
const Info = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
& > p {
text-align: left;
text-decoration: underline;
height: var(--lsd-label1-lineHeight);
overflow: hidden;
text-overflow: ellipsis;
}
& > span {
flex: 0 0 auto;
}
`

View File

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

View File

@ -1,13 +1,17 @@
import { Button, Typography } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import React from 'react'
import { Grid, GridItem } from '../../components/Grid/Grid'
import { Hero } from '../../components/Hero'
import { PostsGrid } from '../../components/PostsGrid'
import { Section } from '../../components/Section/Section'
import { TagCard } from '../../components/TagCard'
import { uiConfigs } from '../../configs/ui.configs'
import { useRecentPosts } from '../../queries/useRecentPosts.query'
import { ApiPaginatedPayload } from '../../types/data.types'
import { LPE } from '../../types/lpe.types'
import { lsdUtils } from '../../utils/lsd.utils'
import { formatTagText } from '../../utils/string.utils'
import { PodcastShowsPreview } from '../PodcastShowsPreview'
export type HomePageProps = React.DetailedHTMLProps<
@ -15,7 +19,7 @@ export type HomePageProps = React.DetailedHTMLProps<
HTMLDivElement
> & {
data: {
tags: string[]
tags: LPE.Tag.Document[]
shows: LPE.Podcast.Show[]
latest: ApiPaginatedPayload<LPE.Post.Document[]>
highlighted: LPE.Post.Document[]
@ -32,74 +36,132 @@ export const HomePage: React.FC<HomePageProps> = ({
return (
<Root {...props}>
<HeroContainer>
<Hero tags={tags} />
<Hero />
</HeroContainer>
<Container>
<PostsGrid
posts={highlighted.slice(0, 1)}
pattern={[{ cols: 1, size: 'large' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 1, size: 'large' }],
},
]}
/>
<Section title="Latest posts">
<div>
<PostsGrid
pattern={[{ cols: 4, size: 'small' }]}
posts={highlighted.slice(0, 1)}
pattern={[{ cols: 1, size: 'large' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{
cols: 1,
size: 'small',
},
],
},
{
breakpoint: 'sm',
pattern: [
{
cols: 2,
size: 'small',
},
],
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'md',
pattern: [
{
cols: 4,
size: 'small',
},
],
pattern: [{ cols: 1, size: 'large' }],
},
]}
posts={query.posts.slice(0, 8)}
/>
</Section>
{/* {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>
)} */}
<Section title="Latest posts">
<PostsGrid
pattern={[{ cols: 4, size: 'small' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{
cols: 1,
size: 'small',
},
],
},
{
breakpoint: 'sm',
pattern: [
{
cols: 2,
size: 'small',
},
],
},
{
breakpoint: 'md',
pattern: [
{
cols: 4,
size: 'small',
},
],
},
]}
posts={query.posts
.filter((post) => !post.highlighted)
.slice(0, 8)}
/>
</Section>
</div>
<PodcastShowsPreview data={{ shows }} />
<BrowseAll title="Browser all" size="large">
<div>
<Typography component="h2" variant="body1">
Tags
</Typography>
</div>
<Grid xs={{ cols: 1 }} sm={{ cols: 4 }}>
{tags.map((tag) => (
<GridItem key={tag.value} cols={1}>
<TagCard
href={`/search?topic=${tag.value}`}
name={formatTagText(tag.value)}
count={tag.count}
/>
</GridItem>
))}
</Grid>
<AllPosts title="All posts">
<PostsGrid
pattern={[{ cols: 4, size: 'small' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{
cols: 1,
size: 'small',
},
],
},
{
breakpoint: 'sm',
pattern: [
{
cols: 2,
size: 'small',
},
],
},
{
breakpoint: 'md',
pattern: [
{
cols: 4,
size: 'small',
},
],
},
]}
posts={query.posts}
/>
</AllPosts>
{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>
)}
</BrowseAll>
</Container>
</Root>
)
@ -113,27 +175,16 @@ const Root = styled('div')`
.load-more {
width: 100%;
text-align: center;
button {
width: 340px;
}
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
button {
width: 236px;
}
}
margin-top: var(--lsd-spacing-24);
${(props) => lsdUtils.breakpoint(props.theme, 'xs', 'exact')} {
margin-top: var(--lsd-spacing-16);
button {
width: 100%;
}
}
}
.podcasts {
margin-top: 40px;
}
`
const HeroContainer = styled.div`
@ -149,4 +200,28 @@ const Container = styled.div`
@media (max-width: ${uiConfigs.maxContainerWidth}px) {
padding: 0 var(--main-content-padding);
}
display: flex;
flex-direction: column;
gap: var(--lsd-spacing-120) 0;
${(props) => lsdUtils.breakpoint(props.theme, 'xs', 'exact')} {
gap: var(--lsd-spacing-80) 0;
}
`
const BrowseAll = styled(Section)`
& > .section__content {
& > div:first-of-type {
padding: var(--lsd-spacing-24) 0;
}
}
`
const AllPosts = styled(Section)`
margin-top: var(--lsd-spacing-64);
${(props) => lsdUtils.breakpoint(props.theme, 'xs', 'exact')} {
margin-top: var(--lsd-spacing-40);
}
`

View File

@ -9533,6 +9533,14 @@ export type GetAllTopicsQuery = {
__typename?: 'AggregateGoogleDocGroupedByObj'
value: string
}
tags: {
__typename?: 'AggregateGoogleDoctagsObj'
topOccurrences: Array<{
__typename?: 'AggregateGoogleDoctagsTopOccurrencesObj'
value: string
occurs: number
}>
}
}>
}
}
@ -11252,6 +11260,32 @@ export const GetAllTopicsDocument = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'tags' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'topOccurrences' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'value' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'occurs' },
},
],
},
},
],
},
},
],
},
},

View File

@ -31,7 +31,7 @@ export const getStaticProps: GetStaticProps<PageProps> = async () => {
const { data: highlighted } = await unbodyApi.getHighlightedPosts()
const { data: latest } = await unbodyApi.getRecentPosts({
skip: 0,
limit: 15,
limit: 12,
})
const { data: _shows = [] } = await unbodyApi.getPodcastShows({

View File

@ -126,7 +126,7 @@ export async function getStaticProps() {
return {
props: {
topics,
topics: topics.map((topic) => topic.value),
shows,
},
revalidate: 10,

View File

@ -111,6 +111,12 @@ export const GET_ALL_TOPICS_QUERY = gql`
groupedBy {
value
}
tags {
topOccurrences {
value
occurs
}
}
}
}
}

View File

@ -98,6 +98,7 @@ type Data = {
episodes: LPE.Podcast.Document[]
draftEpisodes: LPE.Podcast.Document[]
highlightedEpisodes: LPE.Podcast.Document[]
publishedPosts: LPE.Post.Document[]
staticPages: LPE.StaticPage.Document[]
draftStaticPages: LPE.StaticPage.Document[]
allRecords: PageRecord[]
@ -119,6 +120,7 @@ export class UnbodyService {
allRecords: [],
draftArticles: [],
draftEpisodes: [],
publishedPosts: [],
draftStaticPages: [],
highlightedArticles: [],
highlightedEpisodes: [],
@ -206,6 +208,7 @@ export class UnbodyService {
allRecords: [],
draftArticles: [],
draftEpisodes: [],
publishedPosts: [],
draftStaticPages: [],
highlightedArticles: [],
highlightedEpisodes: [],
@ -229,6 +232,11 @@ export class UnbodyService {
}
newData.posts = [...newData.articles, ...newData.episodes].sort(sortPosts)
newData.publishedPosts = [
...newData.posts,
...newData.highlightedArticles,
...newData.highlightedEpisodes,
].sort(sortPosts)
newData.allRecords = [...articles, ...episodes, ...staticPages]
const oldData = { ...this.data }
@ -1171,13 +1179,13 @@ export class UnbodyService {
this.handleRequest<ApiPaginatedPayload<LPE.Post.Document[]>>(
async () => {
await this.fetchData()
const { posts } = this.data
const { publishedPosts } = this.data
const data = posts.slice(skip, skip + limit)
const data = publishedPosts.slice(skip, skip + limit)
return {
data,
hasMore: posts.length > skip + limit,
hasMore: publishedPosts.length > skip + limit,
}
},
{
@ -1596,7 +1604,13 @@ export class UnbodyService {
},
})
const topics = data.Aggregate.GoogleDoc.map((doc) => doc.groupedBy.value)
const topics = data.Aggregate.GoogleDoc.map((doc) => ({
value: doc.groupedBy.value,
count:
(doc.tags.topOccurrences || []).find(
(t) => t.value === doc.groupedBy.value,
)?.occurs ?? 1,
}))
return topics
}, [])
@ -1753,7 +1767,7 @@ unbodyApi.onChange(async (oldData, data, changes, firstLoad) => {
},
]),
)
topics.forEach((topic) => feed.addCategory(formatTagText(topic)))
topics.forEach((topic) => feed.addCategory(formatTagText(topic.value)))
feed.addCategory(articleCategory.name)
Object.values(showCategories).forEach((cat) => feed.addCategory(cat.name))

View File

@ -24,6 +24,13 @@ export namespace LPE {
}
}
export namespace Tag {
export type Document = {
value: string
count: number
}
}
export namespace Post {
export type Footnote = {
index: number