Start over
This commit is contained in:
parent
99ea311de6
commit
a4d28061c7
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" ]
|
|
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.
|
|
14
README.md
14
README.md
|
@ -1,14 +0,0 @@
|
||||||
# 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~~
|
|
|
@ -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": [
|
|
||||||
]
|
|
||||||
}
|
|
84
package.json
84
package.json
|
@ -1,84 +0,0 @@
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"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": "^2.1.0",
|
|
||||||
"yargs": "^15.3.1"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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'
|
|
|
@ -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'
|
|
|
@ -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}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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,22 +0,0 @@
|
||||||
@import "./variables.scss";
|
|
||||||
|
|
||||||
:global(body, html) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(body) {
|
|
||||||
font-family: $font-paragraph;
|
|
||||||
color: $black;
|
|
||||||
background-color: $body;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 1 through 6 {
|
|
||||||
:global(h#{$i}) {
|
|
||||||
font-family: $font-header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-ms-viewport{
|
|
||||||
width: device-width;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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