1
0
Fork 0
* refactor

* fixes

* fixes

* fix

* package updates
This commit is contained in:
Matthias Kretschmann 2022-11-11 02:31:54 +00:00 committed by GitHub
parent 8d0a900d98
commit 0aaf874538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2107 additions and 2088 deletions

View File

@ -10,41 +10,6 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Cache node_modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-
- run: npm ci
- run: npm test
- uses: actions/upload-artifact@v3
with:
name: coverage
path: coverage/
coverage:
runs-on: ubuntu-latest
needs: [test]
if: ${{ success() && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: coverage
- uses: paambaati/codeclimate-action@v2.7.5
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -76,9 +41,15 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
env: env:
GATSBY_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GATSBY_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }} GATSBY_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }}
GATSBY_MAPBOX_ACCESS_TOKEN: ${{ secrets.GATSBY_MAPBOX_ACCESS_TOKEN }} GATSBY_MAPBOX_ACCESS_TOKEN: ${{ secrets.GATSBY_MAPBOX_ACCESS_TOKEN }}
- run: npm test
- uses: actions/upload-artifact@v3
with:
name: coverage
path: coverage/
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
@ -86,8 +57,21 @@ jobs:
name: public name: public
path: public path: public
coverage:
runs-on: ubuntu-latest
needs: [test]
if: ${{ success() && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: coverage
- uses: paambaati/codeclimate-action@v2.7.5
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
deploy: deploy:
needs: build needs: test
if: success() && github.ref == 'refs/heads/main' if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ plugins/gatsby-redirect-from
coverage coverage
.env .env
.env.development .env.development
.nova .nova
src/@types/Gatsby.d.ts

View File

@ -202,12 +202,6 @@ npm test
All test files live beside the respective component. Testing setup, fixtures, and mocks can be found in `./jest.config.js` and `./jest` folder. All test files live beside the respective component. Testing setup, fixtures, and mocks can be found in `./jest.config.js` and `./jest` folder.
For local development, run the test watcher:
```bash
npm run test:watch
```
### 🎈 Add a new post ### 🎈 Add a new post
```bash ```bash

View File

@ -1,4 +1,4 @@
module.exports = { export default {
siteTitle: 'kremalicious', siteTitle: 'kremalicious',
siteTitleShort: 'krlc', siteTitleShort: 'krlc',
siteDescription: 'Blog of designer & developer Matthias Kretschmann', siteDescription: 'Blog of designer & developer Matthias Kretschmann',

View File

@ -1,15 +0,0 @@
import './src/global/global.css'
import './src/global/imports.css'
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
export const wrapPageElement = wrapPageElementWithLayout
// Display a message when a service worker updates
// https://www.gatsbyjs.org/docs/add-offline-support-with-a-service-worker/#displaying-a-message-when-a-service-worker-updates
export const onServiceWorkerUpdateReady = () => {
const div = document.createElement('div')
div.id = 'toast'
div.classList.add('alert', 'alert-info')
div.innerHTML = `<button onClick="window.location.reload()">This application has been updated. <span>Click to Reload</span>.</button>`
document.body.append(div)
}

18
gatsby-browser.tsx Normal file
View File

@ -0,0 +1,18 @@
import { GatsbyBrowser } from 'gatsby'
import './src/global/global.css'
import './src/global/imports.css'
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
export const wrapPageElement: GatsbyBrowser['wrapPageElement'] =
wrapPageElementWithLayout
// Display a message when a service worker updates
// https://www.gatsbyjs.org/docs/add-offline-support-with-a-service-worker/#displaying-a-message-when-a-service-worker-updates
export const onServiceWorkerUpdateReady: GatsbyBrowser['onServiceWorkerUpdateReady'] =
() => {
const div = document.createElement('div')
div.id = 'toast'
div.classList.add('alert', 'alert-info')
div.innerHTML = `<button onClick="window.location.reload()">This application has been updated. <span>Click to Reload</span>.</button>`
document.body.append(div)
}

View File

@ -1,23 +1,30 @@
require('dotenv').config() import type { GatsbyConfig } from 'gatsby'
import * as dotenv from 'dotenv'
if (!process.env.GATSBY_GITHUB_TOKEN) { dotenv.config()
if (!process.env.GITHUB_TOKEN) {
// eslint-disable-next-line // eslint-disable-next-line
console.warn(` console.warn(`
A GitHub token as GATSBY_GITHUB_TOKEN is required to build some parts of the blog. A GitHub token as GITHUB_TOKEN is required to build some parts of the blog.
Check the README https://github.com/kremalicious/blog#-development. Check the README https://github.com/kremalicious/blog#-development.
`) `)
} }
const siteConfig = require('./config') import siteConfig from './config'
const sources = require('./gatsby/sources') import sources from './gatsby/sources'
const { feedContent } = require('./gatsby/feeds') import { feedContent } from './gatsby/feeds'
// required for gatsby-plugin-meta-redirect // required for gatsby-plugin-meta-redirect
require('regenerator-runtime/runtime') import 'regenerator-runtime/runtime'
module.exports = { const config: GatsbyConfig = {
graphqlTypegen: {
typesOutputPath: './src/@types/Gatsby.d.ts',
generateOnBuild: true
},
siteMetadata: { siteMetadata: {
...siteConfig ...siteConfig
}, },
@ -169,44 +176,44 @@ module.exports = {
`, `,
feeds: [ feeds: [
{ {
serialize: ({ query: { allMarkdownRemark } }) => { serialize: ({ query }: { query: Queries.AllContentFeedQuery }) => {
return allMarkdownRemark.edges.map((edge) => { return query.allMarkdownRemark.edges.map((edge) => {
return Object.assign({}, edge.node.frontmatter, { return Object.assign({}, edge.node.frontmatter, {
title: edge.node.frontmatter.title, title: edge.node.frontmatter?.title,
date: edge.node.fields.date, date: edge.node.fields?.date,
description: edge.node.excerpt, description: edge.node.excerpt,
url: siteConfig.siteUrl + edge.node.fields.slug, url: siteConfig.siteUrl + edge.node.fields?.slug,
categories: edge.node.frontmatter.tags, categories: edge.node.frontmatter?.tags,
author: siteConfig.author.name, author: siteConfig.author.name,
guid: siteConfig.siteUrl + edge.node.fields.slug, guid: siteConfig.siteUrl + edge.node.fields?.slug,
custom_elements: [{ 'content:encoded': feedContent(edge) }] custom_elements: [{ 'content:encoded': feedContent(edge) }]
}) })
}) })
}, },
query: `{ query: `{
allMarkdownRemark(sort: {fields: {date: DESC}}, limit: 40) { allMarkdownRemark(sort: {fields: {date: DESC}}, limit: 40) {
edges { edges {
node { node {
html html
fields { fields {
slug slug
date date
} }
excerpt excerpt
frontmatter { frontmatter {
title title
image { image {
childImageSharp { childImageSharp {
resize(width: 940, quality: 85) { resize(width: 940, quality: 85) {
src src
}
}
}
}
}
}
} }
} }`,
}
}
}
}
}
}`,
output: '/feed.xml', output: '/feed.xml',
title: siteConfig.siteTitle title: siteConfig.siteTitle
} }
@ -226,3 +233,5 @@ module.exports = {
'gatsby-plugin-offline' 'gatsby-plugin-offline'
] ]
} }
export default config

View File

@ -1,155 +0,0 @@
const { createMarkdownFields } = require('./gatsby/createMarkdownFields')
const { createExif } = require('./gatsby/createExif')
const {
generatePostPages,
generateTagPages,
generateRedirectPages,
generateArchivePages,
generatePhotosPages
} = require('./gatsby/createPages')
const { generateJsonFeed } = require('./gatsby/feeds')
exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => {
// Markdown files
if (node.internal.type === 'MarkdownRemark') {
createMarkdownFields(node, actions, getNode)
}
// Image files
if (node.internal.mediaType === 'image/jpeg') {
createExif(node, actions, createNodeId)
}
}
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage, createRedirect } = actions
const result = await graphql(`
{
all: allMarkdownRemark(sort: { fields: { date: DESC } }) {
edges {
next {
fields {
slug
}
frontmatter {
title
}
}
node {
fields {
slug
}
frontmatter {
tags
}
}
previous {
fields {
slug
}
frontmatter {
title
}
}
}
}
photos: allMarkdownRemark(filter: { fields: { type: { eq: "photo" } } }) {
edges {
node {
id
}
}
}
archive: allMarkdownRemark(
filter: { fields: { type: { nin: "photo" } } }
) {
edges {
node {
id
}
}
}
tags: allMarkdownRemark {
group(field: { frontmatter: { tags: SELECT } }) {
tag: fieldValue
totalCount
}
}
}
`)
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
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 post pages
generatePostPages(createPage, all)
// Generate photos archive pages
generatePhotosPages(createPage, photosLength)
// Generate tag pages
generateTagPages(createPage, tags)
// Generate archive pages
generateArchivePages(createPage, archiveLength)
// Create manual redirects
generateRedirectPages(createRedirect)
}
exports.onPostBuild = async ({ graphql }) => {
// JSON Feed query
const result = await graphql(`
{
allMarkdownRemark(sort: { fields: { date: DESC } }) {
edges {
node {
html
fields {
slug
date
}
excerpt
frontmatter {
title
tags
updated
image {
childImageSharp {
resize(width: 940, quality: 85) {
src
}
}
}
}
}
}
}
}
`)
if (result.errors) throw result.errors
// Generate json feed
await generateJsonFeed(result.data.allMarkdownRemark.edges)
return Promise.resolve()
}
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
resolve: {
fallback: {
util: false
}
}
})
}

171
gatsby-node.ts Normal file
View File

@ -0,0 +1,171 @@
import { createMarkdownFields } from './gatsby/createMarkdownFields'
import { createExif } from './gatsby/createExif'
import {
generatePostPages,
generateTagPages,
generateRedirectPages,
generateArchivePages,
generatePhotosPages
} from './gatsby/createPages'
import { generateJsonFeed } from './gatsby/feeds'
import type { GatsbyNode } from 'gatsby'
export const onCreateNode: GatsbyNode['onCreateNode'] = ({
node,
actions,
getNode,
createNodeId
}) => {
// Markdown files
if (node.internal.type === 'MarkdownRemark') {
createMarkdownFields(node, actions, getNode)
}
// Image files
if (node.internal.mediaType === 'image/jpeg') {
createExif(node, actions, createNodeId)
}
}
export const createPages: GatsbyNode['createPages'] = async ({
graphql,
actions,
reporter
}) => {
const { createPage, createRedirect } = actions
const result: { data?: Queries.AllContentQuery; errors?: any } =
await graphql(`
query AllContent {
all: allMarkdownRemark(sort: { fields: { date: DESC } }) {
edges {
next {
fields {
slug
}
frontmatter {
title
}
}
node {
fields {
slug
}
frontmatter {
tags
}
}
previous {
fields {
slug
}
frontmatter {
title
}
}
}
}
photos: allMarkdownRemark(
filter: { fields: { type: { eq: "photo" } } }
) {
edges {
node {
id
}
}
}
archive: allMarkdownRemark(
filter: { fields: { type: { nin: "photo" } } }
) {
edges {
node {
id
}
}
}
tags: allMarkdownRemark {
group(field: { frontmatter: { tags: SELECT } }) {
tag: fieldValue
totalCount
}
}
}
`)
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
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 post pages
generatePostPages(createPage, all)
// Generate photos archive pages
generatePhotosPages(createPage, photosLength)
// Generate tag pages
generateTagPages(createPage, tags)
// Generate archive pages
generateArchivePages(createPage, archiveLength)
// Create manual redirects
generateRedirectPages(createRedirect)
}
export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql }) => {
// JSON Feed query
const result: { data?: Queries.AllContentFeedQuery; errors?: any } =
await graphql(`
query AllContentFeed {
allMarkdownRemark(sort: { fields: { date: DESC } }) {
edges {
node {
html
fields {
slug
date
}
excerpt
frontmatter {
title
tags
updated
image {
childImageSharp {
resize(width: 940, quality: 85) {
src
}
}
}
}
}
}
}
}
`)
if (result.errors) throw result.errors
// Generate json feed
await generateJsonFeed(result?.data?.allMarkdownRemark.edges)
return Promise.resolve()
}
export const onCreateWebpackConfig: GatsbyNode['onCreateWebpackConfig'] = ({
actions
}) => {
actions.setWebpackConfig({
resolve: {
fallback: {
util: false
}
}
})
}

View File

@ -1,2 +0,0 @@
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
export const wrapPageElement = wrapPageElementWithLayout

5
gatsby-ssr.tsx Normal file
View File

@ -0,0 +1,5 @@
import { GatsbySSR } from 'gatsby'
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
export const wrapPageElement: GatsbySSR['wrapPageElement'] =
wrapPageElementWithLayout

View File

@ -1,43 +1,50 @@
const fs = require('fs') import fs from 'fs'
const util = require('util') import util from 'util'
const fastExif = require('fast-exif') import fastExif from 'fast-exif'
const Fraction = require('fraction.js') import Fraction from 'fraction.js'
const getCoordinates = require('dms2dec') import getCoordinates from 'dms2dec'
const iptc = require('node-iptc') import iptc from 'node-iptc'
import type { Actions, NodePluginArgs, Node } from 'gatsby'
const readFile = util.promisify(fs.readFile) const readFile = util.promisify(fs.readFile)
exports.createExif = async (node, actions, createNodeId) => { export const createExif = async (
node: Node,
actions: Actions,
createNodeId: NodePluginArgs['createNodeId']
) => {
try { try {
// exif // exif
const exifData = await fastExif.read(node.absolutePath, true) const exifData = await fastExif.read(node.absolutePath, true)
if (!exifData) return if (!exifData) return
// iptc // iptc
const file = await readFile(node.absolutePath) const file = await readFile(node.absolutePath as string)
const iptcData = iptc(file) const iptcData = iptc(file)
createNodes(exifData, iptcData, node, actions, createNodeId) createNodes(exifData, iptcData, node, actions, createNodeId)
} catch (error) { } catch (error: any) {
console.error(`${node.name}: ${error.message}`) console.error(`${node.name}: ${error.message}`)
} }
} }
function createNodes(exifData, iptcData, node, actions, createNodeId) { function createNodes(
exifData: Queries.ImageExif,
iptcData: any,
node: Node,
actions: Actions,
createNodeId: NodePluginArgs['createNodeId']
) {
const { createNodeField, createNode, createParentChildLink } = actions const { createNodeField, createNode, createParentChildLink } = actions
const exifDataFormatted = formatExif(exifData) const exifDataFormatted = formatExif(exifData)
const exif = { const exif = {
...exifData, ...exifData,
iptc: { iptc: { ...iptcData },
...iptcData formatted: { ...exifDataFormatted }
},
formatted: {
...exifDataFormatted
}
} }
const exifNode = { const exifNode: any = {
id: createNodeId(`${node.id} >> ImageExif`), id: createNodeId(`${node.id} >> ImageExif`),
children: [], children: [],
...exif, ...exif,
@ -49,22 +56,15 @@ function createNodes(exifData, iptcData, node, actions, createNodeId) {
} }
// add exif fields to existing type file // add exif fields to existing type file
createNodeField({ createNodeField({ node, name: 'exif', value: exif })
node,
name: 'exif',
value: exif
})
// create new nodes from all exif data // create new nodes from all exif data
// allowing to be queried with imageExif & AllImageExif // allowing to be queried with imageExif & AllImageExif
createNode(exifNode) createNode(exifNode)
createParentChildLink({ createParentChildLink({ parent: node, child: exifNode })
parent: node,
child: exifNode
})
} }
function formatExif(exifData) { function formatExif(exifData: Queries.ImageExif) {
if (!exifData.exif) return if (!exifData.exif) return
const { Model } = exifData.image const { Model } = exifData.image
@ -107,8 +107,11 @@ function formatExif(exifData) {
} }
} }
function formatGps(gpsData) { function formatGps(gpsData: Queries.ImageExif['gps']): {
if (!gpsData) return latitude: string
longitude: string
} {
if (!gpsData) return { latitude: '', longitude: '' }
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
@ -125,7 +128,7 @@ function formatGps(gpsData) {
return { latitude, longitude } return { latitude, longitude }
} }
function formatExposure(exposureMode) { function formatExposure(exposureMode: Queries.ImageExifExif['ExposureMode']) {
if (exposureMode === null || exposureMode === undefined) return if (exposureMode === null || exposureMode === undefined) return
const exposureShortened = parseFloat(exposureMode.toFixed(2)) const exposureShortened = parseFloat(exposureMode.toFixed(2))

View File

@ -1,62 +0,0 @@
const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const { repoContentPath } = require('../config')
// Create slug, date & github file link for posts from file path values
exports.createMarkdownFields = (node, actions, getNode) => {
const { createNodeField } = actions
const fileNode = getNode(node.parent)
const parsedFilePath = path.parse(fileNode.relativePath)
const slugOriginal = createFilePath({ node, getNode })
createSlug(node, createNodeField, parsedFilePath)
createDate(node, createNodeField, slugOriginal)
// github file link
const type = fileNode.sourceInstanceName
const file = fileNode.relativePath
const githubLink = `${repoContentPath}/${type}/${file}`
createNodeField({
node,
name: 'githubLink',
value: githubLink
})
createNodeField({
node,
name: 'type',
value: type.replace('s', '')
})
}
function createSlug(node, createNodeField, parsedFilePath) {
let slug
if (parsedFilePath.name === 'index') {
slug = `/${parsedFilePath.dir.substring(11)}` // remove date from file dir
} else {
slug = `/${parsedFilePath.name.substring(11)}` // remove date from file path
}
createNodeField({
node,
name: 'slug',
value: slug
})
}
function createDate(node, createNodeField, slugOriginal) {
// grab date from file path
let date = new Date(slugOriginal.substring(1, 11)).toISOString() // grab date from file path
if (node.frontmatter.date) {
date = new Date(node.frontmatter.date).toISOString()
}
createNodeField({
node,
name: 'date',
value: date
})
}

View File

@ -0,0 +1,72 @@
import { parse } from 'path'
import { createFilePath } from 'gatsby-source-filesystem'
import config from '../config'
import { Actions, Node, NodePluginArgs } from 'gatsby'
// Create slug, date & github file link for posts from file path values
export function createMarkdownFields(
node: Node,
actions: Actions,
getNode: NodePluginArgs['getNode']
) {
const { createNodeField } = actions
const fileNode = getNode(node.parent as string)
const parsedFilePath = parse(fileNode?.relativePath as string)
const slugOriginal = createFilePath({ node, getNode })
createSlug(node, createNodeField, parsedFilePath)
createDate(node, createNodeField, slugOriginal)
// github file link
const type = fileNode?.sourceInstanceName as string
const file = fileNode?.relativePath as string
const githubLink = `${config.repoContentPath}/${type}/${file}`
createNodeField({
node,
name: 'githubLink',
value: githubLink
})
createNodeField({
node,
name: 'type',
value: type?.replace('s', '')
})
}
function createSlug(
node: Node,
createNodeField: Actions['createNodeField'],
parsedFilePath: { name: string; dir: string }
) {
let slug
if (parsedFilePath.name === 'index') {
slug = `/${parsedFilePath.dir.substring(11)}` // remove date from file dir
} else {
slug = `/${parsedFilePath.name.substring(11)}` // remove date from file path
}
createNodeField({
node,
name: 'slug',
value: slug
})
}
function createDate(
node: Node,
createNodeField: Actions['createNodeField'],
slugOriginal: string
) {
// grab date from file path
let date = new Date(slugOriginal.substring(1, 11)).toISOString() // grab date from file path
// allow date overwrite in frontmatter
if ((node.frontmatter as any).date) {
date = new Date((node.frontmatter as any).date).toISOString()
}
createNodeField({ node, name: 'date', value: date })
}

View File

@ -1,5 +1,6 @@
const path = require('path') import path from 'path'
const { itemsPerPage } = require('../config') import config from '../config'
import { Actions } from 'gatsby'
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')
@ -11,7 +12,7 @@ const redirects = [
{ f: '/goodies/', t: '/archive/goodies/' } { f: '/goodies/', t: '/archive/goodies/' }
] ]
function getPaginationData(i, numPages, slug) { function getPaginationData(i: number, numPages: number, slug: string) {
const currentPage = i + 1 const currentPage = i + 1
const prevPageNumber = currentPage <= 1 ? null : currentPage - 1 const prevPageNumber = currentPage <= 1 ? null : currentPage - 1
const nextPageNumber = currentPage + 1 > numPages ? null : currentPage + 1 const nextPageNumber = currentPage + 1 > numPages ? null : currentPage + 1
@ -26,29 +27,38 @@ function getPaginationData(i, numPages, slug) {
return { prevPagePath, nextPagePath, path } return { prevPagePath, nextPagePath, path }
} }
exports.generatePostPages = (createPage, posts) => { export const generatePostPages = (
createPage: Actions['createPage'],
posts: Queries.AllContentQuery['all']['edges'] | undefined
) => {
// Create Post pages // Create Post pages
posts.forEach((post) => { posts?.forEach((post) => {
createPage({ createPage({
path: `${post.node.fields.slug}`, path: `${post.node.fields?.slug}`,
component: postTemplate, component: postTemplate,
context: { context: {
slug: post.node.fields.slug, slug: post.node.fields?.slug,
prev: post.previous && { prev: post.previous && {
title: post.previous.frontmatter.title, title: post.previous.frontmatter?.title,
slug: post.previous.fields.slug slug: post.previous.fields?.slug
}, },
next: post.next && { next: post.next && {
title: post.next.frontmatter.title, title: post.next.frontmatter?.title,
slug: post.next.fields.slug slug: post.next.fields?.slug
} }
} }
}) })
}) })
} }
function generateIndexPages(createPage, length, slug, template, tag) { function generateIndexPages(
const numPages = Math.ceil(length / itemsPerPage) createPage: Actions['createPage'],
length: number,
slug: string,
template: string,
tag?: string
) {
const numPages = Math.ceil(length / config.itemsPerPage)
Array.from({ length: numPages }).forEach((_, i) => { Array.from({ length: numPages }).forEach((_, i) => {
const { prevPagePath, nextPagePath, path } = getPaginationData( const { prevPagePath, nextPagePath, path } = getPaginationData(
@ -62,8 +72,8 @@ function generateIndexPages(createPage, length, slug, template, tag) {
component: template, component: template,
context: { context: {
slug, slug,
limit: itemsPerPage, limit: config.itemsPerPage,
skip: i * itemsPerPage, skip: i * config.itemsPerPage,
numPages: numPages, numPages: numPages,
currentPageNumber: i + 1, currentPageNumber: i + 1,
prevPagePath, prevPagePath,
@ -75,29 +85,44 @@ function generateIndexPages(createPage, length, slug, template, tag) {
} }
// Create paginated archive pages // Create paginated archive pages
exports.generateArchivePages = (createPage, archiveLength) => { export const generateArchivePages = (
createPage: Actions['createPage'],
archiveLength: number | undefined
) => {
if (!archiveLength) return
generateIndexPages(createPage, archiveLength, `/archive/`, archiveTemplate) generateIndexPages(createPage, archiveLength, `/archive/`, archiveTemplate)
} }
// Create paginated photos pages // Create paginated photos pages
exports.generatePhotosPages = (createPage, photosLength) => { export const generatePhotosPages = (
createPage: Actions['createPage'],
photosLength: number | undefined
) => {
if (!photosLength) return
generateIndexPages(createPage, photosLength, `/photos/`, photosTemplate) generateIndexPages(createPage, photosLength, `/photos/`, photosTemplate)
} }
// Create paginated tag pages // Create paginated tag pages
exports.generateTagPages = (createPage, tags) => { export const generateTagPages = (
createPage: Actions['createPage'],
tags: Queries.AllContentQuery['tags']['group'] | undefined
) => {
if (!tags) return
tags.forEach(({ tag, totalCount }) => { tags.forEach(({ tag, totalCount }) => {
generateIndexPages( generateIndexPages(
createPage, createPage,
totalCount, totalCount,
`/archive/${tag}/`, `/archive/${tag}/`,
archiveTemplate, archiveTemplate,
tag tag || ''
) )
}) })
} }
exports.generateRedirectPages = (createRedirect) => { export const generateRedirectPages = (
createRedirect: Actions['createRedirect']
) => {
redirects.forEach(({ f, t }) => { redirects.forEach(({ f, t }) => {
createRedirect({ createRedirect({
fromPath: f, fromPath: f,

View File

@ -1,67 +0,0 @@
const fs = require('fs')
const util = require('util')
const path = require('path')
const { siteUrl, siteTitle, siteDescription, author } = require('../config')
const writeFile = util.promisify(fs.writeFile)
const feedContent = (edge) => {
const { image } = edge.node.frontmatter
const { html } = edge.node
const footer =
'<hr />This post was published on <a href="https://kremalicious.com">kremalicious.com</a>'
return image
? `<img src="${image.childImageSharp.resize.src}" /><br />${html}${footer}`
: `${html}${footer}`
}
async function jsonItems(posts) {
return await posts.map((edge) => {
const { frontmatter, fields, excerpt } = edge.node
const { slug, date } = fields
return {
id: path.join(siteUrl, slug),
url: path.join(siteUrl, slug),
title: frontmatter.title,
summary: excerpt,
date_published: new Date(date).toISOString(),
date_modified: frontmatter.updated
? new Date(frontmatter.updated).toISOString()
: new Date(date).toISOString(),
tags: [frontmatter.tags],
content_html: feedContent(edge)
}
})
}
const createJsonFeed = async (posts) => ({
version: 'https://jsonfeed.org/version/1',
title: siteTitle,
description: siteDescription,
home_page_url: siteUrl,
feed_url: path.join(siteUrl, 'feed.json'),
user_comment:
'This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — https://kremalicious.com/feed.json — and add it your reader.',
favicon: path.join(siteUrl, 'favicon.ico'),
icon: path.join(siteUrl, 'apple-touch-icon.png'),
author: {
name: author.name,
url: author.uri
},
items: await jsonItems(posts)
})
const generateJsonFeed = async (posts) => {
await writeFile(
path.join('./public', 'feed.json'),
JSON.stringify(await createJsonFeed(posts)),
'utf8'
).catch((err) => {
throw Error('\nFailed to write JSON Feed file: ', err)
})
console.log('\nsuccess Generating JSON feed')
}
module.exports = { generateJsonFeed, feedContent }

77
gatsby/feeds.ts Normal file
View File

@ -0,0 +1,77 @@
import fs from 'fs'
import util from 'util'
import path from 'path'
import config from '../config'
const writeFile = util.promisify(fs.writeFile)
const feedContent = (
edge: Queries.AllContentFeedQuery['allMarkdownRemark']['edges'][0]
) => {
const { html, frontmatter } = edge.node
const footer =
'<hr />This post was published on <a href="https://kremalicious.com">kremalicious.com</a>'
return frontmatter?.image
? `<img src="${frontmatter?.image?.childImageSharp?.resize?.src}" /><br />${html}${footer}`
: `${html}${footer}`
}
async function jsonItems(
posts: Queries.AllContentFeedQuery['allMarkdownRemark']['edges']
) {
return posts.map((edge) => {
const { frontmatter, fields, excerpt } = edge.node
if (!fields?.slug || !fields?.date) return
return {
id: path.join(config.siteUrl, fields.slug),
url: path.join(config.siteUrl, fields.slug),
title: frontmatter?.title,
summary: excerpt,
date_published: new Date(fields.date).toISOString(),
date_modified: frontmatter?.updated
? new Date(frontmatter?.updated).toISOString()
: new Date(fields.date).toISOString(),
tags: [frontmatter?.tags],
content_html: feedContent(edge)
}
})
}
const createJsonFeed = async (
posts: Queries.AllContentFeedQuery['allMarkdownRemark']['edges']
) => ({
version: 'https://jsonfeed.org/version/1',
title: config.siteTitle,
description: config.siteDescription,
home_page_url: config.siteUrl,
feed_url: path.join(config.siteUrl, 'feed.json'),
user_comment:
'This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — https://kremalicious.com/feed.json — and add it your reader.',
favicon: path.join(config.siteUrl, 'favicon.ico'),
icon: path.join(config.siteUrl, 'apple-touch-icon.png'),
author: {
name: config.author.name,
url: config.author.uri
},
items: await jsonItems(posts)
})
const generateJsonFeed = async (
posts: Queries.AllContentFeedQuery['allMarkdownRemark']['edges'] | undefined
) => {
if (!posts) return
await writeFile(
path.join('./public', 'feed.json'),
JSON.stringify(await createJsonFeed(posts)),
'utf8'
).catch((err) => {
throw Error('\nFailed to write JSON Feed file: ', err)
})
console.log('\nsuccess Generating JSON feed')
}
export { generateJsonFeed, feedContent }

View File

@ -1,6 +1,6 @@
const path = require('path') import path from 'path'
module.exports = [ export default [
{ {
resolve: 'gatsby-source-filesystem', resolve: 'gatsby-source-filesystem',
options: { options: {
@ -43,7 +43,7 @@ module.exports = [
fieldName: 'github', fieldName: 'github',
url: 'https://api.github.com/graphql', url: 'https://api.github.com/graphql',
headers: { headers: {
Authorization: `bearer ${process.env.GATSBY_GITHUB_TOKEN}` Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
} }
// Additional options to pass to node-fetch // Additional options to pass to node-fetch
// fetchOptions: {}, // fetchOptions: {},

2395
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,18 @@
"description": "Blog of Designer & Developer Matthias Kretschmann", "description": "Blog of Designer & Developer Matthias Kretschmann",
"homepage": "https://kremalicious.com", "homepage": "https://kremalicious.com",
"license": "MIT", "license": "MIT",
"main": "index.js",
"scripts": { "scripts": {
"start": "gatsby develop --host 0.0.0.0", "start": "gatsby develop --host 0.0.0.0",
"build": "gatsby build", "build": "gatsby build",
"ssr": "npm run build && serve -s public/", "ssr": "npm run build && serve -s public/",
"test": "npm run lint && jest -c .jest/jest.config.js --coverage --silent", "test": "npm run lint && npm run type-check && npm run jest",
"test:watch": "npm run lint && jest -c .jest/jest.config.js --coverage --watch", "jest": "jest -c .jest/jest.config.js --coverage --silent",
"lint": "run-p --continue-on-error lint:js lint:css lint:md", "lint": "run-p --continue-on-error lint:js lint:css lint:md",
"lint:js": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.tsx .", "lint:js": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:css": "stylelint 'src/**/*.css'", "lint:css": "stylelint 'src/**/*.css'",
"lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'", "lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'",
"format": "prettier --ignore-path .gitignore --write '**/*.{js,jsx,ts,tsx,md,json,css}'", "format": "prettier --ignore-path .gitignore --write '**/*.{js,jsx,ts,tsx,md,json,css}'",
"tsc": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "./scripts/deploy-s3.sh", "deploy:s3": "./scripts/deploy-s3.sh",
"new": "ts-node scripts/new.ts" "new": "ts-node scripts/new.ts"
}, },
@ -29,16 +28,16 @@
], ],
"dependencies": { "dependencies": {
"@kremalicious/react-feather": "^2.1.0", "@kremalicious/react-feather": "^2.1.0",
"@rainbow-me/rainbowkit": "^0.7.0", "@rainbow-me/rainbowkit": "^0.7.4",
"axios": "^0.27.2", "axios": "^1.1.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"dms2dec": "^1.1.0", "dms2dec": "^1.1.0",
"ethers": "^5.7.1", "ethers": "^5.7.2",
"fast-exif": "^1.0.1", "fast-exif": "^1.0.1",
"feather-icons": "^4.29.0", "feather-icons": "^4.29.0",
"fraction.js": "^4.2.0", "fraction.js": "^4.2.0",
"gatsby": "^5.0.0", "gatsby": "^5.0.1",
"gatsby-plugin-catch-links": "^5.0.0", "gatsby-plugin-catch-links": "^5.0.0",
"gatsby-plugin-feed": "^5.0.0", "gatsby-plugin-feed": "^5.0.0",
"gatsby-plugin-image": "^3.0.0", "gatsby-plugin-image": "^3.0.0",
@ -63,7 +62,7 @@
"gatsby-transformer-remark": "^6.0.0", "gatsby-transformer-remark": "^6.0.0",
"gatsby-transformer-sharp": "^5.0.0", "gatsby-transformer-sharp": "^5.0.0",
"nord-visual-studio-code": "github:arcticicestudio/nord-visual-studio-code", "nord-visual-studio-code": "github:arcticicestudio/nord-visual-studio-code",
"pigeon-maps": "^0.21.0", "pigeon-maps": "^0.21.3",
"pigeon-marker": "^0.3.4", "pigeon-marker": "^0.3.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-clipboard.js": "^2.0.16", "react-clipboard.js": "^2.0.16",
@ -79,11 +78,11 @@
"wagmi": "^0.6.6" "wagmi": "^0.6.6"
}, },
"devDependencies": { "devDependencies": {
"@svgr/webpack": "^6.3.1", "@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/jest": "^29.0.3", "@types/jest": "^29.2.2",
"@types/lunr": "^2.3.4", "@types/lunr": "^2.3.4",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
@ -103,17 +102,17 @@
"eslint-plugin-testing-library": "^5.9.1", "eslint-plugin-testing-library": "^5.9.1",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.3.0", "jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.0", "jest-environment-jsdom": "^29.3.1",
"markdownlint-cli": "^0.32.2", "markdownlint-cli": "^0.32.2",
"node-iptc": "^1.0.5", "node-iptc": "^1.0.5",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^6.1.2", "ora": "^6.1.2",
"postcss": "^8.4.18", "postcss": "^8.4.19",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"stylelint": "^14.14.1", "stylelint": "^14.14.1",
"stylelint-config-css-modules": "^4.1.0", "stylelint-config-css-modules": "^4.1.0",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0", "stylelint-config-standard": "^29.0.0",
"stylelint-prettier": "^2.0.0", "stylelint-prettier": "^2.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

View File

@ -1,25 +0,0 @@
export interface GitHubRepo {
name: string
url: string
owner: {
login: string
}
object: {
id: string
text: string
}
}
export interface GitHub {
github: {
viewer: {
repositories: {
edges: [
{
node: GitHubRepo
}
]
}
}
}
}

31
src/@types/Image.d.ts vendored
View File

@ -1,36 +1,7 @@
import { GatsbyImageProps, IGatsbyImageData } from 'gatsby-plugin-image' import { GatsbyImageProps } from 'gatsby-plugin-image'
export interface ImageProps extends GatsbyImageProps { export interface ImageProps extends GatsbyImageProps {
title?: string title?: string
original?: { src: string } original?: { src: string }
className?: string className?: string
} }
export interface ImageNode extends IGatsbyImageData {
fields?: {
exif?: Exif
}
}
export interface ExifFormatted {
iso: string
model: string
fstop: string
shutterspeed: string
focalLength: string
lensModel: string
exposure: string
gps: {
latitude: string
longitude: string
}
}
export interface Exif {
formatted: ExifFormatted
exif?: any
image?: any
thumbnail?: any
gps?: any
iptc?: any
}

35
src/@types/Post.d.ts vendored
View File

@ -1,38 +1,3 @@
import { ImageNode } from './Image'
export interface Fields {
slug: string
date: string
type: 'article' | 'photo' | 'link'
githubLink?: string
}
export interface Frontmatter {
title: string
description?: string
image?: ImageNode
author?: string
updated?: string
tags?: string[]
linkurl?: string
style?: {
publicURL?: string
}
changelog?: string
toc?: boolean
}
export interface Post {
id?: string
html?: string
excerpt?: string
frontmatter: Frontmatter
fields?: Fields
rawMarkdownBody?: string
fileAbsolutePath?: string
tableOfContents?: string
}
export interface PageContext { export interface PageContext {
tag?: string tag?: string
slug: string slug: string

31
src/@types/Site.d.ts vendored
View File

@ -1,31 +0,0 @@
export interface MenuItem {
title: string
link: string
}
export interface Author {
name: string
email: string
uri: string
twitter: string
github: string
bitcoin: string
ether: string
}
export interface Site {
siteTitle: string
siteTitleShort: string
siteDescription: string
siteUrl: string
author: Author
menu: MenuItem[]
rss: string
jsonfeed: string
itemsPerPage: number
repoContentPath: string
darkModeConfig: {
classNameDark: string
classNameLight: string
}
}

5
src/@types/css.d.ts vendored
View File

@ -1,4 +1 @@
declare module '*.module.css' { declare module '*.module.css'
const classes: { [key: string]: string }
export default classes
}

View File

@ -3,3 +3,12 @@ declare module 'pigeon-marker'
declare module 'unified' declare module 'unified'
declare module 'fast-exif' declare module 'fast-exif'
declare module 'node-iptc' declare module 'node-iptc'
declare module 'dms2dec' {
export default function dms2dec(
lat: readonly number[],
latRef: string,
lon: readonly number[],
lonRef: string
): [latDec: string, lonDec: string]
}

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'
import Typekit from './atoms/Typekit' import Typekit from './atoms/Typekit'
import Header from './organisms/Header' import Header from './organisms/Header'
import Footer from './organisms/Footer' import Footer from './organisms/Footer'
import { document, content } from './Layout.module.css' import * as styles from './Layout.module.css'
// if (process.env.NODE_ENV !== 'production') { // if (process.env.NODE_ENV !== 'production') {
// // eslint-disable-next-line // // eslint-disable-next-line
@ -16,8 +16,8 @@ export default function Layout({ children }: { children: any }): ReactElement {
<Typekit /> <Typekit />
<Header /> <Header />
<main className={document} id="document"> <main className={styles.document} id="document">
<div className={content}>{children}</div> <div className={styles.content}>{children}</div>
</main> </main>
<Footer /> <Footer />

View File

@ -10,20 +10,19 @@ import { unified } from 'unified'
import remarkParse from 'remark-parse' import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype' import remarkRehype from 'remark-rehype'
import rehypeReact from 'rehype-react' import rehypeReact from 'rehype-react'
import { content, source } from './Changelog.module.css' import * as styles from './Changelog.module.css'
import { GitHub, GitHubRepo } from '../../@types/GitHub'
export function PureChangelog({ export function PureChangelog({
repo, repo,
repos repos
}: { }: {
repo: string repo: string
repos: [{ node: GitHubRepo }] repos: Queries.GitHubReposQuery['github']['viewer']['repositories']['edges']
}): ReactElement { }): ReactElement | null {
const [changelogHtml, setChangelogHtml] = useState() const [changelogHtml, setChangelogHtml] = useState()
const repoFilteredArray = repos const repoFilteredArray = repos
.map(({ node }: { node: GitHubRepo }) => { .map(({ node }) => {
if (node.name === repo) return node if (node.name === repo) return node
}) })
.filter((n: any) => n) .filter((n: any) => n)
@ -31,24 +30,24 @@ export function PureChangelog({
const repoMatch = repoFilteredArray[0] const repoMatch = repoFilteredArray[0]
useEffect(() => { useEffect(() => {
if (!repoMatch?.object?.text) return if (!(repoMatch?.object as Queries.GitHub_Blob)?.text) return
async function init() { async function init() {
const changelogHtml = await unified() const changelogHtml = await unified()
.use(remarkParse) .use(remarkParse)
.use(remarkRehype) .use(remarkRehype)
.use(rehypeReact, { createElement, Fragment }) .use(rehypeReact, { createElement, Fragment })
.processSync(repoMatch.object.text).result .processSync((repoMatch?.object as Queries.GitHub_Blob).text).result
setChangelogHtml(changelogHtml) setChangelogHtml(changelogHtml)
} }
init() init()
}, [repoMatch?.object?.text]) }, [(repoMatch?.object as Queries.GitHub_Blob)?.text])
return repoMatch ? ( return repoMatch ? (
<div className={content} id="changelog"> <div className={styles.content} id="changelog">
{changelogHtml} {changelogHtml}
<p className={source}> <p className={styles.source}>
sourced from{' '} sourced from{' '}
<a href={`${repoMatch?.url}/tree/main/CHANGELOG.md`}> <a href={`${repoMatch?.url}/tree/main/CHANGELOG.md`}>
<code>{`${repoMatch?.owner.login}/${repo}:CHANGELOG.md`}</code> <code>{`${repoMatch?.owner.login}/${repo}:CHANGELOG.md`}</code>
@ -59,7 +58,7 @@ export function PureChangelog({
} }
const queryGithub = graphql` const queryGithub = graphql`
query GitHubReposInfo { query GitHubRepos {
github { github {
viewer { viewer {
repositories(first: 100, privacy: PUBLIC, isFork: false) { repositories(first: 100, privacy: PUBLIC, isFork: false) {
@ -85,7 +84,7 @@ const queryGithub = graphql`
` `
export default function Changelog({ repo }: { repo: string }): ReactElement { export default function Changelog({ repo }: { repo: string }): ReactElement {
const data: GitHub = useStaticQuery(queryGithub) const data = useStaticQuery<Queries.GitHubReposQuery>(queryGithub)
const repos: [{ node: GitHubRepo }] = data.github.viewer.repositories.edges const repos = data.github.viewer.repositories.edges
return <PureChangelog repo={repo} repos={repos} /> return <PureChangelog repo={repo} repos={repos} />
} }

View File

@ -1,10 +1,10 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { copied, button } from './Copy.module.css' import * as styles from './Copy.module.css'
import Icon from './Icon' import Icon from './Icon'
import Clipboard from 'react-clipboard.js' import Clipboard from 'react-clipboard.js'
const onCopySuccess = (e: any) => { const onCopySuccess = (e: any) => {
e.trigger.classList.add(copied) e.trigger.classList.add(styles.copied)
} }
export default function Copy({ text }: { text: string }): ReactElement { export default function Copy({ text }: { text: string }): ReactElement {
@ -13,7 +13,7 @@ export default function Copy({ text }: { text: string }): ReactElement {
data-clipboard-text={text} data-clipboard-text={text}
button-title="Copy to clipboard" button-title="Copy to clipboard"
onSuccess={(e: ClipboardJS.Event) => onCopySuccess(e)} onSuccess={(e: ClipboardJS.Event) => onCopySuccess(e)}
className={button} className={styles.button}
> >
<Icon name="Copy" /> <Icon name="Copy" />
</Clipboard> </Clipboard>

View File

@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
import Exif from './Exif' import Exif from './Exif'
const exif = { const exif: Partial<Queries.ImageExif> = {
formatted: { formatted: {
iso: '500', iso: '500',
model: 'Canon', model: 'Canon',
@ -12,13 +12,13 @@ const exif = {
focalLength: '200', focalLength: '200',
lensModel: 'Hello', lensModel: 'Hello',
exposure: '200', exposure: '200',
gps: { latitude: '41.89007222222222', longitude: '12.491516666666666' } gps: { latitude: 41.89007222222222, longitude: 12.491516666666666 }
} }
} }
describe('Exif', () => { describe('Exif', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render(<Exif exif={exif} />) const { container } = render(<Exif exif={exif as Queries.ImageExif} />)
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })

View File

@ -1,7 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import ExifMap from './ExifMap' import ExifMap from './ExifMap'
import { exif as styleExif, data, map } from './Exif.module.css' import * as styles from './Exif.module.css'
import { Exif as ExifMeta } from '../../@types/Image'
import Icon from './Icon' import Icon from './Icon'
const ExifData = ({ const ExifData = ({
@ -19,15 +18,19 @@ const ExifData = ({
</span> </span>
) )
export default function Exif({ exif }: { exif: ExifMeta }): ReactElement { export default function Exif({
exif
}: {
exif: Queries.ImageExif
}): ReactElement {
const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } = const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } =
exif.formatted exif.formatted
const formattedModel = model === 'FC7203' ? 'DJI Mavic Mini' : model const formattedModel = model === 'FC7203' ? 'DJI Mavic Mini' : model
return ( return (
<aside className={styleExif}> <aside className={styles.exif}>
<div className={data}> <div className={styles.data}>
{formattedModel && ( {formattedModel && (
<ExifData title="Camera model" value={formattedModel} icon="Camera" /> <ExifData title="Camera model" value={formattedModel} icon="Camera" />
)} )}
@ -45,8 +48,8 @@ export default function Exif({ exif }: { exif: ExifMeta }): ReactElement {
{exposure && <ExifData title="Exposure" value={exposure} icon="Sun" />} {exposure && <ExifData title="Exposure" value={exposure} icon="Sun" />}
{iso && <ExifData title="ISO" value={iso} icon="Maximize" />} {iso && <ExifData title="ISO" value={iso} icon="Maximize" />}
</div> </div>
{gps && gps.latitude && ( {gps?.latitude && (
<div className={map}> <div className={styles.map}>
<ExifMap gps={gps} /> <ExifMap gps={gps} />
</div> </div>
)} )}

View File

@ -17,7 +17,7 @@ const providers = {
export default function ExifMap({ export default function ExifMap({
gps gps
}: { }: {
gps: { latitude: string; longitude: string } gps: { latitude: number; longitude: number }
}): ReactElement { }): ReactElement {
const { value } = useDarkMode() const { value } = useDarkMode()
const isDarkMode = value const isDarkMode = value

View File

@ -1,5 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { button, hamburger, line } from './Hamburger.module.css' import * as styles from './Hamburger.module.css'
export default function Hamburger({ export default function Hamburger({
onClick onClick
@ -7,11 +7,16 @@ export default function Hamburger({
onClick(): void onClick(): void
}): ReactElement { }): ReactElement {
return ( return (
<button type="button" title="Menu" className={button} onClick={onClick}> <button
<span className={hamburger}> type="button"
<span className={line} /> title="Menu"
<span className={line} /> className={styles.button}
<span className={line} /> onClick={onClick}
>
<span className={styles.hamburger}>
<span className={styles.line} />
<span className={styles.line} />
<span className={styles.line} />
</span> </span>
</button> </button>
) )

View File

@ -27,7 +27,7 @@ import {
import { ReactComponent as Jsonfeed } from '../../images/jsonfeed.svg' import { ReactComponent as Jsonfeed } from '../../images/jsonfeed.svg'
import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg' import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
import { ReactComponent as Stopwatch } from '../../images/stopwatch.svg' import { ReactComponent as Stopwatch } from '../../images/stopwatch.svg'
import { icon } from './Icon.module.css' import * as styles from './Icon.module.css'
const components: { const components: {
[key: string]: FunctionComponent<React.SVGProps<SVGSVGElement>> [key: string]: FunctionComponent<React.SVGProps<SVGSVGElement>>
@ -62,7 +62,7 @@ const Icon = ({ name, ...props }: { name: string }): ReactElement => {
// const IconFeather = (Feather as any)[name] // const IconFeather = (Feather as any)[name]
if (!IconMapped) return null if (!IconMapped) return null
return <IconMapped className={icon} {...props} /> return <IconMapped className={styles.icon} {...props} />
} }
export default Icon export default Icon

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'
import { graphql } from 'gatsby' import { graphql } from 'gatsby'
import { GatsbyImage } from 'gatsby-plugin-image' import { GatsbyImage } from 'gatsby-plugin-image'
import { ImageProps } from '../../@types/Image' import { ImageProps } from '../../@types/Image'
import { image as styleImage, imageTitle } from './Image.module.css' import * as styles from './Image.module.css'
export const Image = ({ export const Image = ({
title, title,
@ -12,11 +12,11 @@ export const Image = ({
className className
}: ImageProps): ReactElement => ( }: ImageProps): ReactElement => (
<figure <figure
className={`${styleImage} ${className ? className : ''}`} className={`${styles.image} ${className ? className : ''}`}
data-original={original?.src} data-original={original?.src}
> >
<GatsbyImage image={image} alt={alt} objectFit="contain" /> <GatsbyImage image={image} alt={alt} objectFit="contain" />
{title && <figcaption className={imageTitle}>{title}</figcaption>} {title && <figcaption className={styles.imageTitle}>{title}</figcaption>}
</figure> </figure>
) )

View File

@ -1,9 +1,9 @@
import React, { ReactElement, InputHTMLAttributes } from 'react' import React, { ReactElement, InputHTMLAttributes } from 'react'
import { input } from './Input.module.css' import * as styles from './Input.module.css'
export default function Input({ export default function Input({
className, className,
...props ...props
}: InputHTMLAttributes<HTMLInputElement>): ReactElement { }: InputHTMLAttributes<HTMLInputElement>): ReactElement {
return <input className={`${input} ${className || ''}`} {...props} /> return <input className={`${styles.input} ${className || ''}`} {...props} />
} }

View File

@ -2,11 +2,10 @@ import React, { ReactElement } from 'react'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import { getSrc } from 'gatsby-plugin-image' import { getSrc } from 'gatsby-plugin-image'
import { useSiteMetadata } from '../../../hooks/use-site-metadata' import { useSiteMetadata } from '../../../hooks/use-site-metadata'
import { Post } from '../../../@types/Post'
import MetaTags from './MetaTags' import MetaTags from './MetaTags'
const query = graphql` const query = graphql`
query { query Logo {
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) { logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
edges { edges {
node { node {
@ -17,25 +16,36 @@ const query = graphql`
} }
` `
export interface SeoPost {
frontmatter: {
title: string
description?: string
image?: any
updated?: string
}
fields?: {
date: string
}
excerpt?: string
}
export default function SEO({ export default function SEO({
post, post,
slug, slug
postSEO
}: { }: {
post?: Post post?: SeoPost
slug?: string slug?: string
postSEO?: boolean
}): ReactElement { }): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery<Queries.LogoQuery>(query)
const logo = data.logo.edges[0].node.relativePath const logo = data.logo.edges[0].node.relativePath
const { siteTitle, siteUrl, siteDescription } = useSiteMetadata() const { siteTitle, siteUrl, siteDescription } = useSiteMetadata()
let title let title: string
let description let description: string
let image let image: string
let postURL let postURL: string
if (postSEO) { if (post) {
const postMeta = post.frontmatter const postMeta = post.frontmatter
title = `${postMeta.title} ¦ ${siteTitle}` title = `${postMeta.title} ¦ ${siteTitle}`
description = postMeta.description ? postMeta.description : post.excerpt description = postMeta.description ? postMeta.description : post.excerpt
@ -49,17 +59,17 @@ export default function SEO({
image = `${siteUrl}${image}` image = `${siteUrl}${image}`
const blogURL = siteUrl const blogURL = siteUrl
const url = postSEO ? postURL : blogURL const url = post ? postURL : blogURL
return ( return (
<MetaTags <MetaTags
description={description} description={description}
image={image} image={image}
url={url} url={url || ''}
postSEO={postSEO} postSEO={Boolean(post)}
title={title} title={title}
datePublished={post && post.fields && post.fields.date} datePublished={post?.fields && post.fields.date}
dateModified={post && post.frontmatter.updated} dateModified={post?.frontmatter.updated}
/> />
) )
} }

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import { tag, count as styleCount } from './Tag.module.css' import * as styles from './Tag.module.css'
export default function Tag({ export default function Tag({
name, name,
@ -14,9 +14,9 @@ export default function Tag({
style?: any style?: any
}): ReactElement { }): ReactElement {
return ( return (
<Link className={tag} to={url} style={style}> <Link className={styles.tag} to={url} style={style}>
{name} {name}
{count && <span className={styleCount}>{count}</span>} {count && <span className={styles.count}>{count}</span>}
</Link> </Link>
) )
} }

View File

@ -2,9 +2,8 @@ import React, { ReactElement, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import Hamburger from '../atoms/Hamburger' import Hamburger from '../atoms/Hamburger'
import { menu as styleMenu } from './Menu.module.css' import * as styles from './Menu.module.css'
import { useSiteMetadata } from '../../hooks/use-site-metadata' import { useSiteMetadata } from '../../hooks/use-site-metadata'
import { MenuItem } from '../../@types/Site'
export default function Menu(): ReactElement { export default function Menu(): ReactElement {
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
@ -14,15 +13,13 @@ export default function Menu(): ReactElement {
setMenuOpen(!menuOpen) setMenuOpen(!menuOpen)
} }
const MenuItems = menu.map( const MenuItems = menu.map((item) => (
(item: MenuItem): JSX.Element => ( <li key={item.title}>
<li key={item.title}> <Link onClick={toggleMenu} to={item.link}>
<Link onClick={toggleMenu} to={item.link}> {item.title}
{item.title} </Link>
</Link> </li>
</li> ))
)
)
return ( return (
<> <>
@ -30,7 +27,7 @@ export default function Menu(): ReactElement {
<html className={menuOpen ? 'has-menu-open' : undefined} lang="en" /> <html className={menuOpen ? 'has-menu-open' : undefined} lang="en" />
</Helmet> </Helmet>
<Hamburger onClick={toggleMenu} /> <Hamburger onClick={toggleMenu} />
<nav className={styleMenu}> <nav className={styles.menu}>
<ul>{MenuItems}</ul> <ul>{MenuItems}</ul>
</nav> </nav>
</> </>

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Icon from '../atoms/Icon' import Icon from '../atoms/Icon'
import { link as styleLink } from './Networks.module.css' import * as styles from './Networks.module.css'
function NetworkIcon({ link }: { link: string }) { function NetworkIcon({ link }: { link: string }) {
let IconComp let IconComp
@ -28,7 +28,7 @@ export default function IconLinks({
return ( return (
<p> <p>
{links.map((link: string) => ( {links.map((link: string) => (
<a key={link} className={styleLink} href={link} title={link}> <a key={link} className={styles.link} href={link} title={link}>
<NetworkIcon link={link} /> <NetworkIcon link={link} />
</a> </a>
))} ))}

View File

@ -2,11 +2,7 @@ import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import { PageContext } from '../../@types/Post' import { PageContext } from '../../@types/Post'
import Icon from '../atoms/Icon' import Icon from '../atoms/Icon'
import { import * as styles from './Pagination.module.css'
current as styleCurrent,
number as styleNumber,
pagination
} from './Pagination.module.css'
function PageNumber({ function PageNumber({
i, i,
@ -17,7 +13,7 @@ function PageNumber({
slug: string slug: string
current?: boolean current?: boolean
}): JSX.Element { }): JSX.Element {
const classes = current ? styleCurrent : styleNumber const classes = current ? styles.current : styles.number
const link = i === 0 ? slug : `${slug}page/${i + 1}` const link = i === 0 ? slug : `${slug}page/${i + 1}`
return ( return (
@ -39,7 +35,7 @@ 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} className={styleNumber}> <Link to={link} rel={rel} title={title} className={styles.number}>
{prevPagePath ? ( {prevPagePath ? (
<Icon name="ChevronLeft" /> <Icon name="ChevronLeft" />
) : ( ) : (
@ -60,7 +56,7 @@ export default function Pagination({
const isLast = currentPageNumber === numPages const isLast = currentPageNumber === numPages
return ( return (
<div className={pagination}> <div className={styles.pagination}>
{!isFirst && <PrevNext prevPagePath={prevPagePath} />} {!isFirst && <PrevNext prevPagePath={prevPagePath} />}
{Array.from({ length: numPages }, (_, i) => ( {Array.from({ length: numPages }, (_, i) => (
<PageNumber <PageNumber

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Time from '../atoms/Time' import Time from '../atoms/Time'
import { time } from './PostDate.module.css' import * as styles from './PostDate.module.css'
export default function PostDate({ export default function PostDate({
date, date,
@ -10,7 +10,7 @@ export default function PostDate({
updated?: string updated?: string
}): ReactElement { }): ReactElement {
return ( return (
<div className={time}> <div className={styles.time}>
<Time date={date} /> <Time date={date} />
{updated && ' • updated '} {updated && ' • updated '}
{updated && <Time date={updated} />} {updated && <Time date={updated} />}

View File

@ -5,9 +5,16 @@ import post from '../../../.jest/__fixtures__/post.json'
describe('PostTeaser', () => { describe('PostTeaser', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { container, rerender } = render(<PostTeaser post={post.post} />) const { container, rerender } = render(
<PostTeaser post={post.post as unknown as Queries.PostTeaserFragment} />
)
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
rerender(<PostTeaser post={post.post} toggleSearch={() => null} />) rerender(
<PostTeaser
post={post.post as unknown as Queries.PostTeaserFragment}
toggleSearch={() => null}
/>
)
}) })
}) })

View File

@ -1,13 +1,8 @@
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 { Post } from '../../@types/Post'
import PostTitle from '../templates/Post/Title' import PostTitle from '../templates/Post/Title'
import { import * as styles from './PostTeaser.module.css'
post as stylePost,
empty,
title as styleTitle
} from './PostTeaser.module.css'
export const postTeaserQuery = graphql` export const postTeaserQuery = graphql`
fragment PostTeaser on MarkdownRemark { fragment PostTeaser on MarkdownRemark {
@ -37,7 +32,7 @@ export default function PostTeaser({
toggleSearch, toggleSearch,
hideDate hideDate
}: { }: {
post: Partial<Post> post: Queries.PostTeaserFragment
toggleSearch?: () => void toggleSearch?: () => void
hideDate?: boolean hideDate?: boolean
}): ReactElement { }): ReactElement {
@ -46,7 +41,7 @@ export default function PostTeaser({
return ( return (
<Link <Link
className={stylePost} className={styles.post}
to={slug} to={slug}
onClick={toggleSearch && toggleSearch} onClick={toggleSearch && toggleSearch}
> >
@ -56,14 +51,14 @@ export default function PostTeaser({
alt={title} alt={title}
/> />
) : ( ) : (
<span className={empty} /> <span className={styles.empty} />
)} )}
<PostTitle <PostTitle
title={title} title={title}
date={hideDate ? null : date} date={hideDate ? null : date}
updated={updated} updated={updated}
className={styleTitle} className={styles.title}
/> />
</Link> </Link>
) )

View File

@ -1,12 +1,11 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import PostTeaser from './PostTeaser' import PostTeaser from './PostTeaser'
import { relatedPosts, title, button } from './RelatedPosts.module.css' import * as styles from './RelatedPosts.module.css'
import { Post, Frontmatter } from '../../@types/Post'
import { PhotoThumb } from '../templates/Photos' import { PhotoThumb } from '../templates/Photos'
const query = graphql` const query = graphql`
{ query RelatedPosts {
allMarkdownRemark(sort: { fields: { date: DESC } }) { allMarkdownRemark(sort: { fields: { date: DESC } }) {
edges { edges {
node { node {
@ -18,12 +17,12 @@ const query = graphql`
` `
function postsWithDataFilter( function postsWithDataFilter(
posts: [{ node: Post }], posts: Queries.RelatedPostsQuery['allMarkdownRemark']['edges'],
key: keyof Frontmatter, key: keyof Queries.MarkdownRemarkFrontmatter,
valuesToFind: string[] valuesToFind: string[]
): { node: Post }[] { ) {
const newArray = posts const newArray = posts
.filter(({ node }: { node: Post }) => { .filter(({ node }) => {
const frontmatterKey = node.frontmatter[key] as [] const frontmatterKey = node.frontmatter[key] as []
if ( if (
@ -39,9 +38,11 @@ function postsWithDataFilter(
return newArray return newArray
} }
function photosWithDataFilter(posts: [{ node: Post }]): { node: Post }[] { function photosWithDataFilter(
posts: Queries.RelatedPostsQuery['allMarkdownRemark']['edges']
) {
const newArray = posts const newArray = posts
.filter((post: { node: Post }) => { .filter((post) => {
const { fileAbsolutePath } = post.node const { fileAbsolutePath } = post.node
if (fileAbsolutePath.includes('content/photos')) { if (fileAbsolutePath.includes('content/photos')) {
@ -61,7 +62,7 @@ export default function RelatedPosts({
tags: string[] tags: string[]
isPhotos?: boolean isPhotos?: boolean
}): ReactElement { }): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery<Queries.RelatedPostsQuery>(query)
const posts = data.allMarkdownRemark.edges const posts = data.allMarkdownRemark.edges
function getPosts() { function getPosts() {
@ -78,15 +79,15 @@ export default function RelatedPosts({
} }
return ( return (
<aside className={relatedPosts}> <aside className={styles.relatedPosts}>
<h1 className={title}> <h1 className={styles.title}>
Related {isPhotos ? 'Photos' : 'Posts'}{' '} Related {isPhotos ? 'Photos' : 'Posts'}{' '}
<button className={button} onClick={() => refreshPosts()}> <button className={styles.button} onClick={() => refreshPosts()}>
Refresh Refresh
</button> </button>
</h1> </h1>
<ul> <ul>
{filteredPosts?.map(({ node }: { node: Post }) => ( {filteredPosts?.map(({ node }) => (
<li key={node.id}> <li key={node.id}>
{isPhotos ? ( {isPhotos ? (
<PhotoThumb photo={node} /> <PhotoThumb photo={node} />

View File

@ -1,12 +1,12 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { searchButton } from './SearchButton.module.css' import * as styles from './SearchButton.module.css'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
const SearchButton = ({ onClick }: { onClick: () => void }): ReactElement => ( const SearchButton = ({ onClick }: { onClick: () => void }): ReactElement => (
<button <button
type="button" type="button"
title="Search" title="Search"
className={searchButton} className={styles.searchButton}
onClick={onClick} onClick={onClick}
> >
<Icon name="Search" /> <Icon name="Search" />

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react' import React, { ChangeEvent, ReactElement } from 'react'
import Input from '../../atoms/Input' import Input from '../../atoms/Input'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
import { searchInput, searchInputClose } from './SearchInput.module.css' import * as styles from './SearchInput.module.css'
export default function SearchInput({ export default function SearchInput({
value, value,
@ -10,12 +10,12 @@ export default function SearchInput({
}: { }: {
value: string value: string
onToggle(): void onToggle(): void
onChange(e: Event): void onChange(e: ChangeEvent<HTMLInputElement>): void
}): ReactElement { }): ReactElement {
return ( return (
<> <>
<Input <Input
className={searchInput} className={styles.searchInput}
type="search" type="search"
placeholder="Search everything" placeholder="Search everything"
autoFocus // eslint-disable-line autoFocus // eslint-disable-line
@ -23,7 +23,7 @@ export default function SearchInput({
onChange={onChange} onChange={onChange}
/> />
<button <button
className={searchInputClose} className={styles.searchInputClose}
onClick={onToggle} onClick={onToggle}
title="Close search" title="Close search"
> >

View File

@ -3,18 +3,14 @@ import ReactDOM from 'react-dom'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import PostTeaser from '../PostTeaser' import PostTeaser from '../PostTeaser'
import SearchResultsEmpty from './SearchResultsEmpty' import SearchResultsEmpty from './SearchResultsEmpty'
import { import * as styles from './SearchResults.module.css'
searchResults,
results as styleResults
} from './SearchResults.module.css'
import { Post } from '../../../@types/Post'
export interface Results { export interface Results {
slug: string slug: string
} }
const query = graphql` const query = graphql`
query { query SearchResults {
allMarkdownRemark { allMarkdownRemark {
edges { edges {
node { node {
@ -31,21 +27,19 @@ function SearchResultsPure({
toggleSearch, toggleSearch,
posts posts
}: { }: {
posts: [{ node: Post }] posts: Queries.SearchResultsQuery['allMarkdownRemark']['edges']
searchQuery: string searchQuery: string
results: Results[] results: Results[]
toggleSearch(): void toggleSearch(): void
}) { }) {
return ( return (
<div className={searchResults}> <div className={styles.searchResults}>
{results.length > 0 ? ( {results.length > 0 ? (
<ul className={styleResults}> <ul className={styles.results}>
{results.map((page: { slug: string }) => {results.map((page: { slug: string }) =>
posts posts
.filter( .filter(({ node }) => node.fields.slug === page.slug)
({ node }: { node: Post }) => node.fields.slug === page.slug .map(({ node }) => (
)
.map(({ node }: { node: Post }) => (
<li key={page.slug}> <li key={page.slug}>
<PostTeaser post={node} toggleSearch={toggleSearch} /> <PostTeaser post={node} toggleSearch={toggleSearch} />
</li> </li>
@ -68,7 +62,7 @@ export default function SearchResults({
results: Results[] results: Results[]
toggleSearch(): void toggleSearch(): void
}): ReactElement { }): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery<Queries.SearchResultsQuery>(query)
const posts = data.allMarkdownRemark.edges const posts = data.allMarkdownRemark.edges
// creating portal to break out of DOM node we're in // creating portal to break out of DOM node we're in

View File

@ -1,9 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { import * as styles from './SearchResultsEmpty.module.css'
empty,
emptyMessage,
emptyMessageText
} from './SearchResultsEmpty.module.css'
import { Results } from './SearchResults' import { Results } from './SearchResults'
const SearchResultsEmpty = ({ const SearchResultsEmpty = ({
@ -13,9 +9,9 @@ const SearchResultsEmpty = ({
searchQuery: string searchQuery: string
results: Results[] results: Results[]
}): ReactElement => ( }): ReactElement => (
<div className={empty}> <div className={styles.empty}>
<header className={emptyMessage}> <header className={styles.emptyMessage}>
<p className={emptyMessageText}> <p className={styles.emptyMessageText}>
{searchQuery.length > 0 && results.length === 0 {searchQuery.length > 0 && results.length === 0
? 'No results found' ? 'No results found'
: 'Awaiting your input'} : 'Awaiting your input'}

View File

@ -1,6 +1,6 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { themeSwitch, checkbox, label } from './ThemeSwitch.module.css' import * as styles from './ThemeSwitch.module.css'
import Icon from '../atoms/Icon' import Icon from '../atoms/Icon'
import useDarkMode from '../../hooks/useDarkMode' import useDarkMode from '../../hooks/useDarkMode'
@ -40,8 +40,8 @@ const HeadMarkup = ({
export default function ThemeSwitch(): ReactElement { export default function ThemeSwitch(): ReactElement {
const { value, toggle } = useDarkMode() const { value, toggle } = useDarkMode()
const [themeColor, setThemeColor] = useState('') const [themeColor, setThemeColor] = useState<string>()
const [bodyClass, setBodyClass] = useState('') const [bodyClass, setBodyClass] = useState<string>()
useEffect(() => { useEffect(() => {
setBodyClass(value ? 'dark' : null) setBodyClass(value ? 'dark' : null)
@ -51,15 +51,15 @@ export default function ThemeSwitch(): ReactElement {
return ( return (
<> <>
<HeadMarkup themeColor={themeColor} bodyClass={bodyClass} /> <HeadMarkup themeColor={themeColor} bodyClass={bodyClass} />
<aside className={themeSwitch} title="Toggle Dark Mode"> <aside className={styles.themeSwitch} title="Toggle Dark Mode">
<label <label
htmlFor="toggle" htmlFor="toggle"
className={checkbox} className={styles.checkbox}
onClick={toggle} onClick={toggle}
onKeyPress={toggle} onKeyPress={toggle}
role="presentation" role="presentation"
> >
<span className={label}>Toggle Dark Mode</span> <span className={styles.label}>Toggle Dark Mode</span>
<ThemeToggleInput isDark={value} toggleDark={toggle} /> <ThemeToggleInput isDark={value} toggleDark={toggle} />
<div aria-live="assertive"> <div aria-live="assertive">
{value ? <Icon name="Sun" /> : <Icon name="Moon" />} {value ? <Icon name="Sun" /> : <Icon name="Moon" />}

View File

@ -2,11 +2,11 @@ import React, { ReactElement } from 'react'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import { getSrc } from 'gatsby-plugin-image' import { getSrc } from 'gatsby-plugin-image'
import IconLinks from './Networks' import IconLinks from './Networks'
import { avatar as styleAvatar, description } from './Vcard.module.css' import * as styles from './Vcard.module.css'
import { useSiteMetadata } from '../../hooks/use-site-metadata' import { useSiteMetadata } from '../../hooks/use-site-metadata'
const query = graphql` const query = graphql`
query { query Avatar {
avatar: allFile(filter: { name: { eq: "avatar" } }) { avatar: allFile(filter: { name: { eq: "avatar" } }) {
edges { edges {
node { node {
@ -25,7 +25,7 @@ const query = graphql`
` `
export default function Vcard(): ReactElement { export default function Vcard(): ReactElement {
const data = useStaticQuery(query) const data = useStaticQuery<Queries.AvatarQuery>(query)
const { author, rss, jsonfeed } = useSiteMetadata() const { author, rss, jsonfeed } = useSiteMetadata()
const { twitter, github, name, uri } = author const { twitter, github, name, uri } = author
const avatar = getSrc(data.avatar.edges[0].node) const avatar = getSrc(data.avatar.edges[0].node)
@ -34,13 +34,13 @@ export default function Vcard(): ReactElement {
return ( return (
<> <>
<img <img
className={styleAvatar} className={styles.avatar}
src={avatar} src={avatar}
width="80" width="80"
height="80" height="80"
alt="avatar" alt="avatar"
/> />
<p className={description}> <p className={styles.description}>
Blog of designer &amp; developer{' '} Blog of designer &amp; developer{' '}
<a className="fn" rel="author" href={uri}> <a className="fn" rel="author" href={uri}>
{name} {name}

View File

@ -1,5 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { success, error, alert } from './Alert.module.css' import * as styles from './Alert.module.css'
export function getTransactionMessage(transactionHash?: string): { export function getTransactionMessage(transactionHash?: string): {
[key: string]: string [key: string]: string
@ -23,7 +23,11 @@ const constructMessage = (
: message && message.text : message && message.text
const classes = (status: string) => const classes = (status: string) =>
status === 'success' ? success : status === 'error' ? error : alert status === 'success'
? styles.success
: status === 'error'
? styles.error
: styles.alert
export default function Alert({ export default function Alert({
transactionHash, transactionHash,

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, ReactElement } from 'react' import React, { useState, useEffect, ReactElement } from 'react'
import axios from 'axios' import axios from 'axios'
import { conversion as styleConversion } from './Conversion.module.css' import * as styles from './Conversion.module.css'
import { useNetwork } from 'wagmi' import { useNetwork } from 'wagmi'
export async function getFiat( export async function getFiat(
@ -23,7 +23,7 @@ export default function Conversion({
}: { }: {
amount: string amount: string
}): ReactElement { }): ReactElement {
const { activeChain } = useNetwork() const { chain } = useNetwork()
const [conversion, setConversion] = useState({ const [conversion, setConversion] = useState({
euro: '0.00', euro: '0.00',
@ -32,12 +32,12 @@ export default function Conversion({
const { dollar, euro } = conversion const { dollar, euro } = conversion
useEffect(() => { useEffect(() => {
if (!activeChain?.nativeCurrency?.symbol) return if (!chain?.nativeCurrency?.symbol) return
async function getFiatResponse() { async function getFiatResponse() {
try { try {
const tokenId = const tokenId =
activeChain?.nativeCurrency?.symbol === 'MATIC' chain?.nativeCurrency?.symbol === 'MATIC'
? 'matic-network' ? 'matic-network'
: 'ethereum' : 'ethereum'
const { dollar, euro } = await getFiat(Number(amount), tokenId) const { dollar, euro } = await getFiat(Number(amount), tokenId)
@ -48,10 +48,10 @@ export default function Conversion({
} }
getFiatResponse() getFiatResponse()
}, [amount, activeChain?.nativeCurrency?.symbol]) }, [amount, chain?.nativeCurrency?.symbol])
return ( return (
<div className={styleConversion}> <div className={styles.conversion}>
<span>{dollar !== '0.00' && `= $ ${dollar}`}</span> <span>{dollar !== '0.00' && `= $ ${dollar}`}</span>
<span>{euro !== '0.00' && `= € ${euro}`}</span> <span>{euro !== '0.00' && `= € ${euro}`}</span>
</div> </div>

View File

@ -2,12 +2,7 @@ import React, { ReactElement } from 'react'
import { useAccount, useNetwork } from 'wagmi' import { useAccount, useNetwork } from 'wagmi'
import Input from '../../atoms/Input' import Input from '../../atoms/Input'
import Conversion from './Conversion' import Conversion from './Conversion'
import { import * as styles from './InputGroup.module.css'
inputGroup,
input,
inputInput,
currency
} from './InputGroup.module.css'
export default function InputGroup({ export default function InputGroup({
amount, amount,
@ -21,18 +16,18 @@ export default function InputGroup({
return ( return (
<> <>
<div className={inputGroup}> <div className={styles.inputGroup}>
<div className={input}> <div className={styles.input}>
<Input <Input
type="text" type="text"
inputMode="decimal" inputMode="decimal"
pattern="[0-9.]*" pattern="[0-9.]*"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
className={inputInput} className={styles.inputInput}
disabled={!address} disabled={!address}
/> />
<div className={currency}> <div className={styles.currency}>
<span>{chain?.nativeCurrency?.symbol || 'ETH'}</span> <span>{chain?.nativeCurrency?.symbol || 'ETH'}</span>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import { parseEther } from '@ethersproject/units'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import InputGroup from './InputGroup' import InputGroup from './InputGroup'
import Alert, { getTransactionMessage } from './Alert' import Alert, { getTransactionMessage } from './Alert'
import { web3 as styleWeb3 } from './index.module.css' import * as styles from './index.module.css'
import { useSendTransaction, usePrepareSendTransaction } from 'wagmi' import { useSendTransaction, usePrepareSendTransaction } from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit' import { ConnectButton } from '@rainbow-me/rainbowkit'
@ -53,7 +53,7 @@ export default function Web3Donation({
return ( return (
<form <form
className={styleWeb3} className={styles.web3}
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
handleSendTransaction() handleSendTransaction()

View File

@ -3,14 +3,14 @@ import { Link } from 'gatsby'
import Icon from '../atoms/Icon' import Icon from '../atoms/Icon'
import Vcard from '../molecules/Vcard' import Vcard from '../molecules/Vcard'
import { useSiteMetadata } from '../../hooks/use-site-metadata' import { useSiteMetadata } from '../../hooks/use-site-metadata'
import { copyright, btc, footer } from './Footer.module.css' import * as styles from './Footer.module.css'
function Copyright() { function Copyright() {
const { name, uri, github } = useSiteMetadata().author const { name, uri, github } = useSiteMetadata().author
const year = new Date().getFullYear() const year = new Date().getFullYear()
return ( return (
<section className={copyright}> <section className={styles.copyright}>
<p> <p>
&copy; 2005&ndash; &copy; 2005&ndash;
{year + ' '} {year + ' '}
@ -21,7 +21,7 @@ function Copyright() {
<Icon name="GitHub" /> <Icon name="GitHub" />
View source View source
</a> </a>
<Link to="/thanks" className={btc}> <Link to="/thanks" className={styles.btc}>
<Icon name="Bitcoin" /> <Icon name="Bitcoin" />
Say Thanks Say Thanks
</Link> </Link>
@ -32,7 +32,7 @@ function Copyright() {
export default function Footer(): JSX.Element { export default function Footer(): JSX.Element {
return ( return (
<footer role="contentinfo" className={footer}> <footer role="contentinfo" className={styles.footer}>
<Vcard /> <Vcard />
<Copyright /> <Copyright />
</footer> </footer>

View File

@ -4,19 +4,19 @@ import Search from '../molecules/Search'
import Menu from '../molecules/Menu' import Menu from '../molecules/Menu'
import ThemeSwitch from '../molecules/ThemeSwitch' import ThemeSwitch from '../molecules/ThemeSwitch'
import { ReactComponent as Logo } from '../../images/logo.svg' import { ReactComponent as Logo } from '../../images/logo.svg'
import { header, headerContent, title, logo, nav } from './Header.module.css' import * as styles from './Header.module.css'
export default function Header(): JSX.Element { export default function Header(): JSX.Element {
return ( return (
<header role="banner" className={header}> <header role="banner" className={styles.header}>
<div className={headerContent}> <div className={styles.headerContent}>
<h1 className={title}> <h1 className={styles.title}>
<Link to="/"> <Link to="/">
<Logo className={logo} /> kremalicious <Logo className={styles.logo} /> kremalicious
</Link> </Link>
</h1> </h1>
<nav role="navigation" className={nav}> <nav role="navigation" className={styles.nav}>
<ThemeSwitch /> <ThemeSwitch />
<Search /> <Search />
<Menu /> <Menu />

View File

@ -14,7 +14,10 @@ describe('Archive', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
<Archive data={data} pageContext={pageContext} /> <Archive
data={data as unknown as Queries.ArchiveTemplateQuery}
pageContext={pageContext}
/>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })

View File

@ -1,22 +1,22 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { graphql } from 'gatsby' import { graphql } from 'gatsby'
import { Post, PageContext } from '../../@types/Post' import { PageContext } from '../../@types/Post'
import Pagination from '../molecules/Pagination' import Pagination from '../molecules/Pagination'
import PostTeaser from '../molecules/PostTeaser' import PostTeaser from '../molecules/PostTeaser'
import Page from './Page' import Page from './Page'
import { posts } from './Archive.module.css' import * as styles from './Archive.module.css'
export default function Archive({ export default function Archive({
data, data,
pageContext pageContext
}: { }: {
data: any data: Queries.ArchiveTemplateQuery
pageContext: PageContext pageContext: PageContext
}): 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 }) => (
<PostTeaser key={node.id} post={node} /> <PostTeaser key={node.id} post={node} />
)) ))
@ -40,14 +40,14 @@ export default function Archive({
post={page} post={page}
pathname={pageContext.slug} pathname={pageContext.slug}
> >
<div className={posts}>{PostsList}</div> <div className={styles.posts}>{PostsList}</div>
{numPages > 1 && <Pagination pageContext={pageContext} />} {numPages > 1 && <Pagination pageContext={pageContext} />}
</Page> </Page>
) )
} }
export const archiveQuery = graphql` export const archiveQuery = graphql`
query ($tag: String, $skip: Int, $limit: Int) { query ArchiveTemplate($tag: String, $skip: Int, $limit: Int) {
allMarkdownRemark( allMarkdownRemark(
filter: { filter: {
fields: { type: { nin: "photo" } } fields: { type: { nin: "photo" } }

View File

@ -1,8 +1,7 @@
import React, { ReactElement, ReactNode } from 'react' import React, { ReactElement, ReactNode } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Post } from '../../@types/Post' import SEO, { SeoPost } from '../atoms/SEO'
import SEO from '../atoms/SEO' import * as styles from './Page.module.css'
import { pagetitle } from './Page.module.css'
export default function Page({ export default function Page({
title, title,
@ -15,14 +14,14 @@ export default function Page({
children: ReactNode children: ReactNode
pathname: string pathname: string
section?: string section?: string
post?: Post post?: SeoPost
}): ReactElement { }): ReactElement {
return ( return (
<> <>
<Helmet title={title} /> <Helmet title={title} />
<SEO slug={pathname} postSEO post={post} /> <SEO slug={pathname} post={post} />
<h1 className={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

@ -13,8 +13,9 @@ describe('/photos', () => {
} }
const { container } = render( const { container } = render(
// @ts-expect-error: only testing first render
<Photos <Photos
data={data} data={data as unknown as Queries.PhotosTemplateQuery}
pageContext={pageContext} pageContext={pageContext}
location={{ pathname: '/photos' } as any} location={{ pathname: '/photos' } as any}
/> />

View File

@ -1,18 +1,22 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { graphql, Link, PageProps } from 'gatsby' import { graphql, Link, PageProps } from 'gatsby'
import { Post, PageContext } from '../../@types/Post' import { PageContext } from '../../@types/Post'
import { Image } from '../atoms/Image' import { Image } from '../atoms/Image'
import Pagination from '../molecules/Pagination' import Pagination from '../molecules/Pagination'
import Page from './Page' import Page from './Page'
import { photo as stylePhoto, photos as stylePhotos } from './Photos.module.css' import * as styles from './Photos.module.css'
export const PhotoThumb = ({ photo }: { photo: Post }): ReactElement => { export const PhotoThumb = ({
photo
}: {
photo: Queries.PhotosTemplateQuery['allMarkdownRemark']['edges'][0]['node']
}): ReactElement => {
const { title, image } = photo.frontmatter const { title, image } = photo.frontmatter
const { slug } = photo.fields const { slug } = photo.fields
const { gatsbyImageData } = (image as any).childImageSharp const { gatsbyImageData } = (image as any).childImageSharp
return ( return (
<article className={stylePhoto}> <article className={styles.photo}>
{image && ( {image && (
<Link to={slug}> <Link to={slug}>
<Image title={title} image={gatsbyImageData} alt={title} /> <Image title={title} image={gatsbyImageData} alt={title} />
@ -23,9 +27,7 @@ export const PhotoThumb = ({ photo }: { photo: Post }): ReactElement => {
} }
interface PhotosPageProps extends PageProps { interface PhotosPageProps extends PageProps {
data: { data: Queries.PhotosTemplateQuery
allMarkdownRemark: { edges: { node: Post }[] }
}
pageContext: PageContext pageContext: PageContext
} }
@ -55,8 +57,8 @@ export default function Photos(props: PhotosPageProps): ReactElement {
post={page} post={page}
pathname={props.location.pathname} pathname={props.location.pathname}
> >
<section className={stylePhotos}> <section className={styles.photos}>
{photos.map(({ node }: { node: Post }) => ( {photos.map(({ node }) => (
<PhotoThumb key={node.id} photo={node} /> <PhotoThumb key={node.id} photo={node} />
))} ))}
</section> </section>
@ -67,7 +69,7 @@ export default function Photos(props: PhotosPageProps): ReactElement {
} }
export const photosQuery = graphql` export const photosQuery = graphql`
query ($skip: Int, $limit: Int) { query PhotosTemplate($skip: Int, $limit: Int) {
allMarkdownRemark( allMarkdownRemark(
filter: { fields: { type: { eq: "photo" } } } filter: { fields: { type: { eq: "photo" } } }
sort: { fields: { date: DESC } } sort: { fields: { date: DESC } }

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { useSiteMetadata } from '../../../hooks/use-site-metadata' import { useSiteMetadata } from '../../../hooks/use-site-metadata'
import { action, actionTitle, actionText, actions } from './Actions.module.css' import * as styles from './Actions.module.css'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
interface ActionProps { interface ActionProps {
@ -13,10 +13,10 @@ interface ActionProps {
const Action = ({ title, text, url, icon, onClick }: ActionProps) => { const Action = ({ title, text, url, icon, onClick }: ActionProps) => {
return ( return (
<a className={action} href={url} onClick={onClick}> <a className={styles.action} href={url} onClick={onClick}>
<Icon name={icon} /> <Icon name={icon} />
<h1 className={actionTitle}>{title}</h1> <h1 className={styles.actionTitle}>{title}</h1>
<p className={actionText}>{text}</p> <p className={styles.actionText}>{text}</p>
</a> </a>
) )
} }
@ -32,7 +32,7 @@ export default function PostActions({
const urlTwitter = `https://twitter.com/intent/tweet?text=@kremalicious&url=${siteUrl}${slug}` const urlTwitter = `https://twitter.com/intent/tweet?text=@kremalicious&url=${siteUrl}${slug}`
return ( return (
<aside className={actions}> <aside className={styles.actions}>
<Action <Action
title="Have a comment?" title="Have a comment?"
text="Hit me up @kremalicious" text="Hit me up @kremalicious"

View File

@ -1,10 +1,13 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Changelog from '../../atoms/Changelog' import Changelog from '../../atoms/Changelog'
import { Post } from '../../../@types/Post'
import PostToc from './Toc' import PostToc from './Toc'
import { content as styleContent } from './Content.module.css' import * as styles from './Content.module.css'
export default function PostContent({ post }: { post: Post }): ReactElement { export default function PostContent({
post
}: {
post: Queries.BlogPostBySlugQuery['post']
}): ReactElement {
const separator = '<!-- more -->' const separator = '<!-- more -->'
const changelog = post.frontmatter.changelog const changelog = post.frontmatter.changelog
@ -27,7 +30,7 @@ export default function PostContent({ post }: { post: Post }): ReactElement {
)} )}
<div <div
dangerouslySetInnerHTML={{ __html: content }} dangerouslySetInnerHTML={{ __html: content }}
className={styleContent} className={styles.content}
/> />
{changelog && <Changelog repo={changelog} />} {changelog && <Changelog repo={changelog} />}
</> </>

View File

@ -1,6 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { lead as styleLead } from './Lead.module.css' import * as styles from './Lead.module.css'
import { Post } from '../../../@types/Post'
// Extract lead paragraph from content // Extract lead paragraph from content
// Grab everything before more tag, or just first paragraph // Grab everything before more tag, or just first paragraph
@ -8,7 +7,7 @@ const PostLead = ({
post, post,
className className
}: { }: {
post: Partial<Post> post: Queries.BlogPostBySlugQuery['post']
className?: string className?: string
}): ReactElement => { }): ReactElement => {
let lead let lead
@ -23,7 +22,7 @@ const PostLead = ({
return ( return (
<div <div
className={`${styleLead} ${className && className}`} className={`${styles.lead} ${className && className}`}
dangerouslySetInnerHTML={{ __html: lead }} dangerouslySetInnerHTML={{ __html: lead }}
/> />
) )

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import { postMore } from './More.module.css' import * as stylesMore from './More.module.css'
import { postLinkActions } from './LinkActions.module.css' import * as styles from './LinkActions.module.css'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
const PostLinkActions = ({ const PostLinkActions = ({
@ -11,8 +11,8 @@ const PostLinkActions = ({
linkurl?: string linkurl?: string
slug: string slug: string
}): ReactElement => ( }): ReactElement => (
<aside className={postLinkActions}> <aside className={styles.postLinkActions}>
<a className={postMore} href={linkurl}> <a className={stylesMore.postMore} href={linkurl}>
Go to source <Icon name="ExternalLink" /> Go to source <Icon name="ExternalLink" />
</a> </a>
<Link to={slug} rel="tooltip" title="Permalink"> <Link to={slug} rel="tooltip" title="Permalink">

View File

@ -3,25 +3,22 @@ import { Link } from 'gatsby'
import slugify from 'slugify' import slugify from 'slugify'
import Tag from '../../atoms/Tag' import Tag from '../../atoms/Tag'
import { useSiteMetadata } from '../../../hooks/use-site-metadata' import { useSiteMetadata } from '../../../hooks/use-site-metadata'
import { import * as styles from './Meta.module.css'
entryMeta,
byline,
by,
type as styleType,
tags as styleTags
} from './Meta.module.css'
import { Post } from '../../../@types/Post'
import PostDate from '../../molecules/PostDate' import PostDate from '../../molecules/PostDate'
export default function PostMeta({ post }: { post: Post }): ReactElement { export default function PostMeta({
post
}: {
post: Queries.BlogPostBySlugQuery['post']
}): ReactElement {
const siteMeta = useSiteMetadata() const siteMeta = useSiteMetadata()
const { author, updated, tags } = post.frontmatter const { author, updated, tags } = post.frontmatter
const { date, type } = post.fields const { date, type } = post.fields
return ( return (
<footer className={entryMeta}> <footer className={styles.entryMeta}>
<div className={byline}> <div className={styles.byline}>
<span className={by}>by</span> <span className={styles.by}>by</span>
<a className="fn" rel="author" href={siteMeta.author.uri}> <a className="fn" rel="author" href={siteMeta.author.uri}>
{author || siteMeta.author.name} {author || siteMeta.author.name}
</a> </a>
@ -30,14 +27,14 @@ export default function PostMeta({ post }: { post: Post }): ReactElement {
<PostDate date={date} updated={updated} /> <PostDate date={date} updated={updated} />
{type && type === 'photo' && ( {type && type === 'photo' && (
<div className={styleType}> <div className={styles.type}>
<Link to={`/${slugify(type)}s/`}>{type}s</Link> <Link to={`/${slugify(type)}s/`}>{type}s</Link>
</div> </div>
)} )}
{tags && ( {tags && (
<div className={styleTags}> <div className={styles.tags}>
{tags.map((tag: string) => { {tags.map((tag) => {
const url = `/archive/${slugify(tag)}/` const url = `/archive/${slugify(tag)}/`
return <Tag key={tag} name={tag} url={url} /> return <Tag key={tag} name={tag} url={url} />
})} })}

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
import { postMore } from './More.module.css' import * as styles from './More.module.css'
const PostMore = ({ const PostMore = ({
to, to,
@ -10,7 +10,7 @@ const PostMore = ({
to: string to: string
children: string children: string
}): ReactElement => ( }): ReactElement => (
<Link className={postMore} to={to}> <Link className={styles.postMore} to={to}>
{children} {children}
<Icon name="ChevronRight" /> <Icon name="ChevronRight" />
</Link> </Link>

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
import { prevnext, label, title } from './PrevNext.module.css' import * as styles from './PrevNext.module.css'
interface Node { interface Node {
title: string title: string
@ -14,21 +14,21 @@ interface PrevNextProps {
} }
const PrevNext = ({ prev, next }: PrevNextProps): ReactElement => ( const PrevNext = ({ prev, next }: PrevNextProps): ReactElement => (
<nav className={prevnext}> <nav className={styles.prevnext}>
<div> <div>
{prev && ( {prev && (
<Link to={prev.slug}> <Link to={prev.slug}>
<Icon name="ChevronLeft" /> <Icon name="ChevronLeft" />
<p className={label}>Newer</p> <p className={styles.label}>Newer</p>
<h3 className={title}>{prev.title}</h3> <h3 className={styles.title}>{prev.title}</h3>
</Link> </Link>
)} )}
</div> </div>
<div> <div>
{next && ( {next && (
<Link to={next.slug}> <Link to={next.slug}>
<p className={label}>Older</p> <p className={styles.label}>Older</p>
<h3 className={title}>{next.title}</h3> <h3 className={styles.title}>{next.title}</h3>
<Icon name="ChevronRight" /> <Icon name="ChevronRight" />
</Link> </Link>
)} )}

View File

@ -1,9 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { import * as styles from './Title.module.css'
title as styleTitle,
titleLink,
linkurl as styleLinkurl
} from './Title.module.css'
import Icon from '../../atoms/Icon' import Icon from '../../atoms/Icon'
import PostDate from '../../molecules/PostDate' import PostDate from '../../molecules/PostDate'
@ -24,16 +20,20 @@ export default function PostTitle({
return linkurl ? ( return linkurl ? (
<> <>
<h1 className={`${styleTitle} ${titleLink} ${className && className}`}> <h1
className={`${styles.title} ${styles.titleLink} ${
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" />
</a> </a>
</h1> </h1>
<div className={styleLinkurl}>{linkHostname}</div> <div className={styles.linkurl}>{linkHostname}</div>
</> </>
) : ( ) : (
<> <>
<h1 className={`${styleTitle} ${className && className}`}>{title}</h1> <h1 className={`${styles.title} ${className && className}`}>{title}</h1>
{date && <PostDate date={date} updated={updated} />} {date && <PostDate date={date} updated={updated} />}
</> </>
) )

View File

@ -1,5 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { toc } from './Toc.module.css' import * as styles from './Toc.module.css'
const PostToc = ({ const PostToc = ({
tableOfContents tableOfContents
@ -8,7 +8,7 @@ const PostToc = ({
}): ReactElement => { }): ReactElement => {
return ( return (
<nav <nav
className={toc} className={styles.toc}
dangerouslySetInnerHTML={{ __html: tableOfContents }} dangerouslySetInnerHTML={{ __html: tableOfContents }}
/> />
) )

View File

@ -14,11 +14,19 @@ describe('Post', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container, rerender } = render( const { container, rerender } = render(
<Post data={post} pageContext={pageContext} /> <Post
data={post as unknown as Queries.BlogPostBySlugQuery}
pageContext={pageContext}
/>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
rerender(<Post data={postWithMore} pageContext={pageContext} />) rerender(
<Post
data={postWithMore as unknown as Queries.BlogPostBySlugQuery}
pageContext={pageContext}
/>
)
rerender(<Post data={link} pageContext={pageContext} />) rerender(<Post data={link} pageContext={pageContext} />)
}) })
}) })

View File

@ -1,7 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { graphql } from 'gatsby' import { graphql } from 'gatsby'
import { Post as PostMetadata } from '../../../@types/Post'
import Exif from '../../atoms/Exif' import Exif from '../../atoms/Exif'
import SEO from '../../atoms/SEO' import SEO from '../../atoms/SEO'
import RelatedPosts from '../../molecules/RelatedPosts' import RelatedPosts from '../../molecules/RelatedPosts'
@ -12,14 +11,14 @@ import PostActions from './Actions'
import PostLinkActions from './LinkActions' import PostLinkActions from './LinkActions'
import PostMeta from './Meta' import PostMeta from './Meta'
import PrevNext from './PrevNext' import PrevNext from './PrevNext'
import { hentry, image as styleImage } from './index.module.css' import * as styles from './index.module.css'
import { Image } from '../../atoms/Image' import { Image } from '../../atoms/Image'
export default function Post({ export default function Post({
data, data,
pageContext: { next, prev } pageContext: { next, prev }
}: { }: {
data: { post: PostMetadata } data: Queries.BlogPostBySlugQuery
pageContext: { pageContext: {
next: { title: string; slug: string } next: { title: string; slug: string }
prev: { title: string; slug: string } prev: { title: string; slug: string }
@ -35,9 +34,9 @@ export default function Post({
{style && <link rel="stylesheet" href={style.publicURL} />} {style && <link rel="stylesheet" href={style.publicURL} />}
</Helmet> </Helmet>
<SEO slug={slug} post={post} postSEO /> <SEO slug={slug} post={post} />
<article className={hentry}> <article className={styles.hentry}>
<header> <header>
<PostTitle <PostTitle
linkurl={linkurl} linkurl={linkurl}
@ -52,14 +51,16 @@ export default function Post({
{image && ( {image && (
<Image <Image
className={styleImage} className={styles.image}
image={(image as any).childImageSharp.gatsbyImageData} image={(image as any).childImageSharp.gatsbyImageData}
alt={title} alt={title}
/> />
)} )}
{type === 'photo' ? ( {type === 'photo' ? (
image?.fields && <Exif exif={image.fields.exif} /> image?.fields && (
<Exif exif={image.fields.exif as Queries.ImageExif} />
)
) : ( ) : (
<PostContent post={post} /> <PostContent post={post} />
)} )}
@ -69,7 +70,7 @@ export default function Post({
<PostActions slug={slug} githubLink={githubLink} /> <PostActions slug={slug} githubLink={githubLink} />
</article> </article>
<RelatedPosts isPhotos={type === 'photo'} tags={tags} /> <RelatedPosts isPhotos={type === 'photo'} tags={tags as string[]} />
<PrevNext prev={prev} next={next} /> <PrevNext prev={prev} next={next} />
</> </>

View File

@ -5,6 +5,7 @@ import WrapPageElement from './wrapPageElement'
describe('wrapPageElement', () => { describe('wrapPageElement', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { container } = render( const { container } = render(
// @ts-expect-error: only testing first render
<WrapPageElement element={'Hello'} props={'hello'} /> <WrapPageElement element={'Hello'} props={'hello'} />
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

@ -1,12 +1,11 @@
import type { GatsbyBrowser, GatsbySSR } from 'gatsby'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Layout from '../components/Layout' import Layout from '../components/Layout'
const wrapPageElement = ({ const wrapPageElement:
element, | GatsbyBrowser['wrapPageElement']
props | GatsbySSR['wrapPageElement'] = ({ element, props }): ReactElement => (
}: { <Layout {...props}>{element}</Layout>
element: any )
props: any
}): ReactElement => <Layout {...props}>{element}</Layout>
export default wrapPageElement export default wrapPageElement

View File

@ -1,8 +1,7 @@
import { useStaticQuery, graphql } from 'gatsby' import { useStaticQuery, graphql } from 'gatsby'
import { Site } from '../@types/Site'
const query = graphql` const query = graphql`
query { query SiteMetadata {
site { site {
siteMetadata { siteMetadata {
siteTitle siteTitle
@ -31,7 +30,7 @@ const query = graphql`
} }
` `
export function useSiteMetadata(): Site { export function useSiteMetadata() {
const { site } = useStaticQuery(query) const { site } = useStaticQuery<Queries.SiteMetadataQuery>(query)
return site.siteMetadata return site.siteMetadata
} }

View File

@ -1,9 +1,10 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Link, PageProps } from 'gatsby' import { Link, PageProps } from 'gatsby'
import Page from '../components/templates/Page' import Page from '../components/templates/Page'
import { hal9000, wrapper, title, text } from './404.module.css' import * as styles from './404.module.css'
import { SeoPost } from '../components/atoms/SEO'
const page = { const page: SeoPost = {
frontmatter: { frontmatter: {
title: '404 - Not Found' title: '404 - Not Found'
} }
@ -11,15 +12,15 @@ const page = {
const NotFound = (props: PageProps): ReactElement => ( const NotFound = (props: PageProps): ReactElement => (
<Page <Page
title={page.frontmatter.title} title={page.frontmatter?.title}
post={page} post={page}
pathname={props.location.pathname} pathname={props.location.pathname}
> >
<div className={hal9000} /> <div className={styles.hal9000} />
<div className={wrapper}> <div className={styles.wrapper}>
<h1 className={title}>{"I'm sorry Dave"}</h1>{' '} <h1 className={styles.title}>{"I'm sorry Dave"}</h1>{' '}
<p className={text}>{"I'm afraid I can't do that"}</p> <p className={styles.text}>{"I'm afraid I can't do that"}</p>
<Link to={'/'}>Back to homepage</Link> <Link to={'/'}>Back to homepage</Link>
</div> </div>
</Page> </Page>

View File

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import NotFound from '../404' import NotFound from '../404'
describe('/404', () => { describe('/404', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
// @ts-expect-error: only testing first render
<NotFound location={{ pathname: '/tags' } as any} /> <NotFound location={{ pathname: '/tags' } as any} />
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

@ -6,7 +6,8 @@ import data from '../../../.jest/__fixtures__/home.json'
describe('/', () => { describe('/', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render(<Home data={data} />) // @ts-expect-error: only testing first render
const { container } = render(<Home data={data as any} />)
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })
}) })

View File

@ -15,6 +15,7 @@ describe('/tags', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = render( const { container } = render(
// @ts-expect-error: only testing first render
<Tags data={data} location={{ pathname: '/tags' } as any} /> <Tags data={data} location={{ pathname: '/tags' } as any} />
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

@ -1,38 +1,35 @@
import { graphql, PageProps } from 'gatsby' import { graphql, PageProps } from 'gatsby'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Post } from '../@types/Post'
import SEO from '../components/atoms/SEO' import SEO from '../components/atoms/SEO'
import PostTeaser from '../components/molecules/PostTeaser' import PostTeaser from '../components/molecules/PostTeaser'
import { PhotoThumb } from '../components/templates/Photos' import { PhotoThumb } from '../components/templates/Photos'
import PostMore from '../components/templates/Post/More' import PostMore from '../components/templates/Post/More'
import { section, articles, articlesLast, photos } from './index.module.css' import * as styles from './index.module.css'
export default function Home({ data }: PageProps): ReactElement { export default function Home(
props: PageProps<Queries.HomePageQuery>
): ReactElement {
return ( return (
<> <>
<SEO /> <SEO />
<section className={section}> <section className={styles.section}>
<div className={articles}> <div className={styles.articles}>
{(data as any).latestArticles.edges {props.data.latestArticles.edges.slice(0, 2).map(({ node }) => (
.slice(0, 2) <PostTeaser key={node.id} post={node} hideDate />
.map(({ node }: { node: Post }) => ( ))}
<PostTeaser key={node.id} post={node} hideDate />
))}
</div> </div>
<div className={`${articles} ${articlesLast}`}> <div className={`${styles.articles} ${styles.articlesLast}`}>
{(data as any).latestArticles.edges {props.data.latestArticles.edges.slice(2, 8).map(({ node }) => (
.slice(2, 8) <PostTeaser key={node.id} post={node} hideDate />
.map(({ node }: { node: Post }) => ( ))}
<PostTeaser key={node.id} post={node} hideDate />
))}
</div> </div>
<PostMore to="/archive">All Articles</PostMore> <PostMore to="/archive">All Articles</PostMore>
</section> </section>
<section className={section}> <section className={styles.section}>
<div className={photos}> <div className={styles.photos}>
{(data as any).latestPhotos.edges.map(({ node }: { node: Post }) => ( {props.data.latestPhotos.edges.map(({ node }) => (
<PhotoThumb key={node.id} photo={node} /> <PhotoThumb key={node.id} photo={node} />
))} ))}
</div> </div>
@ -44,7 +41,7 @@ export default function Home({ data }: PageProps): ReactElement {
} }
export const homeQuery = graphql` export const homeQuery = graphql`
{ query HomePage {
latestArticles: allMarkdownRemark( latestArticles: allMarkdownRemark(
filter: { fields: { type: { ne: "photo" } } } filter: { fields: { type: { ne: "photo" } } }
sort: { fields: { date: DESC } } sort: { fields: { date: DESC } }

View File

@ -2,39 +2,28 @@ import React, { ReactElement } from 'react'
import { graphql, PageProps } from 'gatsby' import { graphql, PageProps } from 'gatsby'
import Page from '../components/templates/Page' import Page from '../components/templates/Page'
import Tag from '../components/atoms/Tag' import Tag from '../components/atoms/Tag'
import { tags } from './tags.module.css' import * as styles from './tags.module.css'
import { SeoPost } from '../components/atoms/SEO'
const page = { const page: SeoPost = {
frontmatter: { frontmatter: {
title: 'Tags', title: 'Tags',
description: 'All the tags being used.' description: 'All the tags being used.'
} }
} }
interface Tag { const TagsPage = ({
fieldValue: string location,
totalCount: number data
} }: PageProps<Queries.TagsPageQuery>): ReactElement => (
<Page title={page.frontmatter.title} post={page} pathname={location.pathname}>
interface TagsPageProps extends PageProps { <ul className={styles.tags}>
data: { {Array.from(data.allMarkdownRemark.group)
allMarkdownRemark: { group: Tag[] }
}
}
const TagsPage = (props: TagsPageProps): ReactElement => (
<Page
title={page.frontmatter.title}
post={page}
pathname={props.location.pathname}
>
<ul className={tags}>
{props.data.allMarkdownRemark.group
.sort((a, b) => b.totalCount - a.totalCount) .sort((a, b) => b.totalCount - a.totalCount)
.map((tag: Tag) => ( .map((tag) => (
<li key={tag.fieldValue}> <li key={tag.fieldValue}>
<Tag <Tag
name={tag.fieldValue} name={tag.fieldValue || ''}
url={`/archive/${tag.fieldValue}/`} url={`/archive/${tag.fieldValue}/`}
count={tag.totalCount} count={tag.totalCount}
style={{ fontSize: `${100 + tag.totalCount * 2}%` }} style={{ fontSize: `${100 + tag.totalCount * 2}%` }}
@ -48,7 +37,7 @@ const TagsPage = (props: TagsPageProps): ReactElement => (
export default TagsPage export default TagsPage
export const tagsPageQuery = graphql` export const tagsPageQuery = graphql`
{ query TagsPage {
allMarkdownRemark { allMarkdownRemark {
group(field: { frontmatter: { tags: SELECT } }) { group(field: { frontmatter: { tags: SELECT } }) {
fieldValue fieldValue

View File

@ -2,16 +2,7 @@ import React, { ReactElement } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useSiteMetadata } from '../hooks/use-site-metadata' import { useSiteMetadata } from '../hooks/use-site-metadata'
import Icon from '../components/atoms/Icon' import Icon from '../components/atoms/Icon'
import { import * as styles from './thanks.module.css'
thanks,
title,
coins as styleCoins,
coin,
code,
buttonBack,
titleCoin,
subTitle
} from './thanks.module.css'
import Web3Donation from '../components/molecules/Web3Donation' import Web3Donation from '../components/molecules/Web3Donation'
import Copy from '../components/atoms/Copy' import Copy from '../components/atoms/Copy'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit' import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
@ -20,9 +11,9 @@ import { chains, theme, wagmiClient } from '../helpers/rainbowkit'
function Coin({ address, title }: { address: string; title: string }) { function Coin({ address, title }: { address: string; title: string }) {
return ( return (
<div className={coin}> <div className={styles.coin}>
<h4 className={titleCoin}>{title}</h4> <h4 className={styles.titleCoin}>{title}</h4>
<pre className={code}> <pre className={styles.code}>
<code>{address}</code> <code>{address}</code>
<Copy text={address} /> <Copy text={address} />
</pre> </pre>
@ -32,7 +23,7 @@ function Coin({ address, title }: { address: string; title: string }) {
const BackButton = () => ( const BackButton = () => (
<button <button
className={`link ${buttonBack}`} className={`link ${styles.buttonBack}`}
onClick={() => window.history.back()} onClick={() => window.history.back()}
> >
<Icon name="ChevronLeft" /> Go Back <Icon name="ChevronLeft" /> Go Back
@ -52,10 +43,10 @@ export default function Thanks(): ReactElement {
<meta name="robots" content="noindex,nofollow" /> <meta name="robots" content="noindex,nofollow" />
</Helmet> </Helmet>
<article className={thanks}> <article className={styles.thanks}>
<BackButton /> <BackButton />
<header> <header>
<h1 className={title}>Say Thanks</h1> <h1 className={styles.title}>Say Thanks</h1>
</header> </header>
<WagmiConfig client={wagmiClient}> <WagmiConfig client={wagmiClient}>
@ -64,8 +55,8 @@ export default function Thanks(): ReactElement {
</RainbowKitProvider> </RainbowKitProvider>
</WagmiConfig> </WagmiConfig>
<div className={styleCoins}> <div className={styles.coins}>
<h3 className={subTitle}> <h3 className={styles.subTitle}>
Send Bitcoin or ERC-20 tokens from any wallet. Send Bitcoin or ERC-20 tokens from any wallet.
</h3> </h3>

View File

@ -4,9 +4,7 @@
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"lib": ["dom", "es2017"], "lib": ["dom", "esnext"],
// "allowJs": true,
// "checkJs": true,
"jsx": "react", "jsx": "react",
// "strict": true, // "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -18,5 +16,12 @@
"plugins": [{ "name": "typescript-plugin-css-modules" }] "plugins": [{ "name": "typescript-plugin-css-modules" }]
}, },
"exclude": ["node_modules", "public", ".cache", "*.js"], "exclude": ["node_modules", "public", ".cache", "*.js"],
"include": ["./src/**/*", "./scripts/*.ts", "./.jest/**/*"] "include": [
"./*.ts",
"./src/**/*",
"./gatsby*",
"./gatsby/**/*",
"./scripts/*.ts",
"./.jest/**/*"
]
} }