feat: make PostGrid component responsive

This commit is contained in:
Hossein Mehrabi 2023-08-24 08:33:41 +03:30
parent debcfc103b
commit f30f2d1b9e
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
5 changed files with 346 additions and 100 deletions

View File

@ -7,7 +7,7 @@ interface Props {
episodes: LPE.Podcast.Document[]
shows?: LPE.Podcast.Show[]
bordered?: boolean
size?: PostsGridProps['size']
size?: string
cols?: number
displayShow?: boolean
}
@ -24,14 +24,14 @@ export default function EpisodesList({
return (
<EpisodeListContainer>
{header}
<PostsGrid
{/* <PostsGrid
shows={shows}
posts={episodes}
bordered={bordered}
cols={cols}
size={size}
displayPodcastShow={displayShow}
/>
/> */}
</EpisodeListContainer>
)
}

View File

@ -1,8 +1,12 @@
import { css } from '@emotion/react'
/** @jsxImportSource @emotion/react */
import { Breakpoints, Theme, useTheme } from '@acid-info/lsd-react'
import { css, SerializedStyles } 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 { lsdUtils } from '../../utils/lsd.utils'
import { lcm } from '../../utils/math.utils'
import { PostCard, PostCardProps } from '../PostCard'
export type PostsGridProps = Partial<React.ComponentProps<typeof Container>> & {
@ -12,41 +16,134 @@ export type PostsGridProps = Partial<React.ComponentProps<typeof Container>> & {
}
export const PostsGrid: React.FC<PostsGridProps> = ({
cols = 4,
size = 'small',
posts = [],
shows = [],
pattern = [],
breakpoints = [],
bordered = false,
displayPodcastShow = true,
...props
}) => {
const groups = useMemo(() => chunkArray(posts, cols), [posts, cols])
const theme = useTheme()
const items = useMemo(() => {
const cols = pattern.map((p) => p.cols)
const chunked = chunkArray(posts, ...cols)
return chunked
.map((posts, i) =>
posts.map((post) => ({
post,
size: pattern[i % pattern.length]?.size,
})),
)
.flat()
}, [pattern, posts])
const postCardStyles = useMemo(
() => ({
xxsmall: PostCard.styles.xxsmall(theme),
xsmall: PostCard.styles.xsmall(theme),
small: PostCard.styles.small(theme),
medium: PostCard.styles.medium(theme),
large: PostCard.styles.large(theme),
}),
[theme],
)
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}
displayPodcastShow={displayPodcastShow}
data={PostCard.toData(post, shows)}
/>
</div>
))}
</div>
))}
<Container
{...props}
pattern={pattern}
breakpoints={breakpoints}
bordered={bordered}
postCardStyles={postCardStyles}
>
<div className="row">
{items.map(({ post, size }) => (
<div key={post.id} className="post-card-wrapper">
<PostCard
size={size as any}
applySizeStyles={false}
className="post-card"
contentType={post.type}
displayPodcastShow={displayPodcastShow}
data={PostCard.toData(post, shows)}
/>
</div>
))}
</div>
</Container>
)
}
const Container = styled.div<{
type Pattern = {
cols: number
bordered: boolean
size: PostCardProps['size']
}
type Breakpoint = {
pattern: Pattern[]
breakpoint: Breakpoints
}
const createGridStyles = ({
theme,
pattern = [],
postCardStyles,
breakpoint = false,
}: {
theme: Theme
postCardStyles: {
[name: string]: SerializedStyles
}
pattern: Pick<Pattern, 'cols' | 'size'>[]
breakpoint?: boolean
}) => {
const cm = pattern.map((p) => p.cols).reduce(lcm, 1)
const sum = Math.max(
1,
pattern.reduce((p, c) => p + c.cols, 0),
)
let selectorNumber = 0
const selectors = pattern.map((p) => {
const start = selectorNumber + 1
selectorNumber += p.cols
return new Array(p.cols)
.fill(null)
.map((i, index) => `${sum}n + ${start + index}`)
})
return css`
> .row {
display: grid;
grid-template-columns: repeat(${cm}, 1fr);
& > div {
${pattern.map(
(p, i) => `
${selectors[i].map((s) => `&:nth-child(${s})`).join(', ')} {
grid-column: span ${cm / p.cols};
.post-card {
${postCardStyles[p.size as string].styles}
}
}
`,
)}
}
}
`
}
const Container = styled.div<{
bordered: boolean
pattern: Pattern[]
breakpoints: Breakpoint[]
postCardStyles: {
[name: string]: SerializedStyles
}
}>`
display: grid;
gap: 16px 0;
@ -54,7 +151,6 @@ const Container = styled.div<{
${(props) => css`
> .row {
display: grid;
grid-template-columns: repeat(${props.cols}, 1fr);
gap: 0 16px;
& > div {
@ -63,55 +159,115 @@ const Container = styled.div<{
rgb(var(--lsd-border-primary));
}
}
${lsdUtils
.breakpoints(props.breakpoints.map((b) => b.breakpoint))
.map((breakpoint) =>
lsdUtils.breakpoint(
props.theme,
breakpoint,
'exact',
)(css`
${createGridStyles({
theme: props.theme,
pattern: props.pattern,
postCardStyles: props.postCardStyles,
breakpoint: true,
})}
`),
)}
`}
${(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``}
${({ breakpoints = [], theme, postCardStyles }) => {
return breakpoints.map((b) =>
lsdUtils.breakpoint(
theme,
b.breakpoint,
'exact',
)(css`
${createGridStyles({
theme,
pattern: b.pattern,
postCardStyles,
breakpoint: true,
})}
`),
)
}}
`
// ${b.size.map(
// ([originalSize, targetSize]) => `
// .post-card--${originalSize} {
// ${postCardStyles[targetSize as string]?.styles}
// }
// `,
// )}
// @media (max-width: ${props.theme.breakpoints.sm.width - 1}px) {
// }
// @media (min-width: ${props.theme.breakpoints.sm.width}px) {
// ${props.pattern.sm && createGridStyles({ pattern: props.pattern.sm })}
// }
// @media (min-width: ${props.theme.breakpoints.md.width}px) {
// ${props.pattern.md && createGridStyles({ pattern: props.pattern.md })}
// }
// @media (min-width: ${props.theme.breakpoints.lg.width}px) {
// ${props.pattern.lg && createGridStyles({ pattern: props.pattern.lg })}
// }
// styled.div`
// ${(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``} // @media (max-width: ${(
// props,
// ) => props.theme.breakpoints.sm.width})
// `

View File

@ -5,7 +5,6 @@ import { Hero } from '../../components/Hero'
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<
@ -28,29 +27,82 @@ export const HomePage: React.FC<HomePageProps> = ({
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.slice(0, 5), query.posts.slice(5)],
[query.posts],
)
return (
<Root {...props}>
<Hero tags={tags} />
<PostsGrid posts={group1[0]} cols={5} bordered size="xxsmall" />
<PostsGrid posts={group1} pattern={[{ cols: 5, size: 'xxsmall' }]} />
<PostsGrid
posts={highlighted.slice(0, 1)}
cols={1}
bordered
size="large"
posts={highlighted.slice(0, 1)}
pattern={[{ cols: 1, size: 'large' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
]}
/>
{group2.map((group, index) => (
<PostsGrid
bordered
key={index}
posts={group}
cols={index % 2 !== 0 ? 2 : 4}
size={index % 2 !== 0 ? 'medium' : 'small'}
/>
))}
<PostsGrid
pattern={[
{ cols: 4, size: 'small' },
{
cols: 2,
size: 'medium',
},
{
cols: 3,
size: 'small',
},
{
cols: 5,
size: 'medium',
},
]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{
cols: 1,
size: 'small',
},
],
},
{
breakpoint: 'sm',
pattern: [
{
cols: 3,
size: 'small',
},
{
cols: 2,
size: 'medium',
},
],
},
{
breakpoint: 'md',
pattern: [
{
cols: 3,
size: 'small',
},
{
cols: 2,
size: 'medium',
},
],
},
]}
posts={group2}
bordered
/>
{query.hasNextPage && (
<div className="load-more">
<Button

View File

@ -1,4 +1,5 @@
import { ArrowDownIcon, Button, Typography } from '@acid-info/lsd-react'
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import clsx from 'clsx'
import Image from 'next/image'
@ -6,6 +7,7 @@ import Link from 'next/link'
import React from 'react'
import { PostsGrid } from '../../components/PostsGrid'
import { LPE } from '../../types/lpe.types'
import { lsdUtils } from '../../utils/lsd.utils'
export type PodcastShowsPreviewProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
@ -81,18 +83,21 @@ export const PodcastShowsPreview: React.FC<PodcastShowsPreviewProps> = ({
<div className="podcasts__show-episodes">
<PostsGrid
posts={(show.episodes || []).slice(0, 2)}
posts={(show.episodes || []).slice(0, 4)}
displayPodcastShow={false}
shows={shows}
size="xsmall"
cols={2}
/>
<PostsGrid
posts={(show.episodes || []).slice(2, 4)}
displayPodcastShow={false}
shows={shows}
size="xsmall"
cols={2}
pattern={[
{
cols: 2,
size: 'xsmall',
},
]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
]}
/>
</div>
</div>
@ -176,4 +181,34 @@ const Root = styled('div')`
}
}
}
${(props) =>
lsdUtils.breakpoint(
props.theme,
'xs',
'exact',
)(css`
.podcasts__shows {
padding-top: 0;
grid-template-columns: repeat(1, 1fr);
}
.podcasts__show {
border-right: none !important;
padding: 0 !important;
&:not(:first-child) {
border-top: 1px solid rgb(var(--lsd-border-primary));
}
}
.podcasts__show-card {
margin-top: 0;
padding: 24px 0px 16px 0px;
}
.podcasts__show-hosts {
margin-top: 80px;
}
`)}
`

3
src/utils/math.utils.ts Normal file
View File

@ -0,0 +1,3 @@
export const gcd = (a: number, b: number): number => (a ? gcd(b % a, a) : b)
export const lcm = (a: number, b: number): number => (a * b) / gcd(a, b)