Merge pull request #93 from acid-info/responsive-landing-page

WIP: Responsive landing page
This commit is contained in:
jeangovil 2023-08-24 23:51:32 +03:30 committed by GitHub
commit b6dffda933
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1037 additions and 304 deletions

View File

@ -2,34 +2,36 @@ import styled from '@emotion/styled'
import { LPE } from '../../types/lpe.types'
import { PostsGrid, PostsGridProps } from '../PostsGrid'
interface Props {
interface Props {}
export type EpisodesListProps = Partial<
React.ComponentProps<typeof EpisodeListContainer>
> & {
header?: React.ReactNode
episodes: LPE.Podcast.Document[]
shows?: LPE.Podcast.Show[]
bordered?: boolean
size?: PostsGridProps['size']
cols?: number
displayShow?: boolean
}
} & Pick<PostsGridProps, 'pattern' | 'breakpoints' | 'bordered'>
export default function EpisodesList({
shows = [],
episodes = [],
pattern,
breakpoints,
bordered,
header,
episodes,
bordered = false,
cols = 4,
size = 'small',
displayShow = true,
}: Props) {
...props
}: EpisodesListProps) {
return (
<EpisodeListContainer>
<EpisodeListContainer {...props}>
{header}
<PostsGrid
shows={shows}
posts={episodes}
bordered={bordered}
cols={cols}
size={size}
pattern={pattern}
breakpoints={breakpoints}
displayPodcastShow={displayShow}
/>
</EpisodeListContainer>

View File

@ -7,9 +7,15 @@ import PostType = LPE.PostType
export type Props = React.ComponentProps<typeof Container> & {
contentType: PostType
date: Date | null
displayYear?: boolean
}
export const PostCardLabel: FC<Props> = ({ contentType, date, ...props }) => {
export const PostCardLabel: FC<Props> = ({
displayYear = true,
contentType,
date,
...props
}) => {
return (
<Container {...props}>
<Typography variant="body3" genericFontFamily="sans-serif">
@ -23,7 +29,11 @@ export const PostCardLabel: FC<Props> = ({ contentType, date, ...props }) => {
date.toLocaleString('en-GB', {
day: 'numeric',
month: 'long', // TODO: Should be uppercase
year: 'numeric',
...(displayYear
? {
year: 'numeric',
}
: {}),
})}
</Typography>
</Container>

View File

@ -1,20 +1,21 @@
import { LPE } from '@/types/lpe.types'
import { Typography } from '@acid-info/lsd-react'
import { Theme, 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'
import Link from 'next/link'
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'
}
type Size = 'small' | 'medium' | 'large'
export type PostCardShowDetailsProps = React.HTMLAttributes<HTMLAnchorElement> &
Partial<CustomLinkProps> & {
title: string
slug: string
episodeNumber: number
logo?: LPE.Image.Document
podcast: LPE.Podcast.Show
}
// TODO
export const PostCardShowDetails = ({
@ -22,48 +23,154 @@ export const PostCardShowDetails = ({
episodeNumber,
podcast,
size = 'medium',
applySizeStyles = true,
...props
}: PostCardShowDetailsProps) => {
return (
<CustomLink {...props} href={`/podcasts/${slug}`}>
<Container>
<CustomLink
{...props}
href={`/podcasts/${slug}`}
applySizeStyles={applySizeStyles}
className={clsx('show-details', `show-details--${size}`, props.className)}
>
<div className="show-details__container">
{podcast && (
<>
<Logo
<Image
src={podcast?.logo?.url}
width={size === 'medium' ? 38 : 28}
height={size === 'medium' ? 38 : 28}
width={38}
height={38}
alt={podcast.logo.alt}
className="show-details__logo"
/>
<PodcastInfo>
<Typography variant="body2">{podcast.title}</Typography>
{size !== 'small' && (
<Typography variant="body3">{episodeNumber} EP</Typography>
)}
</PodcastInfo>
<div className="show-details__info">
<Typography variant="subtitle2" className="show-details__title">
{podcast.title}
</Typography>
<Typography variant="body3" className="show-details__episodes">
{episodeNumber} EP
</Typography>
</div>
</>
)}
</Container>
</div>
</CustomLink>
)
}
const Container = styled.div`
display: flex;
gap: 8px;
align-items: center;
`
PostCardShowDetails.styles = {
small: (theme: Theme) => css`
.show-details__title {
font-size: 12px !important;
font-weight: 400 !important;
line-height: 16px !important;
}
const PodcastInfo = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`
.show-details__episodes {
display: none !important;
}
const CustomLink = styled(Link)`
.show-details__logo {
width: 24px;
height: 24px;
}
`,
medium: (theme: Theme) => css`
.show-details__episodes {
display: none;
}
.show-details__logo {
width: 28px;
height: 28px;
}
`,
large: (theme: Theme) => css`
.show-details__episodes {
display: block !important;
}
.show-details__logo {
width: 38px;
height: 38px;
}
`,
}
type CustomLinkProps = {
size?: Size
xsSize?: Size
smSize?: Size
mdSize?: Size
lgSize?: Size
applySizeStyles?: boolean
}
const CustomLink = styled(Link)<CustomLinkProps>`
text-decoration: none;
`
const Logo = styled(Image)`
border-radius: 100%;
.show-details {
&__container {
display: flex;
gap: 8px;
align-items: center;
}
&__info {
display: flex;
flex-direction: column;
gap: 2px;
}
&__logo {
border-radius: 100%;
}
}
&.show-details--small {
${(props) =>
props.applySizeStyles && PostCardShowDetails.styles.small(props.theme)}
}
&.show-details--medium {
${(props) =>
props.applySizeStyles && PostCardShowDetails.styles.medium(props.theme)}
}
&.show-details--large {
${(props) =>
props.applySizeStyles && PostCardShowDetails.styles.large(props.theme)}
}
&.show-details {
@media (max-width: ${({ theme }) => theme.breakpoints.sm.width - 1}px) {
${(props) =>
props.xsSize &&
props.applySizeStyles &&
PostCardShowDetails.styles[props.xsSize](props.theme)}
}
@media (min-width: ${({ theme }) => theme.breakpoints.sm.width}px) {
${(props) =>
props.smSize &&
props.applySizeStyles &&
PostCardShowDetails.styles[props.smSize](props.theme)}
}
@media (min-width: ${({ theme }) => theme.breakpoints.md.width}px) {
${(props) =>
props.mdSize &&
props.applySizeStyles &&
PostCardShowDetails.styles[props.mdSize](props.theme)}
}
@media (min-width: ${({ theme }) => theme.breakpoints.lg.width}px) {
${(props) =>
props.lgSize &&
props.applySizeStyles &&
PostCardShowDetails.styles[props.lgSize](props.theme)}
}
}
`

View File

@ -1,21 +1,21 @@
import { Tags } from '@/components/Tags'
import { Typography } from '@acid-info/lsd-react'
import { CommonProps } from '@acid-info/lsd-react/dist/utils/useCommonProps'
import styled from '@emotion/styled'
import Link from 'next/link'
import React from 'react'
import { LPE } from '../../types/lpe.types'
import { Authors } from '../Authors'
import { AuthorsDirection } from '../Authors/Authors'
import { ResponsiveImageProps } from '../ResponsiveImage/ResponsiveImage'
import { PostCardCover } from '@/components/PostCard/PostCard.Cover'
import {
PostCardShowDetails,
PostCardShowDetailsProps,
} from '@/components/PostCard/PostCard.ShowDetails'
import { Tags } from '@/components/Tags'
import { Theme, Typography } from '@acid-info/lsd-react'
import { CommonProps } from '@acid-info/lsd-react/dist/utils/useCommonProps'
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import clsx from 'clsx'
import Link from 'next/link'
import React from 'react'
import { LPE } from '../../types/lpe.types'
import { lsdUtils } from '../../utils/lsd.utils'
import { Authors } from '../Authors'
import { AuthorsDirection } from '../Authors/Authors'
import { ResponsiveImageProps } from '../ResponsiveImage/ResponsiveImage'
import { PostCardLabel } from './PostCard.Label'
export type PostAppearanceProps = {
@ -39,7 +39,9 @@ export type PostCardProps = CommonProps &
data: PostDataProps
contentType: LPE.PostType
size?: 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large'
applySizeStyles?: boolean
displayPodcastShow?: boolean
displayYear?: boolean
}
export const PostCard = (_props: PostCardProps) => {
@ -57,7 +59,9 @@ export const PostCard = (_props: PostCardProps) => {
},
size = 'small',
contentType,
applySizeStyles = true,
displayPodcastShow = true,
displayYear = true,
...props
} = _props
@ -68,7 +72,7 @@ export const PostCard = (_props: PostCardProps) => {
const coverImageElement = coverImage && (
<PostCardCover
className="coverImage"
className="post-card__cover-image"
href={link}
imageProps={imageProps}
imageData={coverImage}
@ -76,24 +80,21 @@ export const PostCard = (_props: PostCardProps) => {
)
const labelElement = (
<PostCardLabel className="label" contentType={contentType} date={date} />
<PostCardLabel
className="post-card__label"
contentType={contentType}
displayYear={displayYear}
date={date}
/>
)
const titleElement = (
<Link href={link} className="titleLink">
<Link href={link} className="post-card__title">
<Typography
className="title"
genericFontFamily="serif"
variant={'h3'}
component="h3"
variant={
size === 'xxsmall'
? 'h6'
: size === 'xsmall'
? 'body3'
: size === 'small'
? 'h4'
: 'h2'
}
genericFontFamily="serif"
className="post-card__title-text"
>
{title}
</Typography>
@ -102,7 +103,7 @@ export const PostCard = (_props: PostCardProps) => {
const subtitleElement = subtitle && (
<Typography
className="subtitle"
className="post-card__subtitle"
variant={'body1'}
genericFontFamily="sans-serif"
>
@ -112,7 +113,7 @@ export const PostCard = (_props: PostCardProps) => {
const authorsElement = authors && authors.length > 0 && (
<Authors
className="authors"
className="post-card__authors"
authors={authors}
email={false}
flexDirection={AuthorsDirection.ROW}
@ -123,38 +124,30 @@ export const PostCard = (_props: PostCardProps) => {
const showElement = displayPodcastShow && podcastShowDetails && (
<PostCardShowDetails
{...podcastShowDetails}
size={size === 'large' ? 'medium' : 'small'}
className="showDetails"
className="post-card__show-details"
applySizeStyles={false}
size={'small'}
/>
)
const tagsElement = tags.length > 0 && <Tags className="tags" tags={tags} />
const tagsElement = tags.length > 0 && (
<Tags className="post-card__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
className={clsx(
'post-card',
applySizeStyles && applySizeStyles && `post-card--${size}`,
)}
>
{coverImageElement}
{labelElement}
{titleElement}
{subtitleElement}
{showElement}
{authorsElement}
{tagsElement}
</Container>
)
}
@ -193,115 +186,239 @@ PostCard.toData = (post: LPE.Post.Document, shows: LPE.Podcast.Show[] = []) => {
}
}
PostCard.styles = {
xxsmall: (theme: Theme) => css`
height: 100%;
.post-card__title-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-height: calc(2 * var(--lsd-h6-lineHeight));
${lsdUtils.typography('h6')}
}
.post-card__subtitle {
display: none;
}
.post-card__cover-image {
display: none;
}
.post-card__tags {
display: none;
}
.post-card__authors,
.post-card__show-details {
flex-grow: 1;
display: flex;
align-items: flex-end;
}
.post-card__show-details {
${PostCardShowDetails.styles.small(theme)}
}
${lsdUtils.breakpoint(theme, 'sm', 'exact')} {
.post-card__authors {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px 0;
> div {
> span {
display: none;
}
}
}
}
${lsdUtils.breakpoint(theme, 'md', 'down')} {
.post-card__title-text {
${lsdUtils.typography('subtitle1', true)}
max-height: calc(2 * var(--lsd-subtitle1-lineHeight));
}
}
`,
xsmall: (theme: Theme) => css`
.post-card__title-text {
${lsdUtils.typography('h6')}
}
`,
small: (theme: Theme) => css`
.post-card__title-text {
${lsdUtils.typography('h4')}
}
.post-card__subtitle {
${lsdUtils.typography('subtitle2')}
}
.post-card__show-details {
${PostCardShowDetails.styles.large(theme)}
}
${lsdUtils.breakpoint(theme, 'md', 'down')} {
.post-card__title-text {
${lsdUtils.typography('h5')}
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
max-height: calc(3 * var(--lsd-h5-lineHeight));
}
}
${lsdUtils.breakpoint(theme, 'xs', 'exact')} {
.post-card__show-details {
${PostCardShowDetails.styles.small(theme)}
}
}
`,
medium: (theme: Theme) => css`
.post-card__title-text {
${lsdUtils.typography('h2')}
}
.post-card__subtitle {
${lsdUtils.typography('subtitle2')}
}
.post-card__show-details {
${PostCardShowDetails.styles.large(theme)}
}
${lsdUtils.breakpoint(theme, 'md', 'down')} {
.post-card__title-text {
${lsdUtils.typography('h3')}
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
max-height: calc(3 * var(--lsd-h3-lineHeight));
}
}
`,
large: (theme: Theme) => css`
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-areas:
'info image'
'info image'
'info image'
'info image'
'info image'
'info image'
'. image';
gap: 16px 105px;
.post-card__title-text {
${lsdUtils.typography('h2')}
}
.postcard__subtitle {
${lsdUtils.typography('subtitle2')}
}
.post-card__cover-image {
grid-area: image;
}
.post-card__label {
grid-area: info;
grid-row: auto;
}
.post-card__title {
grid-area: info;
grid-row: auto;
}
.post-card__authors,
.post-card__show-details {
grid-area: info;
grid-row: auto;
}
.post-card__tags {
grid-area: info;
grid-row: auto;
}
.post-card__show-details {
${PostCardShowDetails.styles.large(theme)}
}
${lsdUtils.breakpoint(theme, 'sm', 'exact')} {
gap: 16px 16px;
}
${lsdUtils.breakpoint(theme, 'md', 'exact')} {
gap: 16px 100px;
}
${lsdUtils.breakpoint(theme, 'md', 'down')} {
.post-card__title-text {
${lsdUtils.typography('h3')}
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
max-height: calc(3 * var(--lsd-h3-lineHeight));
}
}
${lsdUtils.breakpoint(theme, 'xs', 'exact')} {
.post-card__title-text {
${lsdUtils.typography('h5')}
}
}
`,
}
const Container = styled.div<Pick<PostCardProps, 'size'>>`
display: flex;
flex-direction: column;
position: 'relative';
gap: 16px;
gap: 16px 0;
.label {
.post-card__label {
margin-bottom: -8px;
}
.titleLink {
.post-card__title {
text-decoration: none;
width: fit-content;
}
.title,
.subtitle {
.post-card__title-text,
.post-card__subtitle {
text-overflow: ellipsis;
overflow: hidden;
word-break: break-word;
}
.title {
@media (max-width: 768px) {
font-size: 28px;
line-height: 36px;
}
&.post-card--xxsmall {
${({ theme }) => PostCard.styles.xxsmall(theme)}
}
.subtitle {
@media (max-width: 768px) {
font-size: 14px;
line-height: 20px;
}
&.post-card--xsmall {
${({ theme }) => PostCard.styles.xsmall(theme)}
}
${({ size }) =>
size === 'xxsmall' &&
css`
.label {
}
&.post-card--small {
${({ theme }) => PostCard.styles.small(theme)}
}
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-height: calc(2 * var(--lsd-h6-lineHeight));
}
&.post-card--medium {
${({ theme }) => PostCard.styles.medium(theme)}
}
.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;
}
`}
&.post-card--large {
${({ theme }) => PostCard.styles.large(theme)}
}
`

View File

@ -1,52 +1,283 @@
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>> & {
shows?: LPE.Podcast.Show[]
posts?: LPE.Post.Document[]
displayPodcastShow?: boolean
displayYear?: boolean
}
export const PostsGrid: React.FC<PostsGridProps> = ({
cols = 4,
size = 'small',
posts = [],
shows = [],
pattern = [],
breakpoints = [],
bordered = false,
horizontal = false,
displayPodcastShow = true,
displayYear = 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}
horizontal={horizontal}
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}
displayYear={displayYear}
displayPodcastShow={displayPodcastShow}
data={PostCard.toData(post, shows)}
/>
</div>
))}
</div>
</Container>
)
}
const Container = styled.div<{
type Pattern = {
cols: number
bordered: boolean
maxWidth?: string
size: PostCardProps['size']
rowBorder?: boolean | 'except-first-row'
}
type Breakpoint = {
pattern: Pattern[]
breakpoint: Breakpoints
}
const createGridStyles = ({
theme,
pattern = [],
postCardStyles,
breakpoint = false,
horizontal = false,
bordered = false,
}: {
theme: Theme
postCardStyles: {
[name: string]: SerializedStyles
}
pattern: Pick<Pattern, 'cols' | 'size' | 'maxWidth' | 'rowBorder'>[]
breakpoint?: boolean
horizontal?: boolean
bordered: boolean | 'except-first-row'
}) => {
const grid = !horizontal
if (grid) {
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}`)
})
const firstRow = new Array(pattern?.[0]?.cols ?? 0)
.fill(null)
.map((v, i) => i + 1)
.map((i) => `&:nth-child(${i})`)
.join(', ')
return css`
> .row {
display: grid;
grid-template-columns: repeat(${cm}, 1fr);
overflow: hidden;
& > div {
${bordered &&
css`
border-top: 1px solid rgb(var(--lsd-border-primary));
`}
${bordered === 'except-first-row' &&
css`
${firstRow} {
border-top: none;
}
`}
${pattern.map((p, i) => {
const firstRow = new Array(p.cols)
.fill(null)
.map((v, i) => i + 1)
.map((i) => `&:nth-child(${i})`)
.join(', ')
const rowSelectors = selectors[i].map((s) => `&:nth-child(${s})`)
const firstSelector = rowSelectors[0]
const lastSelector = rowSelectors[rowSelectors.length - 1]
return css`
${rowSelectors.join(', ')} {
grid-column: span ${cm / p.cols};
position: relative;
${p.rowBorder &&
bordered &&
css`
border-top: none;
`}
${p.rowBorder &&
css`
${firstSelector} {
&::before {
width: calc(100% * ${p.cols} + 16px);
height: 1px;
content: ' ';
top: 0;
left: 0px;
position: absolute;
display: block;
background: rgb(var(--lsd-border-primary));
}
}
`}
${p.rowBorder === 'except-first-row' &&
css`
${firstRow} {
&::before {
display: none !important;
}
}
`}
.post-card {
--post-card-size: ${p.size};
${postCardStyles[p.size as string].styles}
}
}
`
})}
}
}
`
} else {
return css`
overflow: hidden;
> .row {
display: flex;
flex-direction: row;
flex-wrap: unwrap;
justify-content: flex-start;
width: 100%;
overflow: scroll;
scroll-snap-type: x mandatory;
gap: 0 32px;
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
width: 0;
display: none;
}
/* Firefox, Edge and IE */
-ms-overflow-style: none;
scrollbar-width: none;
& > div {
${pattern.map(
(p, i) => css`
max-width: ${p.maxWidth ? p.maxWidth : 'unset'};
flex-grow: 1 auto;
flex-shrink: 0;
width: calc((100% - (${p.cols - 1} * 32px)) / ${p.cols});
flex-basis: calc((100% - (${p.cols - 1} * 32px)) / ${p.cols});
scroll-snap-align: start !important;
position: relative;
.post-card {
--post-card-size: ${p.size};
${postCardStyles[p.size as string].styles}
}
&:not(:last-child) {
&::after {
width: 1px;
height: calc(100% - 48px);
content: ' ';
right: -16px;
top: 24px;
background: rgb(var(--lsd-border-primary));
position: absolute;
}
}
`,
)}
}
}
`
}
}
const Container = styled.div<{
bordered: boolean | 'except-first-row'
horizontal?: boolean
pattern: Pattern[]
breakpoints: Breakpoint[]
postCardStyles: {
[name: string]: SerializedStyles
}
}>`
display: grid;
gap: 16px 0;
@ -54,64 +285,49 @@ const Container = styled.div<{
${(props) => css`
> .row {
display: grid;
grid-template-columns: repeat(${props.cols}, 1fr);
gap: 0 16px;
& > div {
padding: 24px 0;
border-top: ${props.bordered ? '1px' : '0'} solid
rgb(var(--lsd-border-primary));
}
}
${lsdUtils
.breakpoints(props.breakpoints.map((b) => b.breakpoint))
.map((breakpoint) =>
lsdUtils.responsive(
props.theme,
breakpoint,
'exact',
)(css`
${createGridStyles({
theme: props.theme,
horizontal: props.horizontal,
pattern: props.pattern,
postCardStyles: props.postCardStyles,
breakpoint: true,
bordered: props.bordered,
})}
`),
)}
`}
${(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, horizontal, bordered }) => {
return breakpoints.map((b) =>
lsdUtils.responsive(
theme,
b.breakpoint,
'exact',
)(css`
${createGridStyles({
theme,
horizontal,
pattern: b.pattern,
postCardStyles,
breakpoint: true,
bordered,
})}
`),
)
}}
`

View File

@ -5,7 +5,7 @@ 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 { lsdUtils } from '../../utils/lsd.utils'
import { PodcastShowsPreview } from '../PodcastShowsPreview'
export type HomePageProps = React.DetailedHTMLProps<
@ -28,29 +28,93 @@ 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={highlighted.slice(0, 1)}
cols={1}
bordered
size="large"
posts={group1}
horizontal
displayYear={false}
pattern={[{ cols: 5, size: 'xxsmall' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1.5, size: 'xxsmall', maxWidth: '192px' }],
},
{
breakpoint: 'sm',
pattern: [{ cols: 4, size: 'xxsmall' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 4, size: 'xxsmall' }],
},
]}
/>
{group2.map((group, index) => (
<PostsGrid
bordered
key={index}
posts={group}
cols={index % 2 !== 0 ? 2 : 4}
size={index % 2 !== 0 ? 'medium' : 'small'}
/>
))}
<PostsGrid
bordered
posts={highlighted.slice(0, 1)}
pattern={[{ cols: 1, size: 'large' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
]}
/>
<PostsGrid
pattern={[
{ cols: 4, size: 'small' },
{
cols: 2,
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
@ -82,6 +146,18 @@ const Root = styled('div')`
button {
width: 340px;
}
${(props) => lsdUtils.breakpoint(props.theme, 'md', 'down')} {
button {
width: 236px;
}
}
${(props) => lsdUtils.breakpoint(props.theme, 'xs', 'exact')} {
button {
width: 100%;
}
}
}
.podcasts {

View File

@ -47,6 +47,9 @@ export const useLSDTheme = () => {
lg: {
width: 1280,
},
xl: {
width: 1440,
},
},
palette: {},
typography: {},

View File

@ -29,21 +29,58 @@ const PodcastShowContainer = (props: Props) => {
<PodcastShowCard show={show} />
<PodcastSection>
<EpisodesList
cols={2}
size="medium"
shows={[show]}
displayShow={false}
episodes={highlightedEpisodes}
header={<Typography variant="body2">All episodes</Typography>}
bordered="except-first-row"
pattern={[
{
cols: 2,
size: 'medium',
},
]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{
cols: 1,
size: 'small',
rowBorder: 'except-first-row',
},
],
},
]}
/>
</PodcastSection>
<EpisodesList
cols={4}
bordered
size="small"
shows={[show]}
displayShow={false}
episodes={query.posts}
bordered={
highlightedEpisodes.length > 0 ? true : 'except-first-row'
}
pattern={[
{
cols: 4,
size: 'small',
},
]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'sm',
pattern: [{ cols: 4, size: 'xsmall' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 4, size: 'xsmall' }],
},
]}
/>
</PodcastsBodyContainer>
</PodcastsGrid>

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,40 @@ 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',
},
{
cols: 2,
size: 'xsmall',
rowBorder: true,
},
]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small', rowBorder: true }],
},
{
breakpoint: 'sm',
pattern: [
{ cols: 2, size: 'small' },
{ cols: 2, size: 'small', rowBorder: true },
],
},
{
breakpoint: 'md',
pattern: [
{ cols: 2, size: 'small' },
{ cols: 2, size: 'small', rowBorder: true },
],
},
]}
/>
</div>
</div>
@ -176,4 +200,34 @@ const Root = styled('div')`
}
}
}
${(props) =>
lsdUtils.responsive(
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;
}
`)}
`

View File

@ -23,16 +23,45 @@ const PodcastsContainer = (props: Props) => {
<PodcastSection>
<EpisodesList
cols={2}
size="medium"
episodes={highlightedEpisodes.slice(0, 2)}
bordered="except-first-row"
header={<EpisodeListHeader>Latest Episodes</EpisodeListHeader>}
pattern={[{ cols: 2, size: 'medium' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [
{ cols: 1, size: 'small', rowBorder: 'except-first-row' },
],
},
{
breakpoint: 'sm',
pattern: [{ cols: 2, size: 'small' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 2, size: 'small' }],
},
]}
/>
<EpisodesList
cols={4}
size="small"
bordered
episodes={highlightedEpisodes.slice(2)}
pattern={[{ cols: 4, size: 'small' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small', rowBorder: false }],
},
{
breakpoint: 'sm',
pattern: [{ cols: 2, size: 'small' }],
},
{
breakpoint: 'md',
pattern: [{ cols: 2, size: 'small' }],
},
]}
/>
</PodcastSection>
@ -56,8 +85,20 @@ const PodcastsContainer = (props: Props) => {
</EpisodeListHeader>
}
shows={[show]}
bordered="except-first-row"
displayShow={false}
episodes={show.episodes as LPE.Podcast.Document[]}
episodes={(show.episodes as LPE.Podcast.Document[]).slice(0, 4)}
pattern={[{ cols: 4, size: 'small' }]}
breakpoints={[
{
breakpoint: 'xs',
pattern: [{ cols: 1, size: 'small' }],
},
{
breakpoint: 'sm',
pattern: [{ cols: 2, size: 'small' }],
},
]}
/>
</PodcastSection>
))}

View File

@ -4,6 +4,8 @@ export const chunkArray = <T>(arr: T[], ...pattern: number[]): T[][] => {
let index = 0
let iteration = 0
if (pattern.length === 0) return [arr]
while (index < arr.length) {
const take = pattern[iteration % pattern.length]
const elements = arr.slice(index, index + take)

65
src/utils/lsd.utils.ts Normal file
View File

@ -0,0 +1,65 @@
import {
Breakpoints,
Theme,
TypographyVariants,
THEME_BREAKPOINTS,
} from '@acid-info/lsd-react'
import { css, SerializedStyles } from '@emotion/react'
export class LsdUtils {
breakpoints = (exclude: Breakpoints[] = []) =>
THEME_BREAKPOINTS.filter((b) => !exclude.find((b2) => b2 === b))
typography = (variant: TypographyVariants | 'subtitle3', important = false) =>
variant === 'subtitle3'
? `
font-size: 12px !important;
font-weight: 400 !important;
line-height: 16px !important;
`
: `
font-size: var(--lsd-${variant}-fontSize)${important ? '!important' : ''};
font-weight: var(--lsd-${variant}-fontWeight)${
important ? '!important' : ''
};
line-height: var(--lsd-${variant}-lineHeight)${
important ? '!important' : ''
};
`
breakpoint = (
theme: Theme,
breakpoint: Breakpoints,
func: 'exact' | 'up' | 'down' = 'up',
) => {
const width = theme.breakpoints[breakpoint].width
const idx = THEME_BREAKPOINTS.findIndex((b) => b === breakpoint)
const next = theme.breakpoints[THEME_BREAKPOINTS[idx + 1]]
const min = width
const max = next?.width ? next.width - 1 : Number.MAX_SAFE_INTEGER
let media = `@media `
if (func === 'up') {
media += `(min-width: ${min}px)`
} else if (func === 'down') media += `(max-width: ${max}px)`
else media += `(min-width: ${min}px) and (max-width: ${max}px)`
return `${media}`
}
responsive = (
theme: Theme,
breakpoint: Breakpoints,
func: 'exact' | 'up' | 'down' = 'up',
) => {
const media = lsdUtils.breakpoint(theme, breakpoint, func)
return (styles: SerializedStyles) => css`
${media} {
${styles}
}
`
}
}
export const lsdUtils = new LsdUtils()

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)