mirror of
https://github.com/kremalicious/blog.git
synced 2025-01-03 18:35:07 +01:00
Merge pull request #63 from kremalicious/feature/related-posts
related posts based on tags & pagination
This commit is contained in:
commit
0497764f2a
24
README.md
24
README.md
@ -16,16 +16,38 @@
|
|||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
- [🎉 Features](#-features)
|
||||||
|
- [🎆 EXIF extraction](#-exif-extraction)
|
||||||
|
- [🕸 Related Posts](#-related-posts)
|
||||||
|
- [🏆 SEO component](#-seo-component)
|
||||||
|
- [📈 Matomo (formerly Piwik) analytics tracking](#-matomo-formerly-piwik-analytics-tracking)
|
||||||
|
- [gatsby-redirect-from](#-gatsby-redirect-from)
|
||||||
|
- [💎 Importing SVG assets](#-importing-svg-assets)
|
||||||
|
- [🍬 Typekit component](#-typekit-component)
|
||||||
|
- [✨ Development](#-development)
|
||||||
|
- [🔮 Linting](#-linting)
|
||||||
|
- [🎈 Add a new project](#-add-a-new-project)
|
||||||
|
- [🚚 Deployment](#-deployment)
|
||||||
|
- [🏛 Licenses](#-licenses)
|
||||||
|
- [Posts](#-posts)
|
||||||
|
- [Photos & images](#-photos-images)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎉 Features
|
## 🎉 Features
|
||||||
|
|
||||||
The whole [blog](https://kremalicious.com) is a React-based Single Page App built with [Gatsby v2](https://www.gatsbyjs.org).
|
The whole [blog](https://kremalicious.com) is a React-based Single Page App built with [Gatsby v2](https://www.gatsbyjs.org).
|
||||||
|
|
||||||
### EXIF extraction
|
### 🎆 EXIF extraction
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
### 🕸 Related Posts
|
||||||
|
|
||||||
|
Under each post a list of related posts is displayed which are based on the tags of the currently viewed post. Also allows loading more related posts in place.
|
||||||
|
|
||||||
|
If you want to know how, have a look at the respective component under [`src/components/molecules/Pagination.jsx`](src/components/molecules/Pagination.jsx)
|
||||||
|
|
||||||
### 🏆 SEO component
|
### 🏆 SEO component
|
||||||
|
|
||||||
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.
|
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.
|
||||||
|
@ -10,10 +10,10 @@ updated: 2018-07-11 00:52:46+02:00
|
|||||||
featured: true
|
featured: true
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- goodies
|
|
||||||
- tutorial
|
- tutorial
|
||||||
- tor
|
- tor
|
||||||
- macos
|
- macos
|
||||||
|
- goodies
|
||||||
|
|
||||||
coinhive: true
|
coinhive: true
|
||||||
---
|
---
|
||||||
|
@ -2,7 +2,6 @@ const path = require('path')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const yaml = require('js-yaml')
|
const yaml = require('js-yaml')
|
||||||
const { createFilePath } = require('gatsby-source-filesystem')
|
const { createFilePath } = require('gatsby-source-filesystem')
|
||||||
const { paginate } = require('gatsby-awesome-pagination')
|
|
||||||
const fastExif = require('fast-exif')
|
const fastExif = require('fast-exif')
|
||||||
const Fraction = require('fraction.js')
|
const Fraction = require('fraction.js')
|
||||||
const dms2dec = require('dms2dec')
|
const dms2dec = require('dms2dec')
|
||||||
@ -172,12 +171,13 @@ exports.createPages = ({ graphql, actions }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const posts = result.data.allMarkdownRemark.edges
|
const posts = result.data.allMarkdownRemark.edges
|
||||||
|
const numPages = Math.ceil(posts.length / itemsPerPage)
|
||||||
|
|
||||||
// Generate posts & posts index
|
// Generate posts & posts index
|
||||||
generateContent(createPage, posts)
|
generateContent(createPage, posts, numPages)
|
||||||
|
|
||||||
// Generate Tag Pages
|
// Generate Tag Pages
|
||||||
generateTagPages(createPage, posts)
|
generateTagPages(createPage, posts, numPages)
|
||||||
|
|
||||||
// create manual redirects
|
// create manual redirects
|
||||||
redirects.forEach(({ f, t }) => {
|
redirects.forEach(({ f, t }) => {
|
||||||
@ -196,7 +196,7 @@ exports.createPages = ({ graphql, actions }) => {
|
|||||||
|
|
||||||
const postsTemplate = path.resolve('src/templates/Posts.jsx')
|
const postsTemplate = path.resolve('src/templates/Posts.jsx')
|
||||||
|
|
||||||
const generateContent = (createPage, posts) => {
|
const generateContent = (createPage, posts, numPages) => {
|
||||||
const postTemplate = path.resolve('src/templates/Post.jsx')
|
const postTemplate = path.resolve('src/templates/Post.jsx')
|
||||||
|
|
||||||
// Create Post pages
|
// Create Post pages
|
||||||
@ -210,69 +210,49 @@ const generateContent = (createPage, posts) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create paginated front page
|
// Create paginated Blog index pages
|
||||||
paginate({
|
Array.from({ length: numPages }).forEach((_, i) => {
|
||||||
createPage,
|
createPage({
|
||||||
items: posts,
|
path: i === 0 ? '/' : `/page/${i + 1}`,
|
||||||
itemsPerPage: itemsPerPage,
|
component: postsTemplate,
|
||||||
pathPrefix: '/',
|
context: {
|
||||||
component: postsTemplate
|
limit: itemsPerPage,
|
||||||
|
skip: i * itemsPerPage,
|
||||||
|
numPages,
|
||||||
|
currentPageNumber: i + 1,
|
||||||
|
prevPage: i - 1,
|
||||||
|
nextPage: i + 2
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateTagPages = (createPage, posts) => {
|
const generateTagPages = (createPage, posts) => {
|
||||||
const tagSet = new Set()
|
const tagList = arrayReducer(posts, 'tags')
|
||||||
const tagMap = new Map()
|
|
||||||
|
|
||||||
posts.forEach(post => {
|
|
||||||
if (post.node.frontmatter.tags) {
|
|
||||||
post.node.frontmatter.tags.forEach(tag => {
|
|
||||||
tagSet.add(tag)
|
|
||||||
|
|
||||||
const array = tagMap.has(tag) ? tagMap.get(tag) : []
|
|
||||||
array.push(post)
|
|
||||||
tagMap.set(tag, array)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const tagList = Array.from(tagSet)
|
|
||||||
|
|
||||||
tagList.forEach(tag => {
|
tagList.forEach(tag => {
|
||||||
if (tag === 'goodies') return
|
if (tag === 'goodies') return
|
||||||
|
|
||||||
// Create tag pages
|
// Create tag pages
|
||||||
createPage({
|
createPage({
|
||||||
path: `/tag/${tag}/`,
|
path: `/tags/${tag}/`,
|
||||||
component: postsTemplate,
|
component: postsTemplate,
|
||||||
context: { tag }
|
context: { tag }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Object.keys(posts).forEach(tagName => {
|
|
||||||
// const pageSize = 5
|
|
||||||
// const pagesSum = Math.ceil(posts[tagName].length / pageSize)
|
|
||||||
|
|
||||||
// for (let page = 1; page <= pagesSum; page++) {
|
|
||||||
// createPage({
|
|
||||||
// path:
|
|
||||||
// page === 1
|
|
||||||
// ? `/tag/${tagName.toLowerCase()}`
|
|
||||||
// : `/tag/${tagName.toLowerCase()}/page/${page}`,
|
|
||||||
// component: postsTemplate,
|
|
||||||
// context: {
|
|
||||||
// posts: paginate(posts[tagName], pageSize, page),
|
|
||||||
// tag: tagName,
|
|
||||||
// pagesSum,
|
|
||||||
// page
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function paginate(array, page_size, page_number) {
|
// https://www.adamjberkowitz.com/tags-and-categories-in-gatsby-js/
|
||||||
// return array
|
const arrayReducer = (postsArray, type) => {
|
||||||
// .slice(0)
|
return (postsArray = postsArray
|
||||||
// .slice((page_number - 1) * page_size, page_number * page_size)
|
.map(({ node }) => {
|
||||||
// }
|
return node.frontmatter[type]
|
||||||
|
})
|
||||||
|
.reduce((a, b) => {
|
||||||
|
return a.concat(b)
|
||||||
|
}, [])
|
||||||
|
.filter((type, index, array) => {
|
||||||
|
return array.indexOf(type) === index
|
||||||
|
})
|
||||||
|
.sort())
|
||||||
|
}
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
"fast-exif": "^1.0.1",
|
"fast-exif": "^1.0.1",
|
||||||
"fraction.js": "^4.0.9",
|
"fraction.js": "^4.0.9",
|
||||||
"gatsby": "^2.0.12",
|
"gatsby": "^2.0.12",
|
||||||
"gatsby-awesome-pagination": "^0.3.1",
|
|
||||||
"gatsby-image": "^2.0.12",
|
"gatsby-image": "^2.0.12",
|
||||||
"gatsby-plugin-catch-links": "^2.0.3",
|
"gatsby-plugin-catch-links": "^2.0.3",
|
||||||
"gatsby-plugin-favicon": "^3.1.4",
|
"gatsby-plugin-favicon": "^3.1.4",
|
||||||
|
@ -29,4 +29,12 @@ export const imageSizeDefault = graphql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const imageSizeThumb = graphql`
|
||||||
|
fragment ImageFluidThumb on ImageSharp {
|
||||||
|
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
|
||||||
|
...GatsbyImageSharpFluid_withWebp_noBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default Image
|
export default Image
|
||||||
|
@ -27,15 +27,10 @@
|
|||||||
@include ellipsis();
|
@include ellipsis();
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font-family: $font-family-base;
|
font-family: $font-family-base;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
padding: ($spacer/4) 0;
|
padding: ($spacer/4) 0;
|
||||||
max-width: 70%;
|
margin-top: -($spacer);
|
||||||
margin: -($spacer) auto $spacer auto;
|
margin-bottom: $spacer;
|
||||||
|
|
||||||
@media (min-width: $screen-sm) {
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,7 @@ const query = graphql`
|
|||||||
title
|
title
|
||||||
image {
|
image {
|
||||||
childImageSharp {
|
childImageSharp {
|
||||||
fluid(maxWidth: 300, maxHeight: 130, cropFocus: CENTER) {
|
...ImageFluidThumb
|
||||||
...GatsbyImageSharpFluid_withWebp_noBase64
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,25 +3,61 @@ import PropTypes from 'prop-types'
|
|||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import styles from './Pagination.module.scss'
|
import styles from './Pagination.module.scss'
|
||||||
|
|
||||||
const Pagination = ({ pageContext }) => {
|
const PageNumber = ({ i, current }) => (
|
||||||
const { previousPagePath, nextPagePath } = pageContext
|
<Link
|
||||||
|
className={current ? styles.current : styles.number}
|
||||||
|
to={i === 0 ? '' : `/page/${i + 1}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
PageNumber.propTypes = {
|
||||||
|
i: PropTypes.number.isRequired,
|
||||||
|
current: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrevNext = ({ prevPagePath, nextPagePath }) => {
|
||||||
|
const link = prevPagePath || nextPagePath
|
||||||
|
const rel = prevPagePath ? 'prev' : 'next'
|
||||||
|
const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pagination}>
|
<Link to={link} rel={rel} title={title}>
|
||||||
{nextPagePath ? (
|
{prevPagePath ? '←' : '→'}
|
||||||
<Link className={styles.paginationLink} to={nextPagePath}>
|
</Link>
|
||||||
« Older Posts
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
{previousPagePath ? (
|
|
||||||
<Link className={styles.paginationLink} to={previousPagePath}>
|
|
||||||
Newer Posts »
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrevNext.propTypes = {
|
||||||
|
prevPagePath: PropTypes.string,
|
||||||
|
nextPagePath: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination = ({ pageContext }) => {
|
||||||
|
const { currentPageNumber, numPages, prevPage, nextPage } = pageContext
|
||||||
|
const isFirst = currentPageNumber === 1
|
||||||
|
const isLast = currentPageNumber === numPages
|
||||||
|
const prevPagePath = currentPageNumber === 2 ? '/' : `/page/${prevPage}`
|
||||||
|
const nextPagePath = `/page/${nextPage}`
|
||||||
|
|
||||||
|
return nextPage > 1 ? (
|
||||||
|
<div className={styles.pagination}>
|
||||||
|
<div>{!isFirst && <PrevNext prevPagePath={prevPagePath} />}</div>
|
||||||
|
<div>
|
||||||
|
{Array.from({ length: numPages }, (_, i) => (
|
||||||
|
<PageNumber
|
||||||
|
key={`pagination-number${i + 1}`}
|
||||||
|
i={i}
|
||||||
|
current={currentPageNumber === i + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>{!isLast && <PrevNext nextPagePath={nextPagePath} />}</div>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
Pagination.propTypes = {
|
Pagination.propTypes = {
|
||||||
pageContext: PropTypes.object.isRequired
|
pageContext: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,41 @@
|
|||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: $spacer * 2;
|
margin-top: $spacer * 2;
|
||||||
margin-bottom: $spacer * 2;
|
margin-bottom: $spacer;
|
||||||
}
|
justify-content: center;
|
||||||
|
|
||||||
.paginationLink {
|
> div {
|
||||||
flex: 1 1 50%;
|
&:first-child {
|
||||||
display: block;
|
margin-right: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
text-align: right;
|
margin-left: $spacer;
|
||||||
}
|
text-align: right;
|
||||||
|
}
|
||||||
&:only-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
text-align: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: rgba(255, 255, 255, .3);
|
||||||
|
border-color: darken($brand-grey-dimmed, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
composes: number;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 1px solid darken($brand-grey-dimmed, 5%);
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
||||||
|
@ -3,17 +3,14 @@ import PropTypes from 'prop-types'
|
|||||||
import { Link } from 'gatsby'
|
import { Link } from 'gatsby'
|
||||||
import Time from 'react-time'
|
import Time from 'react-time'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
import PostLinkActions from '../atoms/PostLinkActions'
|
|
||||||
import styles from './PostMeta.module.scss'
|
import styles from './PostMeta.module.scss'
|
||||||
|
|
||||||
const PostMeta = ({ post, meta }) => {
|
const PostMeta = ({ post, meta }) => {
|
||||||
const { author, updated, tags, type, linkurl } = post.frontmatter
|
const { author, updated, tags, type } = post.frontmatter
|
||||||
const { date, slug } = post.fields
|
const { date } = post.fields
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={styles.entryMeta}>
|
<footer className={styles.entryMeta}>
|
||||||
{type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />}
|
|
||||||
|
|
||||||
<div className={styles.byline}>
|
<div className={styles.byline}>
|
||||||
<span className={styles.by}>by</span>
|
<span className={styles.by}>by</span>
|
||||||
<a className="fn" rel="author" href={meta.author.uri}>
|
<a className="fn" rel="author" href={meta.author.uri}>
|
||||||
@ -45,7 +42,7 @@ const PostMeta = ({ post, meta }) => {
|
|||||||
{tags && (
|
{tags && (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
{tags.map(tag => {
|
{tags.map(tag => {
|
||||||
const to = tag === 'goodies' ? '/goodies' : `/tag/${slugify(tag)}/`
|
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={tag} className={styles.tag} to={to}>
|
<Link key={tag} className={styles.tag} to={to}>
|
||||||
|
120
src/components/molecules/RelatedPosts.jsx
Normal file
120
src/components/molecules/RelatedPosts.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { Fragment, PureComponent } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Link, graphql, StaticQuery } from 'gatsby'
|
||||||
|
import Image from '../atoms/Image'
|
||||||
|
import styles from './RelatedPosts.module.scss'
|
||||||
|
|
||||||
|
const query = graphql`
|
||||||
|
query {
|
||||||
|
allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
frontmatter {
|
||||||
|
title
|
||||||
|
type
|
||||||
|
linkurl
|
||||||
|
tags
|
||||||
|
image {
|
||||||
|
childImageSharp {
|
||||||
|
...ImageFluidThumb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
date(formatString: "MMMM DD, YYYY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const postsWithDataFilter = (postsArray, key, valuesToFind) => {
|
||||||
|
const newArray = postsArray.filter(post => {
|
||||||
|
const frontmatterKey = post.node.frontmatter[key]
|
||||||
|
|
||||||
|
if (
|
||||||
|
frontmatterKey !== null &&
|
||||||
|
frontmatterKey.some(r => valuesToFind.includes(r))
|
||||||
|
) {
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return newArray
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostItem = ({ post }) => {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Link to={post.node.fields.slug}>
|
||||||
|
{post.node.frontmatter.image ? (
|
||||||
|
<Fragment>
|
||||||
|
<Image
|
||||||
|
fluid={post.node.frontmatter.image.childImageSharp.fluid}
|
||||||
|
alt={post.node.frontmatter.title}
|
||||||
|
/>
|
||||||
|
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
|
||||||
|
</Fragment>
|
||||||
|
) : (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PostItem.propTypes = {
|
||||||
|
post: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelatedPosts extends PureComponent {
|
||||||
|
shufflePosts = () => {
|
||||||
|
this.forceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<StaticQuery
|
||||||
|
query={query}
|
||||||
|
render={data => {
|
||||||
|
const posts = data.allMarkdownRemark.edges
|
||||||
|
const filteredPosts = postsWithDataFilter(
|
||||||
|
posts,
|
||||||
|
'tags',
|
||||||
|
this.props.tags
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={styles.relatedPosts}>
|
||||||
|
<h1 className={styles.title}>Related Posts</h1>
|
||||||
|
<ul>
|
||||||
|
{filteredPosts
|
||||||
|
.sort(() => 0.5 - Math.random())
|
||||||
|
.slice(0, 6)
|
||||||
|
.map(post => (
|
||||||
|
<PostItem key={post.node.id} post={post} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} btn`}
|
||||||
|
onClick={this.shufflePosts}
|
||||||
|
>
|
||||||
|
More Related Posts
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RelatedPosts.propTypes = {
|
||||||
|
tags: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelatedPosts
|
88
src/components/molecules/RelatedPosts.module.scss
Normal file
88
src/components/molecules/RelatedPosts.module.scss
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacer / 4;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.relatedPosts {
|
||||||
|
margin-top: -($spacer * 2);
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 48%;
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
|
||||||
|
@media (min-width: $screen-sm) {
|
||||||
|
flex-basis: 31%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include media-frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
> div {
|
||||||
|
border-color: $link-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: $link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include heading-band;
|
||||||
|
|
||||||
|
font-size: $font-size-h3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postTitle {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: $spacer / 4;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
line-height: $line-height-small;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
padding-left: .2rem;
|
||||||
|
padding-right: .2rem;
|
||||||
|
transition: color .2s ease-out;
|
||||||
|
|
||||||
|
@media (min-width: $screen-md) {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
}
|
@ -202,7 +202,6 @@ picture {
|
|||||||
height: auto;
|
height: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: $border-radius;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -323,14 +322,14 @@ cite {
|
|||||||
blockquote,
|
blockquote,
|
||||||
blockquote > p {
|
blockquote > p {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: $brand-grey;
|
color: $brand-grey-light;
|
||||||
}
|
}
|
||||||
// stylelint-enable no-descending-specificity
|
// stylelint-enable no-descending-specificity
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 0 0 $spacer;
|
margin: 0 0 $spacer;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: $spacer * 1.5;
|
padding-left: $spacer * 1.25;
|
||||||
|
|
||||||
// quotation marks
|
// quotation marks
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -8,16 +8,26 @@ import PostTitle from '../components/atoms/PostTitle'
|
|||||||
import PostLead from '../components/atoms/PostLead'
|
import PostLead from '../components/atoms/PostLead'
|
||||||
import PostContent from '../components/atoms/PostContent'
|
import PostContent from '../components/atoms/PostContent'
|
||||||
import PostActions from '../components/atoms/PostActions'
|
import PostActions from '../components/atoms/PostActions'
|
||||||
|
import PostLinkActions from '../components/atoms/PostLinkActions'
|
||||||
import SEO from '../components/atoms/SEO'
|
import SEO from '../components/atoms/SEO'
|
||||||
import Coinhive from '../components/atoms/Coinhive'
|
import Coinhive from '../components/atoms/Coinhive'
|
||||||
import PostMeta from '../components/molecules/PostMeta'
|
import PostMeta from '../components/molecules/PostMeta'
|
||||||
import Exif from '../components/atoms/Exif'
|
import Exif from '../components/atoms/Exif'
|
||||||
|
import RelatedPosts from '../components/molecules/RelatedPosts'
|
||||||
import styles from './Post.module.scss'
|
import styles from './Post.module.scss'
|
||||||
|
|
||||||
const Post = ({ data, location }) => {
|
const Post = ({ data, location }) => {
|
||||||
const { markdownRemark: post } = data
|
const { markdownRemark: post } = data
|
||||||
const { contentYaml: meta } = data
|
const { contentYaml: meta } = data
|
||||||
const { title, image, type, linkurl, style, coinhive } = post.frontmatter
|
const {
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
type,
|
||||||
|
linkurl,
|
||||||
|
style,
|
||||||
|
coinhive,
|
||||||
|
tags
|
||||||
|
} = post.frontmatter
|
||||||
const { slug } = post.fields
|
const { slug } = post.fields
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,9 +47,12 @@ const Post = ({ data, location }) => {
|
|||||||
)}
|
)}
|
||||||
{image && image.fields && <Exif exif={image.fields.exif} />}
|
{image && image.fields && <Exif exif={image.fields.exif} />}
|
||||||
<PostContent post={post} />
|
<PostContent post={post} />
|
||||||
|
{type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />}
|
||||||
<PostActions slug={slug} url={meta.url} />
|
<PostActions slug={slug} url={meta.url} />
|
||||||
<PostMeta post={post} meta={meta} />
|
<PostMeta post={post} meta={meta} />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{type === 'post' && <RelatedPosts tags={tags} />}
|
||||||
</Layout>
|
</Layout>
|
||||||
{coinhive && <Coinhive />}
|
{coinhive && <Coinhive />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -15,7 +15,7 @@ import styles from './Posts.module.scss'
|
|||||||
|
|
||||||
const Posts = ({ data, location, pageContext }) => {
|
const Posts = ({ data, location, pageContext }) => {
|
||||||
const edges = data.allMarkdownRemark.edges
|
const edges = data.allMarkdownRemark.edges
|
||||||
const { tag, previousPagePath, humanPageNumber, numberOfPages } = pageContext
|
const { tag, currentPageNumber, numPages } = pageContext
|
||||||
|
|
||||||
const PostsList = edges.map(({ node }) => {
|
const PostsList = edges.map(({ node }) => {
|
||||||
const { type, linkurl, title, image } = node.frontmatter
|
const { type, linkurl, title, image } = node.frontmatter
|
||||||
@ -58,11 +58,11 @@ const Posts = ({ data, location, pageContext }) => {
|
|||||||
<Layout location={location}>
|
<Layout location={location}>
|
||||||
<SEO />
|
<SEO />
|
||||||
{location.pathname === '/' && <Featured />}
|
{location.pathname === '/' && <Featured />}
|
||||||
{tag && <h1 className={styles.archiveTitle}>{tag}</h1>}
|
{tag && <h1 className={styles.archiveTitle}>#{tag}</h1>}
|
||||||
{previousPagePath && (
|
{currentPageNumber > 1 && (
|
||||||
<h1
|
<h1
|
||||||
className={styles.archiveTitle}
|
className={styles.archiveTitle}
|
||||||
>{`Page ${humanPageNumber} / ${numberOfPages}`}</h1>
|
>{`Page ${currentPageNumber} / ${numPages}`}</h1>
|
||||||
)}
|
)}
|
||||||
{PostsList}
|
{PostsList}
|
||||||
<Pagination pageContext={pageContext} />
|
<Pagination pageContext={pageContext} />
|
||||||
|
Loading…
Reference in New Issue
Block a user