Merge pull request #11 from LordMathis/gatsby

Port website to Gatsby.js
This commit is contained in:
Matúš Námešný 2020-11-08 15:46:01 +01:00 committed by GitHub
commit cff36a2a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 9998 additions and 4768 deletions

77
.gitignore vendored
View File

@ -1,7 +1,72 @@
node_modules # Logs
public logs
*.log *.log
content npm-debug.log*
data.json yarn-debug.log*
build yarn-error.log*
config.json
# 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

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
.cache
package.json
package-lock.json
public

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"arrowParens": "avoid",
"semi": false
}

View File

@ -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" ]

14
LICENSE Normal file
View File

@ -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.

View File

@ -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.

105
README.md
View File

@ -1,14 +1,99 @@
# Personal Website <!-- 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>
This is the source code for my website [namesny.com](https://namesny.com). 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.
This website is built with React and is open source. _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/)._
## To do ## 🚀 Quick start
* ~~implement draft support~~ 1. **Create a Gatsby site.**
* add cv/resume section
* support comments Use the Gatsby CLI to create a new site, specifying the default starter.
* live reload
* ~~eliminate most hardcoded things (move them to config file)~~ ```shell
* ~~remove unnecessary code and overall code cleanup~~ # 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 sites 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 youd 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 wont change this file directly).**
11. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the projects 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 -->

View File

@ -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')
}

View File

@ -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"
]
}

View File

@ -1,4 +0,0 @@
{
"scripts": [
]
}

7
gatsby-browser.js Normal file
View File

@ -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

76
gatsby-config.js Normal file
View File

@ -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`,
],
}

93
gatsby-node.js Normal file
View File

@ -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,
},
})
})
}

7
gatsby-ssr.js Normal file
View File

@ -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

View File

@ -1,84 +1,47 @@
{ {
"name": "portfolio", "name": "gatsby-starter-default",
"version": "2.0.0", "private": true,
"description": "portfolio", "description": "A simple starter to get up and developing quickly with Gatsby",
"main": "index.js", "version": "0.1.0",
"scripts": { "author": "Kyle Mathews <mathews.kyle@gmail.com>",
"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",
"dependencies": { "dependencies": {
"ajv": "^6.12.2", "gatsby": "^2.24.61",
"async": "^3.1.0", "gatsby-image": "^2.4.19",
"axios": "^0.19.1", "gatsby-plugin-manifest": "^2.4.30",
"chokidar": "^3.3.0", "gatsby-plugin-offline": "^3.2.28",
"express": "^4.13.4", "gatsby-plugin-react-helmet": "^3.3.11",
"express-static-gzip": "^2.0.5", "gatsby-plugin-sass": "^2.3.13",
"front-matter": "^3.0.2", "gatsby-plugin-sharp": "^2.6.36",
"helmet": "^3.21.1", "gatsby-source-filesystem": "^2.3.30",
"jsonfile": "^5.0.0", "gatsby-transformer-remark": "^2.8.35",
"markdown-it": "^10.0.0", "gatsby-transformer-sharp": "^2.5.15",
"moment": "^2.24.0", "node-sass": "^4.14.1",
"mongoose": "^5.7.14",
"morgan": "^1.9.1",
"node-sass": "^4.9.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.7.0", "react": "^16.12.0",
"react-dom": "^16.7.0", "react-dom": "^16.12.0",
"react-jss": "^10.0.0", "react-helmet": "^6.1.0"
"react-router-dom": "^5.0.1",
"serialize-javascript": "^3.1.0",
"yargs": "^15.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.2.2", "prettier": "2.1.1"
"@babel/node": "^7.2.2", },
"@babel/plugin-proposal-class-properties": "^7.3.3", "keywords": [
"@babel/plugin-proposal-object-rest-spread": "^7.2.0", "gatsby"
"@babel/plugin-transform-runtime": "^7.2.0", ],
"@babel/plugin-transform-template-literals": "^7.2.0", "license": "0BSD",
"@babel/preset-env": "^7.2.3", "scripts": {
"@babel/preset-react": "^7.0.0", "build": "gatsby build",
"@babel/runtime": "^7.2.0", "develop": "gatsby develop",
"babel-eslint": "^10.0.1", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"babel-loader": "^8.0.5", "start": "npm run develop",
"babel-plugin-css-modules-transform": "^1.6.2", "serve": "gatsby serve",
"compression-webpack-plugin": "^3.0.0", "clean": "gatsby clean",
"create-file-webpack": "^1.0.2", "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
"css-loader": "^3.2.0", },
"css-modules-require-hook": "^4.2.3", "repository": {
"eslint": "^6.4.0", "type": "git",
"eslint-config-standard": "^14.1.0", "url": "https://github.com/gatsbyjs/gatsby-starter-default"
"eslint-plugin-babel": "^5.3.0", },
"eslint-plugin-import": "^2.16.0", "bugs": {
"eslint-plugin-node": "^10.0.0", "url": "https://github.com/gatsbyjs/gatsby/issues"
"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"
} }
} }

View File

@ -1,2 +0,0 @@
module.exports = {
}

View File

@ -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')
)

View File

@ -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>
)
}
}

View File

@ -1 +0,0 @@
@import "../stylesheets/variables.scss";

View File

@ -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>
)
}
}

View File

@ -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>
)
}
};

View File

@ -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;
}

View File

@ -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>
)
}
}

View File

@ -1,14 +0,0 @@
@import "../stylesheets/variables.scss";
.left {
flex: 30%;
}
.right {
flex: 70%;
}
.column {
box-sizing: border-box;
margin: 20px;
}

View File

@ -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 &copy; {new Date().getFullYear()} { this.props.config.name }</p>
</footer>
)
}
}

View File

@ -1,6 +0,0 @@
@import "../stylesheets/variables.scss";
.footer {
padding: 10px;
text-align: center;
}

View File

@ -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>
)
}
}

View File

@ -1,11 +0,0 @@
@import "../stylesheets/variables.scss";
.mainHeader {
border-left: 5px solid $blue;
padding: 5px;
margin: 0;
text-align: left;
h1 {
margin: 0;
}
}

View File

@ -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>
)
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>
)
}
}

View File

@ -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;
}

View File

@ -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&apos;re lost</p>
<p>404 Page not found</p>
</div>
</div>
</div>
)
}
export default NotFoundPage

View File

@ -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>
)
}
}

View File

@ -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;
}

View File

@ -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>
)
}
}

View File

@ -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;
}

View File

@ -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>
)
}
}

View File

@ -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;
}

View File

@ -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>
)
}
}

View File

@ -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;
}
}

View File

@ -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>
)
}
}

View File

@ -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);
}
}

View File

@ -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>
)
}
}

View File

@ -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;
}
}

21
src/components/blog.js Normal file
View File

@ -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

12
src/components/footer.js Normal file
View File

@ -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

35
src/components/header.js Normal file
View File

@ -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

32
src/components/image.js Normal file
View File

@ -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

View File

@ -1,15 +1,19 @@
export { default as Home } from './Home' import { Link } from "gatsby"
export { default as Blog } from './Blog' import PropTypes from "prop-types"
export { default as About } from './About' import React from "react"
export { default as Post } from './Post' import styles from "../styles/index.module.scss"
export { default as NotFoundPage } from './NotFoundPage' import Social from "./social"
export { default as Spinner } from './Spinner'
export { default as Header } from './Header' const Index = ({ author, social, email }) => (
export { default as Wrapper } from './Wrapper' <div className={styles.indexWrapper}>
export { default as Navbar } from './Navbar' <div>
export { default as App } from './App' <h1 className={styles.header}>{ author }</h1>
export { default as SocialLinks } from './SocialLinks' </div>
export { default as Column } from './Column' <Social social={social} email={email}/>
export { default as Resume } from './Resume' </div>
export { default as Footer } from './Footer' )
export { default as SearchBox } from './SearchBox'
Index.propTypes = {
}
export default Index

48
src/components/layout.js Normal file
View File

@ -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

View File

@ -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

43
src/components/social.js Normal file
View File

@ -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>
)
}
}

View File

@ -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)} />
)
}
}

View File

@ -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} />
)
}
}
}

View File

@ -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>
)
}
}

View File

@ -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>
)
}
}

View File

@ -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'

BIN
src/images/gatsby-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

12
src/pages/404.js Normal file
View File

@ -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&#39;t exist... the sadness.</p>
</Layout>
)
export default NotFoundPage

41
src/pages/blog.js Normal file
View File

@ -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

33
src/pages/index.js Normal file
View File

@ -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

View File

@ -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}`)
}
})
}

View File

@ -0,0 +1,5 @@
@import "./variables.scss";
.blogPostWrapper {
text-align: left;
}

View File

@ -0,0 +1,5 @@
@import "./variables.scss";
.header {
text-align: left;
}

View File

@ -0,0 +1,11 @@
@import "./variables.scss";
.footer {
padding: 15px;
text-align: center;
background-color: $backgroundDarker;
}
.link {
color: $white;
}

View File

@ -5,15 +5,18 @@
} }
:global(body) { :global(body) {
font-family: $font-paragraph; font-family: $fontParagraph;
color: $black; color: $white;
background-color: $body; background-color: $backgroundDark;
margin: 0; margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
} }
@for $i from 1 through 6 { @for $i from 1 through 6 {
:global(h#{$i}) { :global(h#{$i}) {
font-family: $font-header; font-family: $fontHeader;
} }
} }

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
.indexWrapper {
display: flex;
flex-direction: column;
justify-content: center;
}
.header {
font-size: 3em;
}

View File

@ -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%;
}

View File

@ -0,0 +1,5 @@
@import "./variables.scss";
.pageWrapper {
text-align: left;
}

View File

@ -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;
}

View File

@ -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;
}
}

17
src/styles/variables.scss Normal file
View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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
}
}
}
`

27
src/templates/page.js Normal file
View File

@ -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
}
}
}
`

View File

@ -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))
}
}
}

View File

@ -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']
}
}

View File

@ -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"
]
}
]
}
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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&amp;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>
`
}

View File

@ -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
}
}

View File

@ -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
})
})
})
}
}

View File

@ -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

View File

@ -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

11598
yarn.lock

File diff suppressed because it is too large Load Diff