Merge pull request #11 from LordMathis/gatsby
Port website to Gatsby.js
This commit is contained in:
commit
cff36a2a18
|
@ -1,7 +1,72 @@
|
|||
node_modules
|
||||
public
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
content
|
||||
data.json
|
||||
build
|
||||
config.json
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variable files
|
||||
.env*
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Custom
|
||||
/content
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": false
|
||||
}
|
20
Dockerfile
20
Dockerfile
|
@ -1,20 +0,0 @@
|
|||
# Stage 1 - build app
|
||||
FROM node:12 as build-deps
|
||||
WORKDIR /usr/src/app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn
|
||||
COPY . ./
|
||||
RUN yarn build
|
||||
|
||||
# Stage 2 - run
|
||||
FROM node:12-alpine
|
||||
WORKDIR /app
|
||||
COPY --chown=node:node --from=build-deps /usr/src/app/build /app/build
|
||||
COPY --chown=node:node --from=build-deps /usr/src/app/public /app/public
|
||||
COPY --chown=node:node --from=build-deps /usr/src/app/node_modules /app/node_modules
|
||||
VOLUME /app/config
|
||||
VOLUME /app/content
|
||||
RUN chown node:node /app
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
CMD [ "node", "build/server.js" ]
|
|
@ -0,0 +1,14 @@
|
|||
The BSD Zero Clause License (0BSD)
|
||||
|
||||
Copyright (c) 2020 Gatsby Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
21
LICENSE.txt
21
LICENSE.txt
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Matúš Námešný
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
113
README.md
113
README.md
|
@ -1,14 +1,99 @@
|
|||
# Personal Website
|
||||
|
||||
This is the source code for my website [namesny.com](https://namesny.com).
|
||||
|
||||
This website is built with React and is open source.
|
||||
|
||||
## To do
|
||||
|
||||
* ~~implement draft support~~
|
||||
* add cv/resume section
|
||||
* support comments
|
||||
* live reload
|
||||
* ~~eliminate most hardcoded things (move them to config file)~~
|
||||
* ~~remove unnecessary code and overall code cleanup~~
|
||||
<!-- AUTO-GENERATED-CONTENT:START (STARTER) -->
|
||||
<p align="center">
|
||||
<a href="https://www.gatsbyjs.com">
|
||||
<img alt="Gatsby" src="https://www.gatsbyjs.com/Gatsby-Monogram.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Gatsby's default starter
|
||||
</h1>
|
||||
|
||||
Kick off your project with this default boilerplate. This starter ships with the main Gatsby configuration files you might need to get up and running blazing fast with the blazing fast app generator for React.
|
||||
|
||||
_Have another more specific idea? You may want to check out our vibrant collection of [official and community-created starters](https://www.gatsbyjs.com/docs/gatsby-starters/)._
|
||||
|
||||
## 🚀 Quick start
|
||||
|
||||
1. **Create a Gatsby site.**
|
||||
|
||||
Use the Gatsby CLI to create a new site, specifying the default starter.
|
||||
|
||||
```shell
|
||||
# create a new Gatsby site using the default starter
|
||||
gatsby new my-default-starter https://github.com/gatsbyjs/gatsby-starter-default
|
||||
```
|
||||
|
||||
1. **Start developing.**
|
||||
|
||||
Navigate into your new site’s directory and start it up.
|
||||
|
||||
```shell
|
||||
cd my-default-starter/
|
||||
gatsby develop
|
||||
```
|
||||
|
||||
1. **Open the source code and start editing!**
|
||||
|
||||
Your site is now running at `http://localhost:8000`!
|
||||
|
||||
_Note: You'll also see a second link: _`http://localhost:8000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.com/tutorial/part-five/#introducing-graphiql)._
|
||||
|
||||
Open the `my-default-starter` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time!
|
||||
|
||||
## 🧐 What's inside?
|
||||
|
||||
A quick look at the top-level files and directories you'll see in a Gatsby project.
|
||||
|
||||
.
|
||||
├── node_modules
|
||||
├── src
|
||||
├── .gitignore
|
||||
├── .prettierrc
|
||||
├── gatsby-browser.js
|
||||
├── gatsby-config.js
|
||||
├── gatsby-node.js
|
||||
├── gatsby-ssr.js
|
||||
├── LICENSE
|
||||
├── package-lock.json
|
||||
├── package.json
|
||||
└── README.md
|
||||
|
||||
1. **`/node_modules`**: This directory contains all of the modules of code that your project depends on (npm packages) are automatically installed.
|
||||
|
||||
2. **`/src`**: This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for “source code”.
|
||||
|
||||
3. **`.gitignore`**: This file tells git which files it should not track / not maintain a version history for.
|
||||
|
||||
4. **`.prettierrc`**: This is a configuration file for [Prettier](https://prettier.io/). Prettier is a tool to help keep the formatting of your code consistent.
|
||||
|
||||
5. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.com/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
|
||||
|
||||
6. **`gatsby-config.js`**: This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins you’d like to include, etc. (Check out the [config docs](https://www.gatsbyjs.com/docs/gatsby-config/) for more detail).
|
||||
|
||||
7. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.com/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
|
||||
|
||||
8. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.com/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
|
||||
|
||||
9. **`LICENSE`**: This Gatsby starter is licensed under the 0BSD license. This means that you can see this file as a placeholder and replace it with your own license.
|
||||
|
||||
10. **`package-lock.json`** (See `package.json` below, first). This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(You won’t change this file directly).**
|
||||
|
||||
11. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the project’s name, author, etc). This manifest is how npm knows which packages to install for your project.
|
||||
|
||||
12. **`README.md`**: A text file containing useful reference information about your project.
|
||||
|
||||
## 🎓 Learning Gatsby
|
||||
|
||||
Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.com/). Here are some places to start:
|
||||
|
||||
- **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.com/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
|
||||
|
||||
- **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.com/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
|
||||
|
||||
## 💫 Deploy
|
||||
|
||||
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/gatsbyjs/gatsby-starter-default)
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/gatsbyjs/gatsby-starter-default)
|
||||
|
||||
<!-- AUTO-GENERATED-CONTENT:END -->
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
module.exports = function (api) {
|
||||
const presets = [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react'
|
||||
]
|
||||
const plugins = [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-transform-template-literals',
|
||||
'@babel/plugin-proposal-class-properties'
|
||||
]
|
||||
|
||||
if (api.env() === 'development') {
|
||||
plugins.push([
|
||||
'css-modules-transform', {
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||
preprocessCss: processSass,
|
||||
extensions: ['.css', '.scss']
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
return {
|
||||
presets,
|
||||
plugins
|
||||
}
|
||||
}
|
||||
|
||||
var sass = require('node-sass')
|
||||
|
||||
function processSass (data, filename) {
|
||||
var result
|
||||
result = sass.renderSync({
|
||||
data: data,
|
||||
file: filename
|
||||
}).css
|
||||
return result.toString('utf8')
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"title": "John Doe - Portfolio",
|
||||
"name": "John Doe",
|
||||
"email": "john.doe@example.com",
|
||||
"social": {
|
||||
"github": "github_url",
|
||||
"twitter": "twitter_url",
|
||||
"linkedin": "linkedin_url"
|
||||
},
|
||||
"baseUrl": "example.com",
|
||||
"contentPath": "./content",
|
||||
"storage": "file",
|
||||
"specialFiles": [
|
||||
"about.md", "resume.md"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"scripts": [
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Implement Gatsby's Browser APIs in this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/browser-apis/
|
||||
*/
|
||||
|
||||
// You can delete this file if you're not using it
|
|
@ -0,0 +1,76 @@
|
|||
module.exports = {
|
||||
siteMetadata: {
|
||||
author: `Matúš Námešný`,
|
||||
user: "hello",
|
||||
hostname: "namesny.com",
|
||||
email: "matus@namesny.com",
|
||||
social: [
|
||||
{
|
||||
name: "github",
|
||||
link: "https://github.com/LordMathis",
|
||||
},
|
||||
{
|
||||
name: "codepen",
|
||||
link: "https://codepen.io/LordMathis/",
|
||||
},
|
||||
{
|
||||
name: "linkedin",
|
||||
link: "https://www.linkedin.com/in/mat%C3%BA%C5%A1-n%C3%A1me%C5%A1n%C3%BD-3903b6128/",
|
||||
}
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: `pages`,
|
||||
path: `${__dirname}/content/pages`,
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-transformer-remark`,
|
||||
options: {
|
||||
filter: node => node.sourceInstanceName === `pages`,
|
||||
type: `MarkdownPage`
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: `posts`,
|
||||
path: `${__dirname}/content/posts/`,
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-transformer-remark`,
|
||||
options: {
|
||||
filter: node => node.sourceInstanceName === `posts`,
|
||||
excerpt_separator: `<!-- end -->`,
|
||||
type: `BlogPost`
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: `images`,
|
||||
path: `${__dirname}/src/images`,
|
||||
},
|
||||
},
|
||||
`gatsby-transformer-sharp`,
|
||||
`gatsby-plugin-sharp`,
|
||||
`gatsby-plugin-react-helmet`,
|
||||
{
|
||||
resolve: `gatsby-plugin-manifest`,
|
||||
options: {
|
||||
name: `gatsby-starter-default`,
|
||||
short_name: `starter`,
|
||||
start_url: `/`,
|
||||
background_color: `#663399`,
|
||||
theme_color: `#663399`,
|
||||
display: `minimal-ui`,
|
||||
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
|
||||
},
|
||||
},
|
||||
`gatsby-plugin-sass`,
|
||||
],
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
const path = require(`path`)
|
||||
const { createFilePath } = require(`gatsby-source-filesystem`)
|
||||
|
||||
exports.onCreateNode = ({ node, getNode, actions }) => {
|
||||
const { createNodeField } = actions
|
||||
if (node.internal.type === `MarkdownRemark`) {
|
||||
const slug = createFilePath({ node, getNode, basePath: `pages` })
|
||||
const parent = getNode(node.parent)
|
||||
createNodeField({
|
||||
node,
|
||||
name: `slug`,
|
||||
value: slug,
|
||||
})
|
||||
createNodeField({
|
||||
node,
|
||||
name: 'collection',
|
||||
value: parent.sourceInstanceName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
const { createPage } = actions
|
||||
|
||||
// Posts query
|
||||
const posts = await graphql(
|
||||
`
|
||||
{
|
||||
allMarkdownRemark(filter:{frontmatter: {draft: {ne: true}}, fields: {collection: {eq: "posts"}}}) {
|
||||
edges {
|
||||
node {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// Handle errors
|
||||
if (posts.errors) {
|
||||
reporter.panicOnBuild(`Error while running GraphQL query.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Create pages for each markdown file.
|
||||
const blogPostTemplate = path.resolve(`src/templates/blog-post.js`)
|
||||
posts.data.allMarkdownRemark.edges.forEach(({ node }) => {
|
||||
createPage({
|
||||
path: `/posts${node.fields.slug}`,
|
||||
component: blogPostTemplate,
|
||||
context: {
|
||||
slug: node.fields.slug,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const pages = await graphql(
|
||||
`
|
||||
{
|
||||
allMarkdownRemark(filter: {fields: {collection: {eq: "pages"}}}) {
|
||||
edges {
|
||||
node {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// Handle errors
|
||||
if (pages.errors) {
|
||||
reporter.panicOnBuild(`Error while running GraphQL query.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Create pages for each markdown file.
|
||||
const pageTemplate = path.resolve(`src/templates/page.js`)
|
||||
pages.data.allMarkdownRemark.edges.forEach(({ node }) => {
|
||||
createPage({
|
||||
path: node.fields.slug,
|
||||
component: pageTemplate,
|
||||
context: {
|
||||
slug: node.fields.slug,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.org/docs/ssr-apis/
|
||||
*/
|
||||
|
||||
// You can delete this file if you're not using it
|
117
package.json
117
package.json
|
@ -1,84 +1,47 @@
|
|||
{
|
||||
"name": "portfolio",
|
||||
"version": "2.0.0",
|
||||
"description": "portfolio",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npm run build-client && npm run build-server",
|
||||
"build-client": "NODE_ENV=production webpack --config webpack.client.js -p --progress",
|
||||
"build-server": "NODE_ENV=production webpack --config webpack.server.js -p --progress",
|
||||
"start": "NODE_ENV=production node ./build/server.js",
|
||||
"start-dev": "NODE_ENV=development nodemon --exec babel-node ./src/server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"porfolio",
|
||||
"react"
|
||||
],
|
||||
"author": "Matúš Námešný",
|
||||
"license": "MIT",
|
||||
"name": "gatsby-starter-default",
|
||||
"private": true,
|
||||
"description": "A simple starter to get up and developing quickly with Gatsby",
|
||||
"version": "0.1.0",
|
||||
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.2",
|
||||
"async": "^3.1.0",
|
||||
"axios": "^0.19.1",
|
||||
"chokidar": "^3.3.0",
|
||||
"express": "^4.13.4",
|
||||
"express-static-gzip": "^2.0.5",
|
||||
"front-matter": "^3.0.2",
|
||||
"helmet": "^3.21.1",
|
||||
"jsonfile": "^5.0.0",
|
||||
"markdown-it": "^10.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.7.14",
|
||||
"morgan": "^1.9.1",
|
||||
"node-sass": "^4.9.0",
|
||||
"gatsby": "^2.24.61",
|
||||
"gatsby-image": "^2.4.19",
|
||||
"gatsby-plugin-manifest": "^2.4.30",
|
||||
"gatsby-plugin-offline": "^3.2.28",
|
||||
"gatsby-plugin-react-helmet": "^3.3.11",
|
||||
"gatsby-plugin-sass": "^2.3.13",
|
||||
"gatsby-plugin-sharp": "^2.6.36",
|
||||
"gatsby-source-filesystem": "^2.3.30",
|
||||
"gatsby-transformer-remark": "^2.8.35",
|
||||
"gatsby-transformer-sharp": "^2.5.15",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-jss": "^10.0.0",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"serialize-javascript": "^3.1.0",
|
||||
"yargs": "^15.3.1"
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.3.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
|
||||
"@babel/plugin-transform-runtime": "^7.2.0",
|
||||
"@babel/plugin-transform-template-literals": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.3",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-css-modules-transform": "^1.6.2",
|
||||
"compression-webpack-plugin": "^3.0.0",
|
||||
"create-file-webpack": "^1.0.2",
|
||||
"css-loader": "^3.2.0",
|
||||
"css-modules-require-hook": "^4.2.3",
|
||||
"eslint": "^6.4.0",
|
||||
"eslint-config-standard": "^14.1.0",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-node": "^10.0.0",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"nodemon": "^2.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"uglifyjs-webpack-plugin": "^2.1.3",
|
||||
"url-loader": "^2.1.0",
|
||||
"webpack": "^4.7.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^3.3.4",
|
||||
"webpack-dev-middleware": "^3.1.3",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-manifest-plugin": "^2.0.4",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
"prettier": "2.1.1"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"license": "0BSD",
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
|
||||
"start": "npm run develop",
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gatsbyjs/gatsby-starter-default"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/gatsbyjs/gatsby/issues"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
module.exports = {
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
import { hydrate } from 'react-dom'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import { App } from './components'
|
||||
|
||||
hydrate(
|
||||
<Router>
|
||||
<App data={window.__INITIAL_DATA__}/>
|
||||
</Router>,
|
||||
document.getElementById('root')
|
||||
)
|
|
@ -1,34 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { Spinner, Header } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
export default class About extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
about: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const md = MarkdownIt()
|
||||
const result = md.render(this.props.about)
|
||||
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<div className={contentStyle.contentWrapper} id="about">
|
||||
<Spinner/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={contentStyle.content} role="region" aria-label="About me">
|
||||
<Header header={'About Me'} role="heading" aria-level="2"/>
|
||||
<div dangerouslySetInnerHTML={{ __html: result }} role="article">
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
|
@ -1,26 +0,0 @@
|
|||
import { NotFoundContainer } from '../containers'
|
||||
import React, { Component } from 'react'
|
||||
import routes from '../utils/routes'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
|
||||
export default class App extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
{routes.map(({ path, exact, component: C, ...rest }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
exact={exact}
|
||||
render={(props) => (
|
||||
<C {...props} {...rest} />
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Route render={(props) => <NotFoundContainer {...props} />} />
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { Spinner, Header, SearchBox } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import styles from './Blog.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
|
||||
export default class Blog extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
posts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
searchString: PropTypes.string,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleFocus: PropTypes.func.isRequired,
|
||||
handleBlur: PropTypes.func.isRequired,
|
||||
handleEnter: PropTypes.func.isRequired,
|
||||
handleSearch: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const md = MarkdownIt()
|
||||
let postsHTML
|
||||
|
||||
if (this.props.isLoading) {
|
||||
postsHTML = <Spinner />
|
||||
} else {
|
||||
const posts = this.props.posts.sort((a, b) => {
|
||||
return new Date(b.published) - new Date(a.published)
|
||||
})
|
||||
|
||||
if (posts.length < 1) {
|
||||
postsHTML = (
|
||||
<div>
|
||||
<span>No posts found</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
postsHTML = posts.map((post) =>
|
||||
<div key={post.title} className={styles.postListItem} role="listitem">
|
||||
<div className={styles.postHeader} >
|
||||
<a href={post.link} className={styles.postTitle}>{post.title}</a>
|
||||
<span className={styles.postDate}>{post.published}</span>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: md.render(post.summary) }}>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${contentStyle.content}`} id="blog" role="region" aria-label="Blog posts">
|
||||
<div className={styles.headerContainer}>
|
||||
<Header header={'Blog'} role="heading" aria-level="2"/>
|
||||
<SearchBox searchString={this.props.searchString}
|
||||
expanded={this.props.expanded}
|
||||
handleChange={this.props.handleChange}
|
||||
handleFocus={this.props.handleFocus}
|
||||
handleBlur={this.props.handleBlur}
|
||||
handleEnter={this.props.handleEnter}
|
||||
handleSearch={this.props.handleSearch} />
|
||||
</div>
|
||||
<div className={`${styles.postsList}`} role="list">
|
||||
{postsHTML}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.postDate {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.postTitle {
|
||||
color: $blue;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
font-family: $font-header;
|
||||
font-size: 1.2em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.postHeader {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.postsList {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.postListItem {
|
||||
border-bottom: 5px solid $body;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './Column.scss'
|
||||
|
||||
export default class Column extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired,
|
||||
left: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
left: true
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={`${styles.column} ${this.props.left ? styles.left : styles.right}`}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.left {
|
||||
flex: 30%;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 70%;
|
||||
}
|
||||
|
||||
.column {
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import '../stylesheets/globals.scss'
|
||||
import style from './Footer.scss'
|
||||
|
||||
export default class Footer extends Component {
|
||||
static propTypes = {
|
||||
config: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<footer className={style.footer}>
|
||||
<p>Copyright © {new Date().getFullYear()} { this.props.config.name }</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.footer {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './Header.scss'
|
||||
|
||||
export default class Header extends Component {
|
||||
static propTypes = {
|
||||
header: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.mainHeader}>
|
||||
<h1>{this.props.header}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.mainHeader {
|
||||
border-left: 5px solid $blue;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { SocialLinks } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './Home.scss'
|
||||
|
||||
export default class Home extends Component {
|
||||
static propTypes = {
|
||||
config: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div id={styles.coverPage} className={styles.coverPageFull} role="region" aria-label="Home page">
|
||||
<div id={styles.coverPageContent}>
|
||||
<div role="heading" aria-level="1">
|
||||
<h1 id={styles.coverPageName}><Link to="/">{ this.props.config.name }</Link></h1>
|
||||
</div>
|
||||
<SocialLinks config={ this.props.config }/>
|
||||
<div className={styles.menuLinks} role="navigation">
|
||||
<ul>
|
||||
<li key="about">
|
||||
<a href="#about">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li key="blog">
|
||||
<span>/ <a href="#blog">Blog</a></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
#coverPage {
|
||||
background: url('/static/background.jpg') no-repeat center center fixed;
|
||||
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#coverPage.coverPageFull{
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#coverPageName {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
#coverPageName a, #coverPageName a:hover {
|
||||
color: $white;
|
||||
text-shadow: $black 0px 0px 2px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.social {
|
||||
text-align: center;
|
||||
ul {
|
||||
list-style: none;
|
||||
li {
|
||||
display: inline;
|
||||
a {
|
||||
color: $white;
|
||||
text-shadow: $black 0px 0px 2px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menuLinks {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-shadow: $black 0px 0px 2px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
ul{
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding: 10px;
|
||||
|
||||
li {
|
||||
margin: 5px;
|
||||
display: inline;
|
||||
|
||||
a {
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-bottom: solid 2px $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { SocialLinks } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './Navbar.scss'
|
||||
|
||||
export default class Navbar extends Component {
|
||||
static propTypes = {
|
||||
config: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.navbar} role="navigation">
|
||||
<div className={styles.links}>
|
||||
<ul>
|
||||
<li key="index">
|
||||
<a href='/'>
|
||||
<span className={styles.nameLink}>{this.props.config.name} |</span>
|
||||
</a>
|
||||
</li>
|
||||
<li key="about">
|
||||
<a href='/#about'>
|
||||
<span>About</span>
|
||||
</a>
|
||||
</li>
|
||||
<li key="blog">
|
||||
<a href='/#blog'>
|
||||
<span>Blog</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<SocialLinks config={ this.props.config } home={ false }/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.navbar {
|
||||
background-color: $navbar;
|
||||
color: $white;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $white;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.social {
|
||||
float: right;
|
||||
vertical-align: bottom;
|
||||
a {
|
||||
color: $white;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
float: left;
|
||||
ul {
|
||||
list-style: none;
|
||||
li {
|
||||
font-family: $font-header;
|
||||
display: inline;
|
||||
margin: 5px;
|
||||
a {
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nameLink {
|
||||
font-size: 1.4em;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react'
|
||||
import { Navbar, Header } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
|
||||
const NotFoundPage = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<Navbar config={props.config}/>
|
||||
<div className={contentStyle.contentWrapper}>
|
||||
<Header header={'Uhm... WHAT?'} />
|
||||
<div className={contentStyle.content}>
|
||||
<p>Looks like you're lost</p>
|
||||
<p>404 Page not found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
|
@ -1,50 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Spinner, Header, Navbar, Wrapper, Footer } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
import styles from './Post.scss'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import fm from 'front-matter'
|
||||
import moment from 'moment'
|
||||
|
||||
export default class Post extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
post: PropTypes.string.isRequired,
|
||||
config: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const md = MarkdownIt()
|
||||
const content = fm(this.props.post)
|
||||
const title = content.attributes.title
|
||||
const date = moment(content.attributes.date, 'YYYY-MM-DD')
|
||||
const body = md.render(content.body)
|
||||
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<div className={contentStyle.contentWrapper}>
|
||||
<Spinner/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar config={this.props.config} />
|
||||
<Wrapper>
|
||||
<div className={`${contentStyle.content} ${styles.column}`}>
|
||||
<Header header={title} role="heading" aria-level="2" />
|
||||
<div className={styles.postDate}>
|
||||
<h3>{date.format('MMMM D, YYYY')}</h3>
|
||||
</div>
|
||||
<div className={styles.postContent} dangerouslySetInnerHTML={{ __html: body }} role="article">
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
<Footer config={this.props.config}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.postContent {
|
||||
clear: both;
|
||||
h1,h2,h2,h4,h5,h6 {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.postDate {
|
||||
float: right;
|
||||
h3 {
|
||||
font-weight: normal
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { Spinner, Navbar, Wrapper, Header, Footer } from '.'
|
||||
import '../stylesheets/globals.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
import style from './Resume.scss'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import fm from 'front-matter'
|
||||
|
||||
export default class About extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
resume: PropTypes.string.isRequired,
|
||||
config: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const md = MarkdownIt()
|
||||
const content = fm(this.props.resume)
|
||||
const title = content.attributes.title
|
||||
const body = md.render(content.body)
|
||||
|
||||
if (this.props.isLoading) {
|
||||
return (
|
||||
<div className={contentStyle.content}>
|
||||
<Spinner/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar config={this.props.config} />
|
||||
<Wrapper>
|
||||
<div className={`${contentStyle.content} ${style.column}`}>
|
||||
<Header header={title} role="heading" aria-level="2" />
|
||||
<div className={style.content} dangerouslySetInnerHTML={{ __html: body }} role="article">
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
<Footer config={this.props.config} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.content {
|
||||
clear: both;
|
||||
h1,h2,h2,h4,h5,h6 {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './SearchBox.scss'
|
||||
|
||||
export default class SearchBox extends Component {
|
||||
static propTypes = {
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleFocus: PropTypes.func.isRequired,
|
||||
handleBlur: PropTypes.func.isRequired,
|
||||
handleEnter: PropTypes.func.isRequired,
|
||||
handleSearch: PropTypes.func.isRequired,
|
||||
searchString: PropTypes.string,
|
||||
expanded: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<input placeholder='Search'
|
||||
className={`${styles.search} ${this.props.expanded ? styles.expanded : ''}`}
|
||||
type="text"
|
||||
value={this.props.searchString}
|
||||
onChange={this.props.handleChange}
|
||||
onFocus={this.props.handleFocus}
|
||||
onBlur={this.props.handleBlur}
|
||||
onKeyDown={this.props.handleEnter} />
|
||||
<span onClick={this.props.handleSearch} >
|
||||
<i className={`fa fa-search ${styles.icon}`}></i>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
padding: 15px 40px 15px 20px;
|
||||
width: 20px;
|
||||
color: $black;
|
||||
font-size: 16px;
|
||||
letter-spacing: 2px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: $white;
|
||||
transition: width 0.4s ease;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.expanded {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.icon{
|
||||
position: relative;
|
||||
left: -37px;
|
||||
color: $black;
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './SocialLinks.scss'
|
||||
|
||||
export default class SocialLinks extends Component {
|
||||
static propTypes = {
|
||||
config: PropTypes.object.isRequired,
|
||||
home: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
home: true
|
||||
}
|
||||
|
||||
render () {
|
||||
let key = 0
|
||||
const objKeys = Object.keys(this.props.config.social)
|
||||
|
||||
const socialLinks = objKeys.map((val) => {
|
||||
const link = (
|
||||
<li key={key}>
|
||||
<a href={this.props.config.social[val]} role="link">
|
||||
<i className={`fa fa-${val} ${this.props.home ? 'fa-3x' : ''}`} aria-hidden="true" />
|
||||
<span className="sr-only">{val}</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
key += 1
|
||||
return link
|
||||
})
|
||||
|
||||
socialLinks.push(
|
||||
<li key={key}>
|
||||
<a href={`mailto:${this.props.config.email}`} role="link">
|
||||
<i className={`fa fa-envelope-o ${this.props.home ? 'fa-3x' : ''}`} aria-hidden="true" />
|
||||
<span className="sr-only">e-mail</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
|
||||
const className = this.props.home ? styles['social-home'] : styles['social-navbar']
|
||||
|
||||
return (
|
||||
<div className={className} role="list">
|
||||
<ul>
|
||||
{socialLinks}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.social-home {
|
||||
text-align: center;
|
||||
ul {
|
||||
list-style: none;
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-navbar {
|
||||
float: right;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.social-home, .social-navbar {
|
||||
ul {
|
||||
list-style: none;
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: $white;
|
||||
text-shadow: $black 0px 0px 2px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import '../stylesheets/globals.scss'
|
||||
import styles from './Spinner.scss'
|
||||
|
||||
export default class Spinner extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.spinnerWrapper}>
|
||||
<div className={styles.ldsEllipsis}>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
from https://loading.io/css/
|
||||
*/
|
||||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.spinnerWrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ldsEllipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ldsEllipsis div {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: $black;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.ldsEllipsis div:nth-child(1) {
|
||||
left: 6px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
.ldsEllipsis div:nth-child(2) {
|
||||
left: 6px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.ldsEllipsis div:nth-child(3) {
|
||||
left: 26px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.ldsEllipsis div:nth-child(4) {
|
||||
left: 45px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(19px, 0);
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import '../stylesheets/globals.scss'
|
||||
import contentStyle from '../stylesheets/content.scss'
|
||||
import styles from './Wrapper.scss'
|
||||
|
||||
export default class Wrapper extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired,
|
||||
flex: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
flex: false
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={` ${contentStyle.contentWrapper} ${styles.centerContent} ${this.props.flex ? styles.flex : styles.noFlex}` } role='main'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
@import "../stylesheets/variables.scss";
|
||||
|
||||
.centerContent {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.flex {
|
||||
@media only screen and (min-width: $break-large) {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.noFlex {
|
||||
@media (min-width: $break-large) {
|
||||
width: 960px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react"
|
||||
import PostLink from "./post-link"
|
||||
import styles from "../styles/blog.module.scss"
|
||||
|
||||
const Blog = ({ edges }) => {
|
||||
const Posts = edges
|
||||
.map(edge => <PostLink key={edge.node.id} post={edge.node} />)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
<div>
|
||||
{Posts}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blog
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react"
|
||||
import styles from '../styles/footer.module.scss'
|
||||
|
||||
const Footer = ({authorName}) => (
|
||||
<footer className={styles.footer}>
|
||||
© {new Date().getFullYear()} {authorName}, Built with
|
||||
{` `}
|
||||
<a href="https://www.gatsbyjs.org" className={styles.link}>Gatsby</a>
|
||||
</footer>
|
||||
)
|
||||
|
||||
export default Footer
|
|
@ -0,0 +1,35 @@
|
|||
import { Link } from "gatsby"
|
||||
import PropTypes from "prop-types"
|
||||
import React from "react"
|
||||
import styles from "../styles/header.module.scss"
|
||||
|
||||
const Header = ({ user, hostname }) => (
|
||||
<header className={styles.headerWrapper}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<Link to="/" className={styles.terminal}>{user}@{hostname} ~ $</Link>
|
||||
</div>
|
||||
<nav className={styles.links}>
|
||||
<ul>
|
||||
<li key="about">
|
||||
<a href='/about'>
|
||||
<span>~/about</span>
|
||||
</a>
|
||||
</li>
|
||||
<li key="blog">
|
||||
<a href='/blog'>
|
||||
<span>~/blog</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
user: PropTypes.string.isRequired,
|
||||
hostname: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default Header
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
import Img from "gatsby-image"
|
||||
|
||||
/*
|
||||
* This component is built using `gatsby-image` to automatically serve optimized
|
||||
* images with lazy loading and reduced file sizes. The image is loaded using a
|
||||
* `useStaticQuery`, which allows us to load the image from directly within this
|
||||
* component, rather than having to pass the image data down from pages.
|
||||
*
|
||||
* For more information, see the docs:
|
||||
* - `gatsby-image`: https://gatsby.dev/gatsby-image
|
||||
* - `useStaticQuery`: https://www.gatsbyjs.org/docs/use-static-query/
|
||||
*/
|
||||
|
||||
const Image = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 300) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
|
||||
}
|
||||
|
||||
export default Image
|
|
@ -1,15 +1,19 @@
|
|||
export { default as Home } from './Home'
|
||||
export { default as Blog } from './Blog'
|
||||
export { default as About } from './About'
|
||||
export { default as Post } from './Post'
|
||||
export { default as NotFoundPage } from './NotFoundPage'
|
||||
export { default as Spinner } from './Spinner'
|
||||
export { default as Header } from './Header'
|
||||
export { default as Wrapper } from './Wrapper'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as App } from './App'
|
||||
export { default as SocialLinks } from './SocialLinks'
|
||||
export { default as Column } from './Column'
|
||||
export { default as Resume } from './Resume'
|
||||
export { default as Footer } from './Footer'
|
||||
export { default as SearchBox } from './SearchBox'
|
||||
import { Link } from "gatsby"
|
||||
import PropTypes from "prop-types"
|
||||
import React from "react"
|
||||
import styles from "../styles/index.module.scss"
|
||||
import Social from "./social"
|
||||
|
||||
const Index = ({ author, social, email }) => (
|
||||
<div className={styles.indexWrapper}>
|
||||
<div>
|
||||
<h1 className={styles.header}>{ author }</h1>
|
||||
</div>
|
||||
<Social social={social} email={email}/>
|
||||
</div>
|
||||
)
|
||||
|
||||
Index.propTypes = {
|
||||
}
|
||||
|
||||
export default Index
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
import Header from "./header"
|
||||
import Footer from "./footer"
|
||||
import { Helmet } from "react-helmet"
|
||||
import styles from "../styles/layout.module.scss"
|
||||
|
||||
const Layout = ({ children, title, vertical}) => {
|
||||
|
||||
const data = useStaticQuery(graphql`
|
||||
query SiteTitleQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
author
|
||||
user
|
||||
hostname
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
const classes = vertical ? `${styles.content} ${styles.vertical}` : styles.content
|
||||
|
||||
return (
|
||||
<div className={styles.flexWrapper}>
|
||||
<Helmet
|
||||
titleTemplate={`%s | ${data.site.siteMetadata.author}`}>
|
||||
<html lang="en" amp />
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
<Header
|
||||
user={data.site.siteMetadata.user}
|
||||
hostname={data.site.siteMetadata.hostname} />
|
||||
<div className={classes}>
|
||||
<main className={styles.main}>{children}</main>
|
||||
</div>
|
||||
<Footer authorName={data.site.siteMetadata.author}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
vertical: PropTypes.bool
|
||||
}
|
||||
|
||||
export default Layout
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react"
|
||||
import { Link } from "gatsby"
|
||||
import styles from '../styles/post-link.module.scss'
|
||||
|
||||
const PostLink = ({ post }) => {
|
||||
|
||||
const postDate = new Date(post.frontmatter.date)
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const postDateString = postDate.toLocaleDateString('en', options);
|
||||
const postUrl = "/posts" + post.fields.slug
|
||||
|
||||
return (
|
||||
<Link to={postUrl}>
|
||||
<div className={styles.postListItem} role="listitem">
|
||||
<div className={styles.postHeader} >
|
||||
<span className={styles.postTitle}>{post.frontmatter.title}</span>
|
||||
<span className={styles.postDate}>{postDateString}</span>
|
||||
</div>
|
||||
<div className={styles.postExcerpt}>
|
||||
<p>{post.excerpt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default PostLink
|
|
@ -0,0 +1,43 @@
|
|||
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styles from '../styles/social.module.scss'
|
||||
|
||||
export default class Social extends Component {
|
||||
static propTypes = {
|
||||
social: PropTypes.arrayOf(PropTypes.object),
|
||||
email: PropTypes.string
|
||||
}
|
||||
|
||||
render () {
|
||||
let key = 0
|
||||
|
||||
const socialLinks = this.props.social.map((val) => {
|
||||
const link = (
|
||||
<li key={key}>
|
||||
<a href={val.link} role="link">
|
||||
{val.name}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
key += 1
|
||||
return link
|
||||
})
|
||||
|
||||
socialLinks.push(
|
||||
<li key={key}>
|
||||
<a href={`mailto:${this.props.email}`} role="link">
|
||||
e-mail
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.socialNavbar} role="list">
|
||||
<ul>
|
||||
{socialLinks}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import { Blog } from '../components'
|
||||
import PropTypes from 'prop-types'
|
||||
import axios from 'axios'
|
||||
import '../stylesheets/globals.scss'
|
||||
|
||||
export default class BlogContainer extends Component {
|
||||
static propTypes = {
|
||||
posts: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
posts: props.posts,
|
||||
searchString: '',
|
||||
expanded: false
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (event) {
|
||||
this.setState({ searchString: event.target.value })
|
||||
}
|
||||
|
||||
handleFocus () {
|
||||
this.setState({ expanded: true })
|
||||
}
|
||||
|
||||
handleBlur () {
|
||||
this.setState({ expanded: false })
|
||||
}
|
||||
|
||||
handleEnter (event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.handleSearch()
|
||||
}
|
||||
}
|
||||
|
||||
handleSearch () {
|
||||
if (this.state.expanded && this.state.searchString) {
|
||||
this.search()
|
||||
} else {
|
||||
this.setState({ expanded: true })
|
||||
}
|
||||
}
|
||||
|
||||
search () {
|
||||
this.setState({
|
||||
isLoading: true
|
||||
}, () => {
|
||||
axios.get(`/api/v1/posts?search=${this.state.searchString}`)
|
||||
.then((data) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
posts: data.data
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Blog isLoading={ this.state.isLoading }
|
||||
posts={ this.state.posts }
|
||||
searchString={this.state.searchString}
|
||||
expanded={this.state.expanded}
|
||||
handleChange={this.handleChange.bind(this)}
|
||||
handleFocus={this.handleFocus.bind(this)}
|
||||
handleBlur={this.handleBlur.bind(this)}
|
||||
handleEnter={this.handleEnter.bind(this)}
|
||||
handleSearch={this.handleSearch.bind(this)} />
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Post, Resume } from '../components'
|
||||
|
||||
export default class ContentContainer extends Component {
|
||||
static propTypes = {
|
||||
staticContext: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let data
|
||||
if (typeof window === 'undefined') {
|
||||
data = props.staticContext.context
|
||||
} else {
|
||||
data = window.__INITIAL_DATA__
|
||||
delete window.__INITIAL_DATA__
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isLoading: !data,
|
||||
type: data[0].type,
|
||||
content: data[0].data,
|
||||
config: data[1]
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.state.type === 'resume') {
|
||||
return (
|
||||
<Resume
|
||||
isLoading={this.state.isLoading}
|
||||
resume={this.state.content}
|
||||
config={this.state.config} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Post
|
||||
isLoading={this.state.isLoading}
|
||||
post={this.state.content}
|
||||
config={this.state.config} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { About, Home, Wrapper, Column, Footer } from '../components'
|
||||
import { BlogContainer } from '.'
|
||||
|
||||
export default class MainContainer extends Component {
|
||||
static propTypes = {
|
||||
staticContext: PropTypes.object
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let data
|
||||
if (typeof window === 'undefined') {
|
||||
data = props.staticContext.context
|
||||
} else {
|
||||
data = window.__INITIAL_DATA__
|
||||
delete window.__INITIAL_DATA__
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isLoadingBlog: !data[0].posts,
|
||||
isLoadingAbout: !data[0].other.about,
|
||||
about: data[0].other.about,
|
||||
posts: data[0].posts,
|
||||
config: data[1]
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<Home config={this.state.config} />
|
||||
<Wrapper flex={true}>
|
||||
<Column>
|
||||
<About isLoading={this.state.isLoadingAbout}
|
||||
about={this.state.about}/>
|
||||
</Column>
|
||||
<Column left={false}>
|
||||
<BlogContainer posts={this.state.posts}/>
|
||||
</Column>
|
||||
</Wrapper>
|
||||
<Footer config={this.state.config} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import { Wrapper, NotFoundPage } from '../components'
|
||||
import PropTypes from 'prop-types'
|
||||
import '../stylesheets/globals.scss'
|
||||
|
||||
export default class NotFoundContainer extends Component {
|
||||
static propTypes = {
|
||||
staticContext: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
let data
|
||||
if (typeof window === 'undefined') {
|
||||
data = props.staticContext.context
|
||||
} else {
|
||||
data = window.__INITIAL_DATA__
|
||||
delete window.__INITIAL_DATA__
|
||||
}
|
||||
|
||||
this.state = {
|
||||
config: data[1]
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Wrapper>
|
||||
<NotFoundPage config={this.state.config}/>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export { default as MainContainer } from './MainContainer'
|
||||
export { default as ContentContainer } from './ContentContainer'
|
||||
export { default as NotFoundContainer } from './NotFoundContainer'
|
||||
export { default as BlogContainer } from './BlogContainer'
|
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,12 @@
|
|||
import React from "react"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout title="404: Not found" >
|
||||
<h1>NOT FOUND</h1>
|
||||
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFoundPage
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import Blog from "../components/blog"
|
||||
|
||||
import "../styles/global.scss"
|
||||
|
||||
const IndexPage = () => {
|
||||
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
allMarkdownRemark(
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: {frontmatter: {draft: {ne: true}}, fields: {collection: {eq: "posts"}}}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
excerpt
|
||||
frontmatter {
|
||||
date
|
||||
title
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<Layout title="Blog">
|
||||
<Blog edges={data.allMarkdownRemark.edges}/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPage
|
|
@ -0,0 +1,33 @@
|
|||
import React from "react"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
|
||||
import "../styles/global.scss"
|
||||
import Index from "../components"
|
||||
|
||||
const IndexPage = () => {
|
||||
|
||||
const data = useStaticQuery(graphql`
|
||||
query SiteDataQuery {
|
||||
site {
|
||||
siteMetadata {
|
||||
author
|
||||
email
|
||||
social {
|
||||
name
|
||||
link
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<Layout title="Home" vertical={true} >
|
||||
<Index author={data.site.siteMetadata.author} social={data.site.siteMetadata.social} email={data.site.siteMetadata.email}/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPage
|
|
@ -1,91 +0,0 @@
|
|||
import express from 'express'
|
||||
import helmet from 'helmet'
|
||||
import expressStaticGzip from 'express-static-gzip'
|
||||
import path from 'path'
|
||||
import morgan from 'morgan'
|
||||
import mongoose from 'mongoose'
|
||||
import jsonfile from 'jsonfile'
|
||||
import { ServerRenderer } from './utils/serverRender'
|
||||
import { Scanner } from './utils/scanner'
|
||||
import { FileStorage } from './utils/storage/file'
|
||||
import { MongoStorage } from './utils/storage/mongo'
|
||||
import { Config } from './utils/config'
|
||||
import { Api } from './utils/api'
|
||||
|
||||
const configPath = process.argv[2] || path.join(process.cwd(), 'config/config.json')
|
||||
|
||||
const app = express()
|
||||
app.set('trust proxy', true)
|
||||
|
||||
const config = new Config(jsonfile.readFileSync(configPath))
|
||||
if (config == null) {
|
||||
throw new Error('Config file not found!')
|
||||
}
|
||||
const port = config.port || 3000
|
||||
|
||||
app.use(morgan('common'))
|
||||
|
||||
app.use(helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'", `*.${config.baseUrl}`],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", `*.${config.baseUrl}`],
|
||||
styleSrc: ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com', 'maxcdn.bootstrapcdn.com'],
|
||||
fontSrc: ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com', 'maxcdn.bootstrapcdn.com'],
|
||||
imgSrc: ['*'],
|
||||
workerSrc: false,
|
||||
blockAllMixedContent: true
|
||||
}
|
||||
}))
|
||||
|
||||
app.use('/static', expressStaticGzip('public/static'))
|
||||
|
||||
app.get('/favicon.ico', (req, res) => {
|
||||
res.status(204).send('Not Found !!!')
|
||||
})
|
||||
|
||||
let head = jsonfile.readFileSync(path.join(process.cwd(), 'config/head.json'))
|
||||
if (head == null) {
|
||||
head = {
|
||||
scripts: []
|
||||
}
|
||||
}
|
||||
|
||||
let storage
|
||||
if (config.storage === 'file') {
|
||||
storage = new FileStorage(config)
|
||||
} else if (config.storage === 'mongo') {
|
||||
storage = new MongoStorage(config)
|
||||
}
|
||||
|
||||
if (config.storage === 'mongo') {
|
||||
const postApi = new Api(storage)
|
||||
app.get('/api/v1/posts', postApi.getPosts.bind(postApi))
|
||||
}
|
||||
|
||||
const scanner = new Scanner(config, storage)
|
||||
|
||||
const serverRenderer = new ServerRenderer(head, config, storage)
|
||||
app.get('*', serverRenderer.render.bind(serverRenderer))
|
||||
|
||||
if (config.storage === 'mongo') {
|
||||
mongoose.connect(config.mongoUrl, { useNewUrlParser: true })
|
||||
const db = mongoose.connection
|
||||
db.on('error', (error) => console.error(`[Server] Unable to connect to database\n${error}`))
|
||||
db.once('open', () => {
|
||||
console.log('[Server] Connected to database')
|
||||
startServer()
|
||||
})
|
||||
} else {
|
||||
startServer()
|
||||
}
|
||||
|
||||
function startServer () {
|
||||
scanner.watch()
|
||||
app.listen(port, function (error) {
|
||||
if (error) {
|
||||
console.error(`[Server] Unable to start server\n${error}`)
|
||||
} else {
|
||||
console.info(`[Server] Listening on port ${port}`)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.blogPostWrapper {
|
||||
text-align: left;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.header {
|
||||
text-align: left;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.footer {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
background-color: $backgroundDarker;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $white;
|
||||
}
|
|
@ -5,18 +5,21 @@
|
|||
}
|
||||
|
||||
:global(body) {
|
||||
font-family: $font-paragraph;
|
||||
color: $black;
|
||||
background-color: $body;
|
||||
font-family: $fontParagraph;
|
||||
color: $white;
|
||||
background-color: $backgroundDark;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@for $i from 1 through 6 {
|
||||
:global(h#{$i}) {
|
||||
font-family: $font-header;
|
||||
font-family: $fontHeader;
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-viewport{
|
||||
width: device-width;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.header {
|
||||
font-family: $fontHeader;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media only screen and (min-width: $breakLarge) {
|
||||
width: $width;
|
||||
}
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: $backgroundDarker;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.links {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li {
|
||||
display: inline;
|
||||
margin: 5px;
|
||||
a {
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal {
|
||||
color: $white;
|
||||
text-decoration: none;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.indexWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 3em;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
flex: 1 auto;
|
||||
padding: 20px;
|
||||
|
||||
@media only screen and (min-width: $breakLarge) {
|
||||
display: flex;
|
||||
width: $width;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flexWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.pageWrapper {
|
||||
text-align: left;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.postDate {
|
||||
float: right;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.postTitle {
|
||||
color: $blue;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
font-family: $fontHeader;
|
||||
font-size: 1.2em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.postHeader {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.postsList {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.postListItem {
|
||||
padding: 20px;
|
||||
background-color: $black;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.postExcerpt {
|
||||
text-align: initial;
|
||||
text-decoration: none;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.noDecoration {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:link,
|
||||
a:visited,
|
||||
a:hover,
|
||||
a:active{
|
||||
text-decoration: none;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
|
||||
.socialNavbar {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: $white;
|
||||
text-shadow: $black 0px 0px 2px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Colors
|
||||
$darkGrey: #323232;
|
||||
$white: #f8f8ff;
|
||||
$black: #2f2f2f;
|
||||
$blue: #0f52bf;
|
||||
|
||||
$backgroundDarker: #252627;
|
||||
$backgroundDark: #292a2d;
|
||||
|
||||
//Fonts
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@500&family=Open+Sans&display=swap');
|
||||
$fontHeader: 'Fira Mono', monospace;
|
||||
$fontParagraph: 'Open Sans', sans-serif;
|
||||
|
||||
// Content
|
||||
$breakLarge: 992px;
|
||||
$width: 760px;
|
|
@ -1,15 +0,0 @@
|
|||
@import "./variables.scss";
|
||||
|
||||
.contentWrapper {
|
||||
overflow: auto;
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
background-color: $white;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
$font-header: 'Open Sans Condensed', sans-serif;
|
||||
$font-paragraph: 'Open Sans', sans-serif;
|
||||
$white: #fdfdfd;
|
||||
$black: #2f2f2f;
|
||||
$blue: #144A98;
|
||||
$grey: #A9A9A9;
|
||||
$body: #f1f1f1;
|
||||
$navbar: #1B4367;
|
||||
$break-large: 992px;
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react"
|
||||
import { graphql } from "gatsby"
|
||||
import Layout from "../components/layout"
|
||||
import styles from "../styles/blog-post.module.scss"
|
||||
|
||||
export default function BlogPost({ data }) {
|
||||
const post = data.markdownRemark
|
||||
return (
|
||||
<Layout>
|
||||
<div className={styles.blogPostWrapper}>
|
||||
<h1>{post.frontmatter.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.html }} />
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query($slug: String!) {
|
||||
markdownRemark(fields: { slug: { eq: $slug } }) {
|
||||
html
|
||||
frontmatter {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react"
|
||||
import { graphql } from "gatsby"
|
||||
import Layout from "../components/layout"
|
||||
import styles from "../styles/page.module.scss"
|
||||
|
||||
export default function Page({ data }) {
|
||||
const page = data.markdownRemark
|
||||
return (
|
||||
<Layout>
|
||||
<div className={styles.pageWrapper}>
|
||||
<h1>{page.frontmatter.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: page.html }} />
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query($slug: String!) {
|
||||
markdownRemark(fields: { slug: { eq: $slug } }) {
|
||||
html
|
||||
frontmatter {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
|
@ -1,24 +0,0 @@
|
|||
export class Api {
|
||||
constructor (storage) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
getPosts (req, res) {
|
||||
const limit = req.query.limit ? req.query.limit : 10
|
||||
const skip = req.query.skip ? req.query.skip : 0
|
||||
|
||||
if (req.query.search) {
|
||||
this.storage.Post.find(
|
||||
{ $text: { $search: req.query.search } },
|
||||
{ body: false })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.then(posts => res.send(posts))
|
||||
} else {
|
||||
this.storage.Post.find({}, { body: false })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.then(posts => res.send(posts))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import Ajv from 'ajv'
|
||||
import configSchema from './configSchema.json'
|
||||
|
||||
export class Config {
|
||||
constructor (configFile) {
|
||||
const ajv = new Ajv()
|
||||
|
||||
var valid = ajv.validate(configSchema, configFile)
|
||||
if (!valid) {
|
||||
throw ajv.errors
|
||||
}
|
||||
|
||||
this.title = configFile.title
|
||||
this.name = configFile.name
|
||||
this.email = configFile.email || ''
|
||||
this.social = configFile.social || []
|
||||
this.baseUrl = configFile.baseUrl || 'localhost'
|
||||
this.contentPath = configFile.contentPath || './content'
|
||||
this.storage = configFile.storage || 'file'
|
||||
if (this.storage === 'mongo') {
|
||||
this.mongoUrl = configFile.mongoUrl
|
||||
}
|
||||
this.specialFiles = configFile.specialFiles || ['about.md']
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "http://example.com/example.json",
|
||||
"type": "object",
|
||||
"title": "The root schema",
|
||||
"description": "The root schema comprises the entire JSON document.",
|
||||
"default": {},
|
||||
"required": [
|
||||
"title",
|
||||
"name",
|
||||
"baseUrl"
|
||||
],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"title": {
|
||||
"$id": "#/properties/title",
|
||||
"type": "string",
|
||||
"title": "The title schema",
|
||||
"description": "The title of the website"
|
||||
},
|
||||
"name": {
|
||||
"$id": "#/properties/name",
|
||||
"type": "string",
|
||||
"title": "The name schema",
|
||||
"description": "Your name",
|
||||
"examples": [
|
||||
"John Doe"
|
||||
]
|
||||
},
|
||||
"email": {
|
||||
"$id": "#/properties/email",
|
||||
"type": "string",
|
||||
"title": "The email schema",
|
||||
"description": "Your email",
|
||||
"default": "",
|
||||
"examples": [
|
||||
"john.doe@example.com"
|
||||
]
|
||||
},
|
||||
"social": {
|
||||
"$id": "#/properties/social",
|
||||
"type": "object",
|
||||
"title": "The social schema",
|
||||
"description": "Links to your social accounts",
|
||||
"default": {},
|
||||
"examples": [
|
||||
{
|
||||
"linkedin": "https://www.linkedin.com/in/johndoe/",
|
||||
"codepen": "https://codepen.io/johndoe/",
|
||||
"github": "https://github.com/johndoe"
|
||||
}
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"baseUrl": {
|
||||
"$id": "#/properties/baseUrl",
|
||||
"type": "string",
|
||||
"title": "The baseUrl schema",
|
||||
"description": "Base URL of your website",
|
||||
"examples": [
|
||||
"example.com"
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"$id": "#/properties/storage",
|
||||
"type": "string",
|
||||
"enum": ["mongo", "file"],
|
||||
"title": "The storage schema",
|
||||
"description": "The content storage configuration",
|
||||
"default": "file"
|
||||
},
|
||||
"mongourl": {
|
||||
"$id": "#/properties/mongourl",
|
||||
"type": "string",
|
||||
"title": "The mongourl schema",
|
||||
"description": "Sets MongoDb url if mongo is specified as a storage option.",
|
||||
"examples": [
|
||||
"mongodb://localhost:27017"
|
||||
]
|
||||
},
|
||||
"contentPath": {
|
||||
"$id": "#/properties/contentPath",
|
||||
"type": "string",
|
||||
"title": "The contentPath schema",
|
||||
"description": "The path to your content.",
|
||||
"examples": [
|
||||
"./content"
|
||||
]
|
||||
},
|
||||
"specialFiles": {
|
||||
"$id": "#/properties/specialFiles",
|
||||
"type": "array",
|
||||
"title": "The specialFiles schema",
|
||||
"description": "Files that are not normal blog posts.",
|
||||
"default": [],
|
||||
"examples": [
|
||||
[
|
||||
"about.md",
|
||||
"resume.md"
|
||||
]
|
||||
],
|
||||
"additionalItems": true,
|
||||
"items": {
|
||||
"$id": "#/properties/specialFiles/items",
|
||||
"anyOf": [
|
||||
{
|
||||
"$id": "#/properties/specialFiles/items/anyOf/0",
|
||||
"type": "string",
|
||||
"title": "The first anyOf schema",
|
||||
"description": "An explanation about the purpose of this instance.",
|
||||
"default": "",
|
||||
"examples": [
|
||||
"about.md",
|
||||
"resume.md"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { MainContainer, ContentContainer } from '../containers'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: MainContainer
|
||||
},
|
||||
{
|
||||
path: '/post/:postname',
|
||||
component: ContentContainer
|
||||
},
|
||||
{
|
||||
path: '/resume',
|
||||
component: ContentContainer
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
|
@ -1,144 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import fm from 'front-matter'
|
||||
import moment from 'moment'
|
||||
import zlib from 'zlib'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
export class Scanner {
|
||||
constructor (config, dataHolder) {
|
||||
this.config = config
|
||||
this.dataHolder = dataHolder
|
||||
}
|
||||
|
||||
watch () {
|
||||
const watcher = chokidar.watch(this.config.contentPath, {
|
||||
ignored: /(^|[/\\])\../, // ignore dotfiles
|
||||
persistent: true
|
||||
})
|
||||
|
||||
watcher
|
||||
.on('add', filepath => {
|
||||
console.log(`[Scanner] File ${filepath} has been added`)
|
||||
this.addFile(filepath)
|
||||
})
|
||||
.on('change', filepath => {
|
||||
console.log(`[Scanner] File ${filepath} has been changed`)
|
||||
this.addFile(filepath)
|
||||
})
|
||||
.on('unlink', filepath => {
|
||||
console.log(`[Scanner] File ${filepath} has been removed`)
|
||||
this.deleteFile(filepath)
|
||||
})
|
||||
}
|
||||
|
||||
addFile (filepath) {
|
||||
if (path.extname(filepath) === '.jpg' || path.extname(filepath) === '.png' || path.extname(filepath) === '.gif') {
|
||||
this.copyImage(filepath)
|
||||
.then((file) => this.gzipImage(file))
|
||||
} else {
|
||||
this.readfile(filepath)
|
||||
.then((data) => this.processFile(data[0], data[1]))
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile (filepath) {
|
||||
this.dataHolder.deleteFile(filepath)
|
||||
}
|
||||
|
||||
readfile (filePath) {
|
||||
const relPath = path.relative(path.join(process.cwd(), 'content'), filePath)
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve([relPath, data])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
copyImage (filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const relPath = path.relative(path.join(process.cwd(), 'content'), filePath)
|
||||
const outputPath = path.join(process.cwd(), 'public/static', relPath)
|
||||
fs.copyFile(filePath, outputPath, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(relPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
gzipImage (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const inputPath = path.join(process.cwd(), 'public/static', filename)
|
||||
const outputPath = path.join(process.cwd(), 'public/static', `${filename}.gz`)
|
||||
|
||||
const fileContents = fs.createReadStream(inputPath)
|
||||
const writeStream = fs.createWriteStream(outputPath)
|
||||
const zip = zlib.createGzip()
|
||||
fileContents.pipe(zip).pipe(writeStream).on('finish', (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
processFile (file, data) {
|
||||
const filePath = path.join(process.cwd(), 'content', file)
|
||||
const metadata = this.fileMetadata(filePath)
|
||||
|
||||
if (this.config.specialFiles.indexOf(file) === -1) {
|
||||
const frontMatter = fm(data)
|
||||
|
||||
if (frontMatter.attributes.draft) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
let published
|
||||
if (frontMatter.attributes.date) {
|
||||
published = moment(frontMatter.attributes.date)
|
||||
} else {
|
||||
published = moment()
|
||||
}
|
||||
|
||||
const summary = frontMatter.body.split('\n\n', 1)[0]
|
||||
|
||||
const post = {
|
||||
published: published.format('MMMM DD, YYYY'),
|
||||
filename: metadata.filename,
|
||||
title: frontMatter.attributes.title,
|
||||
summary: summary,
|
||||
link: '/post/' + metadata.filename,
|
||||
body: data
|
||||
}
|
||||
|
||||
this.dataHolder.addPost(post)
|
||||
} else {
|
||||
this.dataHolder.addOther(metadata.filename, data)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
fileMetadata (filepath) {
|
||||
const paths = filepath.split('/')
|
||||
const basename = path.basename(filepath)
|
||||
|
||||
const metadata = {
|
||||
basename,
|
||||
filename: basename.substr(0, basename.lastIndexOf('.')),
|
||||
parrent: paths[paths.length - 2],
|
||||
dirname: path.dirname(filepath)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import React from 'react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { StaticRouter as Router, matchPath } from 'react-router-dom'
|
||||
import { App } from '../components'
|
||||
import routes from './routes'
|
||||
import serialize from 'serialize-javascript'
|
||||
import manifest from '../../public/static/manifest.json'
|
||||
|
||||
export class ServerRenderer {
|
||||
constructor (head, config, dataHolder) {
|
||||
this.head = head
|
||||
this.config = config
|
||||
this.dataHolder = dataHolder
|
||||
}
|
||||
|
||||
render (req, res, next) {
|
||||
const activeRoute = routes.find((route) => matchPath(req.url, route)) || false
|
||||
const head = this.head
|
||||
const config = this.config
|
||||
|
||||
if (!activeRoute) {
|
||||
const context = [{}, config]
|
||||
const markup = renderToString(
|
||||
<Router location={req.url} context={{ context }}>
|
||||
<App/>
|
||||
</Router>
|
||||
)
|
||||
res.status(404).send(renderFullPage(markup, head, {}, config))
|
||||
} else {
|
||||
const promise = this.dataHolder.getData(req.path)
|
||||
|
||||
promise.then((data) => {
|
||||
const context = [data, config]
|
||||
const markup = renderToString(
|
||||
<Router location={req.url} context={{ context }}>
|
||||
<App/>
|
||||
</Router>
|
||||
)
|
||||
|
||||
res.status(200).send(renderFullPage(markup, head, data, config))
|
||||
}).catch(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderFullPage (html, head, data, config) {
|
||||
const initialData = [data, config]
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>${config.title}</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&subset=latin-ext" rel="stylesheet" rel="preload">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
|
||||
<!-- Stylesheet -->
|
||||
<link href=${manifest['bundle.css']} rel="stylesheet" rel="preload">
|
||||
<!-- Initial Data -->
|
||||
<script>window.__INITIAL_DATA__ = ${serialize(initialData)}</script>
|
||||
${head.scripts.join('\n')}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">${html}</div>
|
||||
<script src=${manifest['bundle.js']} async></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export class FileStorage {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
this.data = {
|
||||
posts: [],
|
||||
other: {}
|
||||
}
|
||||
}
|
||||
|
||||
addPost (post) {
|
||||
delete post.body
|
||||
const postIndex = this._findWithAttr(this.data.posts, 'filename', post.filename)
|
||||
|
||||
if (postIndex === -1) {
|
||||
this.data.posts.push(post)
|
||||
} else {
|
||||
this.data.posts[postIndex] = post
|
||||
}
|
||||
}
|
||||
|
||||
addOther (filename, data) {
|
||||
this.data.other[filename] = data
|
||||
}
|
||||
|
||||
deleteFile (filepath) {
|
||||
this.data.posts = this.data.posts.filter((post) =>
|
||||
post.filename !== filepath
|
||||
)
|
||||
}
|
||||
|
||||
getData (reqPath) {
|
||||
return this._getDataFromFile(reqPath)
|
||||
}
|
||||
|
||||
_getDataFromFile (reqPath) {
|
||||
reqPath = reqPath.split('/').pop()
|
||||
if (reqPath === '') {
|
||||
return Promise.resolve(this.data)
|
||||
} else if (reqPath === 'resume') {
|
||||
const fileName = path.join(process.cwd(), 'content', reqPath + '.md')
|
||||
return this._readFile(fileName, 'resume', 'utf8')
|
||||
} else {
|
||||
const fileName = path.join(process.cwd(), 'content', reqPath + '.md')
|
||||
return this._readFile(fileName, 'post', 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
_readFile (fileName, type, options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.readFile(fileName, options, (err, data) => {
|
||||
err ? reject(err) : resolve({
|
||||
type: type,
|
||||
data: data
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_findWithAttr (array, attr, value) {
|
||||
for (let i = 0; i < array.length; i += 1) {
|
||||
if (array[i][attr] === value) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import mongoose, { Schema } from 'mongoose'
|
||||
|
||||
export class MongoStorage {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
|
||||
const PostSchema = new Schema({
|
||||
filename: String,
|
||||
published: String,
|
||||
title: String,
|
||||
summary: String,
|
||||
link: String,
|
||||
body: String
|
||||
})
|
||||
PostSchema.index({
|
||||
body: 'text',
|
||||
title: 'text'
|
||||
})
|
||||
PostSchema.index({
|
||||
filename: 'hashed'
|
||||
})
|
||||
this.Post = mongoose.model('Post', PostSchema)
|
||||
|
||||
const OtherSchema = new Schema({
|
||||
filename: String,
|
||||
body: String
|
||||
})
|
||||
OtherSchema.index({ filename: 'hashed' })
|
||||
this.Other = mongoose.model('Other', OtherSchema)
|
||||
|
||||
this.options = {
|
||||
upsert: true,
|
||||
useFindAndModify: false
|
||||
}
|
||||
}
|
||||
|
||||
addPost (post) {
|
||||
const query = {
|
||||
filename: post.filename
|
||||
}
|
||||
this.Post.findOneAndUpdate(query, post, this.options, (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
}
|
||||
|
||||
addOther (filename, data) {
|
||||
const query = {
|
||||
filename: filename
|
||||
}
|
||||
const update = {
|
||||
filename: filename,
|
||||
body: data
|
||||
}
|
||||
this.Other.findOneAndUpdate(query, update, this.options, (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
}
|
||||
|
||||
deleteFile (filepath) {
|
||||
const filename = filepath.split('/').pop()
|
||||
const basename = filename.split('.')[0]
|
||||
if (this.config['non-content-files'].indexOf(filename) === -1) {
|
||||
this.Post.findOneAndDelete({ filename: basename }, (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
} else {
|
||||
this.Other.findOneAndDelete({ filename: basename }, (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getData (reqPath) {
|
||||
if (reqPath === '/') {
|
||||
const data = {
|
||||
posts: [],
|
||||
other: {}
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this._getOther('about'),
|
||||
this._getAllPosts()
|
||||
]).then((res) => {
|
||||
data.other.about = res[0].data
|
||||
data.posts = res[1]
|
||||
return data
|
||||
})
|
||||
} else if (reqPath.startsWith('/post')) {
|
||||
return this._getPost(reqPath.split('/').pop())
|
||||
} else {
|
||||
return this._getOther(reqPath.split('/').pop())
|
||||
}
|
||||
}
|
||||
|
||||
_getOther (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.Other.findOne({ filename: filename }, (err, data) => {
|
||||
err ? reject(err) : resolve({
|
||||
type: filename,
|
||||
data: data.body
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getAllPosts () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.Post.find({}, { body: false }, (err, data) => {
|
||||
err ? reject(err) : resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getPost (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.Post.findOne({ filename: filename }, (err, data) => {
|
||||
err ? reject(err) : resolve({
|
||||
type: 'content',
|
||||
data: data.body
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
const { resolve } = require('path')
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
const ManifestPlugin = require('webpack-manifest-plugin')
|
||||
const WebpackCleanupPlugin = require('webpack-cleanup-plugin')
|
||||
|
||||
const browserConfig = {
|
||||
entry: {
|
||||
bundle: [
|
||||
'./src/app-client.js'
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: resolve(__dirname, 'public/static'),
|
||||
filename: '[name].[contenthash].js',
|
||||
publicPath: '/static/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: [
|
||||
'babel-loader'
|
||||
],
|
||||
include: resolve(__dirname, 'src')
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]'
|
||||
},
|
||||
importLoaders: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader'
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 8192
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new UglifyJsPlugin(),
|
||||
new OptimizeCSSAssetsPlugin({})
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new WebpackCleanupPlugin(),
|
||||
new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
|
||||
new CompressionPlugin({})
|
||||
],
|
||||
node: { fs: 'empty' }
|
||||
}
|
||||
|
||||
module.exports = browserConfig
|
|
@ -1,61 +0,0 @@
|
|||
const { resolve } = require('path')
|
||||
const nodeExternals = require('webpack-node-externals')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const WebpackCleanupPlugin = require('webpack-cleanup-plugin')
|
||||
|
||||
const serverConfig = {
|
||||
entry: './src/server.js',
|
||||
target: 'node',
|
||||
externals: [nodeExternals()],
|
||||
output: {
|
||||
path: resolve(__dirname, 'build'),
|
||||
filename: 'server.js',
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: [
|
||||
'babel-loader'
|
||||
],
|
||||
exclude: '/node_modules/'
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]'
|
||||
},
|
||||
onlyLocals: true,
|
||||
importLoaders: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader'
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin(),
|
||||
new WebpackCleanupPlugin()
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = serverConfig
|
Loading…
Reference in New Issue