1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-11-22 01:46:51 +01:00
* 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:
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
steps:
- uses: actions/checkout@v3
@ -76,9 +41,15 @@ jobs:
- run: npm ci
- run: npm run build
env:
GATSBY_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GATSBY_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }}
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
if: github.ref == 'refs/heads/main'
@ -86,8 +57,21 @@ jobs:
name: 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:
needs: build
needs: test
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ coverage
.env
.env.development
.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.
For local development, run the test watcher:
```bash
npm run test:watch
```
### 🎈 Add a new post
```bash

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
siteTitle: 'kremalicious',
siteTitleShort: 'krlc',
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
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.
`)
}
const siteConfig = require('./config')
const sources = require('./gatsby/sources')
const { feedContent } = require('./gatsby/feeds')
import siteConfig from './config'
import sources from './gatsby/sources'
import { feedContent } from './gatsby/feeds'
// 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: {
...siteConfig
},
@ -169,44 +176,44 @@ module.exports = {
`,
feeds: [
{
serialize: ({ query: { allMarkdownRemark } }) => {
return allMarkdownRemark.edges.map((edge) => {
serialize: ({ query }: { query: Queries.AllContentFeedQuery }) => {
return query.allMarkdownRemark.edges.map((edge) => {
return Object.assign({}, edge.node.frontmatter, {
title: edge.node.frontmatter.title,
date: edge.node.fields.date,
title: edge.node.frontmatter?.title,
date: edge.node.fields?.date,
description: edge.node.excerpt,
url: siteConfig.siteUrl + edge.node.fields.slug,
categories: edge.node.frontmatter.tags,
url: siteConfig.siteUrl + edge.node.fields?.slug,
categories: edge.node.frontmatter?.tags,
author: siteConfig.author.name,
guid: siteConfig.siteUrl + edge.node.fields.slug,
guid: siteConfig.siteUrl + edge.node.fields?.slug,
custom_elements: [{ 'content:encoded': feedContent(edge) }]
})
})
},
query: `{
allMarkdownRemark(sort: {fields: {date: DESC}}, limit: 40) {
edges {
node {
html
fields {
slug
date
}
excerpt
frontmatter {
title
image {
childImageSharp {
resize(width: 940, quality: 85) {
src
allMarkdownRemark(sort: {fields: {date: DESC}}, limit: 40) {
edges {
node {
html
fields {
slug
date
}
excerpt
frontmatter {
title
image {
childImageSharp {
resize(width: 940, quality: 85) {
src
}
}
}
}
}
}
}
}
}
}
}
}
}
}`,
}`,
output: '/feed.xml',
title: siteConfig.siteTitle
}
@ -226,3 +233,5 @@ module.exports = {
'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')
const util = require('util')
const fastExif = require('fast-exif')
const Fraction = require('fraction.js')
const getCoordinates = require('dms2dec')
const iptc = require('node-iptc')
import fs from 'fs'
import util from 'util'
import fastExif from 'fast-exif'
import Fraction from 'fraction.js'
import getCoordinates from 'dms2dec'
import iptc from 'node-iptc'
import type { Actions, NodePluginArgs, Node } from 'gatsby'
const readFile = util.promisify(fs.readFile)
exports.createExif = async (node, actions, createNodeId) => {
export const createExif = async (
node: Node,
actions: Actions,
createNodeId: NodePluginArgs['createNodeId']
) => {
try {
// exif
const exifData = await fastExif.read(node.absolutePath, true)
if (!exifData) return
// iptc
const file = await readFile(node.absolutePath)
const file = await readFile(node.absolutePath as string)
const iptcData = iptc(file)
createNodes(exifData, iptcData, node, actions, createNodeId)
} catch (error) {
} catch (error: any) {
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 exifDataFormatted = formatExif(exifData)
const exif = {
...exifData,
iptc: {
...iptcData
},
formatted: {
...exifDataFormatted
}
iptc: { ...iptcData },
formatted: { ...exifDataFormatted }
}
const exifNode = {
const exifNode: any = {
id: createNodeId(`${node.id} >> ImageExif`),
children: [],
...exif,
@ -49,22 +56,15 @@ function createNodes(exifData, iptcData, node, actions, createNodeId) {
}
// add exif fields to existing type file
createNodeField({
node,
name: 'exif',
value: exif
})
createNodeField({ node, name: 'exif', value: exif })
// create new nodes from all exif data
// allowing to be queried with imageExif & AllImageExif
createNode(exifNode)
createParentChildLink({
parent: node,
child: exifNode
})
createParentChildLink({ parent: node, child: exifNode })
}
function formatExif(exifData) {
function formatExif(exifData: Queries.ImageExif) {
if (!exifData.exif) return
const { Model } = exifData.image
@ -107,8 +107,11 @@ function formatExif(exifData) {
}
}
function formatGps(gpsData) {
if (!gpsData) return
function formatGps(gpsData: Queries.ImageExif['gps']): {
latitude: string
longitude: string
} {
if (!gpsData) return { latitude: '', longitude: '' }
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
@ -125,7 +128,7 @@ function formatGps(gpsData) {
return { latitude, longitude }
}
function formatExposure(exposureMode) {
function formatExposure(exposureMode: Queries.ImageExifExif['ExposureMode']) {
if (exposureMode === null || exposureMode === undefined) return
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')
const { itemsPerPage } = require('../config')
import path from 'path'
import config from '../config'
import { Actions } from 'gatsby'
const postTemplate = path.resolve('src/components/templates/Post/index.tsx')
const archiveTemplate = path.resolve('src/components/templates/Archive.tsx')
@ -11,7 +12,7 @@ const redirects = [
{ f: '/goodies/', t: '/archive/goodies/' }
]
function getPaginationData(i, numPages, slug) {
function getPaginationData(i: number, numPages: number, slug: string) {
const currentPage = i + 1
const prevPageNumber = currentPage <= 1 ? null : currentPage - 1
const nextPageNumber = currentPage + 1 > numPages ? null : currentPage + 1
@ -26,29 +27,38 @@ function getPaginationData(i, numPages, slug) {
return { prevPagePath, nextPagePath, path }
}
exports.generatePostPages = (createPage, posts) => {
export const generatePostPages = (
createPage: Actions['createPage'],
posts: Queries.AllContentQuery['all']['edges'] | undefined
) => {
// Create Post pages
posts.forEach((post) => {
posts?.forEach((post) => {
createPage({
path: `${post.node.fields.slug}`,
path: `${post.node.fields?.slug}`,
component: postTemplate,
context: {
slug: post.node.fields.slug,
slug: post.node.fields?.slug,
prev: post.previous && {
title: post.previous.frontmatter.title,
slug: post.previous.fields.slug
title: post.previous.frontmatter?.title,
slug: post.previous.fields?.slug
},
next: post.next && {
title: post.next.frontmatter.title,
slug: post.next.fields.slug
title: post.next.frontmatter?.title,
slug: post.next.fields?.slug
}
}
})
})
}
function generateIndexPages(createPage, length, slug, template, tag) {
const numPages = Math.ceil(length / itemsPerPage)
function generateIndexPages(
createPage: Actions['createPage'],
length: number,
slug: string,
template: string,
tag?: string
) {
const numPages = Math.ceil(length / config.itemsPerPage)
Array.from({ length: numPages }).forEach((_, i) => {
const { prevPagePath, nextPagePath, path } = getPaginationData(
@ -62,8 +72,8 @@ function generateIndexPages(createPage, length, slug, template, tag) {
component: template,
context: {
slug,
limit: itemsPerPage,
skip: i * itemsPerPage,
limit: config.itemsPerPage,
skip: i * config.itemsPerPage,
numPages: numPages,
currentPageNumber: i + 1,
prevPagePath,
@ -75,29 +85,44 @@ function generateIndexPages(createPage, length, slug, template, tag) {
}
// 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)
}
// 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)
}
// 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 }) => {
generateIndexPages(
createPage,
totalCount,
`/archive/${tag}/`,
archiveTemplate,
tag
tag || ''
)
})
}
exports.generateRedirectPages = (createRedirect) => {
export const generateRedirectPages = (
createRedirect: Actions['createRedirect']
) => {
redirects.forEach(({ f, t }) => {
createRedirect({
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',
options: {
@ -43,7 +43,7 @@ module.exports = [
fieldName: 'github',
url: 'https://api.github.com/graphql',
headers: {
Authorization: `bearer ${process.env.GATSBY_GITHUB_TOKEN}`
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
}
// Additional options to pass to node-fetch
// 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",
"homepage": "https://kremalicious.com",
"license": "MIT",
"main": "index.js",
"scripts": {
"start": "gatsby develop --host 0.0.0.0",
"build": "gatsby build",
"ssr": "npm run build && serve -s public/",
"test": "npm run lint && jest -c .jest/jest.config.js --coverage --silent",
"test:watch": "npm run lint && jest -c .jest/jest.config.js --coverage --watch",
"test": "npm run lint && npm run type-check && npm run jest",
"jest": "jest -c .jest/jest.config.js --coverage --silent",
"lint": "run-p --continue-on-error lint:js lint:css lint:md",
"lint:js": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:css": "stylelint 'src/**/*.css'",
"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}'",
"tsc": "tsc --noEmit",
"type-check": "tsc --noEmit",
"deploy:s3": "./scripts/deploy-s3.sh",
"new": "ts-node scripts/new.ts"
},
@ -29,16 +28,16 @@
],
"dependencies": {
"@kremalicious/react-feather": "^2.1.0",
"@rainbow-me/rainbowkit": "^0.7.0",
"axios": "^0.27.2",
"@rainbow-me/rainbowkit": "^0.7.4",
"axios": "^1.1.3",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"dms2dec": "^1.1.0",
"ethers": "^5.7.1",
"ethers": "^5.7.2",
"fast-exif": "^1.0.1",
"feather-icons": "^4.29.0",
"fraction.js": "^4.2.0",
"gatsby": "^5.0.0",
"gatsby": "^5.0.1",
"gatsby-plugin-catch-links": "^5.0.0",
"gatsby-plugin-feed": "^5.0.0",
"gatsby-plugin-image": "^3.0.0",
@ -63,7 +62,7 @@
"gatsby-transformer-remark": "^6.0.0",
"gatsby-transformer-sharp": "^5.0.0",
"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",
"react": "^18.2.0",
"react-clipboard.js": "^2.0.16",
@ -79,11 +78,11 @@
"wagmi": "^0.6.6"
},
"devDependencies": {
"@svgr/webpack": "^6.3.1",
"@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^29.0.3",
"@types/jest": "^29.2.2",
"@types/lunr": "^2.3.4",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
@ -103,17 +102,17 @@
"eslint-plugin-testing-library": "^5.9.1",
"fs-extra": "^10.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.3.0",
"jest-environment-jsdom": "^29.3.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"markdownlint-cli": "^0.32.2",
"node-iptc": "^1.0.5",
"npm-run-all": "^4.1.5",
"ora": "^6.1.2",
"postcss": "^8.4.18",
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"stylelint": "^14.14.1",
"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-prettier": "^2.0.0",
"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 {
title?: string
original?: { src: 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 {
tag?: 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' {
const classes: { [key: string]: string }
export default classes
}
declare module '*.module.css'

View File

@ -3,3 +3,12 @@ declare module 'pigeon-marker'
declare module 'unified'
declare module 'fast-exif'
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 Header from './organisms/Header'
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') {
// // eslint-disable-next-line
@ -16,8 +16,8 @@ export default function Layout({ children }: { children: any }): ReactElement {
<Typekit />
<Header />
<main className={document} id="document">
<div className={content}>{children}</div>
<main className={styles.document} id="document">
<div className={styles.content}>{children}</div>
</main>
<Footer />

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import React, { ReactElement } from 'react'
import ExifMap from './ExifMap'
import { exif as styleExif, data, map } from './Exif.module.css'
import { Exif as ExifMeta } from '../../@types/Image'
import * as styles from './Exif.module.css'
import Icon from './Icon'
const ExifData = ({
@ -19,15 +18,19 @@ const ExifData = ({
</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 } =
exif.formatted
const formattedModel = model === 'FC7203' ? 'DJI Mavic Mini' : model
return (
<aside className={styleExif}>
<div className={data}>
<aside className={styles.exif}>
<div className={styles.data}>
{formattedModel && (
<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" />}
{iso && <ExifData title="ISO" value={iso} icon="Maximize" />}
</div>
{gps && gps.latitude && (
<div className={map}>
{gps?.latitude && (
<div className={styles.map}>
<ExifMap gps={gps} />
</div>
)}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import React, { ReactElement, InputHTMLAttributes } from 'react'
import { input } from './Input.module.css'
import * as styles from './Input.module.css'
export default function Input({
className,
...props
}: 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 { getSrc } from 'gatsby-plugin-image'
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
import { Post } from '../../../@types/Post'
import MetaTags from './MetaTags'
const query = graphql`
query {
query Logo {
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
edges {
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({
post,
slug,
postSEO
slug
}: {
post?: Post
post?: SeoPost
slug?: string
postSEO?: boolean
}): ReactElement {
const data = useStaticQuery(query)
const data = useStaticQuery<Queries.LogoQuery>(query)
const logo = data.logo.edges[0].node.relativePath
const { siteTitle, siteUrl, siteDescription } = useSiteMetadata()
let title
let description
let image
let postURL
let title: string
let description: string
let image: string
let postURL: string
if (postSEO) {
if (post) {
const postMeta = post.frontmatter
title = `${postMeta.title} ¦ ${siteTitle}`
description = postMeta.description ? postMeta.description : post.excerpt
@ -49,17 +59,17 @@ export default function SEO({
image = `${siteUrl}${image}`
const blogURL = siteUrl
const url = postSEO ? postURL : blogURL
const url = post ? postURL : blogURL
return (
<MetaTags
description={description}
image={image}
url={url}
postSEO={postSEO}
url={url || ''}
postSEO={Boolean(post)}
title={title}
datePublished={post && post.fields && post.fields.date}
dateModified={post && post.frontmatter.updated}
datePublished={post?.fields && post.fields.date}
dateModified={post?.frontmatter.updated}
/>
)
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'
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 }) {
let IconComp
@ -28,7 +28,7 @@ export default function IconLinks({
return (
<p>
{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} />
</a>
))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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): {
[key: string]: string
@ -23,7 +23,11 @@ const constructMessage = (
: message && message.text
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({
transactionHash,

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { parseEther } from '@ethersproject/units'
import { useDebounce } from 'use-debounce'
import InputGroup from './InputGroup'
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 { ConnectButton } from '@rainbow-me/rainbowkit'
@ -53,7 +53,7 @@ export default function Web3Donation({
return (
<form
className={styleWeb3}
className={styles.web3}
onSubmit={(e) => {
e.preventDefault()
handleSendTransaction()

View File

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

View File

@ -4,19 +4,19 @@ import Search from '../molecules/Search'
import Menu from '../molecules/Menu'
import ThemeSwitch from '../molecules/ThemeSwitch'
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 {
return (
<header role="banner" className={header}>
<div className={headerContent}>
<h1 className={title}>
<header role="banner" className={styles.header}>
<div className={styles.headerContent}>
<h1 className={styles.title}>
<Link to="/">
<Logo className={logo} /> kremalicious
<Logo className={styles.logo} /> kremalicious
</Link>
</h1>
<nav role="navigation" className={nav}>
<nav role="navigation" className={styles.nav}>
<ThemeSwitch />
<Search />
<Menu />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
import React, { ReactElement } from 'react'
import Changelog from '../../atoms/Changelog'
import { Post } from '../../../@types/Post'
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 changelog = post.frontmatter.changelog
@ -27,7 +30,7 @@ export default function PostContent({ post }: { post: Post }): ReactElement {
)}
<div
dangerouslySetInnerHTML={{ __html: content }}
className={styleContent}
className={styles.content}
/>
{changelog && <Changelog repo={changelog} />}
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,11 +14,19 @@ describe('Post', () => {
it('renders without crashing', () => {
const { container, rerender } = render(
<Post data={post} pageContext={pageContext} />
<Post
data={post as unknown as Queries.BlogPostBySlugQuery}
pageContext={pageContext}
/>
)
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} />)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,8 @@ import data from '../../../.jest/__fixtures__/home.json'
describe('/', () => {
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()
})
})

View File

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

View File

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

View File

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

View File

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

View File

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