├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
├── dependabot.yml
└── workflows
│ ├── calibreapp-image-actions.yml
│ ├── codeql-analysis.yml
│ ├── dependabotAutoMerge.yml
│ ├── dockerimage.yml
│ └── nodejs.yml
├── .gitignore
├── .prettierrc
├── .vscode
├── settings.json
└── snippets.code-snippets
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── README.md
├── __mocks__
├── file-mock.js
├── gatsby.js
├── style-mock.js
└── svg-mock.js
├── content
├── meta
│ └── config.js
├── pages
│ ├── resources
│ │ └── index.mdx
│ ├── success
│ │ └── index.mdx
│ └── uses
│ │ └── index.mdx
├── parts
│ └── author.mdx
└── posts
│ ├── 2018
│ ├── 2018-11-10--react-tutorial-adding-typescript
│ │ ├── index.mdx
│ │ └── react-logo.png
│ └── 2018-11-20--javascript-copyright-date
│ │ ├── 2019.jpg
│ │ └── index.mdx
│ ├── 2019
│ └── 2019-04-30--change-specflow-build
│ │ ├── index.mdx
│ │ └── sf-logo.png
│ ├── 2020
│ ├── 2020-02-08--gatsby-change-from-md-to-mdx
│ │ ├── gatsby-mdx.png
│ │ └── index.mdx
│ ├── 2020-02-11--gatsby-create-published-filter-for-posts
│ │ ├── gatsby-blue-green.png
│ │ └── index.mdx
│ ├── 2020-05-21--gatsby-create-an-audience-with-mailchimp
│ │ ├── finished_form.png
│ │ ├── index.mdx
│ │ └── mail.jpg
│ ├── 2020-06-01--compress-your-images-with-tinify
│ │ ├── image.jpg
│ │ └── index.mdx
│ ├── 2020-06-30--add-linting-to-create-react-app
│ │ ├── image.jpg
│ │ └── index.mdx
│ └── 2020-07-10--debug-gatsby-simulator
│ │ ├── index.mdx
│ │ └── laptop.jpg
│ └── 2021
│ ├── 2021-01-08--gatsby-error-monitoring-with-sentry
│ ├── error_image.jpg
│ └── index.mdx
│ ├── 2021-01-15--react-netlify-client-side-routing
│ ├── computer.jpg
│ └── index.mdx
│ ├── 2021-01-21--ssh-synology-nas
│ ├── index.mdx
│ └── synology.png
│ ├── 2021-01-25--portainer-docker-nas
│ ├── index.mdx
│ └── shipyard.jpg
│ ├── 2021-01-31--pihole-docker-nas
│ ├── index.mdx
│ └── pi-hole.png
│ ├── 2021-02-05--calibre-library-docker-nas
│ ├── index.mdx
│ └── library.jpg
│ └── 2021-02-19--watchtower-docker-nas
│ ├── index.mdx
│ └── watchtower.jpg
├── coverage
├── badge-branches.svg
├── badge-functions.svg
├── badge-lines.svg
└── badge-statements.svg
├── cypress.json
├── cypress
├── e2e
│ ├── a11y.test.js
│ ├── index.test.js
│ └── pages.test.js
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
└── support
│ └── index.js
├── docker-compose.yml
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── jest-preprocess.js
├── jest.config.js
├── loadershim.js
├── netlify.toml
├── package.json
├── postcss.config.js
├── scripts
└── generate-app-icons.sh
├── src
├── components
│ ├── Article
│ │ ├── Article.js
│ │ ├── Bodytext.js
│ │ ├── Headline.js
│ │ ├── SubHeadline.js
│ │ └── index.js
│ ├── AsyncComponent
│ │ ├── AsyncComponent.js
│ │ └── index.js
│ ├── Blog
│ │ ├── Blog.js
│ │ ├── Item.js
│ │ └── index.js
│ ├── Contact
│ │ ├── Contact.js
│ │ └── index.js
│ ├── ErrorBoundary
│ │ ├── ErrorBoundary.js
│ │ └── index.js
│ ├── Footer
│ │ ├── Footer.js
│ │ └── index.js
│ ├── Header
│ │ ├── Header.js
│ │ └── index.js
│ ├── Hero
│ │ ├── Hero.js
│ │ └── index.js
│ ├── List
│ │ ├── List.js
│ │ └── index.js
│ ├── Menu
│ │ ├── Expand.js
│ │ ├── Item.js
│ │ ├── Menu.js
│ │ └── index.js
│ ├── Page
│ │ ├── Page.js
│ │ └── index.js
│ ├── Post
│ │ ├── Author.js
│ │ ├── Meta.js
│ │ ├── NextPrev.js
│ │ ├── Post.js
│ │ ├── Share.js
│ │ └── index.js
│ ├── Project
│ │ ├── Project.js
│ │ └── index.js
│ ├── Search
│ │ ├── Hit.js
│ │ ├── Search.js
│ │ └── index.js
│ ├── Seo
│ │ ├── Seo.js
│ │ └── index.js
│ ├── Skill
│ │ ├── Skill.js
│ │ ├── SkillCard.js
│ │ └── index.js
│ ├── Social
│ │ ├── Social.js
│ │ └── index.js
│ ├── Subscribe
│ │ ├── Subscribe.js
│ │ └── index.js
│ ├── Work
│ │ ├── Education.js
│ │ ├── Volunteer.js
│ │ ├── Work.js
│ │ └── index.js
│ └── __tests__
│ │ ├── __snapshots__
│ │ ├── article.spec.js.snap
│ │ ├── education.spec.js.snap
│ │ ├── footer.spec.js.snap
│ │ ├── skill.spec.js.snap
│ │ ├── social.spec.js.snap
│ │ └── volunteer.spec.js.snap
│ │ ├── article.spec.js
│ │ ├── education.spec.js
│ │ ├── footer.spec.js
│ │ ├── skill.spec.js
│ │ ├── social.spec.js
│ │ └── volunteer.spec.js
├── data
│ └── project.json
├── html.js
├── images
│ ├── app-icons
│ │ ├── apple-icon.png
│ │ └── icon.png
│ ├── jpg
│ │ └── avatar.jpg
│ ├── png
│ │ ├── hero-background.png
│ │ └── mesh.png
│ ├── project
│ │ ├── cardshuffling.png
│ │ ├── chrisottodev.png
│ │ ├── fitnesseformat.png
│ │ ├── library.png
│ │ └── vscodefitnesse.png
│ └── svg-icons
│ │ ├── algolia-full.svg
│ │ ├── algolia.svg
│ │ ├── email.svg
│ │ ├── facebook.svg
│ │ └── search-by-algolia.svg
├── layouts
│ └── index.js
├── pages
│ ├── 404.js
│ ├── about.js
│ ├── contact.js
│ ├── project.js
│ ├── search.js
│ └── tag.js
├── templates
│ ├── PageTemplate.js
│ ├── PostTemplate.js
│ ├── TagTemplate.js
│ └── index.js
├── theme
│ └── theme.json
└── utils
│ ├── algolia.js
│ └── helpers.js
├── static
├── _headers
├── favicon.ico
└── icons
│ ├── apple-icon-114x114.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-144x144.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-180x180.png
│ ├── apple-icon-57x57.png
│ ├── apple-icon-60x60.png
│ ├── apple-icon-72x72.png
│ ├── apple-icon-76x76.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── icon-144x144.png
│ ├── icon-192x192.png
│ ├── icon-256x256.png
│ ├── icon-384x384.png
│ ├── icon-48x48.png
│ ├── icon-512x512.png
│ └── icon-96x96.png
├── util
├── compressImages.js
├── newPost.js
└── registry.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@babel/plugin-proposal-optional-chaining"],
3 | "presets": [
4 | [
5 | "babel-preset-gatsby",
6 | {
7 | "targets": {
8 | "browsers": [">0.25%", "not dead"]
9 | }
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/*
2 | static/*
3 | .cache/*
4 | src/components/__tests__/*
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "google",
5 | "prettier",
6 | "prettier/react",
7 | "eslint:recommended",
8 | "plugin:react/recommended"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 2016,
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "jsx": true
15 | }
16 | },
17 | "plugins": ["prettier", "react", "jsx-a11y", "import", "graphql"],
18 | "env": {
19 | "browser": true,
20 | "es6": true,
21 | "node": true
22 | },
23 | "rules": {
24 | "arrow-body-style": "off",
25 | "camelcase": "warn",
26 | "func-names": "off",
27 | "global-require": "warn",
28 | "guard-for-in": "off",
29 | "import/no-dynamic-require": "warn",
30 | "import/no-extraneous-dependencies": "off",
31 | "no-console": "off",
32 | "no-invalid-this": "off",
33 | "no-multi-assign": "off",
34 | "no-param-reassign": "warn",
35 | "no-plusplus": "off",
36 | "no-shadow": "warn",
37 | "no-underscore-dangle": "warn",
38 | "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }],
39 | "no-nested-ternary": "off",
40 | "no-unused-vars": "warn",
41 | "prefer-destructuring": "off",
42 | "prettier/prettier": ["error"],
43 | "react/jsx-uses-vars": "error",
44 | "require-jsdoc": "off"
45 | },
46 | "overrides": [
47 | {
48 | "files": ["*.spec.js", "*.integration.js"],
49 | "rules": {
50 | "no-unused-expressions": "off"
51 | }
52 | }
53 | ],
54 | "settings": {
55 | "react": {
56 | "pragma": "React",
57 | "version": "detect"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | time: '11:00'
8 | open-pull-requests-limit: 50
9 | target-branch: dependencies
10 | labels:
11 | - dependencies
12 |
--------------------------------------------------------------------------------
/.github/workflows/calibreapp-image-actions.yml:
--------------------------------------------------------------------------------
1 | name: Compress images
2 | on: pull_request
3 | jobs:
4 | build:
5 | name: calibreapp/image-actions
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout Repo
9 | uses: actions/checkout@master
10 | - name: Compress Images
11 | uses: calibreapp/image-actions@master
12 | with:
13 | githubToken: ${{ secrets.GITHUB_TOKEN }}
14 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master, dependencies]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 21 * * 4'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['javascript']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 |
35 | # Initializes the CodeQL tools for scanning.
36 | - name: Initialize CodeQL
37 | uses: github/codeql-action/init@v1
38 | with:
39 | languages: ${{ matrix.language }}
40 | # If you wish to specify custom queries, you can do so here or in a config file.
41 | # By default, queries listed here will override any specified in a config file.
42 | # Prefix the list here with "+" to use these queries and those in the config file.
43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
44 |
45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
46 | # If this step fails, then you should remove it and run the build manually (see below)
47 | - name: Autobuild
48 | uses: github/codeql-action/autobuild@v1
49 |
50 | # ℹ️ Command-line programs to run using the OS shell.
51 | # 📚 https://git.io/JvXDl
52 |
53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
54 | # and modify them (or add more) to build your code if your project
55 | # uses a compiled language
56 |
57 | #- run: |
58 | # make bootstrap
59 | # make release
60 |
61 | - name: Perform CodeQL Analysis
62 | uses: github/codeql-action/analyze@v1
63 |
--------------------------------------------------------------------------------
/.github/workflows/dependabotAutoMerge.yml:
--------------------------------------------------------------------------------
1 | name: auto-merge
2 | on:
3 | pull_request:
4 | jobs:
5 | auto-merge:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2
9 | - uses: ahmadnassri/action-dependabot-auto-merge@v2
10 | with:
11 | target: minor
12 | github-token: ${{ secrets.PAT }}
13 |
--------------------------------------------------------------------------------
/.github/workflows/dockerimage.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | env:
11 | ALGOLIA_ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}
12 | ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
13 | ALGOLIA_INDEX_NAME: ${{ secrets.ALGOLIA_INDEX_NAME }}
14 | ALGOLIA_SEARCH_ONLY_API_KEY: ${{ secrets.ALGOLIA_SEARCH_ONLY_API_KEY }}
15 | FB_APP_ID: ${{ secrets.FB_APP_ID }}
16 | GOOGLE_ANALYTICS_ID: ${{ secrets.GOOGLE_ANALYTICS_ID}}
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Publish to Registry
20 | uses: elgohr/Publish-Docker-Github-Action@master
21 | with:
22 | name: ${{ github.repository }}/chrisottodev
23 | username: chrisotto6
24 | password: ${{ secrets.AUTH }}
25 | registry: docker.pkg.github.com
26 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Build, Lint and Test CI
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | env:
7 | ALGOLIA_ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}
8 | ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
9 | ALGOLIA_INDEX_NAME: ${{ secrets.ALGOLIA_INDEX_NAME }}
10 | ALGOLIA_SEARCH_ONLY_API_KEY: ${{ secrets.ALGOLIA_SEARCH_ONLY_API_KEY }}
11 | FB_APP_ID: ${{ secrets.FB_APP_ID }}
12 | GOOGLE_ANALYTICS_ID: ${{ secrets.GOOGLE_ANALYTICS_ID}}
13 | strategy:
14 | matrix:
15 | node-version: [12.0.0]
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - name: Install And Lint
23 | run: yarn install && npm run lint
24 | env:
25 | CI: true
26 | - name: Build And Test
27 | run: npm run build:local && npm test && npm run e2e
28 | env:
29 | CI: true
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # env
2 | .env.development
3 |
4 | # Cypress
5 | cypress/screenshots/*
6 | coverage/lcov-report
7 | coverage/*.json
8 | coverage/*.xml
9 | coverage/*.info
10 |
11 | # Logs
12 | logs
13 | *.log
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (http://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Typescript v1 declaration files
47 | typings/
48 |
49 | # Optional npm cache directory
50 | .npm
51 |
52 | # Optional eslint cache
53 | .eslintcache
54 |
55 | # Optional REPL history
56 | .node_repl_history
57 |
58 | # Output of 'npm pack'
59 | *.tgz
60 |
61 | # Yarn Integrity file
62 | .yarn-integrity
63 |
64 | # dotenv environment variables file
65 | .env
66 |
67 | .cache/
68 | .DS_Store
69 | public/
70 | yarn-error.log
71 |
72 | # editors
73 | .idea/
74 |
75 | # other
76 | report/
77 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "printWidth": 100,
4 | "endOfLine": "lf",
5 | "semi": false,
6 | "singleQuote": true,
7 | "tabWidth": 2,
8 | "trailingComma": "es5"
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.vscode/snippets.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | // Place your chrisottodev workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7 | // Placeholders with the same ids are connected.
8 | // Example:
9 | // "Print to console": {
10 | // "scope": "javascript,typescript",
11 | // "prefix": "log",
12 | // "body": [
13 | // "console.log('$1');",
14 | // "$2"
15 | // ],
16 | // "description": "Log output to console"
17 | // }
18 | "RC": {
19 | "scope": "javascript,javascriptreact",
20 | "prefix": "rc",
21 | "body": [
22 | "import React from 'react'",
23 | "import PropTypes from 'prop-types'",
24 | "",
25 | "const ${1} = (props) => {",
26 | " const {} = props",
27 | "",
28 | " return (",
29 | " ",
30 | " {/* --- STYLES --- */}",
31 | " ",
34 | " ",
35 | " )",
36 | "}",
37 | "",
38 | "${1}.propTypes = {}",
39 | "",
40 | "export default ${1}",
41 | ""
42 | ],
43 | "description": "Create new React Component for this"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at greglobinski@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 |
3 | # Also exposing VSCode debug ports
4 | EXPOSE 8000 9929 9230
5 |
6 | RUN \
7 | apk add --no-cache python make g++ && \
8 | apk add vips-dev fftw-dev --update-cache \
9 | --repository http://dl-3.alpinelinux.org/alpine/edge/community \
10 | --repository http://dl-3.alpinelinux.org/alpine/edge/main \
11 | && rm -fR /var/cache/apk/*
12 |
13 | RUN npm install -g gatsby-cli
14 |
15 | WORKDIR /app
16 | COPY ./package.json .
17 | RUN yarn install && yarn cache clean
18 | COPY . .
19 | CMD ["yarn", "develop", "-H", "0.0.0.0" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Chris Otto
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chris Otto's Blog
2 |
3 | ## Build and Deploy
4 |
5 |  [](https://app.netlify.com/sites/gatsby-otto/deploys)
6 |
7 | ## Coverage
8 |
9 | [](https://github.com/chrisotto6/gatsby-starter) [](https://github.com/chrisotto6/gatsby-starter) [](https://github.com/chrisotto6/gatsby-starter) [](https://github.com/chrisotto6/gatsby-starter)
10 |
11 | ## Getting Started
12 |
13 | Clone the repository and run:
14 |
15 | ```text
16 | yarn install
17 | ```
18 |
19 | To run the local develop build run:
20 |
21 | ```text
22 | gatsby develop
23 | ```
24 |
25 | To run the production build and serve locally run:
26 |
27 | ```text
28 | yarn build:local && gatsby serve
29 | ```
30 |
--------------------------------------------------------------------------------
/__mocks__/file-mock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub'
2 |
--------------------------------------------------------------------------------
/__mocks__/gatsby.js:
--------------------------------------------------------------------------------
1 | const React = require('react')
2 |
3 | const gatsby = jest.requireActual('gatsby')
4 |
5 | module.exports = {
6 | ...gatsby,
7 | graphql: jest.fn(),
8 | Link: jest.fn().mockImplementation(
9 | // these props are invalid for an `a` tag
10 | ({
11 | activeClassName,
12 | activeStyle,
13 | getProps,
14 | innerRef,
15 | partiallyActive,
16 | ref,
17 | replace,
18 | to,
19 | ...rest
20 | }) =>
21 | React.createElement('a', {
22 | ...rest,
23 | href: to,
24 | })
25 | ),
26 | StaticQuery: jest.fn(),
27 | useStaticQuery: jest.fn(),
28 | }
29 |
--------------------------------------------------------------------------------
/__mocks__/style-mock.js:
--------------------------------------------------------------------------------
1 | // __mocks__/styleMock.js
2 |
3 | module.exports = {}
4 |
--------------------------------------------------------------------------------
/__mocks__/svg-mock.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = 'div'
3 |
--------------------------------------------------------------------------------
/content/meta/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteTitle: 'Chris Otto | Father, Developer, Test Engineer', //
3 | siteDescription: 'Thoughts and projects',
4 | siteUrl: 'https://chrisotto.dev',
5 | siteLanguage: 'en',
6 |
7 | /* author */
8 | authorName: 'Chris Otto',
9 | authorTwitterAccount: 'wisco_cmo',
10 |
11 | /* info */
12 | headerTitle: 'Chris Otto',
13 | headerSubTitle: 'Developer, Test Engineer',
14 |
15 | /* manifest.json */
16 | manifestName: 'Chris Otto | Father, Developer, Test Engineer',
17 | manifestShortName: 'Chris Otto', // max 12 characters
18 | manifestStartUrl: '/index.html',
19 | manifestBackgroundColor: 'white',
20 | manifestThemeColor: '#666',
21 | manifestDisplay: 'standalone',
22 |
23 | // gravatar
24 | gravatarImgMd5: '',
25 |
26 | // social
27 | authorSocialLinks: [
28 | { name: 'dev', url: 'https://dev.to/chrisotto' },
29 | { name: 'github', url: 'https://github.com/chrisotto6' },
30 | { name: 'instagram', url: 'https://instagram.com/wisco_cmo' },
31 | { name: 'linkedin', url: 'https://www.linkedin.com/in/ottochristopher/' },
32 | { name: 'twitter', url: 'https://twitter.com/wisco_cmo' },
33 | ],
34 | }
35 |
--------------------------------------------------------------------------------
/content/pages/success/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Success
3 | published: true
4 | ---
5 |
6 | Thank you for reaching out!
7 | I will answer your message as soon as possible.
8 |
--------------------------------------------------------------------------------
/content/pages/uses/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Uses
3 | published: true
4 | ---
5 |
6 | # Uses
7 |
8 | What I `use` for my developer setup, gear, software, configs, etc.
9 |
10 | ## Computers
11 |
12 | ### Desktop
13 |
14 | #### Core
15 |
16 | - Case: [NZXT S340 Mid Tower](https://www.amazon.com/gp/product/B00NGMIBXC)
17 | - Motherboard: [ROG Maximus IX Code](https://www.amazon.com/gp/product/B01NGTRXOOASUS)
18 | - Processor: [Intel Core i7-7700K](https://www.amazon.com/gp/product/B01MXSI216)
19 | - CPU Cooler: [NZXT Kraken X61 280mm Liquid Cooling System](https://www.amazon.com/gp/product/B00L0YLJJG)
20 | - RAM: 32 GB [Corsair Vengeance LPX DDR4](https://www.amazon.com/gp/product/B0134EW7G8)
21 | - Boot Drive: [MyDigitalSSD BPX M.2 PCIE 480GB](https://www.amazon.com/gp/product/B01MDRUXNZ)
22 | - Power Supply: [EVGA 500W](https://www.amazon.com/gp/product/B00H33SFJU)
23 | - Graphics Card: [EVGA GeForce GTX 970](https://www.amazon.com/gp/product/B00U2ON9B6)
24 |
25 | #### Peripherals
26 |
27 | - Monitor: 2x - [Acer 23.8 in IPS](https://www.amazon.com/gp/product/B01LY3DB9J)
28 | - Monitor Mount: [VIVO Dual Monitor Desk Mount](https://www.amazon.com/gp/product/B009S750LA)
29 | - Headset: [Astro A50](https://www.amazon.com/gp/product/B01G3WBCQY)
30 | - Mouse: [Razer DeathAdder Elite](https://www.amazon.com/gp/product/B01LXC1QL0)
31 | - Keyboard: [Corsair K55](https://www.amazon.com/gp/product/B01M4LIKLI)
32 | - Webcam: [Logitech C920](https://www.amazon.com/gp/product/B006JH8T3S)
33 |
34 | ### Laptop
35 |
36 | #### Core
37 |
38 | - 2015 MacBook Pro
39 | - Processor: 2.5 GHz Quad-core Intel Core i7
40 | - Memory: 16 GB 1600 MHz DDR3L
41 | - Storage: 512GB Flash Storage
42 |
43 | #### Peripherals
44 |
45 | - Mouse: [Logitech MX Master](https://www.amazon.com/gp/product/B076VKQVK3)
46 | - Lapdesk: [LapGear Home Office Lap Desk](https://www.amazon.com/gp/product/B01C785EJ4)
47 | - Stand: [Rain Design mStand Laptop Stand](https://www.amazon.com/gp/product/B000OOYECC)
48 |
--------------------------------------------------------------------------------
/content/parts/author.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: author
3 | ---
4 |
5 | **Chris Otto** is a test engineer by day and developer by night. My hobbies include side projects, video games and building things with my hands.
6 |
--------------------------------------------------------------------------------
/content/posts/2018/2018-11-10--react-tutorial-adding-typescript/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: React Tutorial - Add Typescript
3 | cover: react-logo.png
4 | author: Chris Otto
5 | tags: ['react', 'typescript']
6 | published: true
7 | ---
8 |
9 | This week I was going through the React [tutorial](https://reactjs.org/tutorial/tutorial.html). While going through each phase of the tutorial; going over state, JSX and React Components I wanted to be writing it in Typescript. I'll go through the tutorial code of what I had to change in order to get it working.
10 |
11 | - Use node to install the typescript dependencies we need:
12 |
13 | ```bash
14 | npm install --save typescript @types/node @types/react @types/react-dom @types/jest
15 | ```
16 |
17 | - Change the function definition of Square, pass (props: any) instead of just (props):
18 |
19 | ```js
20 | function Square(props: any) {}
21 | ```
22 |
23 | - Modify the Board and Game Component to accept any in the Component definition:
24 |
25 | ```js
26 | class Board extends React.Component {}
27 |
28 | class Game extends React.Component {}
29 | ```
30 |
31 | - Change the file type to be .tsx instead of .js
32 |
33 | Just like that, with a few package installs and a few code changes you're able to compile the project using typescript instead of normal JS. You will probably want to make other modifications to take advantage of other useful parts of typescript.
34 |
--------------------------------------------------------------------------------
/content/posts/2018/2018-11-10--react-tutorial-adding-typescript/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2018/2018-11-10--react-tutorial-adding-typescript/react-logo.png
--------------------------------------------------------------------------------
/content/posts/2018/2018-11-20--javascript-copyright-date/2019.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2018/2018-11-20--javascript-copyright-date/2019.jpg
--------------------------------------------------------------------------------
/content/posts/2018/2018-11-20--javascript-copyright-date/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dynamically Set Footer Copyright Date
3 | cover: 2019.jpg
4 | author: Chris Otto
5 | tags: ['javascript']
6 | published: true
7 | ---
8 |
9 | With 2018 winding down I found myself thinking that I will need to leave a note to update the footer on my site to change the year to 2019. Having to come back year after year to statically change the copyright year in my footer is something I will no longer do. So why not have it set dynamically?
10 |
11 | ```html
12 |
15 | ```
16 |
17 | Instead of just adding the year I also included a copyright character and my name wrapping the new script.
18 |
19 | ```html
20 | ©
21 |
24 | Chris Otto
25 | ```
26 |
--------------------------------------------------------------------------------
/content/posts/2019/2019-04-30--change-specflow-build/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Create Specflow `feature.cs` At Build Time
3 | cover: sf-logo.png
4 | author: Chris Otto
5 | tags: ['specflow', 'vs2017', 'c#']
6 | published: true
7 | ---
8 |
9 | ## Change Feature File to Generate the `~.feature.cs` at Compile Time
10 |
11 | Whenever a new Specflow file is created in the solution it is automatically created with the SpecFlowSingleFileGenerator specification in the Custom Tool attribute. This file generator creates the background file on save instead of during the build
12 | process, follow these steps to make the change:
13 |
14 | ### Prerequisite
15 |
16 | To make the change you will need to add an extension to your instance of Visual Studio if you don't have it already.
17 |
18 | * In Visual Studio go to Extension Manage Extensions
19 | * Select Online and search for 'File Nesting'
20 | * Install the 'File Nesting' extension and close Visual Studio, once closed the installation will begin
21 | * When prompted after VS closes, select 'Modify' from the pop-up window
22 |
23 | ### Making the Change
24 |
25 | With the 'File Nesting' extension installed we are now ready to change how the ~.feature.cs is created for our feature file.
26 |
27 | * Modify the existing properties
28 | * In the Solution Explorer, R-Click on the feature file Select Properties
29 | * The Properties window should pop up below the Solution Explorer
30 | * In the Custom Tool attribute, delete 'SpecFlowSingleFileGenerator' leaving the Custom Tool attribute blank
31 | * Creating the File dependency
32 | * With the Custom Tool removed, build the solution
33 | * Once built make sure that you have 'Show All Files' selected and you should see the '~.feature.cs' file for your feature file in the Solution Explorer
34 | * If you don't see it and you already had 'Show All Files' enabled, try refreshing the Solution Explorer and it should populate
35 | * R-Click on the `~.feature.cs` file Select 'File Nesting'
36 | * Nest the file under the respective feature file
37 |
--------------------------------------------------------------------------------
/content/posts/2019/2019-04-30--change-specflow-build/sf-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2019/2019-04-30--change-specflow-build/sf-logo.png
--------------------------------------------------------------------------------
/content/posts/2020/2020-02-08--gatsby-change-from-md-to-mdx/gatsby-mdx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-02-08--gatsby-change-from-md-to-mdx/gatsby-mdx.png
--------------------------------------------------------------------------------
/content/posts/2020/2020-02-11--gatsby-create-published-filter-for-posts/gatsby-blue-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-02-11--gatsby-create-published-filter-for-posts/gatsby-blue-green.png
--------------------------------------------------------------------------------
/content/posts/2020/2020-02-11--gatsby-create-published-filter-for-posts/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Gatsby - Add a Published Filter To Posts
3 | cover: gatsby-blue-green.png
4 | author: Chris Otto
5 | tags: ['gatsby','react','graphql']
6 | published: true
7 | ---
8 |
9 | I like [Dev.to](www.dev.to)'s published filter. It allows you to continue to write posts until they're ready to be
10 | viewed by everyone. If you are not familiar with the feature; in the `frontmatter` of your post there is a `boolean` named
11 | `published`. If it's set to false, the post is visible to you and people that have the link. Once it's set to true then
12 | it is visible to the world!
13 |
14 | I wanted to add that feature to my personal [Gatsby](www.gatsbyjs.com) site so that I can have articles in progress while
15 | not finishing them if I need to commit other changes. The one difference with my implementation is that the post won't be
16 | available to the client at all until it is published. The outline for the change:
17 |
18 | - Update all existing posts have `published: true` in their `frontmatter`
19 | - Add/Update filters to Graphql queries throughout my site keying in on the field set to `true`
20 | - gatsby-config.js
21 | - gatsby-node.js
22 | - Pages
23 | - Templates
24 |
25 | ## Applying the Updates 😎
26 |
27 | ### Add `published` to the frontmatter
28 |
29 | ```markdown
30 | tags: ['gatsby','react']
31 | + published: true
32 | ---
33 | ```
34 |
35 | This change was made to all existing posts so that right away the Graphql queries I update down the line return data. Also
36 | I created a dummy post that had the `published` boolean set to false to verify that it was excluded from the list of posts.
37 |
38 | ### Adding the filter to `gatsby-config.js` queries
39 |
40 | Within my `gatsby-config.js` I have two queries. One query hooks up to the Algolia search for my site and the other populates
41 | the RSS feed. For both queries, I do not want posts that are not published to show up.
42 |
43 | #### Algolia query
44 |
45 | ```graphql
46 | allMdx(
47 | filter: {
48 | fields: { slug: { ne: null } },
49 | fileAbsolutePath: { regex: "/posts/"},
50 | + frontmatter: { published: { eq: true } }
51 | }
52 | ```
53 |
54 | #### RSS Feed
55 |
56 | ```graphql
57 | frontmatter: {
58 | author: { ne: null },
59 | + published: { eq: true}
60 | }
61 | ```
62 |
63 | ### Apply filter to `gatsby-node.js`, components and templates
64 |
65 | The `createPages` in `gatsby-node.js` function uses the query to find out which pages should be created in my case this applies to posts and pages.
66 | Applying the filter to the components and pages makes sure that the unpublished posts don't creep in and cause errors because there
67 | is no post page to route to.
68 |
69 | #### gatsby-node
70 |
71 | ```graphql
72 | allMdx(
73 | + filter: { fields: { slug: { ne: null } }, frontmatter: { published: { eq: true } } }
74 | sort: { fields: [fields___prefix], order: DESC }
75 | limit: 1000
76 | )
77 | ```
78 |
79 | #### Tag Page and Index Template
80 |
81 | ```graphql
82 | posts: allMdx(
83 | filter: {
84 | fileAbsolutePath: { regex: "//posts/[0-9]+.*--/" }
85 | + frontmatter: { published: { eq: true } }
86 | }
87 | ```
88 |
89 | #### Tag Template
90 |
91 | ```graphql
92 | query PostsByTag($tag: String) {
93 | allMdx(
94 | limit: 1000
95 | sort: { fields: [fields___prefix], order: DESC }
96 | + filter: { frontmatter: { tags: { in: [$tag] }, published: { eq: true } } }
97 | )
98 | ```
99 |
100 | ### Testing and Wrap Up 🙌
101 |
102 | At this point all the changes have been made to test out the change. All existing posts should be present and flow through
103 | searching, post pages, tag pages and RSS feed, all except for the one dummy post. I did some smoke testing by navigating around
104 | the components and pages updated to ensure that everything was working as expected and all existing automated tests passed. Win-win.
105 | Now I can keep posts as a work in progress until they're ready to be published and still get other development work merged in.
106 |
107 | Do you filter out in progress work from your Gatsby site? What approach did you take?
--------------------------------------------------------------------------------
/content/posts/2020/2020-05-21--gatsby-create-an-audience-with-mailchimp/finished_form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-05-21--gatsby-create-an-audience-with-mailchimp/finished_form.png
--------------------------------------------------------------------------------
/content/posts/2020/2020-05-21--gatsby-create-an-audience-with-mailchimp/mail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-05-21--gatsby-create-an-audience-with-mailchimp/mail.jpg
--------------------------------------------------------------------------------
/content/posts/2020/2020-06-01--compress-your-images-with-tinify/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-06-01--compress-your-images-with-tinify/image.jpg
--------------------------------------------------------------------------------
/content/posts/2020/2020-06-30--add-linting-to-create-react-app/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-06-30--add-linting-to-create-react-app/image.jpg
--------------------------------------------------------------------------------
/content/posts/2020/2020-06-30--add-linting-to-create-react-app/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Add Linting to Create-React-App
3 | cover: image.jpg
4 | author: Chris Otto
5 | tags: ['react', 'javascript', 'node']
6 | published: true
7 | ---
8 |
9 | Image from [Free Illustrations](https://freellustrations.com/).
10 |
11 | Create-React-App gives a nice bootstrapped project. I like being able to lint outside of the build or run process of the application, like on pre-commit hooks with husky. To do that I needed to add linting to my create-react-app. Follow these steps to add linting to your create-react-app project and get linting outside of your build process.
12 |
13 | ## Install packages 📦
14 |
15 | This is the most painful part of the process I kept installing one package after another to see if linting worked. All-in-all eight packages later I finally had everything there and ready to run based on the configurations from create-react-app.
16 |
17 | ```bash
18 | npm i --save-dev babel-eslint eslint-config-react-app eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
19 | ```
20 |
21 | ```bash
22 | yarn add --dev babel-eslint eslint-config-react-app eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
23 | ```
24 |
25 | ## Add ESLint Configuration to `package.json`
26 |
27 | You can either supply this in a separate file (`.eslintrc.json`/`.eslintrc.js`) or right in your `package.json`. For larger configuration changes I would recommend a separate file but I'm just extending the react-app configuration and applying a couple of rule adjustments.
28 |
29 | ```json
30 | "eslintConfig": {
31 | "extends": "react-app",
32 | "rules": {
33 | "semi": 0,
34 | "no-console": 0
35 | }
36 | },
37 | ```
38 |
39 | ## Add Linting Script
40 |
41 | Now, all we need to do is to add the linting script to our `package.json` and we'll be able to lint whenever we want. Even hook up husky or add a specific step for linting in our CI/CD pipeline.
42 |
43 | ```json
44 | "scripts": {
45 | ...
46 | "lint": "node_modules/.bin/eslint --ext js src"
47 | },
48 | ```
49 |
--------------------------------------------------------------------------------
/content/posts/2020/2020-07-10--debug-gatsby-simulator/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Gatsby Develop with iOS Simulator
3 | cover: laptop.jpg
4 | author: Chris Otto
5 | tags: ['gatsby', 'react', 'ios']
6 | published: true
7 | ---
8 |
9 | Image from [@norwood](https://unsplash.com/@nordwood) at Unsplash.
10 |
11 | Chrome DevTools have come a long way in enabling users to mimic mobile devices. However, if you wanted to debug your Gatsby application locally for mobile specific features you can do that through the iOS Simulator. In order to do this you need to have a Mac, there are other ways around it without a Mac but this post will focus on setting up your Gatsby site to be accessed through the simulator.
12 |
13 | ## Prerequisites
14 |
15 | - A Mac computer
16 | - XCode installed
17 | - Developer Tools Enabled in your Mac's Safari browser
18 | - Launch Safari
19 | - Click Safari in the Menu
20 | - Preferences
21 | - Advanced
22 | - Enable `Show Develop menu in menu bar`
23 |
24 | ### Start Gatsby 👨🚀
25 |
26 | First thing we'll need to do is start the Gatsby site in a special way allowing for local network access. This can be done by using the `-H` parameter when starting `gatsby develop`
27 |
28 | ```bash
29 | gatsby develop -H 0.0.0.0
30 | ```
31 |
32 | ### Find IP Address 💻
33 |
34 | Next you need to find your local IP Address within your network and that can be done in the terminal with the following command.
35 |
36 | ```bash
37 | ipconfig getifaddr en0
38 | ```
39 |
40 | ### Launch the Simulator 🚀
41 |
42 | For this next part we'll be starting XCode and starting to iOS Simulator.
43 |
44 | - Launch XCode
45 | - Select XCode in the Menu
46 | - Open Developer Tool
47 | - Simulator
48 |
49 | With the simulator up and running we'll need to navigate to our locally served instance of our Gatsby site.
50 |
51 | - Open Safari in the simulator
52 | - In the address bar paste in %IP_ADDRESS%:8000
53 |
54 | With the site active in the simulator we can now open up the Safari Developer Tools for debugging our site in the simulator.
55 |
56 | - Start Safari on your computer
57 | - Go the Develop in the menu
58 | - Expand the Simulator option
59 | - Click on the IP Address listed for your site
60 |
61 | Now you'll be able to use your site like a true mobile user would can even change geolocation settings, accelerometer settings and all the other benefits of iOS Simulator for using your Gatsby application.
62 |
--------------------------------------------------------------------------------
/content/posts/2020/2020-07-10--debug-gatsby-simulator/laptop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2020/2020-07-10--debug-gatsby-simulator/laptop.jpg
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-08--gatsby-error-monitoring-with-sentry/error_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-01-08--gatsby-error-monitoring-with-sentry/error_image.jpg
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-08--gatsby-error-monitoring-with-sentry/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Gatsby Error Monitoring with Sentry
3 | cover: error_image.jpg
4 | author: Chris Otto
5 | tags: ['react', 'gatsby', 'sentry']
6 | published: true
7 | ---
8 |
9 | Image from [Free Illustrations](https://freellustrations.com/).
10 |
11 | Being able to monitor when your application encounters a bug in production is a nice thing to have for your personal or professional projects. The folks at [Sentry](https://sentry.io/welcome/) have created a solution to monitor errors within your applications and alert you when they happen. I have set this up for my personal site and a website I made for my wife. This guide will walk you through what you need to do to add sentry monitoring to your Gatsby applications.
12 |
13 | ## Create Sentry Account and Project
14 |
15 | First, we need to create an account with [Sentry](https://sentry.io/welcome/). Head over to their site, select `Sign Up` and either create an account or use Github or any of the other integrations that they have.
16 |
17 | Once signed in:
18 |
19 | - Create a new project
20 | - Choose `React` for the platform
21 | - Set your notification preferences
22 | - Name your project
23 | - I use the domain or the Github project name of the website I'm adding sentry to
24 | - Choose or create the team that the project should live in
25 | - Hit `Create Project`
26 |
27 | The next page will give you some default React code for adding Sentry to your application. You can disregard this for now, just copy your Sentry DSN from the code, we'll need this later for setting up the Gatsby plugin.
28 |
29 | ## Install Packages 📦
30 |
31 | In your project, add the `gatsby-plugin-sentry`:
32 |
33 | ```sh
34 | npm install gatsby-plugin-sentry
35 | ```
36 |
37 | or
38 |
39 | ```sh
40 | yarn add gatsby-plugin-sentry
41 | ```
42 |
43 | ## Gatsby Configuration Changes 👨💻
44 |
45 | Using the Sentry DSN we copied from creating our project, we now need to configure our Gatsby plugin to hook up to our Sentry project.
46 |
47 | In your `gatsby-config.js` add:
48 |
49 | ```js
50 | {
51 | resolve: 'gatsby-plugin-sentry',
52 | options: {
53 | dsn: process.env.SENTRY_DSN,
54 | },
55 | },
56 | ```
57 |
58 | You'll want to set up your Sentry DSN up as an environment variable because you don't want your secret for your project being exposed. This means adding it to any CI/CD (Github Actions, TravisCI, etc.) or deployment (Netlify, Vercel, etc.) configurations as well.
59 |
60 | ## Create the Error Boundary Component 🧩
61 |
62 | We're going to create an Error Boundary component to catch any errors in our application and use it to send the details to Sentry. Later we're going to wrap a Gatsby layout component with our new error boundary so any page or post in your site will have the error boundary available.
63 |
64 | Create a new file called `ErrorBoundary.js` where you define your components in your project (for me this is in `src/common/components`):
65 |
66 | ```js
67 | import React from 'react'
68 | import PropTypes from 'prop-types'
69 | import Sentry from 'gatsby-plugin-sentry'
70 |
71 | class ErrorBoundary extends React.Component {
72 | constructor(props) {
73 | super(props)
74 | this.state = { error: null }
75 | }
76 |
77 | componentDidCatch(error, errorInfo) {
78 | this.setState({ error })
79 | Sentry.configureScope((scope) => {
80 | Object.keys(errorInfo).forEach((key) => {
81 | scope.setExtra(key, errorInfo[key])
82 | })
83 | })
84 | Sentry.captureException(error)
85 | }
86 |
87 | render() {
88 | if (this.state.error) {
89 | // render fallback UI
90 | return Something went wrong!
91 | } else {
92 | // when there's not an error, render children untouched
93 | return this.props.children
94 | }
95 | }
96 | }
97 |
98 | ErrorBoundary.propTypes = {
99 | children: PropTypes.object.isRequired,
100 | }
101 |
102 | export default ErrorBoundary
103 | ```
104 |
105 | ## Wrap Contents of Layout Component with Error Boundary
106 |
107 | Now in your layout component import the new ErrorBoundary component:
108 |
109 | ```js
110 | import ErrorBoundary from '../components/ErrorBoundary'
111 | ```
112 |
113 | Wrap whatever was in your Layout component with the ErrorBoundary component:
114 |
115 | ```js
116 | const Layout = (props) => (
117 | +
118 |
119 |
120 |
121 |
122 |
123 | {props.children}
124 |
125 |
126 | +
127 | )
128 | ```
129 |
130 | ## Conclusion
131 |
132 | Just like that you should be all setup. Make sure to keep an eye out for Sentry e-mails coming from your application and this will give you a great way to issues reported from your applications in production. Cheers 🍻!
133 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-15--react-netlify-client-side-routing/computer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-01-15--react-netlify-client-side-routing/computer.jpg
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-15--react-netlify-client-side-routing/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Netlify - Client Side Routing
3 | cover: computer.jpg
4 | author: Chris Otto
5 | tags: ['netlify', 'react', 'react-router']
6 | published: true
7 | ---
8 |
9 | I have a nice little [side project](https://library.chrisotto.dev/) based on the Goodreads API (yes, my key is still active). I had it up for a while and whenever I would refresh a page while it was deployed it would lose context to the page. This is because Netlify didn't know how to handle the state I had gotten myself into because the routing in `create-react-app` that I was using was `react-router`.
10 |
11 | Luckily enough this is an easy fix, so if you also have the following:
12 |
13 | - Create React App client side routing through `react-router`
14 | - Deploy your site on Netlify
15 |
16 | ...then you will be able to make the same change and have your page context persist after refreshes!
17 |
18 | ## Creating a Redirects File 💻
19 |
20 | In your `public` directory create a new `__redirects` file with the following code:
21 |
22 | ```txt
23 | /* /index.html 200
24 | ```
25 |
26 | Now when the project gets built by Netlify, Create-React-App will place the contents of the `public` directory into the build output. Allowing Netlify to handle `pushState` from within your application. That's it!
27 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-21--ssh-synology-nas/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Synology NAS - SSH To Find User Info
3 | cover: synology.png
4 | author: Chris Otto
5 | tags: ['synology', 'nas']
6 | published: true
7 | ---
8 |
9 | Image courteous of [unsplash](https://unsplash.com/).
10 |
11 | The user information for your user is useful for setting up Docker images/containers that need access. You can either setup a dedicated Docker user in your NAS or any other user that has `admin` privileges.
12 |
13 | ## Enable SSH
14 |
15 | In order to be able to SSH into the NAS, first we need to enable SSH:
16 |
17 | - In DSM open up the Control Panel
18 | - Terminal & SNMP (make sure you're in advanced mode in the control panel)
19 | - In the Terminal Tab
20 | - Click the checkbox to Enable SSH Service
21 | - Change the port from the default port of `22`
22 | - Apply
23 |
24 | ## Open Up The Terminal And Find User Info
25 |
26 | Now that SSH is enabled we head to the terminal in order to login into our NAS via SSH. If you're on Windows using the default Command Prompt will work or Terminal on MacOS/Linux.
27 |
28 | With the terminal open enter the following command:
29 |
30 | ```bash
31 | ssh %USERNAME%@%IP_ADDRESS% -p %PORT%
32 | ```
33 |
34 | - %USERNAME% - The username for the account that has `admin` access in your NAS. It's standard practice to disable the default admin and guest account for your NAS.
35 | - %IP_ADDRESS% - The IP address of your Synology NAS
36 | - %PORT% - Port you set up for SSH when enabling SSH
37 |
38 | You'll then be prompted to enter your password, afterwords you should be greeted with the following your terminal:
39 |
40 | ```bash
41 | %USERNAME%@%SYNOLOGY_NAS_NAME%:~$
42 | ```
43 |
44 | Now enter in `id` and see the information for your user:
45 |
46 | ```bash
47 | %USERNAME%@%SYNOLOGY_NAS_NAME%:~$id
48 | uid=1033(%USERNAME%) gid=100(users) groups=100(users),101(administrators)...
49 | ```
50 |
51 | The key information that we need to take out of this is the `uid` and the administrators group ID. A lot of the containers on [Linux Server](https://fleet.linuxserver.io/) require these IDs but they will be mapped a little bit differently:
52 |
53 | - `uid` = `PUID`
54 | - Admin Group ID = `PGID`
55 |
56 | That's it. With those two IDs saved off you should now have the prerequisites to set up some Docker containers on your NAS. I will have follow-up posts for some of the containers I've set up on my NAS. Cheers! 🍻
57 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-21--ssh-synology-nas/synology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-01-21--ssh-synology-nas/synology.png
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-25--portainer-docker-nas/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Portainer - Docker Container Manager For Your NAS
3 | cover: shipyard.jpg
4 | author: Chris Otto
5 | tags: ['synology', 'docker', 'nas']
6 | published: true
7 | ---
8 |
9 | Image courteous of [Pixabay](https://pixabay.com/).
10 |
11 | Portainer is an application that makes it easy for you to manage docker containers that you have running on your system. It is a great tool to add to any NAS running containers or homeserver doing the same. It can also be used for Kubernetes management if that is something you have running or are interested. Some of the features I like:
12 |
13 | - SSH into the container right from the UI
14 | - View logs
15 | - See resources consumed by each container
16 |
17 | ## Prerequisites 📃
18 |
19 | - Download Docker from Package Center
20 | - In DSM head over to Package Center
21 | - Search for Docker
22 | - Install
23 | - Enable SSH for your NAS
24 | - If you don't have SSH enabled follow this [guide](https://chrisotto.dev/ssh-synology-nas/).
25 |
26 | ## Create Folder Structure 📂
27 |
28 | In DSM, open up the File Station:
29 |
30 | - Installing docker creates a `docker` folder at the root of your volume
31 | - Create a new folder in the `docker` folder for Portainer
32 | - I always create the folder based on the name of the image/container I'm setting up, in this case `portainer`
33 |
34 | ## SSH Into Your NAS And Run the Image 📦
35 |
36 | Using your preferred terminal SSH into your NAS, once logged in run the following:
37 |
38 | ```bash
39 | %USERNAME%@%NAS_NAME%:~$sudo su -
40 | ```
41 |
42 | You may be prompted to re-enter your password and you should do so. This command puts us in the root environment with the root user. Your terminal windows should now look like:
43 |
44 | ```bash
45 | root@%NAS_NAME%:~#
46 | ```
47 |
48 | Copy and paste the following snippet in your terminal
49 |
50 | ```bash
51 | root@%NAS_NAME%:~# docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /volume1/docker/portainer:/data portainer/portainer-ce@latest
52 | ```
53 |
54 | If you have any other containers already setup that may be using ports `8000` or `9000` make sure to change that before executing the Docker run command. Now we just have a to setup the portainer instance in the web interface and we'll be all set!
55 |
56 | ## Setup Portainer 🛳
57 |
58 | In your browser navigate to `http://%SYNOLOGY_IP_ADDRESS%:9000`. Here we will be prompted to create a new user for Portainer.
59 |
60 | - Create your username
61 | - Create a new password
62 | - Confirm your password
63 | - Uncheck `Allow collection...`
64 | - Click `Create User`
65 |
66 | Once your user is created you will be brought to a screen to choose which container environment we want to manage with Portainer.
67 |
68 | - Select Docker (or if you are using this to manage Kubernetes then choose that one)
69 | - Click `Connect`
70 |
71 | Now you should be looking at the Portainer dashboard, congratulations you have successfully set up Portainer in your NAS/homeserver through Docker! Feel free to get out their [image](https://hub.docker.com/r/portainer/portainer-ce) in Docker Hub or the Community Edition [documentation](https://www.portainer.io/products/community-edition) for more information! Cheers! 🍻
72 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-25--portainer-docker-nas/shipyard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-01-25--portainer-docker-nas/shipyard.jpg
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-31--pihole-docker-nas/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: PiHole - Docker NAS Setup
3 | cover: pi-hole.png
4 | author: Chris Otto
5 | tags: ['synology', 'docker', 'nas']
6 | published: true
7 | ---
8 |
9 | Pi-hole is an awesome tool that let's you block certain requests on your home network from devices. The way that Pi-hole does this by either hosting it on a Raspberry Pi or hosted on a machine on your network through Docker. Once that is done you set the IP address of the Pi-hole as your DNS server or if you can't modify your DNS server due ISP restrictions you can set it as your DHCP server. This guide we'll walk through setting up Pi-hole through Docker on a Synology NAS.
10 |
11 | ## Prerequisites 📃
12 |
13 | - Download Docker from Package Center
14 | - In DSM head over to Package Center
15 | - Search for Docker
16 | - Install
17 |
18 | ## Create Folder Structure 📂
19 |
20 | In DSM, open up the File Station:
21 |
22 | - Installing docker creates a `docker` folder at the root of your volume
23 | - Create a new folder in the `docker` folder for Portainer
24 | - I always create the folder based on the name of the image/container I'm setting up, in this case `pihole`
25 | - In the `pihole` directory create two new folders `pihole` and `dnsmasq.d`
26 | - These folders will get mounted as volumes for the docker container later on
27 |
28 | ## Downloading and Configuring the Image 🚢
29 |
30 | Open on `Docker` on your NAS. In the `Registry` tab search for pihole. The image that we're looking for is the official Pi-hole image, `pihole/pihole`. Select the image and select Download or just double click it. Once the image has finished downloading, head on over to the `Images` tab and click Launch or again double click it.
31 |
32 | Now you should be in the `Create Container` pop-over:
33 |
34 | - Enter the container name, `pihole`
35 | - Enable `Execute container using high privileges`
36 | - Click Advanced Settings
37 |
38 | In the `Advanced Settings` main tab:
39 |
40 | - Enable `Enable auto-restart`
41 |
42 | In the `Volume` tab, we're going to add the folders we created to the volumes for container, make you match the mount path below:
43 |
44 | | File/Folder | Mount Path |
45 | | :---------------------- | :------------- |
46 | | docker/pihole/dnsmasq.d | /etc/dnsmasq.d |
47 | | docker/pihole/pihole | /etc/pihole |
48 |
49 |
50 | In the `Network` tab:
51 |
52 | - Enable `Use the same network as Docker Host`
53 |
54 | Finally, in the `Environment` tab we need to set/change some of the default environment variables that come with the image. You'll need to come up with a password for the web interface and that will get set for one of the variables. Also, decide on what port you want this to run on, the default is `8080` so if you have another application using that port pick a new one. Lastly, we'll need to set one of variables to the IP address of your Synology NAS or IP address of where ever you are running the container:
55 |
56 | | Variable | Value |
57 | | :---------------- | :--------------------- |
58 | | WEBPASSWORD | [Create a password] |
59 | | DNSMASQ_LISTENING | local |
60 | | WEB_PORT | 8080 |
61 | | ServerIP | [NAS IP or Machine IP] |
62 |
63 | ## Launch the Container 🚀
64 |
65 | With everything configured you're now ready to click `Apply` and launch your new Pi-hole container. Once the container has started, head on over to `http://[YOUR IP ADDRESS]:[YOUR PORT]/admin`. You'll need to login with the password that we set as the environment variables earlier.
66 |
67 | ## Router Changes 📶
68 |
69 | Pi-hole is running and ready to block traffic on your network, but you won't see anything on the admin dashboard until you make the necessary changes to your router. This process is very specific to which router you have so I won't detail it here as it will most likely be different for everyone. But login into your router and change the IP address of your DNS server to the IP address of your NAS. If you are not able to modify your DNS, then you can set Pi-hole to be your DHCP server.
70 |
71 | One thing I have noticed with having the Pi-hole set as my DNS server on my router is that I only log traffic for one device. The router. Which makes sense because all traffic is coming to Pi-hole from the router. One way you could get more detailed information per device is using DHCP or individually changing the DNS per device on your network to the Pi-hole. I don't really mind not seeing device specific statistics, so I left it as-is.
72 |
73 | I hope you enjoyed this little guide and happy blocking, cheers! 🍻
74 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-01-31--pihole-docker-nas/pi-hole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-01-31--pihole-docker-nas/pi-hole.png
--------------------------------------------------------------------------------
/content/posts/2021/2021-02-05--calibre-library-docker-nas/library.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-02-05--calibre-library-docker-nas/library.jpg
--------------------------------------------------------------------------------
/content/posts/2021/2021-02-19--watchtower-docker-nas/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Watchtower - Docker NAS Setup
3 | cover: watchtower.jpg
4 | author: Chris Otto
5 | tags: ['docker','nas','watchtower']
6 | published: true
7 | ---
8 |
9 | Image courteous of [@kristsll on Unsplash](https://unsplash.com/@kristsll).
10 |
11 | Watchtower is a container that will update all your other running containers when a new version is published to the docker registry that they are setup through. This is helpful for your Docker containers to stay up to date on your home server or NAS. If you would like to install on on your home server and not through a NAS you can jump down to the running the container portion.
12 |
13 | ## Prerequisites 📃
14 |
15 | - Enable SSH for your NAS
16 | - If you don't have SSH enabled follow this [guide](https://chrisotto.dev/ssh-synology-nas/)
17 |
18 | ## SSH Into Your NAS 💻
19 |
20 | Now that SSH is enabled on our NAS we need to SSH into the machine so that we can run the `docker run` command. Open up the terminal on your machine and enter the following:
21 |
22 | ```bash
23 | ssh %USERNAME%@%IP_ADDRESS% -p %PORT%
24 | ```
25 |
26 | After you enter in your password you should be greeted with:
27 |
28 | ```bash
29 | %USERNAME%@%SYNOLOGY_NAS_NAME%:~$
30 | ```
31 |
32 | ## Run the Container 🐳
33 |
34 | In your terminal enter the following `docker run` command:
35 |
36 | ```bash
37 | sudo docker run --name="watchtower" -d --restart=always -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower:latest
38 | ```
39 |
40 | This will run Watchtower on your NAS allowing it to update existing running images whenever there is a new version pushed! That's it for the main portion of getting Watchtower up and running. Here a few extra notes since I've been running it for a few months:
41 |
42 | - I highly recommend getting Portainer set up to maintain the old images that get replaced, I have a guide on how to set that up [here](https://chrisotto.dev/portainer-docker-nas/).
43 | - If you want to exclude certain images from being updated you can stop them and rerun them with a label of `
44 | com.centurylinklabs.watchtower.enable=false`
45 |
46 | Happy containerization! Cheers! 🍻
47 |
48 |
--------------------------------------------------------------------------------
/content/posts/2021/2021-02-19--watchtower-docker-nas/watchtower.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/content/posts/2021/2021-02-19--watchtower-docker-nas/watchtower.jpg
--------------------------------------------------------------------------------
/coverage/badge-branches.svg:
--------------------------------------------------------------------------------
1 | Coverage:branches Coverage:branches 100% 100%
--------------------------------------------------------------------------------
/coverage/badge-functions.svg:
--------------------------------------------------------------------------------
1 | Coverage:functions Coverage:functions 100% 100%
--------------------------------------------------------------------------------
/coverage/badge-lines.svg:
--------------------------------------------------------------------------------
1 | Coverage:lines Coverage:lines 100% 100%
--------------------------------------------------------------------------------
/coverage/badge-statements.svg:
--------------------------------------------------------------------------------
1 | Coverage:statements Coverage:statements 100% 100%
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:9000/",
3 | "ignoreTestFiles": [""],
4 | "integrationFolder": "cypress/e2e",
5 | "video": false,
6 | "chromeWebSecurity": false
7 | }
8 |
--------------------------------------------------------------------------------
/cypress/e2e/a11y.test.js:
--------------------------------------------------------------------------------
1 | const A11Y_CONFIG = {
2 | checks: [
3 | {
4 | id: 'aria-valid-attr-value',
5 | enabled: false,
6 | },
7 | ],
8 | }
9 |
10 | describe('Accessibility checks', () => {
11 | it('Main Page', () => {
12 | cy.visit('/')
13 | cy.wait(2000)
14 | cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
15 | })
16 |
17 | //it('About Page', () => {
18 | // cy.visit('/about')
19 | // cy.wait(1000)
20 | // cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
21 | //})
22 |
23 | it('Tags Page', () => {
24 | cy.visit('/tag')
25 | cy.wait(1000)
26 | cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
27 | })
28 |
29 | //it('Projects Page', () => {
30 | // cy.visit('/project')
31 | // cy.wait(1000)
32 | // cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
33 | //})
34 |
35 | //it('Search Page', () => {
36 | // cy.visit('/search')
37 | // cy.injectAxe()
38 | // cy.checkA11y()
39 | //})
40 |
41 | it('Contact Page', () => {
42 | cy.visit('/contact')
43 | cy.wait(1000)
44 | cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
45 | })
46 |
47 | //it('404 Page', () => {
48 | // cy.visit('/404')
49 | // cy.injectAxe().configureAxe(A11Y_CONFIG).checkA11y()
50 | //})
51 | })
52 |
--------------------------------------------------------------------------------
/cypress/e2e/index.test.js:
--------------------------------------------------------------------------------
1 | describe('Index Page', () => {
2 | it('Index Page Navigation', () => {
3 | cy.visit('/')
4 | cy.title().should('eq', 'Chris Otto | Father, Developer, Test Engineer')
5 | cy.get('button[aria-label="scroll"]').click()
6 | })
7 | it('Posts are rendered', () => {
8 | cy.get('#post-list').should('be.visible')
9 | cy.get('#post-list>li').should('be.visible')
10 | cy.get('#post-list>li>.link>.gatsby-image-outer-wrapper')
11 | .first()
12 | .should('be.visible')
13 | cy.get('#post-list>li>.link>div')
14 | .first()
15 | .should('be.visible')
16 | cy.get('#post-list>li>.link>p')
17 | .first()
18 | .should('be.visible')
19 | .next()
20 | .should('be.visible')
21 | })
22 | it('Footer Post Navigation', () => {
23 | cy.get('footer').scrollIntoView()
24 | cy.get('#post-page-list').should('be.visible')
25 | cy.get('#post-page-list>li').should('be.visible')
26 | })
27 | it('Social Links', () => {
28 | cy.get('.social').should('be.visible')
29 | cy.get('.social')
30 | .find('a')
31 | .should('have.length', 5)
32 | cy.get('.social>a')
33 | .first()
34 | .should('have.attr', 'title', 'dev')
35 | .next()
36 | .should('have.attr', 'title', 'github')
37 | .next()
38 | .should('have.attr', 'title', 'instagram')
39 | .next()
40 | .should('have.attr', 'title', 'linkedin')
41 | .next()
42 | .should('have.attr', 'title', 'twitter')
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/cypress/e2e/pages.test.js:
--------------------------------------------------------------------------------
1 | describe('Markdown Generated Pages Tests', () => {
2 | it('Resources Page', () => {
3 | cy.visit('/resources')
4 | cy.get('[id=Resources]').contains('Resources')
5 | })
6 |
7 | it('Success Page', () => {
8 | cy.visit('/success')
9 | cy.get('[id=Success]').contains('Success')
10 | })
11 | })
12 |
13 | describe('Pages Tests', () => {
14 | it('About Page', () => {
15 | cy.visit('/about')
16 | cy.get('[id=About]').contains('About')
17 | })
18 |
19 | it('Tags Page', () => {
20 | cy.visit('/tag')
21 | cy.get('[id="Posts by tags"]').contains('Posts by tags')
22 | })
23 |
24 | it('Projects Page', () => {
25 | cy.visit('/project')
26 | cy.get('[id=Projects]').contains('Projects')
27 | })
28 |
29 | it('Uses Page', () => {
30 | cy.visit('/uses')
31 | cy.get('[id=Uses]').contains('Uses')
32 | })
33 |
34 | it('Search Page', () => {
35 | cy.visit('/search')
36 | cy.get('.ais-SearchBox-input').should('have.attr', 'placeholder', 'Search')
37 | })
38 |
39 | it('Contact Page', () => {
40 | cy.visit('/contact')
41 | cy.get('[id=Contact]').contains('Contact')
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // cypress/plugins/index.js
2 |
3 | // export a function
4 | module.exports = (on, config) => {
5 | // configure plugins here
6 | }
7 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import 'cypress-axe'
2 | import '@testing-library/cypress/add-commands'
3 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | web:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | ports:
8 | - "8000:8000"
9 | - "9929:9929"
10 | - "9230:9230"
11 | volumes:
12 | - /app/node_modules
13 | - .:/app
14 | environment:
15 | - NODE_ENV=development
16 | - GATSBY_WEBPACK_PUBLICPATH=/
17 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | // Prism themes
2 |
3 | require('prismjs/plugins/command-line/prism-command-line.css')
4 | //require("prismjs/themes/prism.css")
5 | require('prismjs/themes/prism-okaidia.css')
6 | //require("prismjs/themes/prism-tomorrow.css")
7 | //require("prismjs/themes/prism-solarizedlight.css")
8 | //require("prismjs/themes/prism-coy.css")
9 | //require("prismjs/themes/prism-dark.css")
10 | //require("prismjs/themes/prism-twilight.css")
11 | //require("prismjs/themes/prism-funky.css")
12 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | //const webpack = require("webpack");
2 | const _ = require('lodash')
3 | const path = require('path')
4 | const Promise = require('bluebird')
5 |
6 | const { createFilePath } = require(`gatsby-source-filesystem`)
7 |
8 | exports.onCreateNode = ({ node, getNode, actions }) => {
9 | const { createNodeField } = actions
10 | if (node.internal.type === `Mdx`) {
11 | const slug = createFilePath({ node, getNode })
12 | const fileNode = getNode(node.parent)
13 | const source = fileNode.sourceInstanceName
14 | const separtorIndex = ~slug.indexOf('--') ? slug.indexOf('--') : 0
15 | const shortSlugStart = separtorIndex ? separtorIndex + 2 : 0
16 |
17 | if (source !== 'parts') {
18 | createNodeField({
19 | node,
20 | name: `slug`,
21 | value: `${separtorIndex ? '/' : ''}${slug.substring(shortSlugStart)}`,
22 | })
23 | }
24 | createNodeField({
25 | node,
26 | name: `prefix`,
27 | value: separtorIndex ? slug.substring(6, separtorIndex) : '',
28 | })
29 | createNodeField({
30 | node,
31 | name: `source`,
32 | value: source,
33 | })
34 | }
35 | }
36 |
37 | exports.createPages = ({ graphql, actions }) => {
38 | const { createPage } = actions
39 |
40 | return new Promise((resolve, reject) => {
41 | const postTemplate = path.resolve('./src/templates/PostTemplate.js')
42 | const pageTemplate = path.resolve('./src/templates/PageTemplate.js')
43 | const tagTemplate = path.resolve('./src/templates/TagTemplate.js')
44 |
45 | resolve(
46 | graphql(
47 | `
48 | {
49 | allMdx(
50 | filter: { fields: { slug: { ne: null } }, frontmatter: { published: { eq: true } } }
51 | sort: { fields: [fields___prefix], order: DESC }
52 | limit: 1000
53 | ) {
54 | edges {
55 | node {
56 | id
57 | fields {
58 | slug
59 | prefix
60 | source
61 | }
62 | frontmatter {
63 | title
64 | tags
65 | }
66 | }
67 | }
68 | }
69 | }
70 | `
71 | ).then((result) => {
72 | if (result.errors) {
73 | console.log(result.errors)
74 | reject(result.errors)
75 | }
76 |
77 | const items = result.data.allMdx.edges
78 |
79 | // Create tag list
80 | const tagSet = new Set()
81 | items.forEach((edge) => {
82 | const {
83 | node: {
84 | frontmatter: { tags },
85 | },
86 | } = edge
87 |
88 | if (tags && tags != null) {
89 | tags.forEach((tag) => {
90 | if (tag && tag !== null) {
91 | tagSet.add(tag)
92 | }
93 | })
94 | }
95 | })
96 |
97 | // Create tag pages
98 | const tagList = Array.from(tagSet)
99 | tagList.forEach((tag) => {
100 | createPage({
101 | path: `/tag/${_.kebabCase(tag)}/`,
102 | component: tagTemplate,
103 | context: {
104 | tag,
105 | },
106 | })
107 | })
108 |
109 | // Create posts
110 | const posts = items.filter((item) => item.node.fields.source === 'posts')
111 | posts.forEach(({ node }, index) => {
112 | const slug = node.fields.slug
113 | const next = index === 0 ? undefined : posts[index - 1].node
114 | const prev = index === posts.length - 1 ? undefined : posts[index + 1].node
115 | const source = node.fields.source
116 |
117 | createPage({
118 | path: slug,
119 | component: postTemplate,
120 | context: {
121 | slug,
122 | prev,
123 | next,
124 | source,
125 | },
126 | })
127 | })
128 |
129 | // and pages.
130 | const pages = items.filter((item) => item.node.fields.source === 'pages')
131 | pages.forEach(({ node }) => {
132 | const slug = node.fields.slug
133 | const source = node.fields.source
134 |
135 | createPage({
136 | path: slug,
137 | component: pageTemplate,
138 | context: {
139 | slug,
140 | source,
141 | },
142 | })
143 | })
144 |
145 | // Create blog post list pages
146 | const postsPerPage = 5
147 | const numPages = Math.ceil(posts.length / postsPerPage)
148 |
149 | _.times(numPages, (i) => {
150 | createPage({
151 | path: i === 0 ? `/` : `/${i + 1}`,
152 | component: path.resolve('./src/templates/index.js'),
153 | context: {
154 | limit: postsPerPage,
155 | skip: i * postsPerPage,
156 | numPages,
157 | currentPage: i + 1,
158 | },
159 | })
160 | })
161 | })
162 | )
163 | })
164 | }
165 |
--------------------------------------------------------------------------------
/jest-preprocess.js:
--------------------------------------------------------------------------------
1 | const babelOptions = {
2 | presets: ['babel-preset-gatsby'],
3 | }
4 |
5 | // eslint-disable-next-line import/no-extraneous-dependencies
6 | module.exports = require('babel-jest').createTransformer(babelOptions)
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.(jsx|js)?$': `/jest-preprocess.js`,
4 | },
5 | moduleNameMapper: {
6 | '.+\\.(css|styl|less|sass|scss)$': `/__mocks__/style-mock.js`,
7 | '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/__mocks__/file-mock.js`,
8 | },
9 | testPathIgnorePatterns: [`node_modules`, `.cache`, `cypress`, `__tests__/util`],
10 | transformIgnorePatterns: ['node_modules/(?!(gatsby|gatsby-plugin-mdx)/)'],
11 | globals: {
12 | __PATH_PREFIX__: ``,
13 | },
14 | testURL: `http://localhost`,
15 | setupFiles: [`/loadershim.js`],
16 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
17 | coverageReporters: ['json-summary', 'text', 'lcov'],
18 | }
19 |
--------------------------------------------------------------------------------
/loadershim.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-underscore-dangle
2 | global.___loader = {
3 | enqueue: jest.fn(),
4 | }
5 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "yarn run build"
3 | publish = "public"
4 |
5 | [[plugins]]
6 | package = "netlify-plugin-gatsby-cache"
7 |
8 | [[plugins]]
9 | package = "@sentry/netlify-build-plugin"
10 |
11 | [plugins.inputs]
12 | sentryOrg = "ottobot"
13 | sentryProject = "chrisottodev"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-chris-otto-dev",
3 | "version": "1.0.0",
4 | "author": "Chris Otto ",
5 | "license": "MIT",
6 | "scripts": {
7 | "build": "GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages",
8 | "build:local": "gatsby build",
9 | "develop": "gatsby develop",
10 | "serve": "gatsby serve",
11 | "start": "gatsby build && gatsby serve",
12 | "format": "prettier --write 'src/**/*.js'",
13 | "lint": "eslint ./src",
14 | "lintFix": "eslint ./src --fix",
15 | "stylelint": "stylelint src/**/*.js",
16 | "test": "jest",
17 | "badges": "jest --coverage && jest-coverage-badges",
18 | "test:cypress": "cypress run",
19 | "e2e": "start-server-and-test serve http://localhost:9000 test:cypress",
20 | "e2e:open": "cypress open",
21 | "generate-app-icons": "sh ./scripts/generate-app-icons.sh",
22 | "post": "node util/newPost.js",
23 | "compress": "node util/compressImages.js"
24 | },
25 | "dependencies": {
26 | "@mdx-js/mdx": "^1.6.22",
27 | "@mdx-js/react": "^1.6.22",
28 | "@testing-library/jest-dom": "^5.11.9",
29 | "@testing-library/react": "^11.2.5",
30 | "algoliasearch": "^4.8.5",
31 | "antd": "^4.12.3",
32 | "fontfaceobserver": "^2.0.13",
33 | "gatsby": "^2.32.3",
34 | "gatsby-image": "^2.11.0",
35 | "gatsby-plugin-algolia": "^0.16.3",
36 | "gatsby-plugin-catch-links": "^2.10.0",
37 | "gatsby-plugin-feed-mdx": "^1.0.1",
38 | "gatsby-plugin-google-analytics": "^2.11.0",
39 | "gatsby-plugin-layout": "^1.10.0",
40 | "gatsby-plugin-mailchimp": "^5.1.2",
41 | "gatsby-plugin-manifest": "^2.12.0",
42 | "gatsby-plugin-mdx": "^1.10.0",
43 | "gatsby-plugin-netlify": "^2.11.0",
44 | "gatsby-plugin-offline": "^3.10.0",
45 | "gatsby-plugin-react-helmet": "^3.10.0",
46 | "gatsby-plugin-react-helmet-canonical-urls": "^1.4.0",
47 | "gatsby-plugin-react-svg": "^3.0.0",
48 | "gatsby-plugin-robots-txt": "^1.5.5",
49 | "gatsby-plugin-sentry": "^1.0.1",
50 | "gatsby-plugin-sharp": "^2.14.1",
51 | "gatsby-plugin-sitemap": "^2.12.0",
52 | "gatsby-plugin-styled-jsx": "^3.10.0",
53 | "gatsby-plugin-styled-jsx-postcss": "^2.0.2",
54 | "gatsby-remark-autolink-headers": "^2.11.0",
55 | "gatsby-remark-copy-linked-files": "^2.10.0",
56 | "gatsby-remark-embedder": "^4.1.0",
57 | "gatsby-remark-emojis": "^0.4.3",
58 | "gatsby-remark-external-links": "^0.0.4",
59 | "gatsby-remark-images": "^3.11.0",
60 | "gatsby-remark-prismjs": "^3.13.0",
61 | "gatsby-remark-responsive-iframe": "^2.11.0",
62 | "gatsby-remark-smartypants": "^2.10.0",
63 | "gatsby-source-filesystem": "^2.11.0",
64 | "gatsby-transformer-json": "^2.11.0",
65 | "gatsby-transformer-sharp": "^2.12.0",
66 | "jest": "^26.6.3",
67 | "prismjs": "^1.22.0",
68 | "react": "^16.14.0",
69 | "react-dom": "^16.14.0",
70 | "react-helmet": "^6.1.0",
71 | "react-icons": "^4.2.0",
72 | "react-instantsearch-dom": "^6.9.0",
73 | "react-share": "^4.3.1",
74 | "react-visibility-sensor": "^5.1.1",
75 | "styled-jsx": "^3.3.2",
76 | "typeface-open-sans": "^1.1.13"
77 | },
78 | "keywords": [
79 | "gatsby",
80 | "blog",
81 | "personal website",
82 | "portfolio"
83 | ],
84 | "devDependencies": {
85 | "@testing-library/cypress": "^7.0.3",
86 | "axe-core": "^4.1.2",
87 | "babel-eslint": "^10.1.0",
88 | "babel-jest": "^26.6.3",
89 | "babel-preset-gatsby": "^0.12.1",
90 | "cypress": "^6.5.0",
91 | "cypress-axe": "^0.12.1",
92 | "dotenv": "^8.2.0",
93 | "eslint": "^7.20.0",
94 | "eslint-config-google": "^0.14.0",
95 | "eslint-config-prettier": "^7.2.0",
96 | "eslint-plugin-graphql": "^4.0.0",
97 | "eslint-plugin-import": "^2.22.1",
98 | "eslint-plugin-jsx-a11y": "^6.4.1",
99 | "eslint-plugin-prettier": "^3.3.0",
100 | "eslint-plugin-react": "^7.22.0",
101 | "glob": "^7.1.6",
102 | "husky": "^5.0.9",
103 | "lint-staged": "^10.5.4",
104 | "postcss": "^8.2.6",
105 | "postcss-cli": "^8.3.1",
106 | "postcss-cssnext": "^3.1.0",
107 | "postcss-custom-properties": "^11.0.0",
108 | "postcss-custom-selectors": "^6.0.0",
109 | "postcss-easy-media-query": "^1.0.0",
110 | "postcss-load-plugins": "^2.3.0",
111 | "postcss-loader": "^3.0.0",
112 | "postcss-media-variables": "^2.0.1",
113 | "postcss-nested": "^4.2.3",
114 | "postcss-sorting": "^6.0.0",
115 | "postcss-text-remove-gap": "^1.1.1",
116 | "postcss-utilities": "^0.8.4",
117 | "prettier": "^2.2.1",
118 | "react-test-renderer": "^16.14.0",
119 | "serve": "^11.3.2",
120 | "start-server-and-test": "^1.12.0",
121 | "stylelint": "^13.10.0",
122 | "tinify": "^1.6.0-beta.2"
123 | },
124 | "husky": {
125 | "hooks": {
126 | "pre-commit": "lint-staged"
127 | }
128 | },
129 | "lint-staged": {
130 | "*.{html,js,yml,mdx,md,json}": [
131 | "prettier --write"
132 | ],
133 | "*.{js,jsx}": [
134 | "yarn run lint"
135 | ],
136 | "*.{jpg,png,gif}": [
137 | "yarn run compress",
138 | "git add ."
139 | ]
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = ctx => ({
2 | plugins: {
3 | "postcss-easy-media-query": {
4 | breakpoints: {
5 | tablet: 600,
6 | desktop: 1024
7 | }
8 | },
9 | "postcss-text-remove-gap": {
10 | defaultFontFamily: "Open Sans",
11 | defaultLineHeight: "0"
12 | },
13 | "postcss-nested": {},
14 | "postcss-cssnext": {}
15 | }
16 | });
17 |
18 | // "postcss-nested": {},
19 | // "postcss-sorting": {
20 | // order: ["custom-properties", "dollar-variables", "declarations", "at-rules", "rules"],
21 | // "properties-order": "alphabetical",
22 | // "unspecified-properties-position": "bottom"
23 | // },
24 | // "postcss-utilities": {},
25 | // "postcss-cssnext": {}
26 |
--------------------------------------------------------------------------------
/scripts/generate-app-icons.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Exit the script on any command with non 0 return code
4 | set -e
5 |
6 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/favicon-16x16.png resize 16
7 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/favicon-32x32.png resize 32
8 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/favicon-96x96.png resize 96
9 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-512x512.png resize 512
10 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-384x384.png resize 384
11 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-256x256.png resize 256
12 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-192x192.png resize 192
13 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-144x144.png resize 144
14 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-96x96.png resize 96
15 | npx sharp -i ./src/images/app-icons/icon.png -o ./static/icons/icon-48x48.png resize 48
16 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-180x180.png resize 180
17 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-152x152.png resize 152
18 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-144x144.png resize 144
19 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-120x120.png resize 120
20 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-114x114.png resize 114
21 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-76x76.png resize 76
22 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-72x72.png resize 72
23 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-60x60.png resize 60
24 | npx sharp -i ./src/images/app-icons/apple-icon.png -o ./static/icons/apple-icon-57x57.png resize 57
25 |
--------------------------------------------------------------------------------
/src/components/Article/Article.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Article = (props) => {
5 | const { children, theme } = props
6 |
7 | return (
8 |
9 | {children}
10 |
11 | {/* --- STYLES --- */}
12 |
30 |
31 | )
32 | }
33 |
34 | Article.propTypes = {
35 | children: PropTypes.node.isRequired,
36 | theme: PropTypes.object.isRequired,
37 | }
38 |
39 | export default Article
40 |
--------------------------------------------------------------------------------
/src/components/Article/Bodytext.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MDXRenderer } from 'gatsby-plugin-mdx'
3 | import PropTypes from 'prop-types'
4 |
5 | const Bodytext = (props) => {
6 | const { body, theme } = props
7 |
8 | return (
9 |
10 |
11 | {body}
12 |
13 |
14 |
79 |
80 | )
81 | }
82 |
83 | Bodytext.propTypes = {
84 | body: PropTypes.string.isRequired,
85 | theme: PropTypes.object.isRequired,
86 | }
87 |
88 | export default Bodytext
89 |
--------------------------------------------------------------------------------
/src/components/Article/Headline.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Headline = (props) => {
5 | const { title, children, theme } = props
6 |
7 | return (
8 |
9 | {title ? {title} : {children} }
10 |
11 | {/* --- STYLES --- */}
12 |
54 |
55 | )
56 | }
57 |
58 | Headline.propTypes = {
59 | title: PropTypes.string,
60 | children: PropTypes.node,
61 | theme: PropTypes.object.isRequired,
62 | }
63 |
64 | export default Headline
65 |
--------------------------------------------------------------------------------
/src/components/Article/SubHeadline.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const SubHeadline = (props) => {
5 | const { title, children, theme } = props
6 |
7 | return (
8 |
9 | {title ? {title} : {children} }
10 |
11 | {/* --- STYLES --- */}
12 |
54 |
55 | )
56 | }
57 |
58 | SubHeadline.propTypes = {
59 | title: PropTypes.string,
60 | children: PropTypes.node,
61 | theme: PropTypes.object.isRequired,
62 | }
63 |
64 | export default SubHeadline
65 |
--------------------------------------------------------------------------------
/src/components/Article/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Article'
2 |
--------------------------------------------------------------------------------
/src/components/AsyncComponent/AsyncComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function asyncComponent(getComponent, loadingComponent) {
4 | return class AsyncComponent extends React.Component {
5 | state = { component: null }
6 |
7 | componentDidMount() {
8 | if (!this.state.component) {
9 | getComponent().then((component) => {
10 | this.setState({ component })
11 | })
12 | }
13 | }
14 | render() {
15 | const { component: Comp } = this.state
16 | if (Comp) {
17 | return
18 | }
19 | return loadingComponent ? loadingComponent : Loading...
20 | }
21 | }
22 | }
23 |
24 | export default asyncComponent
25 |
--------------------------------------------------------------------------------
/src/components/AsyncComponent/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './AsyncComponent'
2 |
--------------------------------------------------------------------------------
/src/components/Blog/Blog.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 |
4 | import Item from './Item'
5 |
6 | const Blog = (props) => {
7 | const { posts, theme } = props
8 |
9 | return (
10 |
11 |
12 |
13 | {posts.map((post) => {
14 | const {
15 | node,
16 | node: {
17 | fields: { slug },
18 | },
19 | } = post
20 | return
21 | })}
22 |
23 |
24 |
25 | {/* --- STYLES --- */}
26 |
51 |
52 | )
53 | }
54 |
55 | Blog.propTypes = {
56 | posts: PropTypes.array.isRequired,
57 | theme: PropTypes.object.isRequired,
58 | }
59 |
60 | export default Blog
61 |
--------------------------------------------------------------------------------
/src/components/Blog/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Blog'
2 |
--------------------------------------------------------------------------------
/src/components/Contact/Contact.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: 0 */
2 | import { navigate } from 'gatsby'
3 | import { Form, Input, Button } from 'antd'
4 | import PropTypes from 'prop-types'
5 | import React from 'react'
6 |
7 | import 'antd/lib/form/style/index.css'
8 | import 'antd/lib/input/style/index.css'
9 | import 'antd/lib/button/style/index.css'
10 | import { ThemeContext } from '../../layouts'
11 |
12 | const { TextArea } = Input
13 |
14 | const ContactForm = (props) => {
15 | function encode(data) {
16 | return Object.keys(data)
17 | .map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
18 | .join('&')
19 | }
20 |
21 | function sendMessage(values) {
22 | fetch('/', {
23 | method: 'POST',
24 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
25 | body: encode({ 'form-name': 'contact', ...values }),
26 | })
27 | .then(() => {
28 | console.log('Form submission success')
29 | navigate('/success')
30 | })
31 | .catch((error) => {
32 | console.error('Form submission error:', error)
33 | this.handleNetworkError()
34 | })
35 | }
36 |
37 | function handleNetworkError(e) {
38 | console.log('submit Error')
39 | }
40 |
41 | const layout = {
42 | labelCol: { span: 3 },
43 | wrapperCol: { span: 16 },
44 | }
45 |
46 | const tailLayout = {
47 | wrapperCol: { offset: 3, span: 12 },
48 | }
49 |
50 | return (
51 |
52 |
53 | {(theme) => (
54 |
55 |
63 |
64 |
65 |
77 |
78 |
79 |
86 |
92 |
93 |
94 |
95 | Submit
96 |
97 |
98 |
99 |
100 | {/* --- STYLES --- */}
101 |
144 |
145 | )}
146 |
147 |
148 | )
149 | }
150 |
151 | ContactForm.propTypes = {
152 | form: PropTypes.object,
153 | }
154 |
155 | export default ContactForm
156 |
--------------------------------------------------------------------------------
/src/components/Contact/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Contact'
2 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Sentry from 'gatsby-plugin-sentry'
4 |
5 | class ErrorBoundary extends React.Component {
6 | constructor(props) {
7 | super(props)
8 | this.state = { error: null }
9 | }
10 |
11 | componentDidCatch(error, errorInfo) {
12 | this.setState({ error })
13 | Sentry.configureScope((scope) => {
14 | Object.keys(errorInfo).forEach((key) => {
15 | scope.setExtra(key, errorInfo[key])
16 | })
17 | })
18 | Sentry.captureException(error)
19 | }
20 |
21 | render() {
22 | if (this.state.error) {
23 | // render fallback UI
24 | return Something went wrong!
25 | } else {
26 | // when there's not an error, render children untouched
27 | return this.props.children
28 | }
29 | }
30 | }
31 |
32 | ErrorBoundary.propTypes = {
33 | children: PropTypes.object.isRequired,
34 | }
35 |
36 | export default ErrorBoundary
37 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ErrorBoundary'
2 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Social from '../Social'
4 |
5 | const Footer = (props) => {
6 | const { theme } = props
7 |
8 | const year = new Date().getFullYear()
9 |
10 | return (
11 |
12 |
37 |
38 | {/* --- STYLES --- */}
39 |
76 |
77 | )
78 | }
79 |
80 | Footer.propTypes = {
81 | theme: PropTypes.object.isRequired,
82 | }
83 |
84 | export default Footer
85 |
--------------------------------------------------------------------------------
/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Footer'
2 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Header'
2 |
--------------------------------------------------------------------------------
/src/components/Hero/Hero.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { FaArrowDown } from 'react-icons/fa/'
5 |
6 | const Hero = (props) => {
7 | const { scrollToContent, backgrounds, theme } = props
8 |
9 | return (
10 |
11 |
12 | Hi! I’m Chris.
13 |
14 | I’m a software engineer based in Milwaukee, WI.
15 | I enjoy Javascript, DevOps and Testing.
16 |
17 |
18 |
19 |
20 |
21 |
22 | {/* --- STYLES --- */}
23 |
126 |
127 | )
128 | }
129 |
130 | Hero.propTypes = {
131 | scrollToContent: PropTypes.func.isRequired,
132 | backgrounds: PropTypes.object.isRequired,
133 | theme: PropTypes.object.isRequired,
134 | }
135 |
136 | export default Hero
137 |
--------------------------------------------------------------------------------
/src/components/Hero/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Hero'
2 |
--------------------------------------------------------------------------------
/src/components/List/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 |
5 | const List = (props) => {
6 | const { edges, theme } = props
7 |
8 | return (
9 |
10 |
11 | {edges.map((edge) => {
12 | const {
13 | node: {
14 | frontmatter: { title },
15 | fields: { slug },
16 | },
17 | } = edge
18 |
19 | return (
20 |
21 | {title}
22 |
23 | )
24 | })}
25 |
26 |
27 | {/* --- STYLES --- */}
28 |
40 |
41 | )
42 | }
43 |
44 | List.propTypes = {
45 | edges: PropTypes.array.isRequired,
46 | theme: PropTypes.object.isRequired,
47 | }
48 |
49 | export default List
50 |
--------------------------------------------------------------------------------
/src/components/List/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './List'
2 |
--------------------------------------------------------------------------------
/src/components/Menu/Expand.js:
--------------------------------------------------------------------------------
1 | import { FaAngleDown } from 'react-icons/fa/'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 |
5 | const Expand = (props) => {
6 | const { onClick, theme } = props
7 |
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {/* --- STYLES --- */}
15 |
111 |
112 | )
113 | }
114 |
115 | Expand.propTypes = {
116 | onClick: PropTypes.func,
117 | theme: PropTypes.object.isRequired,
118 | }
119 |
120 | export default Expand
121 |
--------------------------------------------------------------------------------
/src/components/Menu/Item.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 |
5 | const Item = (props) => {
6 | const { theme, item: { label, to, icon: Icon } = {}, onClick } = props
7 |
8 | return (
9 |
10 |
11 |
17 | {Icon && } {label}
18 |
19 |
20 |
21 | {/* --- STYLES --- */}
22 |
95 |
96 | )
97 | }
98 |
99 | Item.propTypes = {
100 | item: PropTypes.object,
101 | hidden: PropTypes.bool,
102 | onClick: PropTypes.func,
103 | icon: PropTypes.func,
104 | theme: PropTypes.object.isRequired,
105 | }
106 |
107 | export default Item
108 |
--------------------------------------------------------------------------------
/src/components/Menu/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Menu'
2 |
--------------------------------------------------------------------------------
/src/components/Page/Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Headline from '../Article/Headline'
5 | import Bodytext from '../Article/Bodytext'
6 |
7 | const Page = (props) => {
8 | const {
9 | page: {
10 | body,
11 | frontmatter: { title },
12 | },
13 | theme,
14 | } = props
15 |
16 | return (
17 |
18 |
21 |
22 |
23 | )
24 | }
25 |
26 | Page.propTypes = {
27 | page: PropTypes.object.isRequired,
28 | theme: PropTypes.object.isRequired,
29 | }
30 |
31 | export default Page
32 |
--------------------------------------------------------------------------------
/src/components/Page/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Page'
2 |
--------------------------------------------------------------------------------
/src/components/Post/Author.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MDXRenderer } from 'gatsby-plugin-mdx'
3 | import PropTypes from 'prop-types'
4 |
5 | import config from '../../../content/meta/config'
6 | import avatar from '../../images/jpg/avatar.jpg'
7 |
8 | const Author = (props) => {
9 | const { note, theme } = props
10 |
11 | return (
12 |
13 |
14 |
15 |
19 |
20 |
21 | {note}
22 |
23 |
24 |
25 | {/* --- STYLES --- */}
26 |
61 |
62 | )
63 | }
64 |
65 | Author.propTypes = {
66 | note: PropTypes.string.isRequired,
67 | theme: PropTypes.object.isRequired,
68 | }
69 |
70 | export default Author
71 |
--------------------------------------------------------------------------------
/src/components/Post/Meta.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 |
5 | import { FaCalendar } from 'react-icons/fa/'
6 | import { FaRegClock } from 'react-icons/fa/'
7 | import { FaUser } from 'react-icons/fa/'
8 | import { FaTag } from 'react-icons/fa/'
9 |
10 | const Meta = (props) => {
11 | const { prefix, author: authorName, tags, timeToRead, theme } = props
12 |
13 | return (
14 |
15 |
16 | {prefix}
17 |
18 |
19 | {authorName}
20 |
21 |
22 | {timeToRead} min.
23 |
24 | {tags &&
25 | tags.map((tag) => (
26 |
27 |
28 | {tag}
29 |
30 | ))}
31 |
32 | {/* --- STYLES --- */}
33 |
58 |
59 | )
60 | }
61 |
62 | Meta.propTypes = {
63 | prefix: PropTypes.string.isRequired,
64 | author: PropTypes.string.isRequired,
65 | category: PropTypes.string,
66 | timeToRead: PropTypes.string,
67 | theme: PropTypes.object.isRequired,
68 | tags: PropTypes.object.isRequired,
69 | }
70 |
71 | export default Meta
72 |
--------------------------------------------------------------------------------
/src/components/Post/NextPrev.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 |
5 | import { FaArrowRight } from 'react-icons/fa/'
6 | import { FaArrowLeft } from 'react-icons/fa/'
7 |
8 | const NextPrev = (props) => {
9 | const {
10 | theme,
11 | next: {
12 | fields: { prefix: nextPrefix, slug: nextSlug } = {},
13 | frontmatter: { title: nextTitle } = {},
14 | } = {},
15 | prev: {
16 | fields: { prefix: prevPrefix, slug: prevSlug } = {},
17 | frontmatter: { title: prevTitle } = {},
18 | } = {},
19 | } = props
20 |
21 | return (
22 |
23 |
24 | {nextSlug && (
25 |
26 |
27 |
28 | {nextTitle} {nextPrefix}
29 |
30 |
31 | )}
32 | {prevSlug && (
33 |
34 |
35 |
36 | {prevTitle} {prevPrefix}
37 |
38 |
39 | )}
40 |
41 |
42 | {/* --- STYLES --- */}
43 |
107 |
108 | )
109 | }
110 |
111 | NextPrev.propTypes = {
112 | next: PropTypes.object,
113 | prev: PropTypes.object,
114 | theme: PropTypes.object.isRequired,
115 | }
116 |
117 | export default NextPrev
118 |
--------------------------------------------------------------------------------
/src/components/Post/Post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import 'prismjs/themes/prism-okaidia.css'
4 | import Img from 'gatsby-image'
5 |
6 | import asyncComponent from '../AsyncComponent'
7 | import Headline from '../Article/Headline'
8 | import Bodytext from '../Article/Bodytext'
9 | import Subscribe from '../Subscribe'
10 | import Meta from './Meta'
11 | import Author from './Author'
12 | import NextPrev from './NextPrev'
13 |
14 | const Share = asyncComponent(() =>
15 | import('./Share')
16 | .then((module) => {
17 | return module.default
18 | })
19 | .catch((error) => {})
20 | )
21 |
22 | const Post = (props) => {
23 | const {
24 | post,
25 | post: {
26 | timeToRead,
27 | body,
28 | fields: { prefix },
29 | frontmatter: {
30 | title,
31 | author,
32 | tags,
33 | cover: {
34 | children: [{ fluid }],
35 | },
36 | },
37 | },
38 | authornote,
39 | next: nextPost,
40 | prev: prevPost,
41 | theme,
42 | } = props
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 | )
60 | }
61 |
62 | Post.propTypes = {
63 | post: PropTypes.object.isRequired,
64 | authornote: PropTypes.string.isRequired,
65 | next: PropTypes.object,
66 | prev: PropTypes.object,
67 | theme: PropTypes.object.isRequired,
68 | }
69 |
70 | export default Post
71 |
--------------------------------------------------------------------------------
/src/components/Post/Share.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | FacebookShareButton,
5 | LinkedinShareButton,
6 | TwitterShareButton,
7 | FacebookShareCount,
8 | FacebookIcon,
9 | TwitterIcon,
10 | LinkedinIcon,
11 | } from 'react-share'
12 |
13 | import config from '../../../content/meta/config'
14 |
15 | const PostShare = (props) => {
16 | const {
17 | post: {
18 | fields: { slug },
19 | frontmatter: { title },
20 | excerpt,
21 | },
22 | theme,
23 | } = props
24 |
25 | const url = config.siteUrl + slug
26 |
27 | const iconSize = 36
28 | const filter = (count) => (count > 0 ? count : '')
29 |
30 | return (
31 |
32 |
33 |
SHARE
34 |
35 |
42 |
43 |
44 |
51 |
52 |
53 | {(count) => {filter(count)}
}
54 |
55 |
56 |
64 |
65 |
66 |
67 |
68 |
69 | {/* --- STYLES --- */}
70 |
102 |
103 | )
104 | }
105 |
106 | PostShare.propTypes = {
107 | post: PropTypes.object.isRequired,
108 | theme: PropTypes.object.isRequired,
109 | }
110 |
111 | export default PostShare
112 |
--------------------------------------------------------------------------------
/src/components/Post/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Post'
2 |
--------------------------------------------------------------------------------
/src/components/Project/Project.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Img from 'gatsby-image'
4 | import { FaGithub } from 'react-icons/fa/'
5 | import { FaExternalLinkAlt } from 'react-icons/fa/'
6 |
7 | const Project = (props) => {
8 | const {
9 | theme,
10 | project: { name, description, img, tech, githuburl, projecturl },
11 | images,
12 | } = props
13 |
14 | const image = images.find((x) => {
15 | return x.node.relativePath === `project/${img}`
16 | })
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {name}
26 |
27 |
28 | {description}
29 | {tech.map((item) => (
30 | {item}
31 | ))}
32 |
33 |
34 | {projecturl && (
35 |
36 |
37 |
38 | )}
39 | {githuburl && (
40 |
41 |
42 |
43 | )}
44 |
45 |
46 |
47 | {/* --- STYLES --- */}
48 |
122 |
123 | )
124 | }
125 |
126 | Project.propTypes = {
127 | project: PropTypes.object.isRequired,
128 | theme: PropTypes.object.isRequired,
129 | images: PropTypes.object.isRequired,
130 | }
131 |
132 | export default Project
133 |
--------------------------------------------------------------------------------
/src/components/Project/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Project'
2 |
--------------------------------------------------------------------------------
/src/components/Search/Hit.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 |
5 | const Hit = (props) => {
6 | const { hit } = props
7 |
8 | return (
9 |
10 | {hit.title}
11 |
12 | {/* --- STYLES --- */}
13 |
30 |
31 | )
32 | }
33 |
34 | Hit.propTypes = {
35 | hit: PropTypes.object.isRequired,
36 | }
37 |
38 | export default Hit
39 |
--------------------------------------------------------------------------------
/src/components/Search/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { InstantSearch, SearchBox, Hits, Stats, Pagination } from 'react-instantsearch-dom'
4 | import algoliasearch from 'algoliasearch/lite'
5 |
6 | import Hit from './Hit'
7 |
8 | const Search = (props) => {
9 | const { algolia } = props
10 | const searchClient = algoliasearch(algolia.appId, algolia.searchOnlyApiKey)
11 |
12 | return (
13 |
14 |
15 | {algolia && algolia.appId && (
16 |
17 | }
21 | submit={ }
22 | />
23 |
24 |
25 |
26 |
27 | )}
28 |
29 |
30 | {/* --- STYLES --- */}
31 |
86 |
87 | )
88 | }
89 |
90 | Search.propTypes = {
91 | algolia: PropTypes.object.isRequired,
92 | }
93 |
94 | export default Search
95 |
--------------------------------------------------------------------------------
/src/components/Search/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Search'
2 |
--------------------------------------------------------------------------------
/src/components/Seo/Seo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Helmet } from 'react-helmet'
4 | import config from '../../../content/meta/config'
5 |
6 | const Seo = (props) => {
7 | const { data } = props
8 | const postTitle = ((data || {}).frontmatter || {}).title
9 | const postDescription = ((data || {}).frontmatter || {}).description
10 | const postCover = ((data || {}).frontmatter || {}).cover
11 | const postSlug = ((data || {}).fields || {}).slug
12 |
13 | const title = postTitle ? `${postTitle}` : config.siteTitle
14 | const description = postDescription ? postDescription : config.siteDescription
15 | const image = postCover ? postCover : config.siteImage
16 | const url = config.siteUrl + config.pathPrefix + postSlug
17 |
18 | return (
19 |
25 | {/* General tags */}
26 | {title}
27 |
28 | {/* OpenGraph tags */}
29 |
30 |
31 |
32 |
33 |
34 | {/* Twitter Card tags */}
35 |
36 |
40 |
41 | )
42 | }
43 |
44 | Seo.propTypes = {
45 | data: PropTypes.object,
46 | }
47 |
48 | export default Seo
49 |
--------------------------------------------------------------------------------
/src/components/Seo/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Seo'
2 |
--------------------------------------------------------------------------------
/src/components/Skill/Skill.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SkillCard from './SkillCard'
3 |
4 | const Skill = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {/* --- STYLES --- */}
17 |
27 |
28 | )
29 | }
30 |
31 | export default Skill
32 |
--------------------------------------------------------------------------------
/src/components/Skill/SkillCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Card, Progress } from 'antd'
3 | import PropTypes from 'prop-types'
4 | import 'antd/lib/card/style/index.css'
5 | import 'antd/lib/progress/style/index.css'
6 |
7 | const SkillCard = (props) => {
8 | const { item, percent } = props
9 |
10 | return (
11 |
12 |
13 |
19 | ''}
30 | width={100}
31 | style={{ justifyContent: 'center' }}
32 | />
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | SkillCard.propTypes = {
40 | item: PropTypes.string.isRequired,
41 | percent: PropTypes.string.isRequired,
42 | }
43 |
44 | export default SkillCard
45 |
--------------------------------------------------------------------------------
/src/components/Skill/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Skill'
2 |
--------------------------------------------------------------------------------
/src/components/Social/Social.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { FaGithub } from 'react-icons/fa/'
4 | import { FaLinkedin } from 'react-icons/fa/'
5 | import { FaTwitter } from 'react-icons/fa/'
6 | import { FaInstagram } from 'react-icons/fa/'
7 | import { FaDev } from 'react-icons/fa/'
8 |
9 | import config from '../../../content/meta/config'
10 |
11 | const Social = (props) => {
12 | const { theme } = props
13 | const items = config.authorSocialLinks
14 | const icons = {
15 | twitter: FaTwitter,
16 | github: FaGithub,
17 | linkedin: FaLinkedin,
18 | instagram: FaInstagram,
19 | dev: FaDev,
20 | }
21 |
22 | return (
23 |
24 |
25 | {items.map((item) => {
26 | const Icon = icons[item.name]
27 | return (
28 |
36 |
37 |
38 | )
39 | })}
40 |
41 |
42 | {/* --- STYLES --- */}
43 |
85 |
86 | )
87 | }
88 |
89 | Social.propTypes = {
90 | theme: PropTypes.object.isRequired,
91 | }
92 |
93 | export default Social
94 |
--------------------------------------------------------------------------------
/src/components/Social/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Social'
2 |
--------------------------------------------------------------------------------
/src/components/Subscribe/Subscribe.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import addToMailchimp from 'gatsby-plugin-mailchimp'
3 | import { Form, Input, Button, Divider } from 'antd'
4 | import { UserOutlined, MailOutlined } from '@ant-design/icons'
5 | import 'antd/dist/antd.css'
6 |
7 | export default class Subscribe extends React.Component {
8 | constructor() {
9 | super()
10 | this.state = { name: '', email: '', result: null }
11 | }
12 |
13 | handleSubmit = async (e) => {
14 | const result = await addToMailchimp(this.state.email, { FNAME: this.state.name })
15 | if (result.result === 'error') {
16 | alert(`Whoops, ${this.state.name} you're already subscribed!`)
17 | } else {
18 | alert(`Thank you for subscribing ${this.state.name}!`)
19 | }
20 | this.setState({ result: result })
21 | }
22 |
23 | handleEmailChange = (event) => {
24 | this.setState({ email: event.target.value })
25 | }
26 |
27 | handleNameChange = (event) => {
28 | this.setState({ name: event.target.value })
29 | }
30 |
31 | render() {
32 | const layout = {
33 | labelCol: { span: 5 },
34 | wrapperCol: { span: 16 },
35 | }
36 |
37 | const tailLayout = {
38 | wrapperCol: { offset: 5, span: 12 },
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | Like the article? Subscribe to get notified whenever a new article gets published!
47 |
48 |
54 | } onChange={this.handleEmailChange} />
55 |
56 |
57 |
62 | } onChange={this.handleNameChange} />
63 |
64 |
65 |
66 | Subscribe
67 |
68 |
69 |
70 |
71 |
72 | {/* --- STYLES --- */}
73 |
84 |
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/Subscribe/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Subscribe'
2 |
--------------------------------------------------------------------------------
/src/components/Work/Education.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Education = (props) => {
5 | const { theme, school, degree, gpa, honors } = props
6 |
7 | return (
8 |
9 |
10 |
11 |
{school}
12 |
{degree}
13 |
14 |
15 |
GPA: {gpa}
16 | {honors &&
{honors}
}
17 |
18 |
19 | {/* --- STYLES --- */}
20 |
50 |
51 | )
52 | }
53 |
54 | Education.propTypes = {
55 | theme: PropTypes.object.isRequired,
56 | school: PropTypes.string.isRequired,
57 | degree: PropTypes.string.isRequired,
58 | gpa: PropTypes.string.isRequired,
59 | honors: PropTypes.string,
60 | }
61 |
62 | export default Education
63 |
--------------------------------------------------------------------------------
/src/components/Work/Volunteer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Volunteer = (props) => {
5 | const { theme, organization, duration, role } = props
6 |
7 | return (
8 |
9 |
10 |
11 |
{organization}
12 |
{duration}
13 |
14 |
{role}
15 |
16 |
17 | {/* --- STYLES --- */}
18 |
49 |
50 | )
51 | }
52 |
53 | Volunteer.propTypes = {
54 | theme: PropTypes.object.isRequired,
55 | organization: PropTypes.string.isRequired,
56 | role: PropTypes.string.isRequired,
57 | duration: PropTypes.string.isRequired,
58 | }
59 |
60 | export default Volunteer
61 |
--------------------------------------------------------------------------------
/src/components/Work/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Work'
2 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/education.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Education Component renders correctly with honors 1`] = `
4 | Array [
5 |
8 |
11 |
14 | Lakeland University
15 |
16 |
19 | B.S. Computer Science
20 |
21 |
22 |
25 |
26 | GPA:
27 | 3.9375
28 |
29 |
30 | Summa cum Laude
31 |
32 |
33 |
,
34 | ,
68 | ]
69 | `;
70 |
71 | exports[`Education Component renders correctly without honors 1`] = `
72 | Array [
73 |
76 |
79 |
82 | Milwaukee Area Technical College
83 |
84 |
87 | A.S. Mobile Application Development
88 |
89 |
90 |
93 |
94 | GPA:
95 | 3.729
96 |
97 |
98 |
,
99 | ,
133 | ]
134 | `;
135 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/volunteer.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Volunteer Component renders correctly 1`] = `
4 | Array [
5 |
8 |
11 |
14 | Milwaukee Area Technical College
15 |
16 |
19 | Jan 2015 - Present
20 |
21 |
22 |
25 | ITDEV Advisory Board Member
26 |
27 |
,
28 | ,
62 | ]
63 | `;
64 |
--------------------------------------------------------------------------------
/src/components/__tests__/article.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Article from '../Article'
4 | import Headline from '../Article/Headline'
5 | import SubHeadline from '../Article/SubHeadline'
6 | import Bodytext from '../Article/Bodytext'
7 | const theme = require('../../theme/theme.json')
8 |
9 | describe('Article Component', () => {
10 | it('renders correctly with title', () => {
11 | const tree = create(
12 |
13 |
14 |
15 |
16 | ).toJSON()
17 | expect(tree).toMatchSnapshot()
18 | })
19 | it('renders correctly without title', () => {
20 | const tree = create(
21 |
22 |
23 |
24 |
25 |
26 | ).toJSON()
27 | expect(tree).toMatchSnapshot()
28 | })
29 | })
30 |
31 | const mock_page =
32 | 'function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsx mdx */\nvar _frontmatter = {\n "title": "Test"\n};\n\nvar makeShortcode = function makeShortcode(name) {\n return function MDXDefaultShortcode(props) {\n console.warn("Component " + name + " was not imported, exported, or provided by MDXProvider as global scope");\n return mdx("div", props);\n };\n};\n\nvar layoutProps = {\n _frontmatter: _frontmatter\n};\nvar MDXLayout = "wrapper";\nreturn function MDXContent(_ref) {\n var components = _ref.components,\n props = _objectWithoutProperties(_ref, ["components"]);\n\n return mdx(MDXLayout, _extends({}, layoutProps, props, {\n components: components,\n mdxType: "MDXLayout"\n }), mdx("p", null, "test"));\n}\n;\nMDXContent.isMDXComponent = true;'
33 |
--------------------------------------------------------------------------------
/src/components/__tests__/education.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Education from '../Work/Education'
4 | const theme = require('../../theme/theme.json')
5 |
6 | describe('Education Component', () => {
7 | it('renders correctly without honors', () => {
8 | const tree = create(
9 |
15 | ).toJSON()
16 | expect(tree).toMatchSnapshot()
17 | })
18 | it('renders correctly with honors', () => {
19 | const tree = create(
20 |
27 | ).toJSON()
28 | expect(tree).toMatchSnapshot()
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/components/__tests__/footer.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Footer from '../Footer'
4 | const theme = require('../../theme/theme.json')
5 |
6 | describe('Footer Component', () => {
7 | it('renders correctly', () => {
8 | const tree = create().toJSON()
9 | expect(tree).toMatchSnapshot()
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/src/components/__tests__/skill.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Skill from '../Skill'
4 |
5 | describe('Skill Component', () => {
6 | it('renders correctly', () => {
7 | const tree = create( ).toJSON()
8 | expect(tree).toMatchSnapshot()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/src/components/__tests__/social.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Social from '../Social'
4 | const theme = require('../../theme/theme.json')
5 |
6 | describe('Social Component', () => {
7 | it('renders correctly', () => {
8 | const tree = create( ).toJSON()
9 | expect(tree).toMatchSnapshot()
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/src/components/__tests__/volunteer.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { create } from 'react-test-renderer'
3 | import Volunteer from '../Work/Volunteer'
4 | const theme = require('../../theme/theme.json')
5 |
6 | describe('Volunteer Component', () => {
7 | it('renders correctly', () => {
8 | const tree = create(
9 |
15 | ).toJSON()
16 | expect(tree).toMatchSnapshot()
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/data/project.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "VSCode FitNesse",
4 | "description": "Formats, runs tests and provides syntax highlighting for FitNesse tests in VS Code.",
5 | "img": "vscodefitnesse.png",
6 | "tech": ["Typescript", "Node"],
7 | "githuburl": "https://github.com/chrisotto6/VSCodeFitNesse",
8 | "projecturl": "https://marketplace.visualstudio.com/items?itemName=chrisotto.vscodefitnesse"
9 | },
10 | {
11 | "name": "Personal Website",
12 | "description": "Personal site showcasing my blog, projects and online resume.",
13 | "img": "chrisottodev.png",
14 | "tech": ["Gatsby", "React", "GraphQL", "Node"],
15 | "githuburl": "https://github.com/chrisotto6/chrisottodev",
16 | "projecturl": "https://chrisotto.dev/"
17 | },
18 | {
19 | "name": "Library",
20 | "description": "React application to view parts of my GoodReads Library.",
21 | "img": "library.png",
22 | "tech": ["React", "API", "Node"],
23 | "githuburl": "https://github.com/chrisotto6/library",
24 | "projecturl": "https://library.chrisotto.dev/"
25 | },
26 | {
27 | "name": "FitNesse Format",
28 | "description": "NPM package to format a string the same as the FitNesse client would.",
29 | "img": "fitnesseformat.png",
30 | "tech": ["Javascript", "Node"],
31 | "githuburl": "https://github.com/chrisotto6/fitnesse-format",
32 | "projecturl": "https://www.npmjs.com/package/fitnesse-format"
33 | },
34 | {
35 | "name": "Card Shuffling",
36 | "description": "A simple web application to deal cards to players.",
37 | "img": "cardshuffling.png",
38 | "tech": ["Javascript", "Node"],
39 | "githuburl": "https://github.com/chrisotto6/card-shuffling",
40 | "projecturl": "https://card-shuffling.chrisotto.dev/"
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/src/html.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class HTML extends React.Component {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | {this.props.headComponents}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {this.props.preBodyComponents}
31 |
32 | {this.props.postBodyComponents}
33 |
34 |
35 | )
36 | }
37 | }
38 |
39 | HTML.propTypes = {
40 | htmlAttributes: PropTypes.object,
41 | headComponents: PropTypes.array,
42 | bodyAttributes: PropTypes.object,
43 | preBodyComponents: PropTypes.array,
44 | body: PropTypes.string,
45 | postBodyComponents: PropTypes.array,
46 | }
47 |
--------------------------------------------------------------------------------
/src/images/app-icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/app-icons/apple-icon.png
--------------------------------------------------------------------------------
/src/images/app-icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/app-icons/icon.png
--------------------------------------------------------------------------------
/src/images/jpg/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/jpg/avatar.jpg
--------------------------------------------------------------------------------
/src/images/png/hero-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/png/hero-background.png
--------------------------------------------------------------------------------
/src/images/png/mesh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/png/mesh.png
--------------------------------------------------------------------------------
/src/images/project/cardshuffling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/project/cardshuffling.png
--------------------------------------------------------------------------------
/src/images/project/chrisottodev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/project/chrisottodev.png
--------------------------------------------------------------------------------
/src/images/project/fitnesseformat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/project/fitnesseformat.png
--------------------------------------------------------------------------------
/src/images/project/library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/project/library.png
--------------------------------------------------------------------------------
/src/images/project/vscodefitnesse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/src/images/project/vscodefitnesse.png
--------------------------------------------------------------------------------
/src/images/svg-icons/algolia-full.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/images/svg-icons/algolia.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/svg-icons/email.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/svg-icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/svg-icons/search-by-algolia.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeContext } from '../layouts'
3 | import Article from '../components/Article'
4 | import Headline from '../components/Article/Headline'
5 |
6 | const NotFoundPage = () => (
7 |
8 | {(theme) => (
9 |
10 |
13 | You just hit a route that doesn't exist...the sadness.
14 |
15 | )}
16 |
17 | )
18 |
19 | export default NotFoundPage
20 |
--------------------------------------------------------------------------------
/src/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Article from '../components/Article'
3 | import Education from '../components/Work/Education'
4 | import Headline from '../components/Article/Headline'
5 | import Skill from '../components/Skill'
6 | import SubHeadline from '../components/Article/SubHeadline'
7 | import { ThemeContext } from '../layouts'
8 | import Work from '../components/Work'
9 | import Volunteer from '../components/Work/Volunteer'
10 |
11 | const AboutPage = () => {
12 | return (
13 |
14 |
15 | {(theme) => (
16 |
17 |
20 |
21 |
22 |
23 | Hard-working, detail orientated individual with conviction and integrity. Student of
24 | the fast-paced technology world, fast learner and challenge acceptor. Interested in
25 | applying wealth of experience and knowledge to deliver quality and value.
26 |
27 |
28 |
32 |
36 |
52 |
67 |
68 | )}
69 |
70 |
71 | {/* --- STYLES --- */}
72 |
82 |
83 | )
84 | }
85 |
86 | export default AboutPage
87 |
--------------------------------------------------------------------------------
/src/pages/contact.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeContext } from '../layouts'
3 | import Article from '../components/Article'
4 | import Contact from '../components/Contact'
5 | import Headline from '../components/Article/Headline'
6 | import Seo from '../components/Seo'
7 |
8 | const ContactPage = () => {
9 | return (
10 |
11 |
12 | {(theme) => (
13 |
14 |
17 |
18 | Hello 👋 thanks for stopping by. If you want to chat, reach out below!
19 |
20 |
21 |
22 |
23 | )}
24 |
25 |
26 | {/* --- STYLES --- */}
27 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default ContactPage
41 |
--------------------------------------------------------------------------------
/src/pages/project.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import { graphql, useStaticQuery } from 'gatsby'
4 | import { ThemeContext } from '../layouts'
5 | import Article from '../components/Article'
6 | import Project from '../components/Project'
7 | import Headline from '../components/Article/Headline'
8 |
9 | const ProjectPage = () => {
10 | const data = useStaticQuery(
11 | graphql`
12 | query ProjectQuery {
13 | allProjectJson {
14 | edges {
15 | node {
16 | name
17 | description
18 | img
19 | tech
20 | githuburl
21 | projecturl
22 | }
23 | }
24 | }
25 | projectImages: allFile(
26 | sort: { order: ASC, fields: [absolutePath] }
27 | filter: { relativePath: { regex: "/project/.*.png/" } }
28 | ) {
29 | edges {
30 | node {
31 | relativePath
32 | name
33 | childImageSharp {
34 | fluid(maxWidth: 340, maxHeight: 210, quality: 90, cropFocus: CENTER) {
35 | ...GatsbyImageSharpFluid
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 | `
43 | )
44 |
45 | return (
46 |
47 |
48 | {(theme) => (
49 |
50 |
53 |
54 |
55 | {data.allProjectJson.edges.map((edge) => (
56 |
62 | ))}
63 |
64 |
65 |
66 | )}
67 |
68 |
69 | {/* --- STYLES --- */}
70 |
84 |
85 | )
86 | }
87 |
88 | ProjectPage.propTypes = {
89 | data: PropTypes.object.isRequired,
90 | }
91 |
92 | export default ProjectPage
93 |
--------------------------------------------------------------------------------
/src/pages/search.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import { graphql } from 'gatsby'
4 |
5 | import Article from '../components/Article'
6 | import Search from '../components/Search'
7 | import { ThemeContext } from '../layouts'
8 | import Seo from '../components/Seo'
9 |
10 | import AlgoliaIcon from '!svg-react-loader!../images/svg-icons/search-by-algolia.svg?name=AlgoliaLogo'
11 |
12 | const SearchPage = (props) => {
13 | const {
14 | data: {
15 | site: {
16 | siteMetadata: { algolia },
17 | },
18 | },
19 | } = props
20 |
21 | return (
22 |
23 |
24 | {(theme) => (
25 |
26 |
29 |
30 |
31 |
32 | )}
33 |
34 |
35 |
36 |
37 | {/* --- STYLES --- */}
38 |
48 |
49 | )
50 | }
51 |
52 | SearchPage.propTypes = {
53 | data: PropTypes.object.isRequired,
54 | }
55 |
56 | export default SearchPage
57 |
58 | // eslint-disable-next-line no-undef
59 | export const query = graphql`
60 | query SearchQuery {
61 | site {
62 | siteMetadata {
63 | algolia {
64 | appId
65 | searchOnlyApiKey
66 | indexName
67 | }
68 | }
69 | }
70 | }
71 | `
72 |
--------------------------------------------------------------------------------
/src/pages/tag.js:
--------------------------------------------------------------------------------
1 | import { FaTag } from 'react-icons/fa/'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 | import { graphql } from 'gatsby'
5 | import { ThemeContext } from '../layouts'
6 | import Article from '../components/Article'
7 | import Headline from '../components/Article/Headline'
8 | import List from '../components/List'
9 | import Seo from '../components/Seo'
10 |
11 | const TagPage = (props) => {
12 | const {
13 | data: {
14 | posts: { edges: posts },
15 | },
16 | } = props
17 |
18 | // Create tag list
19 | const tagPosts = {}
20 | posts.forEach((edge) => {
21 | const {
22 | node: {
23 | frontmatter: { tags },
24 | },
25 | } = edge
26 |
27 | if (tags && tags != null) {
28 | tags.forEach((tag) => {
29 | if (tag && tag != null) {
30 | if (!tagPosts[tag]) {
31 | tagPosts[tag] = []
32 | }
33 | tagPosts[tag].push(edge)
34 | }
35 | })
36 | }
37 | })
38 |
39 | const tagList = []
40 |
41 | for (const tag in tagPosts) {
42 | tagList.push([tag, tagPosts[tag]])
43 | }
44 |
45 | // Sort tagList so the tag with the most posts is at the top
46 | tagList.sort((a, b) => {
47 | return b[1].length - a[1].length
48 | })
49 |
50 | return (
51 |
52 |
53 | {(theme) => (
54 |
55 |
58 | {tagList.map((item) => (
59 |
60 |
61 | {item[0]}
62 |
63 |
64 |
65 | ))}
66 | {/* --- STYLES --- */}
67 |
76 |
77 | )}
78 |
79 |
80 |
81 |
82 | )
83 | }
84 |
85 | TagPage.propTypes = {
86 | data: PropTypes.object.isRequired,
87 | }
88 |
89 | export default TagPage
90 |
91 | // eslint-disable-next-line no-undef
92 | export const query = graphql`
93 | query PostsQuery {
94 | posts: allMdx(
95 | filter: {
96 | fileAbsolutePath: { regex: "//posts/[0-9]+.*--/" }
97 | frontmatter: { published: { eq: true } }
98 | }
99 | sort: { fields: [fields___prefix], order: DESC }
100 | ) {
101 | edges {
102 | node {
103 | excerpt
104 | fields {
105 | slug
106 | prefix
107 | }
108 | frontmatter {
109 | title
110 | tags
111 | author
112 | cover {
113 | children {
114 | ... on ImageSharp {
115 | fluid(maxWidth: 800, maxHeight: 360) {
116 | ...GatsbyImageSharpFluid_withWebp
117 | }
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 | `
127 |
--------------------------------------------------------------------------------
/src/templates/PageTemplate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { graphql } from 'gatsby'
4 | import Seo from '../components/Seo'
5 | import Article from '../components/Article'
6 | import Page from '../components/Page'
7 | import { ThemeContext } from '../layouts'
8 |
9 | const PageTemplate = (props) => {
10 | const {
11 | data: { page },
12 | } = props
13 |
14 | return (
15 |
16 |
17 | {(theme) => (
18 |
19 |
20 |
21 | )}
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | PageTemplate.propTypes = {
30 | data: PropTypes.object.isRequired,
31 | }
32 |
33 | export default PageTemplate
34 |
35 | // eslint-disable-next-line no-undef
36 | export const pageQuery = graphql`
37 | query PageByPath($slug: String!) {
38 | page: mdx(fields: { slug: { eq: $slug } }) {
39 | id
40 | body
41 | frontmatter {
42 | title
43 | }
44 | }
45 | }
46 | `
47 |
--------------------------------------------------------------------------------
/src/templates/PostTemplate.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import { graphql } from 'gatsby'
4 | require('prismjs/themes/prism-okaidia.css')
5 |
6 | import Seo from '../components/Seo'
7 | import Article from '../components/Article'
8 | import Post from '../components/Post'
9 | import { ThemeContext } from '../layouts'
10 |
11 | const PostTemplate = (props) => {
12 | const {
13 | data: {
14 | post,
15 | authornote: { body: authorNote },
16 | },
17 | pageContext: { next, prev },
18 | } = props
19 |
20 | return (
21 |
22 |
23 | {(theme) => (
24 |
25 |
26 |
27 | )}
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | PostTemplate.propTypes = {
35 | data: PropTypes.object.isRequired,
36 | pageContext: PropTypes.object.isRequired,
37 | }
38 |
39 | export default PostTemplate
40 |
41 | // eslint-disable-next-line no-undef
42 | export const postQuery = graphql`
43 | query PostBySlug($slug: String!) {
44 | post: mdx(fields: { slug: { eq: $slug } }) {
45 | id
46 | body
47 | excerpt
48 | timeToRead
49 | fields {
50 | slug
51 | prefix
52 | }
53 | frontmatter {
54 | title
55 | author
56 | tags
57 | cover {
58 | children {
59 | ... on ImageSharp {
60 | fluid(maxWidth: 750, maxHeight: 380) {
61 | ...GatsbyImageSharpFluid_withWebp
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 | authornote: mdx(fileAbsolutePath: { regex: "/author/" }) {
69 | id
70 | body
71 | }
72 | }
73 | `
74 |
--------------------------------------------------------------------------------
/src/templates/TagTemplate.js:
--------------------------------------------------------------------------------
1 | import { FaTag } from 'react-icons/fa/'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 | import { graphql } from 'gatsby'
5 | import Seo from '../components/Seo'
6 | import { ThemeContext } from '../layouts'
7 | import Article from '../components/Article'
8 | import Headline from '../components/Article/Headline'
9 | import List from '../components/List'
10 |
11 | const TagTemplate = (props) => {
12 | const {
13 | pageContext: { tag },
14 | data: {
15 | allMdx: { totalCount, edges },
16 | },
17 | } = props
18 |
19 | return (
20 |
21 |
22 | {(theme) => (
23 |
24 |
35 |
36 | )}
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | TagTemplate.propTypes = {
45 | data: PropTypes.object.isRequired,
46 | pageContext: PropTypes.object.isRequired,
47 | }
48 |
49 | export default TagTemplate
50 |
51 | // eslint-disable-next-line no-undef
52 | export const tagQuery = graphql`
53 | query PostsByTag($tag: String) {
54 | allMdx(
55 | limit: 1000
56 | sort: { fields: [fields___prefix], order: DESC }
57 | filter: { frontmatter: { tags: { in: [$tag] }, published: { eq: true } } }
58 | ) {
59 | totalCount
60 | edges {
61 | node {
62 | fields {
63 | slug
64 | }
65 | excerpt
66 | timeToRead
67 | frontmatter {
68 | title
69 | tags
70 | }
71 | }
72 | }
73 | }
74 | }
75 | `
76 |
--------------------------------------------------------------------------------
/src/theme/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "space": {
3 | "default": "20px",
4 | "xxs": "2px",
5 | "xs": "5px",
6 | "s": "10px",
7 | "m": "20px",
8 | "l": "40px",
9 | "xl": "80px",
10 | "inset": {
11 | "default": "20px",
12 | "xs": "5px",
13 | "s": "10px",
14 | "m": "20px",
15 | "l": "40px"
16 | },
17 | "stack": {
18 | "default": "0 0 20px 0",
19 | "xxs": "0 0 2px 0",
20 | "xs": "0 0 5px 0",
21 | "s": "0 0 10px 0",
22 | "m": "0 0 20px 0",
23 | "l": "0 0 40px 0"
24 | },
25 | "inline": {
26 | "default": "0 20px 0 0",
27 | "xxs": "0 2px 0 0",
28 | "xs": "0 5px 0 0",
29 | "s": "0 10px 0 0",
30 | "m": "0 20px 0 0",
31 | "l": "0 40px 0 0"
32 | }
33 | },
34 | "size": {
35 | "radius": {
36 | "default": "10px",
37 | "small": "5px"
38 | }
39 | },
40 | "color": {
41 | "neutral": {
42 | "white": "#ffffff",
43 | "gray": {
44 | "a": "#fafaf9",
45 | "b": "#f3f2f2",
46 | "c": "#ecebea",
47 | "d": "#dddbda",
48 | "e": "#c9c7c5",
49 | "f": "#b0adab",
50 | "g": "#2c3e50",
51 | "h": "#706e6b",
52 | "i": "#514f4d",
53 | "j": "#3e3e3c",
54 | "k": "#2b2826"
55 | },
56 | "blue": {
57 | "antd": "#1890ff"
58 | },
59 | "black": "#000000"
60 | },
61 | "brand": {
62 | "primary": "#2c3e50",
63 | "primaryActive": "#2c3e50",
64 | "light": "#2c3e50",
65 | "lightActive": "#2c3e50",
66 | "dark": "#2c3e50",
67 | "darkActive": "#2c3e50"
68 | },
69 | "special": {
70 | "attention": "#787878"
71 | }
72 | },
73 | "font": {
74 | "family": {
75 | "initial": "Arial, sans-serif",
76 | "target": "Open Sans"
77 | },
78 | "weight": {
79 | "standard": 400,
80 | "bold": 600
81 | },
82 | "size": {
83 | "xxs": ".8em",
84 | "xs": ".95em",
85 | "s": "1.1em",
86 | "m": "1.35em",
87 | "l": "1.7em",
88 | "xl": "2em",
89 | "xxl": "2.2em",
90 | "xxxl": "2.8em"
91 | },
92 | "lineHeight": {
93 | "xs": 1.1,
94 | "s": 1.2,
95 | "m": 1.3,
96 | "l": 1.4,
97 | "xl": 1.5,
98 | "xxl": 1.6,
99 | "reset": 1
100 | }
101 | },
102 | "time": {
103 | "duration": {
104 | "default": "0.5s",
105 | "long": "1s"
106 | }
107 | },
108 | "background": {
109 | "color": {
110 | "primary": "#ffffff",
111 | "alt": "#fafaf9",
112 | "brand": "#2c3e50"
113 | }
114 | },
115 | "text": {
116 | "family": "Open Sans",
117 | "color": {
118 | "primary": "#3e3e3c",
119 | "primaryInverse": "#ffffff",
120 | "brand": "#2c3e50",
121 | "attention": "#787878"
122 | },
123 | "lineHeight": {
124 | "default": 1.6
125 | },
126 | "maxWidth": {
127 | "tablet": "650px",
128 | "desktop": "750px"
129 | }
130 | },
131 | "heading": {
132 | "family": "Open Sans",
133 | "size": {
134 | "h1": "2.2em",
135 | "h2": "1.7em",
136 | "h3": "1.35em"
137 | },
138 | "lineHeight": {
139 | "h1": 1.1,
140 | "h2": 1.1,
141 | "h3": 1.1
142 | },
143 | "weight": 600
144 | },
145 | "line": {
146 | "color": "#ecebea"
147 | },
148 | "icon": {
149 | "color": "#2c3e50"
150 | },
151 | "hero": {
152 | "h1": {
153 | "size": "2.8em",
154 | "color": "#ffffff",
155 | "lineHeight": 1.1
156 | },
157 | "background": "linear-gradient(0deg, #E0306E, #6438B5)"
158 | },
159 | "blog": {
160 | "h1": {
161 | "size": "1.7em",
162 | "lineHeight": 1.1,
163 | "hoverColor": "#2c3e50"
164 | }
165 | },
166 | "header": {
167 | "height": {
168 | "default": "80px",
169 | "fixed": "50px;",
170 | "homepage": "100px;"
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/utils/algolia.js:
--------------------------------------------------------------------------------
1 | module.exports = function (chunksTotal, { node }) {
2 | const {
3 | fields: { slug },
4 | frontmatter: { title, tags },
5 | internal: { content },
6 | } = node
7 |
8 | const noEmojiContent = content.replace(/ /g, '')
9 |
10 | const contentChunks = chunkString(noEmojiContent, 10000)
11 | const record = { title, tags, slug, content }
12 | const recordChunks = contentChunks.reduce((recordChunksTotal, contentChunksItem, idx) => {
13 | return [
14 | ...recordChunksTotal,
15 | { ...record, ...{ content: contentChunksItem }, objectID: `${slug}${idx}` },
16 | ]
17 | }, [])
18 |
19 | return [...chunksTotal, ...recordChunks]
20 | }
21 |
22 | function chunkString(str, length) {
23 | return str.match(new RegExp('(.|[\r\n]){1,' + length + '}', 'g'))
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/helpers.js:
--------------------------------------------------------------------------------
1 | export function getScreenWidth() {
2 | if (typeof window !== `undefined`) {
3 | return window.innerWidth
4 | }
5 | }
6 |
7 | export function isWideScreen() {
8 | if (typeof window !== `undefined`) {
9 | const windowWidth = window.innerWidth
10 | const mediaQueryL = 1024
11 |
12 | return windowWidth >= mediaQueryL
13 | }
14 | }
15 |
16 | export function timeoutThrottlerHandler(timeouts, name, delay, handler) {
17 | if (!timeouts[name]) {
18 | timeouts[name] = setTimeout(() => {
19 | timeouts[name] = null
20 | handler()
21 | }, delay)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/static/_headers:
--------------------------------------------------------------------------------
1 | ## Created with gatsby-plugin-netlify
2 |
3 | /*
4 | X-Frame-Options: DENY
5 | X-XSS-Protection: 1; mode=block
6 | X-Content-Type-Options: nosniff
7 | /
8 | # Cache-Control: public, max-age=0, must-revalidate
9 | /*.js
10 | # Cache-Control: public, max-age=31536000, stale-while-revalidate=2592000
11 | /static/*
12 | Cache-Control: public, max-age=31536000, stale-while-revalidate=2592000
13 | /favicon.ico
14 | # Cache-Control: public, max-age=31536000, stale-while-revalidate=2592000
15 | /icons/*
16 | # Cache-Control: public, max-age=31536000, stale-while-revalidate=2592000
17 |
18 |
19 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/favicon.ico
--------------------------------------------------------------------------------
/static/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/static/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/static/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/static/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/static/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/static/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-144x144.png
--------------------------------------------------------------------------------
/static/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-192x192.png
--------------------------------------------------------------------------------
/static/icons/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-256x256.png
--------------------------------------------------------------------------------
/static/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-384x384.png
--------------------------------------------------------------------------------
/static/icons/icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-48x48.png
--------------------------------------------------------------------------------
/static/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-512x512.png
--------------------------------------------------------------------------------
/static/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisotto6/chrisottodev/38713cee40470b40979f6c61f3734f47864efb36/static/icons/icon-96x96.png
--------------------------------------------------------------------------------
/util/compressImages.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const fs = require('fs')
3 | const glob = require('glob')
4 | const tinify = require('tinify')
5 |
6 | const fileName = 'util/registry.json'
7 | tinify.key = process.env.TINIFY_API_KEY
8 |
9 | const registrar = (array) => {
10 | let registry = JSON.parse(fs.readFileSync(fileName))
11 |
12 | array.forEach((item) => {
13 | if (!registry.done.includes(item)) {
14 | const source = tinify.fromFile(item)
15 | source.toFile(item)
16 | registry.done.push(item)
17 | }
18 | })
19 |
20 | fs.writeFileSync(fileName, JSON.stringify(registry, null, 2))
21 | }
22 |
23 | glob('content/**/*(*.png|*.jpg)', function (er, images) {
24 | if (er) {
25 | throw new Error(er)
26 | }
27 | if (images) {
28 | registrar(images)
29 | }
30 | })
31 |
32 | glob('static/**/*(*.png|*.jpg)', function (er, images) {
33 | if (er) {
34 | throw new Error(er)
35 | }
36 | if (images) {
37 | registrar(images)
38 | }
39 | })
40 |
41 | glob('src/**/*(*.png|*.jpg)', function (er, images) {
42 | if (er) {
43 | throw new Error(er)
44 | }
45 | if (images) {
46 | registrar(images)
47 | }
48 | })
49 |
--------------------------------------------------------------------------------
/util/newPost.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const date = new Date()
3 | const year = date.getFullYear()
4 | const month = ('0' + (date.getMonth() + 1)).slice(-2)
5 | const day = ('0' + date.getDate()).slice(-2)
6 | const name = process.argv[2]
7 |
8 | const newPost = `${year}-${month}-${day}--${name}`
9 |
10 | fs.mkdirSync(`content/posts/${year}/${newPost}`)
11 |
12 | const stream = fs.createWriteStream(`content/posts/${year}/${newPost}/index.mdx`)
13 |
14 | stream.once('open', () => {
15 | stream.write('---\n')
16 | stream.write('title: \n')
17 | stream.write('cover: \n')
18 | stream.write('author: Chris Otto\n')
19 | stream.write("tags: ['']\n")
20 | stream.write('published: false\n')
21 | stream.write('---\n\n')
22 | stream.write('Image courteous of []().')
23 | stream.end()
24 | })
25 |
--------------------------------------------------------------------------------
/util/registry.json:
--------------------------------------------------------------------------------
1 | {
2 | "done": [
3 | "static/icons/apple-icon-114x114.png",
4 | "static/icons/apple-icon-120x120.png",
5 | "static/icons/apple-icon-144x144.png",
6 | "static/icons/apple-icon-152x152.png",
7 | "static/icons/apple-icon-180x180.png",
8 | "static/icons/apple-icon-57x57.png",
9 | "static/icons/apple-icon-60x60.png",
10 | "static/icons/apple-icon-72x72.png",
11 | "static/icons/apple-icon-76x76.png",
12 | "static/icons/favicon-16x16.png",
13 | "static/icons/favicon-32x32.png",
14 | "static/icons/favicon-96x96.png",
15 | "static/icons/icon-144x144.png",
16 | "static/icons/icon-192x192.png",
17 | "static/icons/icon-256x256.png",
18 | "static/icons/icon-384x384.png",
19 | "static/icons/icon-48x48.png",
20 | "static/icons/icon-512x512.png",
21 | "static/icons/icon-96x96.png",
22 | "content/posts/2018-11-10--react-tutorial-adding-typescript/react-logo.png",
23 | "content/posts/2018-11-20--javascript-copyright-date/2019.jpg",
24 | "content/posts/2019-04-30--change-specflow-build/sf-logo.png",
25 | "content/posts/2020-02-08--gatsby-change-from-md-to-mdx/gatsby-mdx.png",
26 | "content/posts/2020-02-11--gatsby-create-published-filter-for-posts/gatsby-blue-green.png",
27 | "content/posts/2020-05-21--gatsby-create-an-audience-with-mailchimp/finished_form.png",
28 | "content/posts/2020-05-21--gatsby-create-an-audience-with-mailchimp/mail.jpg",
29 | "src/images/app-icons/apple-icon.png",
30 | "src/images/app-icons/icon.png",
31 | "src/images/jpg/avatar.jpg",
32 | "src/images/png/hero-background.png",
33 | "src/images/png/mesh.png",
34 | "src/images/project/cardshuffling.png",
35 | "src/images/project/chrisottodev.png",
36 | "src/images/project/fitnesseformat.png",
37 | "src/images/project/vscodefitnesse.png",
38 | "content/posts/2020-06-01--compress-your-images-with-tinify/image.jpg",
39 | "content/posts/2020-06-30--add-linting-to-create-react-app/image.jpg",
40 | "content/posts/2020-07-10--debug-gatsby-simulator/laptop.jpg",
41 | "src/images/project/library.png",
42 | "content/posts/2021-01-08--gatsby-error-monitoring-with-sentry/error_image.jpg",
43 | "content/posts/2021-01-15--react-netlify-client-side-routing/computer.jpg",
44 | "content/posts/2021-01-21--ssh-synology-nas/synology.png",
45 | "content/posts/2021-01-26--portainer-docker-nas/shipyard.jpg",
46 | "content/posts/2021-01-25--portainer-docker-nas/shipyard.jpg",
47 | "content/posts/2021-01-31--pihole-docker-nas/pi-hole.png",
48 | "content/posts/2021-02-05--calibre-library-docker-nas/library.jpg"
49 | ]
50 | }
--------------------------------------------------------------------------------