mirror of
https://github.com/kremalicious/blog.git
synced 2024-11-22 01:46:51 +01:00
remove react-helmet
This commit is contained in:
parent
a0934b1165
commit
cf083d3288
27
.jest/__mocks__/matchMedia.ts
Normal file
27
.jest/__mocks__/matchMedia.ts
Normal file
@ -0,0 +1,27 @@
|
||||
const matchMediaMock = Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn()
|
||||
}))
|
||||
.mockImplementation((query) => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn()
|
||||
}))
|
||||
})
|
||||
|
||||
export default matchMediaMock
|
@ -1,4 +1,5 @@
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import './__mocks__/matchMedia'
|
||||
|
||||
import * as Gatsby from 'gatsby'
|
||||
const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery')
|
||||
|
@ -27,37 +27,28 @@ My office is where my MacBook is, all these devices go wherever I travel to:
|
||||
|
||||
- **[MacBook Pro (16-inch, 2019)](https://www.apple.com/macbook-pro-16/)**
|
||||
_Space Gray, 2.6GHz 6-Core Intel Core i7, 16 GB RAM, 512GB SSD, AMD Radeon Pro 5300M 4GB, US International keyboard_
|
||||
My main workhorse powering my daily work & play. Everything is pretty much amazing and it handles everything I throw at it. Love the screen, keyboard, and speakers. Still miss the SD card slot which Apple deemed not Pro enough.
|
||||
|
||||
- **[iPhone 11 Pro](https://www.apple.com/iphone-11-pro/)**
|
||||
_Space Gray, 256GB_
|
||||
My main camera and used for everything when I'm not on my Mac. I try to keep it only for personal communication and reading. I'm never quite sure what to do with all its unbelievable power, but the screen is the best display I ever looked at. Because of that, replaces a lot the iPad for reading.
|
||||
|
||||
- **[AirPods Pro](https://www.apple.com/airpods-pro/)**
|
||||
Rocked the original AirPods since their introduction, one of the best products Apple has ever made. By now using the Pro variant pretty much all the time when talking into my devices and participating in online meetings, or listening to music on the go.
|
||||
|
||||
- **[iPad Pro (12.9-inch) 2021](https://www.apple.com/ipad-pro/)**
|
||||
_Space Gray, 512GB, WiFi + Cellular_
|
||||
My 4th iPad after the original iPad. Since that time used it as a paper document replacement for reading and research and it became my favorite device to browse and edit photos on. Until I can run a local web development environment on iPadOS it is rarely useful for me to get coding work done but still intrigued by the possibilities and cleanliness of iPadOS compared to macOS.
|
||||
|
||||
- **[Logitech MX Master 3 for Mac](https://www.logitech.com/en-us/products/mice/mx-master-3-mac-wireless-mouse.910-005693.html)**
|
||||
_Black & Space Gray_
|
||||
Still a fan of the simplicity and gestures of the Magic Mouse 2, but it's just no fit for working precisely, or for gaming. Logitech's MX Master 3 provides both, and has great macOS support, unlike the people over at Razer.
|
||||
|
||||
- **[Satechi Slim X1 Bluetooth Backlit Keyboard](https://satechi.net/products/slim-x1-bluetooth-backlit-keyboard)**
|
||||
_US, Space Gray_
|
||||
Great compact keyboard with black keys which have a great key travel. Packed with other welcome features over Apple's keyboards like backlit keys and easy switching between 3 paired devices.
|
||||
|
||||
- **[Apple Watch Series 5](https://www.apple.com/apple-watch-series-5/)**
|
||||
_40mm Space Gray Aluminum Case, Black Solo Loop_
|
||||
Mainly used for its health & fitness features so I can go on a run without my iPhone, and track the shit out of me. I like to keep it unobtrusive, all notifications are turned off, except for calendar event & Activity.app notifications.
|
||||
|
||||
- **[Raspberry Pi 4 Model B](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/)**
|
||||
_4GB RAM_
|
||||
The little Linux and local server playground performing some serious tasks for all devices within any network I arrive at: [Pi-Hole](https://pi-hole.net) keeps all browsing adventures clean, and every network device's DNS traffic is routed through the Raspberry Pi. [Plex](https://www.plex.tv) is delivering a video library, capable of playing 4K content. It runs a [Tor](https://www.torproject.org) non-exit relay. Has a 4 TB hard drive attached for [storage for all network devices](/raspberry-pi-file-and-screen-sharing-macos-ios).
|
||||
|
||||
- **[Kindle Voyage](https://www.amazon.com/Amazon-Kindle-Voyage-6-Inch-4GB-eReader/dp/B00IOY8XWQ)**
|
||||
The best experience for long time reading. Almost all my non-technical books are Kindle books.
|
||||
- **[Kindle Paperwhite 11th Gen](https://www.amazon.com/dp/B08KTZ8249)**
|
||||
|
||||
## Software
|
||||
|
||||
@ -77,7 +68,7 @@ I live with **automatic dark mode** where all my devices and apps have a light t
|
||||
- **[iCloud Drive, 2 TB](https://www.icloud.com)**
|
||||
I have used Dropbox Pro for many years but it became too clunky and Apple's version turned into what I wanted Dropbox to be. Most of my non-code related files live there and are happily synced.
|
||||
|
||||
- **[Tresorit Premium, 500 GB](https://tresorit.com)**
|
||||
- **[Tresorit Premium, 1 TB](https://tresorit.com)**
|
||||
Holds all the personal and sensitive documents. Works like Dropbox or iCloud Drive but with end-to-end encryption with my own private keys, and some nicely paranoid sharing features.
|
||||
|
||||
- **[Scanner Pro](https://readdle.com/scannerpro)**
|
||||
@ -224,7 +215,7 @@ I live with **automatic dark mode** where all my devices and apps have a light t
|
||||
- **[GnuPG](https://gnupg.org)**
|
||||
Use it since I use email but its clunky and rarely anyone uses it. Interacting with it only in Terminal.app for decrypting and encrypting, and use it to sign Git commits. Yes, I'm aware of [GPG Suite](https://gpgtools.org) but the instability it introduces into the whole operating system is not worth the usage to me.
|
||||
|
||||
- **[Messages](https://support.apple.com/explore/messages) & [Telegram](https://telegram.org)**
|
||||
- **[Messages](https://support.apple.com/explore/messages) & [WhatsApp](https://whatsapp.com)**
|
||||
The only messengers I use every day for personal stuff, mostly on my iPhone.
|
||||
|
||||
- **[Signal](https://www.signal.org)**
|
||||
@ -288,16 +279,13 @@ Except within Notes.app, everything I write is composed as [GitHub Flavored Mark
|
||||
- **[iCloud Backup](https://support.apple.com/en-us/HT203977)**
|
||||
All mobile devices simply use this to create their backups.
|
||||
|
||||
- **[Rclone](https://rclone.org)**
|
||||
For moving large cloud data around within and between online storage services. Usually run from my remote web server.
|
||||
|
||||
## Self Hosted
|
||||
|
||||
- I host my **[blog](https://kremalicious.com)** (which also includes my photo stream) and **[portfolio](https://matthiaskretschmann.com)** on **[AWS S3](https://aws.amazon.com/s3/)**, with **[Cloudflare](https://www.cloudflare.com)** in front of it.
|
||||
|
||||
- I run my own **web and development server**, a droplet on **[DigitalOcean](https://m.do.co/c/9882a054acf6)**, running **[Nginx](https://nginx.org)**.
|
||||
|
||||
- I run my own **analytics server** with **[Matomo](https://matomo.org)**.
|
||||
- I run my own **analytics server** with **[Umami](https://umami.is)**.
|
||||
|
||||
- I run my own **Git repository hosting** with **[Gitea](https://gitea.com)** for private projects, and for automatically mirroring every GitHub repository I touch into it. A VPS running within **[Amazon Lightsail](https://aws.amazon.com/lightsail/)**.
|
||||
|
||||
|
@ -85,10 +85,7 @@ const config: GatsbyConfig = {
|
||||
parentSelector: { 'body.dark': 'Nord' }
|
||||
},
|
||||
injectStyles: false,
|
||||
extensions: [
|
||||
'nord-visual-studio-code',
|
||||
`${__dirname}/vendor/polar-0.0.6.vsix`
|
||||
],
|
||||
extensions: ['nord-visual-studio-code', 'polar'],
|
||||
languageAliases: {}
|
||||
}
|
||||
}
|
||||
@ -217,7 +214,6 @@ const config: GatsbyConfig = {
|
||||
excludes: ['/archive', '/archive/**/*', '/thanks', '/tags']
|
||||
}
|
||||
},
|
||||
'gatsby-plugin-react-helmet',
|
||||
'gatsby-plugin-catch-links',
|
||||
'gatsby-redirect-from',
|
||||
'gatsby-plugin-meta-redirect',
|
||||
|
130
package-lock.json
generated
130
package-lock.json
generated
@ -27,7 +27,6 @@
|
||||
"gatsby-plugin-manifest": "^5.0.0",
|
||||
"gatsby-plugin-meta-redirect": "^1.1.1",
|
||||
"gatsby-plugin-offline": "^6.0.0",
|
||||
"gatsby-plugin-react-helmet": "^6.0.0",
|
||||
"gatsby-plugin-sharp": "^5.0.0",
|
||||
"gatsby-plugin-sitemap": "^6.0.0",
|
||||
"gatsby-plugin-svgr": "^3.0.0-beta.0",
|
||||
@ -48,7 +47,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"rehype-react": "^7.1.1",
|
||||
"remark-parse": "^10.0.1",
|
||||
@ -68,11 +66,9 @@
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@welldone-software/why-did-you-render": "^7.0.1",
|
||||
"babel-preset-gatsby": "^3.0.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@ -89,6 +85,7 @@
|
||||
"node-iptc": "^1.0.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^6.1.2",
|
||||
"polar": "https://gitpkg.now.sh/mtyn/polar/vscode?master",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"stylelint": "^14.14.1",
|
||||
@ -9721,15 +9718,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-helmet": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.5.tgz",
|
||||
"integrity": "sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@ -11047,18 +11035,6 @@
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@welldone-software/why-did-you-render": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-7.0.1.tgz",
|
||||
"integrity": "sha512-Qe/8Xxa2G+LMdI6VoazescPzjjkHYduCDa8aHOJR50e9Bgs8ihkfMBY+ev7B4oc3N59Zm547Sgjf8h5y0FOyoA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/context": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz",
|
||||
@ -18629,21 +18605,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby-plugin-react-helmet": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gatsby-plugin-react-helmet/-/gatsby-plugin-react-helmet-6.0.0.tgz",
|
||||
"integrity": "sha512-uK+KManFE06oeWrZcxpmrpDT0dfDLMPtTxBiU1YPeQeP31IMSsR1vMDTYnQcB+8y8Jw7xwM59id+mwmAYz1fZA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"gatsby": "^5.0.0-next",
|
||||
"react-helmet": "^5.1.3 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby-plugin-sharp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gatsby-plugin-sharp/-/gatsby-plugin-sharp-5.0.0.tgz",
|
||||
@ -30359,6 +30320,15 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/polar": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://gitpkg.now.sh/mtyn/polar/vscode?master",
|
||||
"integrity": "sha512-2QrsAD1Xzewu+T4BJxOB1kIrtRibEdC+/xboP81ueu03/YHYGwdlGXz/YYvqQ09uLqHEPD0BgguWoq/YpCk6Xg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"vscode": "^1.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
@ -31686,25 +31656,6 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
},
|
||||
"node_modules/react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.1.1",
|
||||
"react-side-effect": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@ -31822,14 +31773,6 @@
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-side-effect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
|
||||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
@ -45057,15 +45000,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-helmet": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.5.tgz",
|
||||
"integrity": "sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@ -46106,15 +46040,6 @@
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"@welldone-software/why-did-you-render": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-7.0.1.tgz",
|
||||
"integrity": "sha512-Qe/8Xxa2G+LMdI6VoazescPzjjkHYduCDa8aHOJR50e9Bgs8ihkfMBY+ev7B4oc3N59Zm547Sgjf8h5y0FOyoA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4"
|
||||
}
|
||||
},
|
||||
"@wry/context": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz",
|
||||
@ -52439,14 +52364,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"gatsby-plugin-react-helmet": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gatsby-plugin-react-helmet/-/gatsby-plugin-react-helmet-6.0.0.tgz",
|
||||
"integrity": "sha512-uK+KManFE06oeWrZcxpmrpDT0dfDLMPtTxBiU1YPeQeP31IMSsR1vMDTYnQcB+8y8Jw7xwM59id+mwmAYz1fZA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.15.4"
|
||||
}
|
||||
},
|
||||
"gatsby-plugin-sharp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gatsby-plugin-sharp/-/gatsby-plugin-sharp-5.0.0.tgz",
|
||||
@ -60605,6 +60522,11 @@
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
|
||||
},
|
||||
"polar": {
|
||||
"version": "https://gitpkg.now.sh/mtyn/polar/vscode?master",
|
||||
"integrity": "sha512-2QrsAD1Xzewu+T4BJxOB1kIrtRibEdC+/xboP81ueu03/YHYGwdlGXz/YYvqQ09uLqHEPD0BgguWoq/YpCk6Xg==",
|
||||
"dev": true
|
||||
},
|
||||
"posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
@ -61521,22 +61443,6 @@
|
||||
"scheduler": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
},
|
||||
"react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||
"requires": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.1.1",
|
||||
"react-side-effect": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@ -61613,12 +61519,6 @@
|
||||
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-side-effect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
|
||||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
|
@ -45,7 +45,6 @@
|
||||
"gatsby-plugin-manifest": "^5.0.0",
|
||||
"gatsby-plugin-meta-redirect": "^1.1.1",
|
||||
"gatsby-plugin-offline": "^6.0.0",
|
||||
"gatsby-plugin-react-helmet": "^6.0.0",
|
||||
"gatsby-plugin-sharp": "^5.0.0",
|
||||
"gatsby-plugin-sitemap": "^6.0.0",
|
||||
"gatsby-plugin-svgr": "^3.0.0-beta.0",
|
||||
@ -66,7 +65,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"rehype-react": "^7.1.1",
|
||||
"remark-parse": "^10.0.1",
|
||||
@ -86,11 +84,9 @@
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@welldone-software/why-did-you-render": "^7.0.1",
|
||||
"babel-preset-gatsby": "^3.0.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@ -107,6 +103,7 @@
|
||||
"node-iptc": "^1.0.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^6.1.2",
|
||||
"polar": "https://gitpkg.now.sh/mtyn/polar/vscode?master",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"stylelint": "^14.14.1",
|
||||
|
@ -4,12 +4,6 @@ import Header from './organisms/Header'
|
||||
import Footer from './organisms/Footer'
|
||||
import * as styles from './Layout.module.css'
|
||||
|
||||
// if (process.env.NODE_ENV !== 'production') {
|
||||
// // eslint-disable-next-line
|
||||
// const whyDidYouRender = require('@welldone-software/why-did-you-render/dist/no-classes-transpile/umd/whyDidYouRender.min.js')
|
||||
// whyDidYouRender(React)
|
||||
// }
|
||||
|
||||
export default function Layout({ children }: { children: any }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
|
@ -19,8 +19,7 @@ export default function ExifMap({
|
||||
}: {
|
||||
gps: { latitude: number; longitude: number }
|
||||
}): ReactElement {
|
||||
const { value } = useDarkMode()
|
||||
const isDarkMode = value
|
||||
const { isDarkMode } = useDarkMode()
|
||||
const [zoom, setZoom] = useState(12)
|
||||
|
||||
const zoomIn = () => {
|
||||
|
62
src/components/atoms/HeadMeta/SchemaOrg.tsx
Normal file
62
src/components/atoms/HeadMeta/SchemaOrg.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { ImageDataLike } from 'gatsby-plugin-image'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
|
||||
|
||||
type SchemaOrgProps = {
|
||||
post?: {
|
||||
title: string
|
||||
url: string
|
||||
image: ImageDataLike
|
||||
description: string
|
||||
datePublished: string
|
||||
dateModified: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function SchemaOrg({ post }: SchemaOrgProps): ReactElement {
|
||||
const { siteTitle, siteUrl, author } = useSiteMetadata()
|
||||
|
||||
const schemaOrgJsonLd: any = [
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'Blog',
|
||||
url: siteUrl,
|
||||
name: siteTitle
|
||||
}
|
||||
]
|
||||
|
||||
if (post) {
|
||||
schemaOrgJsonLd.push({
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: author.name
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: author.name
|
||||
},
|
||||
url: post.url,
|
||||
name: post.title,
|
||||
headline: post.title,
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: post.image
|
||||
},
|
||||
description: post.description,
|
||||
datePublished: post.datePublished,
|
||||
dateModified: post.dateModified || post.datePublished,
|
||||
mainEntityOfPage: {
|
||||
'@type': 'Blog',
|
||||
'@id': siteUrl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify(schemaOrgJsonLd)}
|
||||
</script>
|
||||
)
|
||||
}
|
72
src/components/atoms/HeadMeta/index.tsx
Normal file
72
src/components/atoms/HeadMeta/index.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import { getSrc, ImageDataLike } from 'gatsby-plugin-image'
|
||||
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
|
||||
import useDarkMode from '../../../hooks/useDarkMode'
|
||||
|
||||
const query = graphql`
|
||||
query Logo {
|
||||
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
|
||||
edges {
|
||||
node {
|
||||
relativePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export type HeadMetaProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: ImageDataLike
|
||||
slug: string
|
||||
children?: ReactElement
|
||||
}
|
||||
|
||||
export default function HeadMeta(props: HeadMetaProps): ReactElement {
|
||||
const data = useStaticQuery<Queries.LogoQuery>(query)
|
||||
const logo = data.logo.edges[0].node.relativePath
|
||||
const { siteTitle, siteUrl, siteDescription, author } = useSiteMetadata()
|
||||
const { themeColor } = useDarkMode()
|
||||
|
||||
const title = props.title
|
||||
? `${props.title} ¦ ${siteTitle}`
|
||||
: `${siteTitle} ¦ ${siteDescription}`
|
||||
const description = props.description
|
||||
? props.description.slice(0, 160)
|
||||
: siteDescription
|
||||
const url = props.slug ? `${siteUrl}${props.slug}` : siteUrl
|
||||
const image = props.image
|
||||
? `${siteUrl}${getSrc(props.image)}`
|
||||
: `${siteUrl}${logo}`
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content={author.twitter} />
|
||||
|
||||
<meta name="theme-color" content={themeColor} />
|
||||
<meta name="msapplication-TileColor" content={themeColor} />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
title="JSON Feed"
|
||||
type="application/json"
|
||||
href={`${siteUrl}/feed.json`}
|
||||
/>
|
||||
{props.children}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
|
||||
import schemaOrg from './schemaOrg'
|
||||
|
||||
function feedLinks(siteUrl: string) {
|
||||
return (
|
||||
<link
|
||||
rel="alternate"
|
||||
title="JSON Feed"
|
||||
type="application/json"
|
||||
href={`${siteUrl}/feed.json`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MetaTags({
|
||||
description,
|
||||
image,
|
||||
url,
|
||||
postSEO,
|
||||
title,
|
||||
datePublished,
|
||||
dateModified
|
||||
}: {
|
||||
description: string
|
||||
image: string
|
||||
url: string
|
||||
postSEO: boolean
|
||||
title: string
|
||||
datePublished: string
|
||||
dateModified: string
|
||||
}): ReactElement {
|
||||
const { siteTitle, siteDescription, siteUrl, author } = useSiteMetadata()
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
defaultTitle={`${siteTitle} ¦ ${siteDescription}`}
|
||||
titleTemplate={`%s ¦ ${siteTitle}`}
|
||||
>
|
||||
<html lang="en" />
|
||||
|
||||
<meta name="description" content={description} />
|
||||
<meta name="image" content={image} />
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
{schemaOrg(
|
||||
siteUrl,
|
||||
title,
|
||||
postSEO,
|
||||
url,
|
||||
image,
|
||||
description,
|
||||
author.name,
|
||||
datePublished,
|
||||
dateModified
|
||||
)}
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:site_name" content={siteTitle}></meta>
|
||||
{postSEO && <meta property="og:type" content="article" />}
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:creator"
|
||||
content={author.twitter ? author.twitter : ''}
|
||||
/>
|
||||
|
||||
{feedLinks(siteUrl)}
|
||||
</Helmet>
|
||||
)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import { getSrc } from 'gatsby-plugin-image'
|
||||
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
|
||||
import MetaTags from './MetaTags'
|
||||
|
||||
const query = graphql`
|
||||
query Logo {
|
||||
logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
|
||||
edges {
|
||||
node {
|
||||
relativePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export interface SeoPost {
|
||||
frontmatter: {
|
||||
title: string
|
||||
description?: string
|
||||
image?: any
|
||||
updated?: string
|
||||
}
|
||||
fields?: {
|
||||
date: string
|
||||
}
|
||||
excerpt?: string
|
||||
}
|
||||
|
||||
export default function SEO({
|
||||
post,
|
||||
slug
|
||||
}: {
|
||||
post?: SeoPost
|
||||
slug?: string
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery<Queries.LogoQuery>(query)
|
||||
const logo = data.logo.edges[0].node.relativePath
|
||||
const { siteTitle, siteUrl, siteDescription } = useSiteMetadata()
|
||||
|
||||
let title: string
|
||||
let description: string
|
||||
let image: string
|
||||
let postURL: string
|
||||
|
||||
if (post) {
|
||||
const postMeta = post.frontmatter
|
||||
title = `${postMeta.title} ¦ ${siteTitle}`
|
||||
description = postMeta.description ? postMeta.description : post.excerpt
|
||||
image = postMeta.image ? getSrc(postMeta.image) : `/${logo}`
|
||||
postURL = `${siteUrl}${slug}`
|
||||
} else {
|
||||
title = `${siteTitle} ¦ ${siteDescription}`
|
||||
description = siteDescription
|
||||
image = `/${logo}`
|
||||
}
|
||||
|
||||
image = `${siteUrl}${image}`
|
||||
const blogURL = siteUrl
|
||||
const url = post ? postURL : blogURL
|
||||
|
||||
return (
|
||||
<MetaTags
|
||||
description={description}
|
||||
image={image}
|
||||
url={url || ''}
|
||||
postSEO={Boolean(post)}
|
||||
title={title}
|
||||
datePublished={post?.fields && post.fields.date}
|
||||
dateModified={post?.frontmatter.updated}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
export default function schemaOrg(
|
||||
blogURL: string,
|
||||
title: string,
|
||||
postSEO: boolean,
|
||||
postURL: string,
|
||||
image: string,
|
||||
description: string,
|
||||
author: string,
|
||||
datePublished: string,
|
||||
dateModified: string
|
||||
): ReactElement {
|
||||
const schemaOrgJSONLD: any = [
|
||||
{
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'Blog',
|
||||
url: blogURL,
|
||||
name: title
|
||||
}
|
||||
]
|
||||
|
||||
if (postSEO) {
|
||||
schemaOrgJSONLD.push({
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: author
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: author
|
||||
},
|
||||
url: postURL,
|
||||
name: title,
|
||||
headline: title,
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: image
|
||||
},
|
||||
description,
|
||||
datePublished,
|
||||
dateModified: dateModified || datePublished,
|
||||
mainEntityOfPage: {
|
||||
'@type': 'Blog',
|
||||
'@id': blogURL
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify(schemaOrgJSONLD)}
|
||||
</script>
|
||||
)
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import { Script } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
const getTypekitScript = () => (
|
||||
<script>
|
||||
{`
|
||||
const script = `
|
||||
(function(d) {
|
||||
var config = {
|
||||
kitId: '${process.env.GATSBY_TYPEKIT_ID}',
|
||||
@ -12,15 +10,8 @@ const getTypekitScript = () => (
|
||||
},
|
||||
h=d.documentElement,t=setTimeout(function(){h.className=h.className.replace(/\bwf-loading\b/g,"")+" wf-inactive";},config.scriptTimeout),tk=d.createElement("script"),f=false,s=d.getElementsByTagName("script")[0],a;h.className+=" wf-loading";tk.src='https://use.typekit.net/'+config.kitId+'.js';tk.async=true;tk.onload=tk.onreadystatechange=function(){a=this.readyState;if(f||a&&a!="complete"&&a!="loaded")return;f=true;clearTimeout(t);try{Typekit.load(config)}catch(e){}};s.parentNode.insertBefore(tk,s)
|
||||
})(document);
|
||||
`}
|
||||
</script>
|
||||
)
|
||||
`
|
||||
|
||||
export default function Typekit(): JSX.Element {
|
||||
return (
|
||||
<Helmet>
|
||||
<link rel="preconnect" href="https://use.typekit.net" />
|
||||
{getTypekitScript()}
|
||||
</Helmet>
|
||||
)
|
||||
return <Script id="typekit" dangerouslySetInnerHTML={{ __html: script }} />
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import Hamburger from '../atoms/Hamburger'
|
||||
import * as styles from './Menu.module.css'
|
||||
@ -13,6 +12,14 @@ export default function Menu(): ReactElement {
|
||||
setMenuOpen(!menuOpen)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
document.body.classList.add('has-menu-open')
|
||||
} else {
|
||||
document.body.classList.remove('has-menu-open')
|
||||
}
|
||||
}, [menuOpen])
|
||||
|
||||
const MenuItems = menu.map((item) => (
|
||||
<li key={item.title}>
|
||||
<Link onClick={toggleMenu} to={item.link}>
|
||||
@ -23,9 +30,6 @@ export default function Menu(): ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html className={menuOpen ? 'has-menu-open' : undefined} lang="en" />
|
||||
</Helmet>
|
||||
<Hamburger onClick={toggleMenu} />
|
||||
<nav className={styles.menu}>
|
||||
<ul>{MenuItems}</ul>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, ReactElement } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
import SearchInput from './SearchInput'
|
||||
import SearchButton from './SearchButton'
|
||||
@ -26,6 +25,14 @@ export default function Search(): ReactElement {
|
||||
)
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchOpen) {
|
||||
document.body.classList.add('hasSearchOpen')
|
||||
} else {
|
||||
document.body.classList.remove('hasSearchOpen')
|
||||
}
|
||||
}, [searchOpen])
|
||||
|
||||
function toggleSearch(): void {
|
||||
setSearchOpen(!searchOpen)
|
||||
}
|
||||
@ -36,10 +43,6 @@ export default function Search(): ReactElement {
|
||||
|
||||
{searchOpen && (
|
||||
<>
|
||||
<Helmet>
|
||||
<html className="hasSearchOpen" lang="en" />
|
||||
</Helmet>
|
||||
|
||||
<CSSTransition
|
||||
appear={searchOpen}
|
||||
in={searchOpen}
|
||||
|
@ -14,9 +14,7 @@ describe('ThemeSwitch', () => {
|
||||
|
||||
const toggle = container.querySelector('input')
|
||||
const label = container.querySelector('label')
|
||||
expect(toggle.checked).toBeFalsy()
|
||||
fireEvent.click(label)
|
||||
fireEvent.change(toggle, { target: { checked: true } })
|
||||
expect(toggle.checked).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@ -1,71 +1,33 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import React, { ReactElement } from 'react'
|
||||
import * as styles from './ThemeSwitch.module.css'
|
||||
import Icon from '../atoms/Icon'
|
||||
import useDarkMode from '../../hooks/useDarkMode'
|
||||
|
||||
const ThemeToggleInput = ({
|
||||
isDark,
|
||||
toggleDark
|
||||
}: {
|
||||
isDark: boolean
|
||||
toggleDark: () => void
|
||||
}) => (
|
||||
<input
|
||||
onChange={() => toggleDark()}
|
||||
type="checkbox"
|
||||
name="toggle"
|
||||
value="toggle"
|
||||
aria-describedby="toggle"
|
||||
checked={isDark}
|
||||
/>
|
||||
)
|
||||
|
||||
const HeadMarkup = ({
|
||||
bodyClass,
|
||||
themeColor
|
||||
}: {
|
||||
bodyClass: string
|
||||
themeColor: string
|
||||
}) => (
|
||||
<Helmet>
|
||||
<body className={bodyClass} />
|
||||
<meta name="theme-color" content={themeColor} />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
</Helmet>
|
||||
)
|
||||
|
||||
export default function ThemeSwitch(): ReactElement {
|
||||
const { value, toggle } = useDarkMode()
|
||||
const [themeColor, setThemeColor] = useState<string>()
|
||||
const [bodyClass, setBodyClass] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
setBodyClass(value ? 'dark' : null)
|
||||
setThemeColor(value ? '#1d2224' : '#e7eef4')
|
||||
}, [value])
|
||||
const { isDarkMode, setIsDarkMode } = useDarkMode()
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadMarkup themeColor={themeColor} bodyClass={bodyClass} />
|
||||
<aside className={styles.themeSwitch} title="Toggle Dark Mode">
|
||||
<label
|
||||
htmlFor="toggle"
|
||||
className={styles.checkbox}
|
||||
onClick={toggle}
|
||||
onKeyPress={toggle}
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
onKeyPress={() => setIsDarkMode(!isDarkMode)}
|
||||
role="presentation"
|
||||
>
|
||||
<span className={styles.label}>Toggle Dark Mode</span>
|
||||
<ThemeToggleInput isDark={value} toggleDark={toggle} />
|
||||
<input
|
||||
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||
type="checkbox"
|
||||
name="toggle"
|
||||
value="toggle"
|
||||
aria-describedby="toggle"
|
||||
checked={isDarkMode}
|
||||
/>
|
||||
<div aria-live="assertive">
|
||||
{value ? <Icon name="Sun" /> : <Icon name="Moon" />}
|
||||
{isDarkMode ? <Icon name="Sun" /> : <Icon name="Moon" />}
|
||||
</div>
|
||||
</label>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -5,6 +5,23 @@ import Pagination from '../molecules/Pagination'
|
||||
import PostTeaser from '../molecules/PostTeaser'
|
||||
import Page from './Page'
|
||||
import * as styles from './Archive.module.css'
|
||||
import HeadMeta, { HeadMetaProps } from '../atoms/HeadMeta'
|
||||
|
||||
function getMetadata(pageContext: PageContext) {
|
||||
const { tag, currentPageNumber, numPages } = pageContext
|
||||
const title = tag ? `#${tag}` : 'Archive'
|
||||
const paginationTitle =
|
||||
numPages > 1 && currentPageNumber > 1
|
||||
? `Page ${currentPageNumber} / ${numPages}`
|
||||
: ''
|
||||
|
||||
const meta: Partial<HeadMetaProps> = {
|
||||
title: `${title} ${paginationTitle}`,
|
||||
description: 'All the articles & links.'
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
export default function Archive({
|
||||
data,
|
||||
@ -14,38 +31,25 @@ export default function Archive({
|
||||
pageContext: PageContext
|
||||
}): ReactElement {
|
||||
const edges = data.allMarkdownRemark.edges
|
||||
const { tag, currentPageNumber, numPages } = pageContext
|
||||
const meta = getMetadata(pageContext)
|
||||
|
||||
const PostsList = edges.map(({ node }) => (
|
||||
<PostTeaser key={node.id} post={node} />
|
||||
))
|
||||
|
||||
const title = tag ? `#${tag}` : 'Archive'
|
||||
|
||||
const paginationTitle =
|
||||
numPages > 1 && currentPageNumber > 1
|
||||
? `Page ${currentPageNumber} / ${numPages}`
|
||||
: ''
|
||||
|
||||
const page = {
|
||||
frontmatter: {
|
||||
title: `${title} ${paginationTitle}`,
|
||||
description: 'All the articles & links.'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={page.frontmatter.title}
|
||||
post={page}
|
||||
pathname={pageContext.slug}
|
||||
>
|
||||
<Page title={meta.title}>
|
||||
<div className={styles.posts}>{PostsList}</div>
|
||||
{numPages > 1 && <Pagination pageContext={pageContext} />}
|
||||
{pageContext.numPages > 1 && <Pagination pageContext={pageContext} />}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export function Head({ pageContext }: { pageContext: PageContext }) {
|
||||
const meta = getMetadata(pageContext)
|
||||
return <HeadMeta {...meta} slug={pageContext.slug} />
|
||||
}
|
||||
|
||||
export const archiveQuery = graphql`
|
||||
query ArchiveTemplate($tag: String, $skip: Int, $limit: Int) {
|
||||
allMarkdownRemark(
|
||||
|
@ -1,26 +1,17 @@
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import SEO, { SeoPost } from '../atoms/SEO'
|
||||
import * as styles from './Page.module.css'
|
||||
|
||||
export default function Page({
|
||||
title,
|
||||
section,
|
||||
children,
|
||||
pathname,
|
||||
post
|
||||
children
|
||||
}: {
|
||||
title: string
|
||||
children: ReactNode
|
||||
pathname: string
|
||||
section?: string
|
||||
post?: SeoPost
|
||||
}): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Helmet title={title} />
|
||||
<SEO slug={pathname} post={post} />
|
||||
|
||||
<h1 className={styles.pagetitle}>{title}</h1>
|
||||
{section ? <section className={section}>{children}</section> : children}
|
||||
</>
|
||||
|
@ -5,6 +5,7 @@ import { Image } from '../atoms/Image'
|
||||
import Pagination from '../molecules/Pagination'
|
||||
import Page from './Page'
|
||||
import * as styles from './Photos.module.css'
|
||||
import HeadMeta, { HeadMetaProps } from '../atoms/HeadMeta'
|
||||
|
||||
export const PhotoThumb = ({
|
||||
photo
|
||||
@ -37,37 +38,45 @@ function getMetadata(currentPageNumber: number, numPages: number) {
|
||||
? `Page ${currentPageNumber} / ${numPages}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
frontmatter: {
|
||||
const meta: Partial<HeadMetaProps> = {
|
||||
title: `Photos ${paginationTitle}`,
|
||||
description:
|
||||
'Personal photos of designer & developer Matthias Kretschmann.'
|
||||
}
|
||||
description: 'Personal photos of designer & developer Matthias Kretschmann.'
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
export default function Photos(props: PhotosPageProps): ReactElement {
|
||||
const photos = props.data.allMarkdownRemark.edges
|
||||
const { currentPageNumber, numPages } = props.pageContext
|
||||
const page = getMetadata(currentPageNumber, numPages)
|
||||
export default function Photos({
|
||||
data,
|
||||
pageContext
|
||||
}: PhotosPageProps): ReactElement {
|
||||
const photos = data.allMarkdownRemark.edges
|
||||
const { currentPageNumber, numPages } = pageContext
|
||||
const meta = getMetadata(currentPageNumber, numPages)
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={page.frontmatter.title}
|
||||
post={page}
|
||||
pathname={props.location.pathname}
|
||||
>
|
||||
<Page title={meta.title}>
|
||||
<section className={styles.photos}>
|
||||
{photos.map(({ node }) => (
|
||||
<PhotoThumb key={node.id} photo={node} />
|
||||
))}
|
||||
</section>
|
||||
|
||||
{numPages > 1 && <Pagination pageContext={props.pageContext} />}
|
||||
{numPages > 1 && <Pagination pageContext={pageContext} />}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export function Head({
|
||||
pageContext
|
||||
}: {
|
||||
pageContext: PhotosPageProps['pageContext']
|
||||
}) {
|
||||
const { currentPageNumber, numPages } = pageContext
|
||||
const meta = getMetadata(currentPageNumber, numPages)
|
||||
return <HeadMeta {...meta} slug={pageContext.slug} />
|
||||
}
|
||||
|
||||
export const photosQuery = graphql`
|
||||
query PhotosTemplate($skip: Int, $limit: Int) {
|
||||
allMarkdownRemark(
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { graphql } from 'gatsby'
|
||||
import Exif from '../../atoms/Exif'
|
||||
import SEO from '../../atoms/SEO'
|
||||
import RelatedPosts from '../../molecules/RelatedPosts'
|
||||
import PostTitle from './Title'
|
||||
import PostLead from './Lead'
|
||||
@ -13,6 +11,10 @@ import PostMeta from './Meta'
|
||||
import PrevNext from './PrevNext'
|
||||
import * as styles from './index.module.css'
|
||||
import { Image } from '../../atoms/Image'
|
||||
import HeadMeta from '../../atoms/HeadMeta'
|
||||
import { PageContext } from '../../../@types/Post'
|
||||
import SchemaOrg from '../../atoms/HeadMeta/schemaOrg'
|
||||
import { useSiteMetadata } from '../../../hooks/use-site-metadata'
|
||||
|
||||
export default function Post({
|
||||
data,
|
||||
@ -25,17 +27,11 @@ export default function Post({
|
||||
}
|
||||
}): ReactElement {
|
||||
const { post } = data
|
||||
const { title, image, linkurl, style, tags, updated } = post.frontmatter
|
||||
const { title, image, linkurl, tags, updated } = post.frontmatter
|
||||
const { slug, githubLink, date, type } = post.fields
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet title={title}>
|
||||
{style && <link rel="stylesheet" href={style.publicURL} />}
|
||||
</Helmet>
|
||||
|
||||
<SEO slug={slug} post={post} />
|
||||
|
||||
<article className={styles.hentry}>
|
||||
<header>
|
||||
<PostTitle
|
||||
@ -77,6 +73,43 @@ export default function Post({
|
||||
)
|
||||
}
|
||||
|
||||
export function Head({
|
||||
pageContext,
|
||||
data
|
||||
}: {
|
||||
pageContext: PageContext
|
||||
data: Queries.BlogPostBySlugQuery
|
||||
}): ReactElement {
|
||||
const { siteUrl } = useSiteMetadata()
|
||||
const { excerpt, rawMarkdownBody } = data.post
|
||||
const { title, image, style, updated } = data.post.frontmatter
|
||||
const { date } = data.post.fields
|
||||
const description = excerpt || rawMarkdownBody
|
||||
|
||||
return (
|
||||
<HeadMeta
|
||||
title={title}
|
||||
description={description}
|
||||
slug={pageContext.slug}
|
||||
image={image}
|
||||
>
|
||||
<>
|
||||
<SchemaOrg
|
||||
post={{
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
url: `${siteUrl}${pageContext.slug}`,
|
||||
datePublished: date,
|
||||
dateModified: updated
|
||||
}}
|
||||
/>
|
||||
{style && <link rel="stylesheet" href={style.publicURL} />}
|
||||
</>
|
||||
</HeadMeta>
|
||||
)
|
||||
}
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query BlogPostBySlug($slug: String!) {
|
||||
post: markdownRemark(fields: { slug: { eq: $slug } }) {
|
||||
|
@ -125,15 +125,6 @@ h6 {
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.wf-loading h1,
|
||||
.wf-loading h2,
|
||||
.wf-loading h3,
|
||||
.wf-loading h4,
|
||||
.wf-loading h5,
|
||||
.wf-loading h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 .anchor.before,
|
||||
h2 .anchor.before,
|
||||
h3 .anchor.before,
|
||||
@ -349,9 +340,6 @@ td {
|
||||
}
|
||||
|
||||
#___gatsby {
|
||||
/* // display: flex;
|
||||
// min-height: 100vh;
|
||||
// flex-direction: column; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,13 @@
|
||||
// adapted from
|
||||
// https://github.com/daveschumaker/react-dark-mode-hook/blob/master/useDarkMode.js
|
||||
//
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
Dispatch,
|
||||
SetStateAction
|
||||
} from 'react'
|
||||
|
||||
const isClient = typeof window === 'object'
|
||||
|
||||
@ -24,25 +30,41 @@ function getDarkMode() {
|
||||
}
|
||||
}
|
||||
|
||||
export default function useDarkMode(): {
|
||||
value: boolean
|
||||
toggle?: () => void
|
||||
} {
|
||||
const [darkMode, setDarkMode] = useState(getDarkMode)
|
||||
export type UseDarkMode = {
|
||||
isDarkMode: boolean
|
||||
themeColor: string
|
||||
setIsDarkMode: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
setDarkMode(!darkMode)
|
||||
export default function useDarkMode(): UseDarkMode {
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(getDarkMode())
|
||||
const [themeColor, setThemeColor] = useState<string>()
|
||||
|
||||
const changeTheme = useCallback(() => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
setThemeColor(isDarkMode === true ? '#1d2224' : '#e7eef4')
|
||||
}, [isDarkMode])
|
||||
|
||||
//
|
||||
// Init
|
||||
//
|
||||
useEffect(() => {
|
||||
changeTheme()
|
||||
}, [changeTheme])
|
||||
|
||||
//
|
||||
// Handle system theme change events
|
||||
//
|
||||
const handleChange = useCallback(() => {
|
||||
setDarkMode(getDarkMode())
|
||||
setIsDarkMode(getDarkMode())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient || process.env.NODE_ENV === 'test') return
|
||||
if (!isClient) return
|
||||
|
||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
@ -58,5 +80,5 @@ export default function useDarkMode(): {
|
||||
.removeEventListener('change', handleChange)
|
||||
}, [handleChange])
|
||||
|
||||
return { value: darkMode, toggle: toggleDarkMode }
|
||||
return { isDarkMode, setIsDarkMode, themeColor }
|
||||
}
|
||||
|
@ -2,28 +2,27 @@ import React, { ReactElement } from 'react'
|
||||
import { Link, PageProps } from 'gatsby'
|
||||
import Page from '../components/templates/Page'
|
||||
import * as styles from './404.module.css'
|
||||
import { SeoPost } from '../components/atoms/SEO'
|
||||
import HeadMeta, { HeadMetaProps } from '../components/atoms/HeadMeta'
|
||||
|
||||
const page: SeoPost = {
|
||||
frontmatter: {
|
||||
title: '404 - Not Found'
|
||||
}
|
||||
const meta: Partial<HeadMetaProps> = {
|
||||
title: `I'm sorry Dave`,
|
||||
description: `I'm afraid I can't do that`
|
||||
}
|
||||
|
||||
const NotFound = (props: PageProps): ReactElement => (
|
||||
<Page
|
||||
title={page.frontmatter?.title}
|
||||
post={page}
|
||||
pathname={props.location.pathname}
|
||||
>
|
||||
const NotFound = (): ReactElement => (
|
||||
<Page title={meta.title}>
|
||||
<div className={styles.hal9000} />
|
||||
|
||||
<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>
|
||||
<h1 className={styles.title}>{meta.title}</h1>{' '}
|
||||
<p className={styles.text}>{meta.description}</p>
|
||||
<Link to={'/'}>Back to homepage</Link>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default NotFound
|
||||
|
||||
export function Head(props: PageProps) {
|
||||
return <HeadMeta {...meta} slug={props.location.pathname} />
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
import React, { ReactElement } from 'react'
|
||||
import SEO from '../components/atoms/SEO'
|
||||
import HeadMeta from '../components/atoms/HeadMeta'
|
||||
import PostTeaser from '../components/molecules/PostTeaser'
|
||||
import { PhotoThumb } from '../components/templates/Photos'
|
||||
import PostMore from '../components/templates/Post/More'
|
||||
@ -11,7 +11,6 @@ export default function Home(
|
||||
): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<SEO />
|
||||
<section className={styles.section}>
|
||||
<div className={styles.articles}>
|
||||
{props.data.latestArticles.edges.slice(0, 2).map(({ node }) => (
|
||||
@ -40,6 +39,10 @@ export default function Home(
|
||||
)
|
||||
}
|
||||
|
||||
export function Head() {
|
||||
return <HeadMeta slug="/" />
|
||||
}
|
||||
|
||||
export const homeQuery = graphql`
|
||||
query HomePage {
|
||||
latestArticles: allMarkdownRemark(
|
||||
|
@ -3,20 +3,15 @@ import { graphql, PageProps } from 'gatsby'
|
||||
import Page from '../components/templates/Page'
|
||||
import Tag from '../components/atoms/Tag'
|
||||
import * as styles from './tags.module.css'
|
||||
import { SeoPost } from '../components/atoms/SEO'
|
||||
import HeadMeta, { HeadMetaProps } from '../components/atoms/HeadMeta'
|
||||
|
||||
const page: SeoPost = {
|
||||
frontmatter: {
|
||||
const meta: Partial<HeadMetaProps> = {
|
||||
title: 'Tags',
|
||||
description: 'All the tags being used.'
|
||||
}
|
||||
}
|
||||
|
||||
const TagsPage = ({
|
||||
location,
|
||||
data
|
||||
}: PageProps<Queries.TagsPageQuery>): ReactElement => (
|
||||
<Page title={page.frontmatter.title} post={page} pathname={location.pathname}>
|
||||
const TagsPage = ({ data }: PageProps<Queries.TagsPageQuery>): ReactElement => (
|
||||
<Page title={meta.title}>
|
||||
<ul className={styles.tags}>
|
||||
{Array.from(data.allMarkdownRemark.group)
|
||||
.sort((a, b) => b.totalCount - a.totalCount)
|
||||
@ -36,6 +31,10 @@ const TagsPage = ({
|
||||
|
||||
export default TagsPage
|
||||
|
||||
export function Head(props: PageProps) {
|
||||
return <HeadMeta {...meta} slug={props.location.pathname} />
|
||||
}
|
||||
|
||||
export const tagsPageQuery = graphql`
|
||||
query TagsPage {
|
||||
allMarkdownRemark {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useSiteMetadata } from '../hooks/use-site-metadata'
|
||||
import Icon from '../components/atoms/Icon'
|
||||
import * as styles from './thanks.module.css'
|
||||
@ -8,6 +7,12 @@ import Copy from '../components/atoms/Copy'
|
||||
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
|
||||
import { WagmiConfig } from 'wagmi'
|
||||
import { chains, theme, wagmiClient } from '../helpers/rainbowkit'
|
||||
import Meta, { HeadMetaProps } from '../components/atoms/HeadMeta'
|
||||
import { HeadProps } from 'gatsby'
|
||||
|
||||
const meta: Partial<HeadMetaProps> = {
|
||||
title: `Say Thanks`
|
||||
}
|
||||
|
||||
function Coin({ address, title }: { address: string; title: string }) {
|
||||
return (
|
||||
@ -37,16 +42,10 @@ export default function Thanks(): ReactElement {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Say thanks</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Helmet>
|
||||
|
||||
<article className={styles.thanks}>
|
||||
<BackButton />
|
||||
<header>
|
||||
<h1 className={styles.title}>Say Thanks</h1>
|
||||
<h1 className={styles.title}>{meta.title}</h1>
|
||||
</header>
|
||||
|
||||
<WagmiConfig client={wagmiClient}>
|
||||
@ -65,6 +64,13 @@ export default function Thanks(): ReactElement {
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Head(props: HeadProps) {
|
||||
return (
|
||||
<Meta {...meta} slug={props.location.pathname}>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Meta>
|
||||
)
|
||||
}
|
||||
|
BIN
vendor/polar-0.0.6.vsix
vendored
BIN
vendor/polar-0.0.6.vsix
vendored
Binary file not shown.
Loading…
Reference in New Issue
Block a user