1
0
Fork 0

more refactor

This commit is contained in:
Matthias Kretschmann 2020-07-19 13:31:27 +02:00
parent 369c1f1c27
commit 2d20a5a1b1
Signed by: m
GPG Key ID: 606EEEF3C479A91F
29 changed files with 451 additions and 428 deletions

View File

@ -19,11 +19,11 @@ module.exports = {
rss: '/feed.xml', rss: '/feed.xml',
jsonfeed: '/feed.json', jsonfeed: '/feed.json',
typekitID: 'msu4qap', typekitID: 'msu4qap',
itemsPerPage: 20, itemsPerPage: 18,
repoContentPath: 'https://github.com/kremalicious/blog/tree/main/content', repoContentPath: 'https://github.com/kremalicious/blog/tree/main/content',
menu: [ menu: [
{ {
title: 'Articles & Links', title: 'Articles',
link: '/archive' link: '/archive'
}, },
{ {

View File

@ -5,7 +5,6 @@ title: Project Purple
image: ../media/Teaser-Project-Purple.png image: ../media/Teaser-Project-Purple.png
download: ../media/project-purple-kremalicious.zip download: ../media/project-purple-kremalicious.zip
author: Matthias Kretschmann author: Matthias Kretschmann
featured: true
date: 2012-08-07 13:15:44+00:00 date: 2012-08-07 13:15:44+00:00

View File

@ -3,7 +3,9 @@ const { createExif } = require('./gatsby/createExif')
const { const {
generatePostPages, generatePostPages,
generateTagPages, generateTagPages,
generateRedirectPages generateRedirectPages,
generateArchivePages,
generatePhotosPages
} = require('./gatsby/createPages') } = require('./gatsby/createPages')
const { generateJsonFeed } = require('./gatsby/feeds') const { generateJsonFeed } = require('./gatsby/feeds')
@ -19,12 +21,12 @@ exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => {
} }
} }
exports.createPages = async ({ graphql, actions }) => { exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage, createRedirect } = actions const { createPage, createRedirect } = actions
const result = await graphql(` const result = await graphql(`
{ {
posts: allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) { all: allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) {
edges { edges {
next { next {
fields { fields {
@ -53,6 +55,16 @@ exports.createPages = async ({ graphql, actions }) => {
} }
} }
photos: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "photo" } } }
) {
edges {
node {
id
}
}
}
archive: allMarkdownRemark( archive: allMarkdownRemark(
filter: { frontmatter: { type: { ne: "photo" } } } filter: { frontmatter: { type: { ne: "photo" } } }
) { ) {
@ -72,14 +84,24 @@ exports.createPages = async ({ graphql, actions }) => {
} }
`) `)
if (result.errors) throw result.errors if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
const posts = result.data.posts.edges const all = result.data.all.edges
const photosLength = result.data.photos.edges.length
const archiveLength = result.data.archive.edges.length const archiveLength = result.data.archive.edges.length
const tags = result.data.tags.group const tags = result.data.tags.group
// Generate posts & posts index // Generate post pages
generatePostPages(createPage, posts, archiveLength) generatePostPages(createPage, all)
// Generate archive pages
generateArchivePages(createPage, archiveLength)
// Generate archive pages
generatePhotosPages(createPage, photosLength)
// Generate tag pages // Generate tag pages
generateTagPages(createPage, tags) generateTagPages(createPage, tags)

View File

@ -3,6 +3,7 @@ const { itemsPerPage } = require('../config')
const postTemplate = path.resolve('src/components/templates/Post/index.tsx') const postTemplate = path.resolve('src/components/templates/Post/index.tsx')
const archiveTemplate = path.resolve('src/components/templates/Archive.tsx') const archiveTemplate = path.resolve('src/components/templates/Archive.tsx')
const photosTemplate = path.resolve('src/components/templates/Photos.tsx')
const redirects = [ const redirects = [
{ f: '/feed', t: '/feed.xml' }, { f: '/feed', t: '/feed.xml' },
@ -25,7 +26,7 @@ function getPaginationData(i, numPages, slug) {
return { prevPagePath, nextPagePath, path } return { prevPagePath, nextPagePath, path }
} }
exports.generatePostPages = (createPage, posts, archiveLength) => { exports.generatePostPages = (createPage, posts) => {
// Create Post pages // Create Post pages
posts.forEach((post) => { posts.forEach((post) => {
createPage({ createPage({
@ -44,26 +45,56 @@ exports.generatePostPages = (createPage, posts, archiveLength) => {
} }
}) })
}) })
}
exports.generateArchivePages = (createPage, archiveLength) => {
// Create paginated archive pages // Create paginated archive pages
const numPages = Math.ceil(archiveLength / itemsPerPage) const numPagesArchive = Math.ceil(archiveLength / itemsPerPage)
const slug = `/archive/` const slugArchive = `/archive/`
Array.from({ length: numPages }).forEach((_, i) => { Array.from({ length: numPagesArchive }).forEach((_, i) => {
const { prevPagePath, nextPagePath, path } = getPaginationData( const { prevPagePath, nextPagePath, path } = getPaginationData(
i, i,
numPages, numPagesArchive,
slug slugArchive
) )
createPage({ createPage({
path, path,
component: archiveTemplate, component: archiveTemplate,
context: { context: {
slug, slug: slugArchive,
limit: itemsPerPage, limit: itemsPerPage,
skip: i * itemsPerPage, skip: i * itemsPerPage,
numPages, numPages: numPagesArchive,
currentPageNumber: i + 1,
prevPagePath,
nextPagePath
}
})
})
}
exports.generatePhotosPages = (createPage, photosLength) => {
// Create paginated photos pages
const numPagesPhotos = Math.ceil(photosLength / itemsPerPage)
const slugPhotos = `/photos/`
Array.from({ length: numPagesPhotos }).forEach((_, i) => {
const { prevPagePath, nextPagePath, path } = getPaginationData(
i,
numPagesPhotos,
slugPhotos
)
createPage({
path,
component: photosTemplate,
context: {
slug: slugPhotos,
limit: itemsPerPage,
skip: i * itemsPerPage,
numPages: numPagesPhotos,
currentPageNumber: i + 1, currentPageNumber: i + 1,
prevPagePath, prevPagePath,
nextPagePath nextPagePath

View File

@ -32,3 +32,12 @@ export interface Post {
fileAbsolutePath?: string fileAbsolutePath?: string
tableOfContents?: string tableOfContents?: string
} }
export interface PageContext {
tag?: string
slug: string
currentPageNumber: number
numPages: number
prevPagePath?: string
nextPagePath?: string
}

View File

@ -33,7 +33,18 @@ export const imageSizeThumb = graphql`
original { original {
src src
} }
fluid(maxWidth: 400, maxHeight: 170, quality: 85, cropFocus: CENTER) { fluid(maxWidth: 420, maxHeight: 140, quality: 85, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`
export const photoSizeThumb = graphql`
fragment PhotoFluidThumb on ImageSharp {
original {
src
}
fluid(maxWidth: 300, maxHeight: 300, quality: 85, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64 ...GatsbyImageSharpFluid_withWebp_noBase64
} }
} }

View File

@ -2,59 +2,11 @@
@import 'mixins'; @import 'mixins';
.featured { .featured {
@include divider;
display: grid; display: grid;
gap: $spacer / 2; gap: $spacer;
grid-template-columns: 1fr 1fr; grid-template-columns: repeat(2, 1fr);
padding-bottom: $spacer * $line-height;
margin-bottom: -($spacer / 2);
@media (min-width: $screen-md) { @media (min-width: $screen-md) {
@include breakoutviewport; grid-template-columns: repeat(3, 1fr);
gap: $spacer;
padding-bottom: $spacer * 3;
}
}
.featuredTitle {
transition: 0.1s ease-out;
font-size: $font-size-base;
margin: 0;
position: absolute;
top: 25%;
min-width: 45%;
text-align: right;
padding: $spacer / 3;
background: rgba($link-color, 0.85);
color: #fff !important;
text-shadow: 0 1px 0 #000;
left: 0;
opacity: 0;
transform: translate3d(0, -20px, 0);
}
.featuredItem {
position: relative;
a {
display: block;
&:hover,
&:focus {
.featuredTitle {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
}
figure {
margin: 0;
}
:global(.gatsby-image-wrapper) {
margin-bottom: 0;
} }
} }

View File

@ -1,8 +1,8 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link, graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import { Image } from '../atoms/Image'
import styles from './Featured.module.scss' import styles from './Featured.module.scss'
import { Post } from '../../@types/Post' import { Post } from '../../@types/Post'
import PostTeaser from './PostTeaser'
const query = graphql` const query = graphql`
query { query {
@ -21,21 +21,12 @@ const query = graphql`
export default function Featured(): ReactElement { export default function Featured(): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery(query)
return ( return (
<div className={styles.featured}> <div className={styles.featured}>
{data.allMarkdownRemark.edges.map(({ node }: { node: Post }) => { {data.allMarkdownRemark.edges.map(({ node }: { node: Post }) => (
const { title, image } = node.frontmatter <PostTeaser key={node.id} post={node} />
const { slug } = node.fields ))}
return (
<article className={styles.featuredItem} key={node.id}>
<Link to={slug}>
<Image fluid={image.childImageSharp.fluid} alt={title} />
<h1 className={styles.featuredTitle}>{title}</h1>
</Link>
</article>
)
})}
</div> </div>
) )
} }

View File

@ -1,36 +1,47 @@
@import 'variables'; @import 'variables';
@import 'mixins';
.pagination { .pagination {
display: flex; @include breakoutviewport;
margin-top: $spacer * 2;
margin-top: $spacer * 3;
margin-bottom: $spacer; margin-bottom: $spacer;
justify-content: space-between; display: flex;
justify-content: center;
> div {
&:first-child {
margin-right: $spacer;
}
&:last-child {
margin-left: $spacer;
text-align: right;
}
}
} }
.number { .number {
text-align: center; text-align: center;
width: 2rem; padding: $spacer / 6 $spacer / 2;
height: 2rem; border: 1px solid $brand-grey-dimmed;
line-height: 1.7; font-variant-numeric: lining-nums;
display: inline-block; margin-left: -1px;
border-radius: 50%; display: flex;
border: 1px solid transparent; align-items: center;
justify-content: center;
min-width: 2.5rem;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8);
color: $brand-grey;
:global(.dark) & {
color: $brand-grey-dimmed;
border-color: darken($body-background-color--dark, 5%);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
}
&:hover, &:hover,
&:focus { &:focus {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.1);
border-color: darken($brand-grey-dimmed, 5%); }
&:first-child {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&:last-child {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
} }
} }
@ -38,10 +49,10 @@
composes: number; composes: number;
cursor: default; cursor: default;
pointer-events: none; pointer-events: none;
border: 1px solid darken($brand-grey-dimmed, 5%); color: $brand-grey-dimmed;
color: $brand-grey-light; background: rgba(255, 255, 255, 0.1);
:global(.dark) & { :global(.dark) & {
border-color: darken($brand-grey-light, 15%); color: $brand-grey-light;
} }
} }

View File

@ -2,6 +2,8 @@ import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import styles from './Pagination.module.scss' import styles from './Pagination.module.scss'
import shortid from 'shortid' import shortid from 'shortid'
import { PageContext } from '../../@types/Post'
import Icon from '../atoms/Icon'
const PageNumber = ({ const PageNumber = ({
i, i,
@ -34,8 +36,12 @@ function PrevNext({
const title = prevPagePath ? 'Newer Posts' : 'Older Posts' const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
return ( return (
<Link to={link} rel={rel} title={title}> <Link to={link} rel={rel} title={title} className={styles.number}>
{prevPagePath ? '←' : '→'} {prevPagePath ? (
<Icon name="ChevronLeft" />
) : (
<Icon name="ChevronRight" />
)}
</Link> </Link>
) )
} }
@ -43,13 +49,7 @@ function PrevNext({
export default function Pagination({ export default function Pagination({
pageContext pageContext
}: { }: {
pageContext: { pageContext: PageContext
slug: string
currentPageNumber: number
numPages: number
prevPagePath?: string
nextPagePath?: string
}
}): ReactElement { }): ReactElement {
const { const {
slug, slug,
@ -63,18 +63,16 @@ export default function Pagination({
return ( return (
<div className={styles.pagination}> <div className={styles.pagination}>
<div>{!isFirst && <PrevNext prevPagePath={prevPagePath} />}</div> {!isFirst && <PrevNext prevPagePath={prevPagePath} />}
<div> {Array.from({ length: numPages }, (_, i) => (
{Array.from({ length: numPages }, (_, i) => ( <PageNumber
<PageNumber key={shortid.generate()}
key={shortid.generate()} i={i}
i={i} slug={slug}
slug={slug} current={currentPageNumber === i + 1}
current={currentPageNumber === i + 1} />
/> ))}
))} {!isLast && <PrevNext nextPagePath={nextPagePath} />}
</div>
<div>{!isLast && <PrevNext nextPagePath={nextPagePath} />}</div>
</div> </div>
) )
} }

View File

@ -1,25 +1,21 @@
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
.post { .title {
figure {
margin: 0;
}
}
.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: 0.2rem; padding-left: 0.2rem;
padding-right: 0.2rem; padding-right: 0.2rem;
color: $brand-grey-light;
margin-top: $spacer / 2;
margin-bottom: 0;
font-size: $font-size-base;
transition: color 0.2s ease-out; transition: color 0.2s ease-out;
}
@media (min-width: $screen-md) { .post {
font-size: $font-size-base; display: block;
figure {
margin: 0;
} }
} }

View File

@ -1,20 +1,24 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link, graphql } from 'gatsby' import { Link, graphql } from 'gatsby'
import { Image } from '../atoms/Image' import { Image } from '../atoms/Image'
import styles from './PostTeaser.module.scss'
import { Post } from '../../@types/Post' import { Post } from '../../@types/Post'
import PostTitle from '../templates/Post/Title'
import styles from './PostTeaser.module.scss'
export const postTeaserQuery = graphql` export const postTeaserQuery = graphql`
fragment PostTeaser on MarkdownRemark { fragment PostTeaser on MarkdownRemark {
id id
fileAbsolutePath fileAbsolutePath
frontmatter { frontmatter {
type
title title
linkurl
image { image {
childImageSharp { childImageSharp {
...ImageFluidThumb ...ImageFluidThumb
} }
} }
tags
} }
fields { fields {
slug slug
@ -29,7 +33,7 @@ export default function PostTeaser({
post: Partial<Post> post: Partial<Post>
toggleSearch?: () => void toggleSearch?: () => void
}): ReactElement { }): ReactElement {
const { image, title } = post.frontmatter const { image, title, type } = post.frontmatter
const { slug } = post.fields const { slug } = post.fields
return ( return (
@ -38,12 +42,16 @@ export default function PostTeaser({
to={slug} to={slug}
onClick={toggleSearch && toggleSearch} onClick={toggleSearch && toggleSearch}
> >
{image ? ( {image && (
<Image fluid={image.childImageSharp.fluid} alt={title} /> <Image
) : ( title={type === 'photo' ? title : null}
<div className={styles.empty} /> fluid={image.childImageSharp.fluid}
alt={title}
original={image.childImageSharp.original}
/>
)} )}
<h4 className={styles.postTitle}>{title}</h4>
<PostTitle slug={slug} title={title} className={styles.title} />
</Link> </Link>
) )
} }

View File

@ -47,16 +47,16 @@ function photosWithDataFilter(posts: []) {
export default function RelatedPosts({ export default function RelatedPosts({
tags, tags,
photos isPhotos
}: { }: {
tags: string[] tags: string[]
photos?: boolean isPhotos?: boolean
}): ReactElement { }): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery(query)
const posts = data.allMarkdownRemark.edges const posts = data.allMarkdownRemark.edges
function getPosts() { function getPosts() {
return photos return isPhotos
? photosWithDataFilter(posts) ? photosWithDataFilter(posts)
: tags && postsWithDataFilter(posts, 'tags', tags) : tags && postsWithDataFilter(posts, 'tags', tags)
} }
@ -71,7 +71,7 @@ export default function RelatedPosts({
return ( return (
<aside className={styles.relatedPosts}> <aside className={styles.relatedPosts}>
<h1 className={styles.title}> <h1 className={styles.title}>
Related {photos ? 'Photos' : 'Posts'}{' '} Related {isPhotos ? 'Photos' : 'Posts'}{' '}
<button className={styles.button} onClick={refreshPosts}> <button className={styles.button} onClick={refreshPosts}>
Refresh Refresh
</button> </button>

View File

@ -7,7 +7,7 @@ import { useSiteMetadata } from '../../hooks/use-site-metadata'
import styles from './Footer.module.scss' import styles from './Footer.module.scss'
function Copyright() { function Copyright() {
const { name, uri, bitcoin, github } = useSiteMetadata().author const { name, uri, github } = useSiteMetadata().author
const year = new Date().getFullYear() const year = new Date().getFullYear()
return ( return (
@ -24,7 +24,7 @@ function Copyright() {
</a> </a>
<Link to="/thanks" className={styles.btc}> <Link to="/thanks" className={styles.btc}>
<Icon name="Bitcoin" /> <Icon name="Bitcoin" />
<code>{bitcoin}</code> Say Thanks
</Link> </Link>
</p> </p>
</section> </section>
@ -37,7 +37,6 @@ export default class Footer extends PureComponent {
<footer role="contentinfo" className={styles.footer}> <footer role="contentinfo" className={styles.footer}>
<Container> <Container>
<Vcard /> <Vcard />
<Copyright /> <Copyright />
</Container> </Container>
</footer> </footer>

View File

@ -1,39 +1,13 @@
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
.hentry { .posts {
@include divider; display: grid;
gap: $spacer * 2;
margin-top: 0; @media (min-width: $screen-sm) {
margin-bottom: 0; @include breakoutviewport;
padding-top: $spacer * 2;
padding-bottom: $spacer * 2;
:global(.gatsby-image-wrapper) { grid-template-columns: repeat(2, 1fr);
max-height: 100vh;
}
@media (min-width: $screen-md) {
padding-top: $spacer * 4;
padding-bottom: $spacer * 4;
} }
} }
.archivetitle {
@include divider;
font-size: $font-size-h3;
margin-top: 0;
margin-bottom: 0;
padding-bottom: $spacer * $line-height;
span {
color: $brand-grey-light;
padding-right: $spacer / 12;
font-size: $font-size-base;
}
}
.paginationTitle {
composes: archivetitle;
}

View File

@ -1,84 +1,44 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link, graphql } from 'gatsby' import { graphql } from 'gatsby'
import { Post } from '../../@types/Post' import { Post, PageContext } from '../../@types/Post'
import Pagination from '../molecules/Pagination' import Pagination from '../molecules/Pagination'
import PostTitle from './Post/Title'
import PostLead from './Post/Lead'
import PostContent from './Post/Content'
import PostMore from './Post/More'
import PostLinkActions from './Post/LinkActions'
import SEO from '../atoms/SEO'
import styles from './Archive.module.scss' import styles from './Archive.module.scss'
import { Image } from '../atoms/Image' import PostTeaser from '../molecules/PostTeaser'
import Page from './Page'
export default function Archive({ export default function Archive({
data, data,
pageContext pageContext
}: { }: {
data: any data: any
pageContext: { pageContext: PageContext
tag: string
slug: string
currentPageNumber: number
numPages: number
}
}): ReactElement { }): ReactElement {
const edges = data.allMarkdownRemark.edges const edges = data.allMarkdownRemark.edges
const { tag, currentPageNumber, numPages } = pageContext const { tag, currentPageNumber, numPages } = pageContext
const PostsList = edges.map(({ node }: { node: Post }) => { const PostsList = edges.map(({ node }: { node: Post }) => (
const { type, linkurl, title, image } = node.frontmatter <PostTeaser key={node.id} post={node} />
const { slug } = node.fields ))
return ( const title = tag ? `#${tag}` : 'Archive'
<article className={styles.hentry} key={node.id}>
<PostTitle type={type} slug={slug} linkurl={linkurl} title={title} />
{image && ( const paginationTitle =
<Link to={slug} title={title}> numPages > 1 && currentPageNumber > 1
<Image ? `Page ${currentPageNumber} / ${numPages}`
title={type === 'photo' ? title : null} : ''
fluid={image.childImageSharp.fluid}
alt={title}
original={image.childImageSharp.original}
/>
</Link>
)}
{type === 'article' && ( const page = {
<> frontmatter: {
<PostLead post={node} index /> title: `${title} ${paginationTitle}`,
<PostMore to={slug}>Continue Reading</PostMore> description: 'All the articles & links.'
</> }
)} }
{type === 'link' && (
<>
<PostContent post={node} />
<PostLinkActions slug={slug} linkurl={linkurl} />
</>
)}
</article>
)
})
return ( return (
<> <Page title={page.frontmatter.title} post={page}>
<SEO /> <div className={styles.posts}>{PostsList}</div>
{tag && (
<h1 className={styles.archivetitle}>
<span>#</span>
{tag}
</h1>
)}
{numPages > 1 && currentPageNumber > 1 && (
<h2
className={styles.paginationTitle}
>{`Page ${currentPageNumber} / ${numPages}`}</h2>
)}
{PostsList}
{numPages > 1 && <Pagination pageContext={pageContext} />} {numPages > 1 && <Pagination pageContext={pageContext} />}
</> </Page>
) )
} }
@ -92,21 +52,7 @@ export const archiveQuery = graphql`
) { ) {
edges { edges {
node { node {
id ...PostTeaser
html
frontmatter {
title
type
linkurl
image {
childImageSharp {
...ImageFluid
}
}
}
fields {
slug
}
} }
} }
} }

View File

@ -1,6 +1,19 @@
@import 'variables'; @import 'variables';
@import 'mixins';
.pageTitle { .pagetitle {
composes: archivetitle from './Archive.module.scss'; @include divider;
margin-bottom: $spacer * 2;
@media (min-width: $screen-md) {
@include breakoutviewport;
}
font-size: $font-size-h3;
margin-top: 0;
margin-bottom: $spacer * $line-height;
padding-bottom: $spacer / $line-height;
color: $brand-grey-light;
display: flex;
justify-content: space-between;
align-items: flex-end;
} }

View File

@ -23,7 +23,7 @@ export default function Page({
<Helmet title={title} /> <Helmet title={title} />
<SEO slug={pathname} postSEO post={post} /> <SEO slug={pathname} postSEO post={post} />
<h1 className={styles.pageTitle}>{title}</h1> <h1 className={styles.pagetitle}>{title}</h1>
{section ? <section className={section}>{children}</section> : children} {section ? <section className={section}>{children}</section> : children}
</> </>
) )

View File

@ -2,16 +2,14 @@
@import 'mixins'; @import 'mixins';
.photos { .photos {
@include breakoutviewport--full; @include breakoutviewport;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: $spacer; gap: $spacer;
padding-left: $spacer;
padding-right: $spacer;
@media (min-width: $screen-sm) { @media (min-width: $screen-sm) {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
} }
} }

View File

@ -1,12 +1,20 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import Photos from '../photos' import Photos from './Photos'
import data from '../../../jest/__fixtures__/photos.json' import data from '../../../jest/__fixtures__/photos.json'
describe('/photos', () => { describe('/photos', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render(<Photos data={data} />) const pageContext = {
slug: '/photos',
currentPageNumber: 2,
numPages: 20
}
const { container } = render(
<Photos data={data} pageContext={pageContext} />
)
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })
}) })

View File

@ -0,0 +1,87 @@
import React, { ReactElement } from 'react'
import { graphql, Link } from 'gatsby'
import Page from './Page'
import { Post, PageContext } from '../../@types/Post'
import { Image } from '../atoms/Image'
import styles from './Photos.module.scss'
import Pagination from '../molecules/Pagination'
export const PhotoThumb = ({ photo }: { photo: Post }): ReactElement => {
const { title, image } = photo.frontmatter
const { slug } = photo.fields
const { fluid } = image.childImageSharp
return (
<article className={styles.photo}>
{image && (
<Link to={slug}>
<Image title={title} fluid={fluid} alt={title} />
</Link>
)}
</article>
)
}
export default function Photos({
data,
pageContext
}: {
data: any
pageContext: PageContext
}): ReactElement {
const photos = data.allMarkdownRemark.edges
const { currentPageNumber, numPages } = pageContext
const paginationTitle =
numPages > 1 && currentPageNumber > 1
? `Page ${currentPageNumber} / ${numPages}`
: ''
const page = {
frontmatter: {
title: `Photos ${paginationTitle}`,
description:
'Personal photos of designer & developer Matthias Kretschmann.'
}
}
return (
<Page title={page.frontmatter.title} post={page}>
<section className={styles.photos}>
{photos.map(({ node }: { node: Post }) => (
<PhotoThumb key={node.id} photo={node} />
))}
</section>
{numPages > 1 && <Pagination pageContext={pageContext} />}
</Page>
)
}
export const photosQuery = graphql`
query($skip: Int, $limit: Int) {
allMarkdownRemark(
filter: { frontmatter: { type: { eq: "photo" } } }
sort: { order: DESC, fields: [fields___date] }
skip: $skip
limit: $limit
) {
edges {
node {
id
frontmatter {
title
image {
childImageSharp {
...PhotoFluidThumb
}
}
}
fields {
slug
}
}
}
}
}
`

View File

@ -10,7 +10,7 @@ const PostContent = ({ post }: { post: Post }): ReactElement => {
let content = post.html let content = post.html
if (post.frontmatter.type === 'post') { if (post.frontmatter.type === 'article') {
if (content.includes(separator)) { if (content.includes(separator)) {
content = content.split(separator)[1] content = content.split(separator)[1]
} else { } else {

View File

@ -6,10 +6,10 @@ import { Post } from '../../../@types/Post'
// Grab everything before more tag, or just first paragraph // Grab everything before more tag, or just first paragraph
const PostLead = ({ const PostLead = ({
post, post,
index className
}: { }: {
post: Post post: Partial<Post>
index?: boolean className?: string
}): ReactElement => { }): ReactElement => {
let lead let lead
const content = post.html const content = post.html
@ -23,7 +23,7 @@ const PostLead = ({
return ( return (
<div <div
className={index ? styles.index : styles.lead} className={`${styles.lead} ${className && className}`}
dangerouslySetInnerHTML={{ __html: lead }} dangerouslySetInnerHTML={{ __html: lead }}
/> />
) )

View File

@ -38,7 +38,7 @@ export default function PostMeta({ post }: { post: Post }): ReactElement {
{tags && ( {tags && (
<div className={styles.tags}> <div className={styles.tags}>
{tags.map((tag: string) => { {tags.map((tag: string) => {
const url = `/tags/${slugify(tag)}/` const url = `/archive/${slugify(tag)}/`
return <Tag key={shortid.generate()} name={tag} url={url} /> return <Tag key={shortid.generate()} name={tag} url={url} />
})} })}
</div> </div>

View File

@ -1,30 +1,31 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link } from 'gatsby'
import styles from './Title.module.scss' import styles from './Title.module.scss'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
import Time from '../../atoms/Time' import Time from '../../atoms/Time'
export default function PostTitle({ export default function PostTitle({
type,
slug, slug,
linkurl, linkurl,
title, title,
date, date,
updated updated,
className
}: { }: {
type?: string
slug?: string slug?: string
linkurl?: string linkurl?: string
title: string title: string
date?: string date?: string
updated?: string updated?: string
className?: string
}): ReactElement { }): ReactElement {
const linkHostname = linkurl ? new URL(linkurl).hostname : null const linkHostname = linkurl ? new URL(linkurl).hostname : null
return type === 'link' ? ( return linkurl ? (
<> <>
<h1 <h1
className={[styles.hentry__title, styles.hentry__title__link].join(' ')} className={`${styles.hentry__title} ${styles.hentry__title__link} ${
className && className
}`}
> >
<a href={linkurl} title={`Go to source: ${linkurl}`}> <a href={linkurl} title={`Go to source: ${linkurl}`}>
{title} <Icon name="ExternalLink" /> {title} <Icon name="ExternalLink" />
@ -33,12 +34,14 @@ export default function PostTitle({
<div className={styles.linkurl}>{linkHostname}</div> <div className={styles.linkurl}>{linkHostname}</div>
</> </>
) : slug ? ( ) : slug ? (
<h1 className={styles.hentry__title}> <h1 className={`${styles.hentry__title} ${className && className}`}>
<Link to={slug}>{title}</Link> {title}
</h1> </h1>
) : ( ) : (
<> <>
<h1 className={styles.hentry__title}>{title}</h1> <h1 className={`${styles.hentry__title} ${className && className}`}>
{title}
</h1>
{date && ( {date && (
<div className={styles.time}> <div className={styles.time}>
{updated && 'published '} {updated && 'published '}

View File

@ -40,35 +40,38 @@ export default function Post({
<article className={styles.hentry}> <article className={styles.hentry}>
<header> <header>
<PostTitle <PostTitle
type={type}
linkurl={linkurl} linkurl={linkurl}
title={title} title={title}
date={date} date={date}
updated={updated} updated={updated}
/> />
{type === 'article' && <PostLead post={post} />} {type === 'article' && <PostLead post={post} />}
{image && (
<Image
fluid={image.childImageSharp.fluid}
alt={title}
original={image.childImageSharp.original}
/>
)}
</header> </header>
{type === 'photo' && <PostContent post={post} />} {type === 'photo' ? (
{image && ( <>
<Image <PostContent post={post} />
fluid={image.childImageSharp.fluid} {image && image.fields && <Exif exif={image.fields.exif} />}
alt={title} </>
original={image.childImageSharp.original} ) : (
/> <PostContent post={post} />
)} )}
{type === 'photo' && image && image.fields && (
<Exif exif={image.fields.exif} />
)}
{type !== 'photo' && <PostContent post={post} />}
{type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />} {type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />}
<PostMeta post={post} /> <PostMeta post={post} />
<PostActions slug={slug} githubLink={githubLink} /> <PostActions slug={slug} githubLink={githubLink} />
</article> </article>
<RelatedPosts photos={type === 'photo'} tags={tags} /> <RelatedPosts isPhotos={type === 'photo'} tags={tags} />
<PrevNext prev={prev} next={next} /> <PrevNext prev={prev} next={next} />
</> </>

View File

@ -1,4 +1,44 @@
@import 'variables'; @import 'variables';
@import 'mixins';
.home { .section {
@media (min-width: $screen-md) {
@include breakoutviewport;
}
}
.section:not(:first-child) {
margin-top: $spacer * 3;
}
.articles,
.photos {
display: grid;
gap: $spacer;
}
.articles {
@media (min-width: $screen-sm) {
gap: $spacer * 2;
grid-template-columns: repeat(2, 1fr);
}
}
.photos {
grid-template-columns: repeat(2, 1fr);
@media (min-width: $screen-sm) {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}
.title {
composes: pagetitle from '../components/templates/Page.module.scss';
margin-left: 0;
margin-right: 0;
a {
margin: 0;
color: $text-color-light;
}
} }

View File

@ -1,57 +1,61 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { graphql, Link, PageProps } from 'gatsby' import { graphql, PageProps } from 'gatsby'
import Page from '../components/templates/Page'
import { Post } from '../@types/Post' import { Post } from '../@types/Post'
import { Image } from '../components/atoms/Image'
import styles from './index.module.scss' import styles from './index.module.scss'
import Featured from '../components/molecules/Featured' import Featured from '../components/molecules/Featured'
import { PhotoThumb } from '../components/templates/Photos'
const page = { import PostTeaser from '../components/molecules/PostTeaser'
frontmatter: { import PostMore from '../components/templates/Post/More'
title: 'home',
description: 'Blog of designer & developer Matthias Kretschmann.'
}
}
export default function Home(props: PageProps): ReactElement { export default function Home(props: PageProps): ReactElement {
return ( return (
<Page title={page.frontmatter.title} post={page} section={styles.home}> <>
<Featured /> <section className={styles.section}>
Latest Articles & Links <h2 className={styles.title}>Featured</h2>
<br /> <Featured />
Latest Photos </section>
</Page>
<section className={styles.section}>
<h2 className={styles.title}>
Latest Articles <PostMore to="/archive">All Articles</PostMore>
</h2>
<div className={styles.articles}>
{(props.data as any).latestArticles.edges.map(
({ node }: { node: Post }) => (
<PostTeaser key={node.id} post={node} />
)
)}
</div>
</section>
<section className={styles.section}>
<h2 className={styles.title}>
Latest Photos <PostMore to="/photos">All Photos</PostMore>
</h2>
<div className={styles.photos}>
{(props.data as any).latestPhotos.edges.map(
({ node }: { node: Post }) => (
<PhotoThumb key={node.id} photo={node} />
)
)}
</div>
</section>
</>
) )
} }
export const homeQuery = graphql` export const homeQuery = graphql`
query { query {
latestArticles: allMarkdownRemark( latestArticles: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "article" } } } filter: { frontmatter: { type: { ne: "photo" } } }
sort: { order: DESC, fields: [fields___date] } sort: { order: DESC, fields: [fields___date] }
limit: 5 limit: 6
) { ) {
edges { edges {
node { node {
frontmatter { ...PostTeaser
title
type
image {
childImageSharp {
fluid(
maxWidth: 400
maxHeight: 400
quality: 85
cropFocus: CENTER
) {
...GatsbyImageSharpFluid_withWebp
}
}
}
}
fields {
slug
}
} }
} }
} }
@ -59,7 +63,7 @@ export const homeQuery = graphql`
latestPhotos: allMarkdownRemark( latestPhotos: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "photo" } } } filter: { frontmatter: { type: { eq: "photo" } } }
sort: { order: DESC, fields: [fields___date] } sort: { order: DESC, fields: [fields___date] }
limit: 10 limit: 15
) { ) {
edges { edges {
node { node {
@ -69,14 +73,7 @@ export const homeQuery = graphql`
type type
image { image {
childImageSharp { childImageSharp {
fluid( ...PhotoFluidThumb
maxWidth: 400
maxHeight: 400
quality: 85
cropFocus: CENTER
) {
...GatsbyImageSharpFluid_withWebp
}
} }
} }
} }

View File

@ -1,73 +0,0 @@
import React, { ReactElement } from 'react'
import { graphql, Link, PageProps } from 'gatsby'
import Page from '../components/templates/Page'
import { Post } from '../@types/Post'
import { Image } from '../components/atoms/Image'
import styles from './photos.module.scss'
const page = {
frontmatter: {
title: 'Photos',
description: 'Personal photos of designer & developer Matthias Kretschmann.'
}
}
const PhotoThumb = ({ photo }: { photo: Post }): ReactElement => {
const { title, image } = photo.frontmatter
const { slug } = photo.fields
const { fluid } = image.childImageSharp
return (
<article className={styles.photo}>
{image && (
<Link to={slug}>
<Image title={title} fluid={fluid} alt={title} />
</Link>
)}
</article>
)
}
export default function Photos({ data }: { data: any }): ReactElement {
return (
<Page title={page.frontmatter.title} post={page} section={styles.photos}>
{data.photos.edges.map(({ node }: { node: Post }) => (
<PhotoThumb key={node.id} photo={node} />
))}
</Page>
)
}
export const photosQuery = graphql`
query {
photos: allMarkdownRemark(
filter: { frontmatter: { type: { eq: "photo" } } }
sort: { order: DESC, fields: [fields___date] }
) {
edges {
node {
id
frontmatter {
title
type
image {
childImageSharp {
fluid(
maxWidth: 400
maxHeight: 400
quality: 85
cropFocus: CENTER
) {
...GatsbyImageSharpFluid_withWebp
}
}
}
}
fields {
slug
}
}
}
}
}
`