Skip to content

Commit

Permalink
Create author page (#492)
Browse files Browse the repository at this point in the history
* Adjust author blog post card and put read time in it

* Add gap between authors in author section from blog post

* Add section one

* Add section two with the grid of blog post cards

* Make author page responsive on mobile devices

* Add author page's title and descirption

* Add author role next to the name in the blog post cards

* Fix author avatar to support more than one avatar using the MUI Avatar and AvatarGroup components

* Use the slug in the authors URL path

* Add section two title

* Create function for generating author path by slug

* Some css code tweaks

* Add a regex to remove html from authors' bio

* The blog post description should be a paragraph, not an h2

* Replace html tag regex with html-to-text lib

---------

Co-authored-by: Breno <breno@estuary.dev>
  • Loading branch information
Brenosalv and Breno committed Sep 17, 2024
1 parent 6cfc60f commit edec23d
Show file tree
Hide file tree
Showing 21 changed files with 920 additions and 345 deletions.
30 changes: 30 additions & 0 deletions gatsby-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GatsbyNode } from 'gatsby';
import { createRemoteFileNode } from 'gatsby-source-filesystem';
import { SUPABASE_CONNECTION_STRING } from './config';
import { normalizeConnector } from './src/utils';
import { getAuthorPathBySlug } from './shared';

/**
* Implement Gatsby's Node APIs in this file.
Expand All @@ -26,6 +27,8 @@ const caseStudyTemplate = path.resolve('./src/layouts/CaseStudy/index.tsx');
const connector = path.resolve('./src/templates/connector/index.tsx');
const connection = path.resolve('./src/templates/connection.tsx');

const authorComponent = path.resolve('./src/templates/author/index.tsx');

export const createPages: GatsbyNode['createPages'] = async ({
graphql,
actions,
Expand Down Expand Up @@ -351,6 +354,33 @@ export const createPages: GatsbyNode['createPages'] = async ({
}
}
}

const authors = await graphql<{
allStrapiAuthor: {
nodes: any[];
};
}>(`
{
allStrapiAuthor {
nodes {
id
slug: Slug
}
}
}
`);

if (authors.data?.allStrapiAuthor.nodes) {
for (const author of authors.data.allStrapiAuthor.nodes) {
createPage({
path: getAuthorPathBySlug(author.slug),
component: authorComponent,
context: {
id: author.id,
},
});
}
}
};

// Hacky hack :(
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"gatsby-transformer-rehype": "^2.0.1",
"gatsby-transformer-remark": "^6.13.1",
"gatsby-transformer-sharp": "^5.13.1",
"html-to-text": "^9.0.5",
"intersection-observer": "^0.12.2",
"lunr": "^2.3.9",
"material-ui-popup-state": "^5.0.5",
Expand Down
3 changes: 3 additions & 0 deletions shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ export const estuaryAddress = {
'postalCode': '10001',
'addressCountry': 'US',
};

export const getAuthorPathBySlug = (slug: string) =>
`/author/${slug.toLowerCase()}`;
122 changes: 122 additions & 0 deletions src/components/AuthorBlogPostCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { Link } from 'gatsby';
import React from 'react';
import { AvatarGroup } from '@mui/material';
import ArrowRight2 from '../../svgs/arrow-right-2.svg';
import Avatar from '../Avatar';
import {
container,
popularArticlesImage,
articleCardHeader,
articleTag,
articleDateAndTime,
articleCardAuthors,
articleCardFooter,
dot,
authorInfo,
} from './styles.module.less';

interface AuthorBlogPostCardProps {
data: {
slug: string;
title: string;
updatedAt: string;
body: {
data: {
childMarkdownRemark: {
fields: { readingTime: { text: string } };
};
};
};
tags: { type: string; name: string }[];
authors: { id: string; name: string; role?: string; picture?: any }[];
hero: { alternativeText: string; localFile: any };
};
}

const AuthorBlogPostCard = ({ data: blogPost }: AuthorBlogPostCardProps) => {
const blogPostImage =
blogPost.hero.localFile?.childImageSharp?.gatsbyImageData;

const blogPostTags = blogPost.tags.filter((tag) => tag.type === 'tag');

const singleAuthorImage =
blogPost.authors[0].picture &&
getImage(
blogPost.authors[0].picture.localFile.childImageSharp
.gatsbyImageData
);

const singleAuthor = blogPost.authors[0];

const readingTime =
blogPost.body.data.childMarkdownRemark.fields.readingTime.text.replace(
'read',
''
);

return (
<Link to={`/${blogPost.slug}`} className={container}>
<GatsbyImage
image={blogPostImage}
alt={blogPost.hero.alternativeText}
className={popularArticlesImage}
/>
<div className={articleCardHeader}>
<span className={articleTag}>{blogPostTags[0]?.name}</span>
<div className={articleDateAndTime}>
<span>{blogPost.updatedAt}</span>
<div className={dot} />
<span>{readingTime}</span>
</div>
</div>
<h3>{blogPost.title}</h3>
<div className={articleCardAuthors}>
{blogPost.authors.length > 1 ? (
<AvatarGroup max={3}>
{blogPost.authors.map((author) => {
const authorImage =
author.picture &&
getImage(
author.picture.localFile.childImageSharp
.gatsbyImageData
);

return (
<Avatar
key={author.id}
image={authorImage}
alt={`Picture of ${author.name}`}
name={author.name}
/>
);
})}
</AvatarGroup>
) : (
<>
<Avatar
image={singleAuthorImage}
alt={`Picture of ${singleAuthor.name}`}
name={singleAuthor.name}
/>
<div className={authorInfo}>
<span>{singleAuthor.name}</span>
{singleAuthor.role ? (
<>
<div className={dot} />
<span>{singleAuthor.role}</span>
</>
) : null}
</div>
</>
)}
</div>
<div className={articleCardFooter}>
<span>Article</span>
<ArrowRight2 />
</div>
</Link>
);
};

export default AuthorBlogPostCard;
123 changes: 123 additions & 0 deletions src/components/AuthorBlogPostCard/styles.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
.container {
display: flex;
flex-direction: column;
padding: 20px;
border: 1px solid #d7dce5;
border-radius: 16px;
transition: var(--default-transition);

@media (max-width: 375px) {
border: none;
padding: 0;
}

&:hover {
border: 1px solid #5072eb;
}

svg {
transform: translateX(-50%);
}

h3 {
font-size: 1.25rem;
line-height: 30px;
font-weight: 600;
color: #47506D;
margin: 16px 0;

@media (min-width: 1049px) {
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
}
}

.popularArticlesImage {
border-radius: 8px;
margin-bottom: 16px;
}

.articleCardHeader {
display: flex;
justify-content: space-between;
gap: 8px;
}

.articleTag {
color: #625eff;
font-size: 1rem;
font-weight: 600;
line-height: 18px;
}

.articleDateAndTime {
font-size: 0.875rem;
line-height: 16.8px;
font-weight: 400;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: right;
column-gap: 8px;
color: #878ea6;
text-align: right;
}

.articleCardAuthors {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 30px;
}

.authorInfo {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;

span {
font-size: 0.875rem;
font-weight: 400;
line-height: 16.8px;
color: #47506d;

}
}

.articleCardFooter {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-top: auto;

@media (max-width: 550px) {
display: none;
}

span {
background-color: #47506d12;
color: #47506d;
border-radius: 36px;
padding: 4px 16px;
font-size: 0.875rem;
font-weight: 500;
line-height: 30px;
}
}

.dot {
width: 1px;
height: 1px;
padding: 2px;
margin: 0;
border-radius: 100%;
background-color: #878ea6;
}
15 changes: 5 additions & 10 deletions src/components/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image';
import * as React from 'react';
import { Avatar as LetterAvatar } from '@mui/material';
import { Container } from './styles';

type Avatar = {
image?: IGatsbyImageData;
alt: string;
name: string;
loading?: 'eager' | 'lazy';
size?: string;
size?: number;
};

const Avatar = ({
image,
alt,
name,
loading = 'eager',
size = '36px',
}: Avatar) => {
const Avatar = ({ image, alt, name, loading = 'eager', size = 40 }: Avatar) => {
return (
<Container $hasImage={!!image} $imgSize={size}>
<Container sx={{ width: size, height: size }}>
{image ? (
<GatsbyImage image={image} alt={alt} loading={loading} />
) : (
<span>{name.charAt(0).toUpperCase()}</span>
<LetterAvatar>{name.charAt(0).toUpperCase()}</LetterAvatar>
)}
</Container>
);
Expand Down
26 changes: 3 additions & 23 deletions src/components/Avatar/styles.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
import styled, { css } from 'styled-components';
import styled from 'styled-components';
import { Avatar } from '@mui/material';

export const Container = styled.div<{ $hasImage: boolean; $imgSize?: string }>`
border-radius: 100%;
export const Container = styled(Avatar)`
overflow: hidden;
background-color: #02a99e;
display: flex;
align-items: center;
justify-content: center;
width: ${({ $imgSize }) => $imgSize};
height: ${({ $imgSize }) => $imgSize};
${({ $hasImage }) =>
!$hasImage &&
css`
padding: 12px;
padding-top: 14px;
span {
font-size: 1rem;
color: #ffffff !important;
}
`}
img {
width: ${({ $imgSize }) => $imgSize};
height: ${({ $imgSize }) => $imgSize};
}
`;
Loading

0 comments on commit edec23d

Please sign in to comment.