mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 14:48:08 +00:00
feat: add the browser all section to the landing page
This commit is contained in:
parent
9db3c686ae
commit
73c3f3e904
@ -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()
|
||||
|
@ -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')}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
74
src/components/TagCard/TagCard.tsx
Normal file
74
src/components/TagCard/TagCard.tsx
Normal 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;
|
||||
}
|
||||
`
|
1
src/components/TagCard/index.ts
Normal file
1
src/components/TagCard/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './TagCard'
|
@ -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);
|
||||
}
|
||||
`
|
||||
|
@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -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({
|
||||
|
@ -126,7 +126,7 @@ export async function getStaticProps() {
|
||||
|
||||
return {
|
||||
props: {
|
||||
topics,
|
||||
topics: topics.map((topic) => topic.value),
|
||||
shows,
|
||||
},
|
||||
revalidate: 10,
|
||||
|
@ -111,6 +111,12 @@ export const GET_ALL_TOPICS_QUERY = gql`
|
||||
groupedBy {
|
||||
value
|
||||
}
|
||||
tags {
|
||||
topOccurrences {
|
||||
value
|
||||
occurs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user