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