mirror of
https://github.com/kremalicious/blog.git
synced 2024-11-22 01:46:51 +01:00
parent
8d0a900d98
commit
0aaf874538
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
@ -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
1
.gitignore
vendored
@ -7,3 +7,4 @@ coverage
|
||||
.env
|
||||
.env.development
|
||||
.nova
|
||||
src/@types/Gatsby.d.ts
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
siteTitle: 'kremalicious',
|
||||
siteTitleShort: 'krlc',
|
||||
siteDescription: 'Blog of designer & developer Matthias Kretschmann',
|
@ -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
18
gatsby-browser.tsx
Normal 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)
|
||||
}
|
@ -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,16 +176,16 @@ 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) }]
|
||||
})
|
||||
})
|
||||
@ -226,3 +233,5 @@ module.exports = {
|
||||
'gatsby-plugin-offline'
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
155
gatsby-node.js
155
gatsby-node.js
@ -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
171
gatsby-node.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
|
||||
export const wrapPageElement = wrapPageElementWithLayout
|
5
gatsby-ssr.tsx
Normal file
5
gatsby-ssr.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { GatsbySSR } from 'gatsby'
|
||||
import wrapPageElementWithLayout from './src/helpers/wrapPageElement'
|
||||
|
||||
export const wrapPageElement: GatsbySSR['wrapPageElement'] =
|
||||
wrapPageElementWithLayout
|
@ -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))
|
@ -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
|
||||
})
|
||||
}
|
72
gatsby/createMarkdownFields.ts
Normal file
72
gatsby/createMarkdownFields.ts
Normal 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 })
|
||||
}
|
@ -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,
|
@ -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
77
gatsby/feeds.ts
Normal 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 }
|
@ -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
2395
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -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",
|
||||
|
25
src/@types/GitHub.d.ts
vendored
25
src/@types/GitHub.d.ts
vendored
@ -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
31
src/@types/Image.d.ts
vendored
@ -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
35
src/@types/Post.d.ts
vendored
@ -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
31
src/@types/Site.d.ts
vendored
@ -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
5
src/@types/css.d.ts
vendored
@ -1,4 +1 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
declare module '*.module.css'
|
||||
|
9
src/@types/node_modules.d.ts
vendored
9
src/@types/node_modules.d.ts
vendored
@ -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]
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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} />
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
@ -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} />
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 => (
|
||||
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>
|
||||
</>
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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
|
||||
|
@ -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} />}
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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} />
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
|
@ -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" />}
|
||||
|
@ -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 & developer{' '}
|
||||
<a className="fn" rel="author" href={uri}>
|
||||
{name}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
© 2005–
|
||||
{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>
|
||||
|
@ -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 />
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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" } }
|
||||
|
@ -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}
|
||||
</>
|
||||
)
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 } }
|
||||
|
@ -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"
|
||||
|
@ -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} />}
|
||||
</>
|
||||
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
|
@ -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">
|
||||
|
@ -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} />
|
||||
})}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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} />}
|
||||
</>
|
||||
)
|
||||
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
|
@ -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} />)
|
||||
})
|
||||
})
|
||||
|
@ -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} />
|
||||
</>
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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()
|
||||
|
@ -1,28 +1,25 @@
|
||||
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 }) => (
|
||||
<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 }) => (
|
||||
<div className={`${styles.articles} ${styles.articlesLast}`}>
|
||||
{props.data.latestArticles.edges.slice(2, 8).map(({ node }) => (
|
||||
<PostTeaser key={node.id} post={node} hideDate />
|
||||
))}
|
||||
</div>
|
||||
@ -30,9 +27,9 @@ export default function Home({ data }: PageProps): ReactElement {
|
||||
<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 } }
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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/**/*"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user