mirror of
https://github.com/kremalicious/blog.git
synced 2025-01-03 10:25:07 +01:00
more refactor
This commit is contained in:
parent
369c1f1c27
commit
2d20a5a1b1
@ -19,11 +19,11 @@ module.exports = {
|
||||
rss: '/feed.xml',
|
||||
jsonfeed: '/feed.json',
|
||||
typekitID: 'msu4qap',
|
||||
itemsPerPage: 20,
|
||||
itemsPerPage: 18,
|
||||
repoContentPath: 'https://github.com/kremalicious/blog/tree/main/content',
|
||||
menu: [
|
||||
{
|
||||
title: 'Articles & Links',
|
||||
title: 'Articles',
|
||||
link: '/archive'
|
||||
},
|
||||
{
|
||||
|
@ -5,7 +5,6 @@ title: Project Purple
|
||||
image: ../media/Teaser-Project-Purple.png
|
||||
download: ../media/project-purple-kremalicious.zip
|
||||
author: Matthias Kretschmann
|
||||
featured: true
|
||||
|
||||
date: 2012-08-07 13:15:44+00:00
|
||||
|
||||
|
@ -3,7 +3,9 @@ const { createExif } = require('./gatsby/createExif')
|
||||
const {
|
||||
generatePostPages,
|
||||
generateTagPages,
|
||||
generateRedirectPages
|
||||
generateRedirectPages,
|
||||
generateArchivePages,
|
||||
generatePhotosPages
|
||||
} = require('./gatsby/createPages')
|
||||
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 result = await graphql(`
|
||||
{
|
||||
posts: allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) {
|
||||
all: allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) {
|
||||
edges {
|
||||
next {
|
||||
fields {
|
||||
@ -53,6 +55,16 @@ exports.createPages = async ({ graphql, actions }) => {
|
||||
}
|
||||
}
|
||||
|
||||
photos: allMarkdownRemark(
|
||||
filter: { frontmatter: { type: { eq: "photo" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
archive: allMarkdownRemark(
|
||||
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 tags = result.data.tags.group
|
||||
|
||||
// Generate posts & posts index
|
||||
generatePostPages(createPage, posts, archiveLength)
|
||||
// Generate post pages
|
||||
generatePostPages(createPage, all)
|
||||
|
||||
// Generate archive pages
|
||||
generateArchivePages(createPage, archiveLength)
|
||||
|
||||
// Generate archive pages
|
||||
generatePhotosPages(createPage, photosLength)
|
||||
|
||||
// Generate tag pages
|
||||
generateTagPages(createPage, tags)
|
||||
|
@ -3,6 +3,7 @@ const { itemsPerPage } = require('../config')
|
||||
|
||||
const postTemplate = path.resolve('src/components/templates/Post/index.tsx')
|
||||
const archiveTemplate = path.resolve('src/components/templates/Archive.tsx')
|
||||
const photosTemplate = path.resolve('src/components/templates/Photos.tsx')
|
||||
|
||||
const redirects = [
|
||||
{ f: '/feed', t: '/feed.xml' },
|
||||
@ -25,7 +26,7 @@ function getPaginationData(i, numPages, slug) {
|
||||
return { prevPagePath, nextPagePath, path }
|
||||
}
|
||||
|
||||
exports.generatePostPages = (createPage, posts, archiveLength) => {
|
||||
exports.generatePostPages = (createPage, posts) => {
|
||||
// Create Post pages
|
||||
posts.forEach((post) => {
|
||||
createPage({
|
||||
@ -44,26 +45,56 @@ exports.generatePostPages = (createPage, posts, archiveLength) => {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.generateArchivePages = (createPage, archiveLength) => {
|
||||
// Create paginated archive pages
|
||||
const numPages = Math.ceil(archiveLength / itemsPerPage)
|
||||
const slug = `/archive/`
|
||||
const numPagesArchive = Math.ceil(archiveLength / itemsPerPage)
|
||||
const slugArchive = `/archive/`
|
||||
|
||||
Array.from({ length: numPages }).forEach((_, i) => {
|
||||
Array.from({ length: numPagesArchive }).forEach((_, i) => {
|
||||
const { prevPagePath, nextPagePath, path } = getPaginationData(
|
||||
i,
|
||||
numPages,
|
||||
slug
|
||||
numPagesArchive,
|
||||
slugArchive
|
||||
)
|
||||
|
||||
createPage({
|
||||
path,
|
||||
component: archiveTemplate,
|
||||
context: {
|
||||
slug,
|
||||
slug: slugArchive,
|
||||
limit: 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,
|
||||
prevPagePath,
|
||||
nextPagePath
|
||||
|
9
src/@types/Post.d.ts
vendored
9
src/@types/Post.d.ts
vendored
@ -32,3 +32,12 @@ export interface Post {
|
||||
fileAbsolutePath?: string
|
||||
tableOfContents?: string
|
||||
}
|
||||
|
||||
export interface PageContext {
|
||||
tag?: string
|
||||
slug: string
|
||||
currentPageNumber: number
|
||||
numPages: number
|
||||
prevPagePath?: string
|
||||
nextPagePath?: string
|
||||
}
|
||||
|
@ -33,7 +33,18 @@ export const imageSizeThumb = graphql`
|
||||
original {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -2,59 +2,11 @@
|
||||
@import 'mixins';
|
||||
|
||||
.featured {
|
||||
@include divider;
|
||||
|
||||
display: grid;
|
||||
gap: $spacer / 2;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding-bottom: $spacer * $line-height;
|
||||
margin-bottom: -($spacer / 2);
|
||||
gap: $spacer;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@media (min-width: $screen-md) {
|
||||
@include breakoutviewport;
|
||||
|
||||
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;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Link, graphql, useStaticQuery } from 'gatsby'
|
||||
import { Image } from '../atoms/Image'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import styles from './Featured.module.scss'
|
||||
import { Post } from '../../@types/Post'
|
||||
import PostTeaser from './PostTeaser'
|
||||
|
||||
const query = graphql`
|
||||
query {
|
||||
@ -21,21 +21,12 @@ const query = graphql`
|
||||
|
||||
export default function Featured(): ReactElement {
|
||||
const data = useStaticQuery(query)
|
||||
|
||||
return (
|
||||
<div className={styles.featured}>
|
||||
{data.allMarkdownRemark.edges.map(({ node }: { node: Post }) => {
|
||||
const { title, image } = node.frontmatter
|
||||
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>
|
||||
)
|
||||
})}
|
||||
{data.allMarkdownRemark.edges.map(({ node }: { node: Post }) => (
|
||||
<PostTeaser key={node.id} post={node} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,36 +1,47 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
margin-top: $spacer * 2;
|
||||
@include breakoutviewport;
|
||||
|
||||
margin-top: $spacer * 3;
|
||||
margin-bottom: $spacer;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
&:first-child {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: $spacer;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.number {
|
||||
text-align: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
line-height: 1.7;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
border: 1px solid transparent;
|
||||
padding: $spacer / 6 $spacer / 2;
|
||||
border: 1px solid $brand-grey-dimmed;
|
||||
font-variant-numeric: lining-nums;
|
||||
margin-left: -1px;
|
||||
display: flex;
|
||||
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,
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: darken($brand-grey-dimmed, 5%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&: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;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
border: 1px solid darken($brand-grey-dimmed, 5%);
|
||||
color: $brand-grey-light;
|
||||
color: $brand-grey-dimmed;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
:global(.dark) & {
|
||||
border-color: darken($brand-grey-light, 15%);
|
||||
color: $brand-grey-light;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import React, { ReactElement } from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import styles from './Pagination.module.scss'
|
||||
import shortid from 'shortid'
|
||||
import { PageContext } from '../../@types/Post'
|
||||
import Icon from '../atoms/Icon'
|
||||
|
||||
const PageNumber = ({
|
||||
i,
|
||||
@ -34,8 +36,12 @@ function PrevNext({
|
||||
const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
|
||||
|
||||
return (
|
||||
<Link to={link} rel={rel} title={title}>
|
||||
{prevPagePath ? '←' : '→'}
|
||||
<Link to={link} rel={rel} title={title} className={styles.number}>
|
||||
{prevPagePath ? (
|
||||
<Icon name="ChevronLeft" />
|
||||
) : (
|
||||
<Icon name="ChevronRight" />
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@ -43,13 +49,7 @@ function PrevNext({
|
||||
export default function Pagination({
|
||||
pageContext
|
||||
}: {
|
||||
pageContext: {
|
||||
slug: string
|
||||
currentPageNumber: number
|
||||
numPages: number
|
||||
prevPagePath?: string
|
||||
nextPagePath?: string
|
||||
}
|
||||
pageContext: PageContext
|
||||
}): ReactElement {
|
||||
const {
|
||||
slug,
|
||||
@ -63,18 +63,16 @@ export default function Pagination({
|
||||
|
||||
return (
|
||||
<div className={styles.pagination}>
|
||||
<div>{!isFirst && <PrevNext prevPagePath={prevPagePath} />}</div>
|
||||
<div>
|
||||
{Array.from({ length: numPages }, (_, i) => (
|
||||
<PageNumber
|
||||
key={shortid.generate()}
|
||||
i={i}
|
||||
slug={slug}
|
||||
current={currentPageNumber === i + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>{!isLast && <PrevNext nextPagePath={nextPagePath} />}</div>
|
||||
{!isFirst && <PrevNext prevPagePath={prevPagePath} />}
|
||||
{Array.from({ length: numPages }, (_, i) => (
|
||||
<PageNumber
|
||||
key={shortid.generate()}
|
||||
i={i}
|
||||
slug={slug}
|
||||
current={currentPageNumber === i + 1}
|
||||
/>
|
||||
))}
|
||||
{!isLast && <PrevNext nextPagePath={nextPagePath} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,25 +1,21 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.post {
|
||||
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;
|
||||
.title {
|
||||
padding-left: 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;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md) {
|
||||
font-size: $font-size-base;
|
||||
.post {
|
||||
display: block;
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,24 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import { Image } from '../atoms/Image'
|
||||
import styles from './PostTeaser.module.scss'
|
||||
import { Post } from '../../@types/Post'
|
||||
import PostTitle from '../templates/Post/Title'
|
||||
import styles from './PostTeaser.module.scss'
|
||||
|
||||
export const postTeaserQuery = graphql`
|
||||
fragment PostTeaser on MarkdownRemark {
|
||||
id
|
||||
fileAbsolutePath
|
||||
frontmatter {
|
||||
type
|
||||
title
|
||||
linkurl
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
}
|
||||
tags
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
@ -29,7 +33,7 @@ export default function PostTeaser({
|
||||
post: Partial<Post>
|
||||
toggleSearch?: () => void
|
||||
}): ReactElement {
|
||||
const { image, title } = post.frontmatter
|
||||
const { image, title, type } = post.frontmatter
|
||||
const { slug } = post.fields
|
||||
|
||||
return (
|
||||
@ -38,12 +42,16 @@ export default function PostTeaser({
|
||||
to={slug}
|
||||
onClick={toggleSearch && toggleSearch}
|
||||
>
|
||||
{image ? (
|
||||
<Image fluid={image.childImageSharp.fluid} alt={title} />
|
||||
) : (
|
||||
<div className={styles.empty} />
|
||||
{image && (
|
||||
<Image
|
||||
title={type === 'photo' ? title : null}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -47,16 +47,16 @@ function photosWithDataFilter(posts: []) {
|
||||
|
||||
export default function RelatedPosts({
|
||||
tags,
|
||||
photos
|
||||
isPhotos
|
||||
}: {
|
||||
tags: string[]
|
||||
photos?: boolean
|
||||
isPhotos?: boolean
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(query)
|
||||
const posts = data.allMarkdownRemark.edges
|
||||
|
||||
function getPosts() {
|
||||
return photos
|
||||
return isPhotos
|
||||
? photosWithDataFilter(posts)
|
||||
: tags && postsWithDataFilter(posts, 'tags', tags)
|
||||
}
|
||||
@ -71,7 +71,7 @@ export default function RelatedPosts({
|
||||
return (
|
||||
<aside className={styles.relatedPosts}>
|
||||
<h1 className={styles.title}>
|
||||
Related {photos ? 'Photos' : 'Posts'}{' '}
|
||||
Related {isPhotos ? 'Photos' : 'Posts'}{' '}
|
||||
<button className={styles.button} onClick={refreshPosts}>
|
||||
Refresh
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@ import { useSiteMetadata } from '../../hooks/use-site-metadata'
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
function Copyright() {
|
||||
const { name, uri, bitcoin, github } = useSiteMetadata().author
|
||||
const { name, uri, github } = useSiteMetadata().author
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
@ -24,7 +24,7 @@ function Copyright() {
|
||||
</a>
|
||||
<Link to="/thanks" className={styles.btc}>
|
||||
<Icon name="Bitcoin" />
|
||||
<code>{bitcoin}</code>
|
||||
Say Thanks
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
@ -37,7 +37,6 @@ export default class Footer extends PureComponent {
|
||||
<footer role="contentinfo" className={styles.footer}>
|
||||
<Container>
|
||||
<Vcard />
|
||||
|
||||
<Copyright />
|
||||
</Container>
|
||||
</footer>
|
||||
|
@ -1,39 +1,13 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.hentry {
|
||||
@include divider;
|
||||
.posts {
|
||||
display: grid;
|
||||
gap: $spacer * 2;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: $spacer * 2;
|
||||
padding-bottom: $spacer * 2;
|
||||
@media (min-width: $screen-sm) {
|
||||
@include breakoutviewport;
|
||||
|
||||
:global(.gatsby-image-wrapper) {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md) {
|
||||
padding-top: $spacer * 4;
|
||||
padding-bottom: $spacer * 4;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -1,84 +1,44 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import { Post } from '../../@types/Post'
|
||||
import { graphql } from 'gatsby'
|
||||
import { Post, PageContext } from '../../@types/Post'
|
||||
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 { Image } from '../atoms/Image'
|
||||
import PostTeaser from '../molecules/PostTeaser'
|
||||
import Page from './Page'
|
||||
|
||||
export default function Archive({
|
||||
data,
|
||||
pageContext
|
||||
}: {
|
||||
data: any
|
||||
pageContext: {
|
||||
tag: string
|
||||
slug: string
|
||||
currentPageNumber: number
|
||||
numPages: number
|
||||
}
|
||||
pageContext: PageContext
|
||||
}): ReactElement {
|
||||
const edges = data.allMarkdownRemark.edges
|
||||
const { tag, currentPageNumber, numPages } = pageContext
|
||||
|
||||
const PostsList = edges.map(({ node }: { node: Post }) => {
|
||||
const { type, linkurl, title, image } = node.frontmatter
|
||||
const { slug } = node.fields
|
||||
const PostsList = edges.map(({ node }: { node: Post }) => (
|
||||
<PostTeaser key={node.id} post={node} />
|
||||
))
|
||||
|
||||
return (
|
||||
<article className={styles.hentry} key={node.id}>
|
||||
<PostTitle type={type} slug={slug} linkurl={linkurl} title={title} />
|
||||
const title = tag ? `#${tag}` : 'Archive'
|
||||
|
||||
{image && (
|
||||
<Link to={slug} title={title}>
|
||||
<Image
|
||||
title={type === 'photo' ? title : null}
|
||||
fluid={image.childImageSharp.fluid}
|
||||
alt={title}
|
||||
original={image.childImageSharp.original}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
const paginationTitle =
|
||||
numPages > 1 && currentPageNumber > 1
|
||||
? `Page ${currentPageNumber} / ${numPages}`
|
||||
: ''
|
||||
|
||||
{type === 'article' && (
|
||||
<>
|
||||
<PostLead post={node} index />
|
||||
<PostMore to={slug}>Continue Reading</PostMore>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'link' && (
|
||||
<>
|
||||
<PostContent post={node} />
|
||||
<PostLinkActions slug={slug} linkurl={linkurl} />
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})
|
||||
const page = {
|
||||
frontmatter: {
|
||||
title: `${title} ${paginationTitle}`,
|
||||
description: 'All the articles & links.'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO />
|
||||
{tag && (
|
||||
<h1 className={styles.archivetitle}>
|
||||
<span>#</span>
|
||||
{tag}
|
||||
</h1>
|
||||
)}
|
||||
{numPages > 1 && currentPageNumber > 1 && (
|
||||
<h2
|
||||
className={styles.paginationTitle}
|
||||
>{`Page ${currentPageNumber} / ${numPages}`}</h2>
|
||||
)}
|
||||
{PostsList}
|
||||
<Page title={page.frontmatter.title} post={page}>
|
||||
<div className={styles.posts}>{PostsList}</div>
|
||||
{numPages > 1 && <Pagination pageContext={pageContext} />}
|
||||
</>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@ -92,21 +52,7 @@ export const archiveQuery = graphql`
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
html
|
||||
frontmatter {
|
||||
title
|
||||
type
|
||||
linkurl
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
...PostTeaser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,19 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.pageTitle {
|
||||
composes: archivetitle from './Archive.module.scss';
|
||||
margin-bottom: $spacer * 2;
|
||||
.pagetitle {
|
||||
@include divider;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export default function Page({
|
||||
<Helmet title={title} />
|
||||
<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}
|
||||
</>
|
||||
)
|
||||
|
@ -2,16 +2,14 @@
|
||||
@import 'mixins';
|
||||
|
||||
.photos {
|
||||
@include breakoutviewport--full;
|
||||
@include breakoutviewport;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $spacer;
|
||||
padding-left: $spacer;
|
||||
padding-right: $spacer;
|
||||
|
||||
@media (min-width: $screen-sm) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import Photos from '../photos'
|
||||
import Photos from './Photos'
|
||||
import data from '../../../jest/__fixtures__/photos.json'
|
||||
|
||||
describe('/photos', () => {
|
||||
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()
|
||||
})
|
||||
})
|
87
src/components/templates/Photos.tsx
Normal file
87
src/components/templates/Photos.tsx
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@ -10,7 +10,7 @@ const PostContent = ({ post }: { post: Post }): ReactElement => {
|
||||
|
||||
let content = post.html
|
||||
|
||||
if (post.frontmatter.type === 'post') {
|
||||
if (post.frontmatter.type === 'article') {
|
||||
if (content.includes(separator)) {
|
||||
content = content.split(separator)[1]
|
||||
} else {
|
||||
|
@ -6,10 +6,10 @@ import { Post } from '../../../@types/Post'
|
||||
// Grab everything before more tag, or just first paragraph
|
||||
const PostLead = ({
|
||||
post,
|
||||
index
|
||||
className
|
||||
}: {
|
||||
post: Post
|
||||
index?: boolean
|
||||
post: Partial<Post>
|
||||
className?: string
|
||||
}): ReactElement => {
|
||||
let lead
|
||||
const content = post.html
|
||||
@ -23,7 +23,7 @@ const PostLead = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={index ? styles.index : styles.lead}
|
||||
className={`${styles.lead} ${className && className}`}
|
||||
dangerouslySetInnerHTML={{ __html: lead }}
|
||||
/>
|
||||
)
|
||||
|
@ -38,7 +38,7 @@ export default function PostMeta({ post }: { post: Post }): ReactElement {
|
||||
{tags && (
|
||||
<div className={styles.tags}>
|
||||
{tags.map((tag: string) => {
|
||||
const url = `/tags/${slugify(tag)}/`
|
||||
const url = `/archive/${slugify(tag)}/`
|
||||
return <Tag key={shortid.generate()} name={tag} url={url} />
|
||||
})}
|
||||
</div>
|
||||
|
@ -1,30 +1,31 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import styles from './Title.module.scss'
|
||||
import Icon from '../../atoms/Icon'
|
||||
import Time from '../../atoms/Time'
|
||||
|
||||
export default function PostTitle({
|
||||
type,
|
||||
slug,
|
||||
linkurl,
|
||||
title,
|
||||
date,
|
||||
updated
|
||||
updated,
|
||||
className
|
||||
}: {
|
||||
type?: string
|
||||
slug?: string
|
||||
linkurl?: string
|
||||
title: string
|
||||
date?: string
|
||||
updated?: string
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
const linkHostname = linkurl ? new URL(linkurl).hostname : null
|
||||
|
||||
return type === 'link' ? (
|
||||
return linkurl ? (
|
||||
<>
|
||||
<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}`}>
|
||||
{title} <Icon name="ExternalLink" />
|
||||
@ -33,12 +34,14 @@ export default function PostTitle({
|
||||
<div className={styles.linkurl}>{linkHostname}</div>
|
||||
</>
|
||||
) : slug ? (
|
||||
<h1 className={styles.hentry__title}>
|
||||
<Link to={slug}>{title}</Link>
|
||||
<h1 className={`${styles.hentry__title} ${className && className}`}>
|
||||
{title}
|
||||
</h1>
|
||||
) : (
|
||||
<>
|
||||
<h1 className={styles.hentry__title}>{title}</h1>
|
||||
<h1 className={`${styles.hentry__title} ${className && className}`}>
|
||||
{title}
|
||||
</h1>
|
||||
{date && (
|
||||
<div className={styles.time}>
|
||||
{updated && 'published '}
|
||||
|
@ -40,35 +40,38 @@ export default function Post({
|
||||
<article className={styles.hentry}>
|
||||
<header>
|
||||
<PostTitle
|
||||
type={type}
|
||||
linkurl={linkurl}
|
||||
title={title}
|
||||
date={date}
|
||||
updated={updated}
|
||||
/>
|
||||
|
||||
{type === 'article' && <PostLead post={post} />}
|
||||
|
||||
{image && (
|
||||
<Image
|
||||
fluid={image.childImageSharp.fluid}
|
||||
alt={title}
|
||||
original={image.childImageSharp.original}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{type === 'photo' && <PostContent post={post} />}
|
||||
{image && (
|
||||
<Image
|
||||
fluid={image.childImageSharp.fluid}
|
||||
alt={title}
|
||||
original={image.childImageSharp.original}
|
||||
/>
|
||||
{type === 'photo' ? (
|
||||
<>
|
||||
<PostContent post={post} />
|
||||
{image && image.fields && <Exif exif={image.fields.exif} />}
|
||||
</>
|
||||
) : (
|
||||
<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} />}
|
||||
<PostMeta post={post} />
|
||||
<PostActions slug={slug} githubLink={githubLink} />
|
||||
</article>
|
||||
|
||||
<RelatedPosts photos={type === 'photo'} tags={tags} />
|
||||
<RelatedPosts isPhotos={type === 'photo'} tags={tags} />
|
||||
|
||||
<PrevNext prev={prev} next={next} />
|
||||
</>
|
||||
|
@ -1,4 +1,44 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,61 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { graphql, Link, PageProps } from 'gatsby'
|
||||
import Page from '../components/templates/Page'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
import { Post } from '../@types/Post'
|
||||
import { Image } from '../components/atoms/Image'
|
||||
import styles from './index.module.scss'
|
||||
import Featured from '../components/molecules/Featured'
|
||||
|
||||
const page = {
|
||||
frontmatter: {
|
||||
title: 'home',
|
||||
description: 'Blog of designer & developer Matthias Kretschmann.'
|
||||
}
|
||||
}
|
||||
import { PhotoThumb } from '../components/templates/Photos'
|
||||
import PostTeaser from '../components/molecules/PostTeaser'
|
||||
import PostMore from '../components/templates/Post/More'
|
||||
|
||||
export default function Home(props: PageProps): ReactElement {
|
||||
return (
|
||||
<Page title={page.frontmatter.title} post={page} section={styles.home}>
|
||||
<Featured />
|
||||
Latest Articles & Links
|
||||
<br />
|
||||
Latest Photos
|
||||
</Page>
|
||||
<>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.title}>Featured</h2>
|
||||
<Featured />
|
||||
</section>
|
||||
|
||||
<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`
|
||||
query {
|
||||
latestArticles: allMarkdownRemark(
|
||||
filter: { frontmatter: { type: { eq: "article" } } }
|
||||
filter: { frontmatter: { type: { ne: "photo" } } }
|
||||
sort: { order: DESC, fields: [fields___date] }
|
||||
limit: 5
|
||||
limit: 6
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
frontmatter {
|
||||
title
|
||||
type
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(
|
||||
maxWidth: 400
|
||||
maxHeight: 400
|
||||
quality: 85
|
||||
cropFocus: CENTER
|
||||
) {
|
||||
...GatsbyImageSharpFluid_withWebp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
...PostTeaser
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,7 +63,7 @@ export const homeQuery = graphql`
|
||||
latestPhotos: allMarkdownRemark(
|
||||
filter: { frontmatter: { type: { eq: "photo" } } }
|
||||
sort: { order: DESC, fields: [fields___date] }
|
||||
limit: 10
|
||||
limit: 15
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
@ -69,14 +73,7 @@ export const homeQuery = graphql`
|
||||
type
|
||||
image {
|
||||
childImageSharp {
|
||||
fluid(
|
||||
maxWidth: 400
|
||||
maxHeight: 400
|
||||
quality: 85
|
||||
cropFocus: CENTER
|
||||
) {
|
||||
...GatsbyImageSharpFluid_withWebp
|
||||
}
|
||||
...PhotoFluidThumb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
Loading…
Reference in New Issue
Block a user