Start over

This commit is contained in:
LordMathis 2020-09-16 13:59:38 +02:00
parent 99ea311de6
commit a4d28061c7
58 changed files with 0 additions and 10875 deletions

View File

@ -1,20 +0,0 @@
# Stage 1 - build app
FROM node:12 as build-deps
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn
COPY . ./
RUN yarn build
# Stage 2 - run
FROM node:12-alpine
WORKDIR /app
COPY --chown=node:node --from=build-deps /usr/src/app/build /app/build
COPY --chown=node:node --from=build-deps /usr/src/app/public /app/public
COPY --chown=node:node --from=build-deps /usr/src/app/node_modules /app/node_modules
VOLUME /app/config
VOLUME /app/content
RUN chown node:node /app
USER node
EXPOSE 3000
CMD [ "node", "build/server.js" ]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Matúš Námešný
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

View File

@ -1,37 +0,0 @@
module.exports = function (api) {
const presets = [
'@babel/preset-env',
'@babel/preset-react'
]
const plugins = [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-template-literals',
'@babel/plugin-proposal-class-properties'
]
if (api.env() === 'development') {
plugins.push([
'css-modules-transform', {
generateScopedName: '[name]__[local]___[hash:base64:5]',
preprocessCss: processSass,
extensions: ['.css', '.scss']
}
])
}
return {
presets,
plugins
}
}
var sass = require('node-sass')
function processSass (data, filename) {
var result
result = sass.renderSync({
data: data,
file: filename
}).css
return result.toString('utf8')
}

View File

@ -1,17 +0,0 @@
{
"title": "John Doe - Portfolio",
"name": "John Doe",
"email": "john.doe@example.com",
"social": {
"github": "github_url",
"twitter": "twitter_url",
"linkedin": "linkedin_url"
},
"baseUrl": "example.com",
"contentPath": "./content",
"storage": "file",
"specialFiles": [
"about.md", "resume.md"
]
}

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
import React from 'react'
import { hydrate } from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'
import { App } from './components'
hydrate(
<Router>
<App data={window.__INITIAL_DATA__}/>
</Router>,
document.getElementById('root')
)

View File

@ -1,34 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Spinner, Header } from '.'
import '../stylesheets/globals.scss'
import contentStyle from '../stylesheets/content.scss'
import MarkdownIt from 'markdown-it'
export default class About extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
about: PropTypes.string.isRequired
}
render () {
const md = MarkdownIt()
const result = md.render(this.props.about)
if (this.props.isLoading) {
return (
<div className={contentStyle.contentWrapper} id="about">
<Spinner/>
</div>
)
}
return (
<div className={contentStyle.content} role="region" aria-label="About me">
<Header header={'About Me'} role="heading" aria-level="2"/>
<div dangerouslySetInnerHTML={{ __html: result }} role="article">
</div>
</div>
)
}
}

View File

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

View File

@ -1,26 +0,0 @@
import { NotFoundContainer } from '../containers'
import React, { Component } from 'react'
import routes from '../utils/routes'
import { Route, Switch } from 'react-router-dom'
export default class App extends Component {
render () {
return (
<div>
<Switch>
{routes.map(({ path, exact, component: C, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={(props) => (
<C {...props} {...rest} />
)}
/>
))}
<Route render={(props) => <NotFoundContainer {...props} />} />
</Switch>
</div>
)
}
}

View File

@ -1,71 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Spinner, Header, SearchBox } from '.'
import '../stylesheets/globals.scss'
import MarkdownIt from 'markdown-it'
import styles from './Blog.scss'
import contentStyle from '../stylesheets/content.scss'
export default class Blog extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
posts: PropTypes.arrayOf(PropTypes.object).isRequired,
searchString: PropTypes.string,
expanded: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired,
handleFocus: PropTypes.func.isRequired,
handleBlur: PropTypes.func.isRequired,
handleEnter: PropTypes.func.isRequired,
handleSearch: PropTypes.func.isRequired
}
render () {
const md = MarkdownIt()
let postsHTML
if (this.props.isLoading) {
postsHTML = <Spinner />
} else {
const posts = this.props.posts.sort((a, b) => {
return new Date(b.published) - new Date(a.published)
})
if (posts.length < 1) {
postsHTML = (
<div>
<span>No posts found</span>
</div>
)
} else {
postsHTML = posts.map((post) =>
<div key={post.title} className={styles.postListItem} role="listitem">
<div className={styles.postHeader} >
<a href={post.link} className={styles.postTitle}>{post.title}</a>
<span className={styles.postDate}>{post.published}</span>
</div>
<div dangerouslySetInnerHTML={{ __html: md.render(post.summary) }}>
</div>
</div>
)
}
}
return (
<div className={`${contentStyle.content}`} id="blog" role="region" aria-label="Blog posts">
<div className={styles.headerContainer}>
<Header header={'Blog'} role="heading" aria-level="2"/>
<SearchBox searchString={this.props.searchString}
expanded={this.props.expanded}
handleChange={this.props.handleChange}
handleFocus={this.props.handleFocus}
handleBlur={this.props.handleBlur}
handleEnter={this.props.handleEnter}
handleSearch={this.props.handleSearch} />
</div>
<div className={`${styles.postsList}`} role="list">
{postsHTML}
</div>
</div>
)
}
};

View File

@ -1,31 +0,0 @@
@import "../stylesheets/variables.scss";
.postDate {
float: right;
}
.postTitle {
color: $blue;
text-decoration: none;
text-transform: capitalize;
font-family: $font-header;
font-size: 1.2em;
float: left;
}
.postHeader {
overflow: hidden;
}
.postsList {
margin-top: 20px;
}
.postListItem {
border-bottom: 5px solid $body;
}
.headerContainer {
display: flex;
justify-content: space-between;
}

View File

@ -1,26 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import '../stylesheets/globals.scss'
import styles from './Column.scss'
export default class Column extends Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
left: PropTypes.bool
}
static defaultProps = {
left: true
}
render () {
return (
<div className={`${styles.column} ${this.props.left ? styles.left : styles.right}`}>
{this.props.children}
</div>
)
}
}

View File

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

View File

@ -1,18 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import '../stylesheets/globals.scss'
import style from './Footer.scss'
export default class Footer extends Component {
static propTypes = {
config: PropTypes.object.isRequired
}
render () {
return (
<footer className={style.footer}>
<p>Copyright &copy; {new Date().getFullYear()} { this.props.config.name }</p>
</footer>
)
}
}

View File

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

View File

@ -1,18 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import '../stylesheets/globals.scss'
import styles from './Header.scss'
export default class Header extends Component {
static propTypes = {
header: PropTypes.string.isRequired
}
render () {
return (
<div className={styles.mainHeader}>
<h1>{this.props.header}</h1>
</div>
)
}
}

View File

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

View File

@ -1,37 +0,0 @@
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import PropTypes from 'prop-types'
import { SocialLinks } from '.'
import '../stylesheets/globals.scss'
import styles from './Home.scss'
export default class Home extends Component {
static propTypes = {
config: PropTypes.object.isRequired
}
render () {
return (
<div id={styles.coverPage} className={styles.coverPageFull} role="region" aria-label="Home page">
<div id={styles.coverPageContent}>
<div role="heading" aria-level="1">
<h1 id={styles.coverPageName}><Link to="/">{ this.props.config.name }</Link></h1>
</div>
<SocialLinks config={ this.props.config }/>
<div className={styles.menuLinks} role="navigation">
<ul>
<li key="about">
<a href="#about">
About
</a>
</li>
<li key="blog">
<span>/ <a href="#blog">Blog</a></span>
</li>
</ul>
</div>
</div>
</div>
)
}
}

View File

@ -1,80 +0,0 @@
@import "../stylesheets/variables.scss";
#coverPage {
background: url('/static/background.jpg') no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
color: $white;
background-position: center;
background-repeat: no-repeat;
}
#coverPage.coverPageFull{
height: 100vh;
width: 100%;
}
#coverPageName {
font-size: 5em;
}
#coverPageName a, #coverPageName a:hover {
color: $white;
text-shadow: $black 0px 0px 2px;
-webkit-font-smoothing: antialiased;
text-decoration: none;
}
.social {
text-align: center;
ul {
list-style: none;
li {
display: inline;
a {
color: $white;
text-shadow: $black 0px 0px 2px;
-webkit-font-smoothing: antialiased;
display: inline-block;
margin: 10px;
}
}
}
}
.menuLinks {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-shadow: $black 0px 0px 2px;
-webkit-font-smoothing: antialiased;
ul{
list-style: none;
margin-left: 0;
padding: 10px;
li {
margin: 5px;
display: inline;
a {
color: $white;
text-decoration: none;
text-transform: uppercase;
font-size: 1.4em;
}
a:hover {
border-bottom: solid 2px $blue;
}
}
}
}

View File

@ -1,38 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { SocialLinks } from '.'
import '../stylesheets/globals.scss'
import styles from './Navbar.scss'
export default class Navbar extends Component {
static propTypes = {
config: PropTypes.object.isRequired
}
render () {
return (
<div className={styles.navbar} role="navigation">
<div className={styles.links}>
<ul>
<li key="index">
<a href='/'>
<span className={styles.nameLink}>{this.props.config.name} |</span>
</a>
</li>
<li key="about">
<a href='/#about'>
<span>About</span>
</a>
</li>
<li key="blog">
<a href='/#blog'>
<span>Blog</span>
</a>
</li>
</ul>
</div>
<SocialLinks config={ this.props.config } home={ false }/>
</div>
)
}
}

View File

@ -1,45 +0,0 @@
@import "../stylesheets/variables.scss";
.navbar {
background-color: $navbar;
color: $white;
background-position: center;
background-repeat: no-repeat;
text-align: left;
overflow: auto;
}
h2 {
color: $white;
padding-left: 5px;
}
.social {
float: right;
vertical-align: bottom;
a {
color: $white;
display: inline-block;
margin: 10px;
}
}
.links {
float: left;
ul {
list-style: none;
li {
font-family: $font-header;
display: inline;
margin: 5px;
a {
color: $white;
text-decoration: none;
}
}
}
}
.nameLink {
font-size: 1.4em;
}

View File

@ -1,21 +0,0 @@
import React from 'react'
import { Navbar, Header } from '.'
import '../stylesheets/globals.scss'
import contentStyle from '../stylesheets/content.scss'
const NotFoundPage = (props) => {
return (
<div>
<Navbar config={props.config}/>
<div className={contentStyle.contentWrapper}>
<Header header={'Uhm... WHAT?'} />
<div className={contentStyle.content}>
<p>Looks like you&apos;re lost</p>
<p>404 Page not found</p>
</div>
</div>
</div>
)
}
export default NotFoundPage

View File

@ -1,50 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Spinner, Header, Navbar, Wrapper, Footer } from '.'
import '../stylesheets/globals.scss'
import contentStyle from '../stylesheets/content.scss'
import styles from './Post.scss'
import MarkdownIt from 'markdown-it'
import fm from 'front-matter'
import moment from 'moment'
export default class Post extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
post: PropTypes.string.isRequired,
config: PropTypes.object.isRequired
}
render () {
const md = MarkdownIt()
const content = fm(this.props.post)
const title = content.attributes.title
const date = moment(content.attributes.date, 'YYYY-MM-DD')
const body = md.render(content.body)
if (this.props.isLoading) {
return (
<div className={contentStyle.contentWrapper}>
<Spinner/>
</div>
)
}
return (
<div>
<Navbar config={this.props.config} />
<Wrapper>
<div className={`${contentStyle.content} ${styles.column}`}>
<Header header={title} role="heading" aria-level="2" />
<div className={styles.postDate}>
<h3>{date.format('MMMM D, YYYY')}</h3>
</div>
<div className={styles.postContent} dangerouslySetInnerHTML={{ __html: body }} role="article">
</div>
</div>
</Wrapper>
<Footer config={this.props.config}/>
</div>
)
}
}

View File

@ -1,20 +0,0 @@
@import "../stylesheets/variables.scss";
.postContent {
clear: both;
h1,h2,h2,h4,h5,h6 {
color: $black;
}
}
.postDate {
float: right;
h3 {
font-weight: normal
}
}
.column {
box-sizing: border-box;
margin: 20px;
}

View File

@ -1,45 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Spinner, Navbar, Wrapper, Header, Footer } from '.'
import '../stylesheets/globals.scss'
import contentStyle from '../stylesheets/content.scss'
import style from './Resume.scss'
import MarkdownIt from 'markdown-it'
import fm from 'front-matter'
export default class About extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
resume: PropTypes.string.isRequired,
config: PropTypes.object.isRequired
}
render () {
const md = MarkdownIt()
const content = fm(this.props.resume)
const title = content.attributes.title
const body = md.render(content.body)
if (this.props.isLoading) {
return (
<div className={contentStyle.content}>
<Spinner/>
</div>
)
}
return (
<div>
<Navbar config={this.props.config} />
<Wrapper>
<div className={`${contentStyle.content} ${style.column}`}>
<Header header={title} role="heading" aria-level="2" />
<div className={style.content} dangerouslySetInnerHTML={{ __html: body }} role="article">
</div>
</div>
</Wrapper>
<Footer config={this.props.config} />
</div>
)
}
}

View File

@ -1,13 +0,0 @@
@import "../stylesheets/variables.scss";
.content {
clear: both;
h1,h2,h2,h4,h5,h6 {
color: $black;
}
}
.column {
box-sizing: border-box;
margin: 20px;
}

View File

@ -1,34 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import '../stylesheets/globals.scss'
import styles from './SearchBox.scss'
export default class SearchBox extends Component {
static propTypes = {
handleChange: PropTypes.func.isRequired,
handleFocus: PropTypes.func.isRequired,
handleBlur: PropTypes.func.isRequired,
handleEnter: PropTypes.func.isRequired,
handleSearch: PropTypes.func.isRequired,
searchString: PropTypes.string,
expanded: PropTypes.bool.isRequired
}
render () {
return (
<div className={styles.container}>
<input placeholder='Search'
className={`${styles.search} ${this.props.expanded ? styles.expanded : ''}`}
type="text"
value={this.props.searchString}
onChange={this.props.handleChange}
onFocus={this.props.handleFocus}
onBlur={this.props.handleBlur}
onKeyDown={this.props.handleEnter} />
<span onClick={this.props.handleSearch} >
<i className={`fa fa-search ${styles.icon}`}></i>
</span>
</div>
)
}
}

View File

@ -1,34 +0,0 @@
@import "../stylesheets/variables.scss";
.container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.search {
position: relative;
padding: 15px 40px 15px 20px;
width: 20px;
color: $black;
font-size: 16px;
letter-spacing: 2px;
border: none;
border-radius: 5px;
background: $white;
transition: width 0.4s ease;
outline: none;
box-sizing: border-box;
}
.expanded {
width: 300px;
}
.icon{
position: relative;
left: -37px;
color: $black;
}

View File

@ -1,52 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import '../stylesheets/globals.scss'
import styles from './SocialLinks.scss'
export default class SocialLinks extends Component {
static propTypes = {
config: PropTypes.object.isRequired,
home: PropTypes.bool
}
static defaultProps = {
home: true
}
render () {
let key = 0
const objKeys = Object.keys(this.props.config.social)
const socialLinks = objKeys.map((val) => {
const link = (
<li key={key}>
<a href={this.props.config.social[val]} role="link">
<i className={`fa fa-${val} ${this.props.home ? 'fa-3x' : ''}`} aria-hidden="true" />
<span className="sr-only">{val}</span>
</a>
</li>
)
key += 1
return link
})
socialLinks.push(
<li key={key}>
<a href={`mailto:${this.props.config.email}`} role="link">
<i className={`fa fa-envelope-o ${this.props.home ? 'fa-3x' : ''}`} aria-hidden="true" />
<span className="sr-only">e-mail</span>
</a>
</li>
)
const className = this.props.home ? styles['social-home'] : styles['social-navbar']
return (
<div className={className} role="list">
<ul>
{socialLinks}
</ul>
</div>
)
}
}

View File

@ -1,32 +0,0 @@
@import "../stylesheets/variables.scss";
.social-home {
text-align: center;
ul {
list-style: none;
li {
display: inline;
}
}
}
.social-navbar {
float: right;
vertical-align: bottom;
}
.social-home, .social-navbar {
ul {
list-style: none;
li {
display: inline;
}
}
a {
color: $white;
text-shadow: $black 0px 0px 2px;
-webkit-font-smoothing: antialiased;
display: inline-block;
margin: 10px;
}
}

View File

@ -1,18 +0,0 @@
import React, { Component } from 'react'
import '../stylesheets/globals.scss'
import styles from './Spinner.scss'
export default class Spinner extends Component {
render () {
return (
<div className={styles.spinnerWrapper}>
<div className={styles.ldsEllipsis}>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
)
}
}

View File

@ -1,67 +0,0 @@
/*
from https://loading.io/css/
*/
@import "../stylesheets/variables.scss";
.spinnerWrapper {
width: 100%;
display: flex;
justify-content: center;
}
.ldsEllipsis {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
margin: 0 auto;
}
.ldsEllipsis div {
position: absolute;
top: 27px;
width: 11px;
height: 11px;
border-radius: 50%;
background: $black;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.ldsEllipsis div:nth-child(1) {
left: 6px;
animation: lds-ellipsis1 0.6s infinite;
}
.ldsEllipsis div:nth-child(2) {
left: 6px;
animation: lds-ellipsis2 0.6s infinite;
}
.ldsEllipsis div:nth-child(3) {
left: 26px;
animation: lds-ellipsis2 0.6s infinite;
}
.ldsEllipsis div:nth-child(4) {
left: 45px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(19px, 0);
}
}

View File

@ -1,27 +0,0 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import '../stylesheets/globals.scss'
import contentStyle from '../stylesheets/content.scss'
import styles from './Wrapper.scss'
export default class Wrapper extends Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
flex: PropTypes.bool
}
static defaultProps = {
flex: false
}
render () {
return (
<div className={` ${contentStyle.contentWrapper} ${styles.centerContent} ${this.props.flex ? styles.flex : styles.noFlex}` } role='main'>
{this.props.children}
</div>
)
}
}

View File

@ -1,19 +0,0 @@
@import "../stylesheets/variables.scss";
.centerContent {
text-align: center;
margin: 0 auto;
}
.flex {
@media only screen and (min-width: $break-large) {
width: 80%;
display: flex;
}
}
.noFlex {
@media (min-width: $break-large) {
width: 960px;
}
}

View File

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

View File

@ -1,75 +0,0 @@
import React, { Component } from 'react'
import { Blog } from '../components'
import PropTypes from 'prop-types'
import axios from 'axios'
import '../stylesheets/globals.scss'
export default class BlogContainer extends Component {
static propTypes = {
posts: PropTypes.arrayOf(PropTypes.object).isRequired
}
constructor (props) {
super(props)
this.state = {
isLoading: false,
posts: props.posts,
searchString: '',
expanded: false
}
}
handleChange (event) {
this.setState({ searchString: event.target.value })
}
handleFocus () {
this.setState({ expanded: true })
}
handleBlur () {
this.setState({ expanded: false })
}
handleEnter (event) {
if (event.key === 'Enter') {
this.handleSearch()
}
}
handleSearch () {
if (this.state.expanded && this.state.searchString) {
this.search()
} else {
this.setState({ expanded: true })
}
}
search () {
this.setState({
isLoading: true
}, () => {
axios.get(`/api/v1/posts?search=${this.state.searchString}`)
.then((data) => {
this.setState({
isLoading: false,
posts: data.data
})
})
})
}
render () {
return (
<Blog isLoading={ this.state.isLoading }
posts={ this.state.posts }
searchString={this.state.searchString}
expanded={this.state.expanded}
handleChange={this.handleChange.bind(this)}
handleFocus={this.handleFocus.bind(this)}
handleBlur={this.handleBlur.bind(this)}
handleEnter={this.handleEnter.bind(this)}
handleSearch={this.handleSearch.bind(this)} />
)
}
}

View File

@ -1,46 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Post, Resume } from '../components'
export default class ContentContainer extends Component {
static propTypes = {
staticContext: PropTypes.object.isRequired
}
constructor (props) {
super(props)
let data
if (typeof window === 'undefined') {
data = props.staticContext.context
} else {
data = window.__INITIAL_DATA__
delete window.__INITIAL_DATA__
}
this.state = {
isLoading: !data,
type: data[0].type,
content: data[0].data,
config: data[1]
}
}
render () {
if (this.state.type === 'resume') {
return (
<Resume
isLoading={this.state.isLoading}
resume={this.state.content}
config={this.state.config} />
)
} else {
return (
<Post
isLoading={this.state.isLoading}
post={this.state.content}
config={this.state.config} />
)
}
}
}

View File

@ -1,48 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { About, Home, Wrapper, Column, Footer } from '../components'
import { BlogContainer } from '.'
export default class MainContainer extends Component {
static propTypes = {
staticContext: PropTypes.object
}
constructor (props) {
super(props)
let data
if (typeof window === 'undefined') {
data = props.staticContext.context
} else {
data = window.__INITIAL_DATA__
delete window.__INITIAL_DATA__
}
this.state = {
isLoadingBlog: !data[0].posts,
isLoadingAbout: !data[0].other.about,
about: data[0].other.about,
posts: data[0].posts,
config: data[1]
}
}
render () {
return (
<div>
<Home config={this.state.config} />
<Wrapper flex={true}>
<Column>
<About isLoading={this.state.isLoadingAbout}
about={this.state.about}/>
</Column>
<Column left={false}>
<BlogContainer posts={this.state.posts}/>
</Column>
</Wrapper>
<Footer config={this.state.config} />
</div>
)
}
}

View File

@ -1,33 +0,0 @@
import React, { Component } from 'react'
import { Wrapper, NotFoundPage } from '../components'
import PropTypes from 'prop-types'
import '../stylesheets/globals.scss'
export default class NotFoundContainer extends Component {
static propTypes = {
staticContext: PropTypes.object.isRequired
}
constructor (props) {
super(props)
let data
if (typeof window === 'undefined') {
data = props.staticContext.context
} else {
data = window.__INITIAL_DATA__
delete window.__INITIAL_DATA__
}
this.state = {
config: data[1]
}
}
render () {
return (
<Wrapper>
<NotFoundPage config={this.state.config}/>
</Wrapper>
)
}
}

View File

@ -1,4 +0,0 @@
export { default as MainContainer } from './MainContainer'
export { default as ContentContainer } from './ContentContainer'
export { default as NotFoundContainer } from './NotFoundContainer'
export { default as BlogContainer } from './BlogContainer'

View File

@ -1,91 +0,0 @@
import express from 'express'
import helmet from 'helmet'
import expressStaticGzip from 'express-static-gzip'
import path from 'path'
import morgan from 'morgan'
import mongoose from 'mongoose'
import jsonfile from 'jsonfile'
import { ServerRenderer } from './utils/serverRender'
import { Scanner } from './utils/scanner'
import { FileStorage } from './utils/storage/file'
import { MongoStorage } from './utils/storage/mongo'
import { Config } from './utils/config'
import { Api } from './utils/api'
const configPath = process.argv[2] || path.join(process.cwd(), 'config/config.json')
const app = express()
app.set('trust proxy', true)
const config = new Config(jsonfile.readFileSync(configPath))
if (config == null) {
throw new Error('Config file not found!')
}
const port = config.port || 3000
app.use(morgan('common'))
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'", `*.${config.baseUrl}`],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", `*.${config.baseUrl}`],
styleSrc: ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com', 'maxcdn.bootstrapcdn.com'],
fontSrc: ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com', 'maxcdn.bootstrapcdn.com'],
imgSrc: ['*'],
workerSrc: false,
blockAllMixedContent: true
}
}))
app.use('/static', expressStaticGzip('public/static'))
app.get('/favicon.ico', (req, res) => {
res.status(204).send('Not Found !!!')
})
let head = jsonfile.readFileSync(path.join(process.cwd(), 'config/head.json'))
if (head == null) {
head = {
scripts: []
}
}
let storage
if (config.storage === 'file') {
storage = new FileStorage(config)
} else if (config.storage === 'mongo') {
storage = new MongoStorage(config)
}
if (config.storage === 'mongo') {
const postApi = new Api(storage)
app.get('/api/v1/posts', postApi.getPosts.bind(postApi))
}
const scanner = new Scanner(config, storage)
const serverRenderer = new ServerRenderer(head, config, storage)
app.get('*', serverRenderer.render.bind(serverRenderer))
if (config.storage === 'mongo') {
mongoose.connect(config.mongoUrl, { useNewUrlParser: true })
const db = mongoose.connection
db.on('error', (error) => console.error(`[Server] Unable to connect to database\n${error}`))
db.once('open', () => {
console.log('[Server] Connected to database')
startServer()
})
} else {
startServer()
}
function startServer () {
scanner.watch()
app.listen(port, function (error) {
if (error) {
console.error(`[Server] Unable to start server\n${error}`)
} else {
console.info(`[Server] Listening on port ${port}`)
}
})
}

View File

@ -1,15 +0,0 @@
@import "./variables.scss";
.contentWrapper {
overflow: auto;
a {
color: $blue;
}
}
.content {
box-sizing: border-box;
background-color: $white;
padding: 20px;
text-align: left;
}

View File

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

View File

@ -1,9 +0,0 @@
$font-header: 'Open Sans Condensed', sans-serif;
$font-paragraph: 'Open Sans', sans-serif;
$white: #fdfdfd;
$black: #2f2f2f;
$blue: #144A98;
$grey: #A9A9A9;
$body: #f1f1f1;
$navbar: #1B4367;
$break-large: 992px;

View File

@ -1,24 +0,0 @@
export class Api {
constructor (storage) {
this.storage = storage
}
getPosts (req, res) {
const limit = req.query.limit ? req.query.limit : 10
const skip = req.query.skip ? req.query.skip : 0
if (req.query.search) {
this.storage.Post.find(
{ $text: { $search: req.query.search } },
{ body: false })
.skip(skip)
.limit(limit)
.then(posts => res.send(posts))
} else {
this.storage.Post.find({}, { body: false })
.skip(skip)
.limit(limit)
.then(posts => res.send(posts))
}
}
}

View File

@ -1,25 +0,0 @@
import Ajv from 'ajv'
import configSchema from './configSchema.json'
export class Config {
constructor (configFile) {
const ajv = new Ajv()
var valid = ajv.validate(configSchema, configFile)
if (!valid) {
throw ajv.errors
}
this.title = configFile.title
this.name = configFile.name
this.email = configFile.email || ''
this.social = configFile.social || []
this.baseUrl = configFile.baseUrl || 'localhost'
this.contentPath = configFile.contentPath || './content'
this.storage = configFile.storage || 'file'
if (this.storage === 'mongo') {
this.mongoUrl = configFile.mongoUrl
}
this.specialFiles = configFile.specialFiles || ['about.md']
}
}

View File

@ -1,121 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"default": {},
"required": [
"title",
"name",
"baseUrl"
],
"additionalProperties": true,
"properties": {
"title": {
"$id": "#/properties/title",
"type": "string",
"title": "The title schema",
"description": "The title of the website"
},
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "The name schema",
"description": "Your name",
"examples": [
"John Doe"
]
},
"email": {
"$id": "#/properties/email",
"type": "string",
"title": "The email schema",
"description": "Your email",
"default": "",
"examples": [
"john.doe@example.com"
]
},
"social": {
"$id": "#/properties/social",
"type": "object",
"title": "The social schema",
"description": "Links to your social accounts",
"default": {},
"examples": [
{
"linkedin": "https://www.linkedin.com/in/johndoe/",
"codepen": "https://codepen.io/johndoe/",
"github": "https://github.com/johndoe"
}
],
"additionalProperties": true
},
"baseUrl": {
"$id": "#/properties/baseUrl",
"type": "string",
"title": "The baseUrl schema",
"description": "Base URL of your website",
"examples": [
"example.com"
]
},
"storage": {
"$id": "#/properties/storage",
"type": "string",
"enum": ["mongo", "file"],
"title": "The storage schema",
"description": "The content storage configuration",
"default": "file"
},
"mongourl": {
"$id": "#/properties/mongourl",
"type": "string",
"title": "The mongourl schema",
"description": "Sets MongoDb url if mongo is specified as a storage option.",
"examples": [
"mongodb://localhost:27017"
]
},
"contentPath": {
"$id": "#/properties/contentPath",
"type": "string",
"title": "The contentPath schema",
"description": "The path to your content.",
"examples": [
"./content"
]
},
"specialFiles": {
"$id": "#/properties/specialFiles",
"type": "array",
"title": "The specialFiles schema",
"description": "Files that are not normal blog posts.",
"default": [],
"examples": [
[
"about.md",
"resume.md"
]
],
"additionalItems": true,
"items": {
"$id": "#/properties/specialFiles/items",
"anyOf": [
{
"$id": "#/properties/specialFiles/items/anyOf/0",
"type": "string",
"title": "The first anyOf schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"about.md",
"resume.md"
]
}
]
}
}
}
}

View File

@ -1,19 +0,0 @@
import { MainContainer, ContentContainer } from '../containers'
const routes = [
{
path: '/',
exact: true,
component: MainContainer
},
{
path: '/post/:postname',
component: ContentContainer
},
{
path: '/resume',
component: ContentContainer
}
]
export default routes

View File

@ -1,144 +0,0 @@
import fs from 'fs'
import path from 'path'
import fm from 'front-matter'
import moment from 'moment'
import zlib from 'zlib'
import chokidar from 'chokidar'
export class Scanner {
constructor (config, dataHolder) {
this.config = config
this.dataHolder = dataHolder
}
watch () {
const watcher = chokidar.watch(this.config.contentPath, {
ignored: /(^|[/\\])\../, // ignore dotfiles
persistent: true
})
watcher
.on('add', filepath => {
console.log(`[Scanner] File ${filepath} has been added`)
this.addFile(filepath)
})
.on('change', filepath => {
console.log(`[Scanner] File ${filepath} has been changed`)
this.addFile(filepath)
})
.on('unlink', filepath => {
console.log(`[Scanner] File ${filepath} has been removed`)
this.deleteFile(filepath)
})
}
addFile (filepath) {
if (path.extname(filepath) === '.jpg' || path.extname(filepath) === '.png' || path.extname(filepath) === '.gif') {
this.copyImage(filepath)
.then((file) => this.gzipImage(file))
} else {
this.readfile(filepath)
.then((data) => this.processFile(data[0], data[1]))
}
}
deleteFile (filepath) {
this.dataHolder.deleteFile(filepath)
}
readfile (filePath) {
const relPath = path.relative(path.join(process.cwd(), 'content'), filePath)
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
reject(err)
} else {
resolve([relPath, data])
}
})
})
}
copyImage (filePath) {
return new Promise((resolve, reject) => {
const relPath = path.relative(path.join(process.cwd(), 'content'), filePath)
const outputPath = path.join(process.cwd(), 'public/static', relPath)
fs.copyFile(filePath, outputPath, (err) => {
if (err) {
reject(err)
} else {
resolve(relPath)
}
})
})
}
gzipImage (filename) {
return new Promise((resolve, reject) => {
const inputPath = path.join(process.cwd(), 'public/static', filename)
const outputPath = path.join(process.cwd(), 'public/static', `${filename}.gz`)
const fileContents = fs.createReadStream(inputPath)
const writeStream = fs.createWriteStream(outputPath)
const zip = zlib.createGzip()
fileContents.pipe(zip).pipe(writeStream).on('finish', (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
processFile (file, data) {
const filePath = path.join(process.cwd(), 'content', file)
const metadata = this.fileMetadata(filePath)
if (this.config.specialFiles.indexOf(file) === -1) {
const frontMatter = fm(data)
if (frontMatter.attributes.draft) {
return Promise.resolve()
}
let published
if (frontMatter.attributes.date) {
published = moment(frontMatter.attributes.date)
} else {
published = moment()
}
const summary = frontMatter.body.split('\n\n', 1)[0]
const post = {
published: published.format('MMMM DD, YYYY'),
filename: metadata.filename,
title: frontMatter.attributes.title,
summary: summary,
link: '/post/' + metadata.filename,
body: data
}
this.dataHolder.addPost(post)
} else {
this.dataHolder.addOther(metadata.filename, data)
}
return Promise.resolve()
}
fileMetadata (filepath) {
const paths = filepath.split('/')
const basename = path.basename(filepath)
const metadata = {
basename,
filename: basename.substr(0, basename.lastIndexOf('.')),
parrent: paths[paths.length - 2],
dirname: path.dirname(filepath)
}
return metadata
}
}

View File

@ -1,70 +0,0 @@
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter as Router, matchPath } from 'react-router-dom'
import { App } from '../components'
import routes from './routes'
import serialize from 'serialize-javascript'
import manifest from '../../public/static/manifest.json'
export class ServerRenderer {
constructor (head, config, dataHolder) {
this.head = head
this.config = config
this.dataHolder = dataHolder
}
render (req, res, next) {
const activeRoute = routes.find((route) => matchPath(req.url, route)) || false
const head = this.head
const config = this.config
if (!activeRoute) {
const context = [{}, config]
const markup = renderToString(
<Router location={req.url} context={{ context }}>
<App/>
</Router>
)
res.status(404).send(renderFullPage(markup, head, {}, config))
} else {
const promise = this.dataHolder.getData(req.path)
promise.then((data) => {
const context = [data, config]
const markup = renderToString(
<Router location={req.url} context={{ context }}>
<App/>
</Router>
)
res.status(200).send(renderFullPage(markup, head, data, config))
}).catch(next)
}
}
}
function renderFullPage (html, head, data, config) {
const initialData = [data, config]
return `
<!DOCTYPE html>
<html lang="en">
<head>
<title>${config.title}</title>
<meta name="viewport" content="width=device-width">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&amp;subset=latin-ext" rel="stylesheet" rel="preload">
<!-- Font Awesome -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<!-- Stylesheet -->
<link href=${manifest['bundle.css']} rel="stylesheet" rel="preload">
<!-- Initial Data -->
<script>window.__INITIAL_DATA__ = ${serialize(initialData)}</script>
${head.scripts.join('\n')}
</head>
<body>
<div id="root">${html}</div>
<script src=${manifest['bundle.js']} async></script>
</body>
</html>
`
}

View File

@ -1,70 +0,0 @@
import fs from 'fs'
import path from 'path'
export class FileStorage {
constructor (config) {
this.config = config
this.data = {
posts: [],
other: {}
}
}
addPost (post) {
delete post.body
const postIndex = this._findWithAttr(this.data.posts, 'filename', post.filename)
if (postIndex === -1) {
this.data.posts.push(post)
} else {
this.data.posts[postIndex] = post
}
}
addOther (filename, data) {
this.data.other[filename] = data
}
deleteFile (filepath) {
this.data.posts = this.data.posts.filter((post) =>
post.filename !== filepath
)
}
getData (reqPath) {
return this._getDataFromFile(reqPath)
}
_getDataFromFile (reqPath) {
reqPath = reqPath.split('/').pop()
if (reqPath === '') {
return Promise.resolve(this.data)
} else if (reqPath === 'resume') {
const fileName = path.join(process.cwd(), 'content', reqPath + '.md')
return this._readFile(fileName, 'resume', 'utf8')
} else {
const fileName = path.join(process.cwd(), 'content', reqPath + '.md')
return this._readFile(fileName, 'post', 'utf8')
}
}
_readFile (fileName, type, options) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, options, (err, data) => {
err ? reject(err) : resolve({
type: type,
data: data
})
})
})
}
_findWithAttr (array, attr, value) {
for (let i = 0; i < array.length; i += 1) {
if (array[i][attr] === value) {
return i
}
}
return -1
}
}

View File

@ -1,124 +0,0 @@
import mongoose, { Schema } from 'mongoose'
export class MongoStorage {
constructor (config) {
this.config = config
const PostSchema = new Schema({
filename: String,
published: String,
title: String,
summary: String,
link: String,
body: String
})
PostSchema.index({
body: 'text',
title: 'text'
})
PostSchema.index({
filename: 'hashed'
})
this.Post = mongoose.model('Post', PostSchema)
const OtherSchema = new Schema({
filename: String,
body: String
})
OtherSchema.index({ filename: 'hashed' })
this.Other = mongoose.model('Other', OtherSchema)
this.options = {
upsert: true,
useFindAndModify: false
}
}
addPost (post) {
const query = {
filename: post.filename
}
this.Post.findOneAndUpdate(query, post, this.options, (err) => {
if (err) throw err
})
}
addOther (filename, data) {
const query = {
filename: filename
}
const update = {
filename: filename,
body: data
}
this.Other.findOneAndUpdate(query, update, this.options, (err) => {
if (err) throw err
})
}
deleteFile (filepath) {
const filename = filepath.split('/').pop()
const basename = filename.split('.')[0]
if (this.config['non-content-files'].indexOf(filename) === -1) {
this.Post.findOneAndDelete({ filename: basename }, (err) => {
if (err) throw err
})
} else {
this.Other.findOneAndDelete({ filename: basename }, (err) => {
if (err) throw err
})
}
}
getData (reqPath) {
if (reqPath === '/') {
const data = {
posts: [],
other: {}
}
return Promise.all([
this._getOther('about'),
this._getAllPosts()
]).then((res) => {
data.other.about = res[0].data
data.posts = res[1]
return data
})
} else if (reqPath.startsWith('/post')) {
return this._getPost(reqPath.split('/').pop())
} else {
return this._getOther(reqPath.split('/').pop())
}
}
_getOther (filename) {
return new Promise((resolve, reject) => {
this.Other.findOne({ filename: filename }, (err, data) => {
err ? reject(err) : resolve({
type: filename,
data: data.body
})
})
})
}
_getAllPosts () {
return new Promise((resolve, reject) => {
this.Post.find({}, { body: false }, (err, data) => {
err ? reject(err) : resolve(data)
})
})
}
_getPost (filename) {
return new Promise((resolve, reject) => {
this.Post.findOne({ filename: filename }, (err, data) => {
err ? reject(err) : resolve({
type: 'content',
data: data.body
})
})
})
}
}

View File

@ -1,75 +0,0 @@
const { resolve } = require('path')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const ManifestPlugin = require('webpack-manifest-plugin')
const WebpackCleanupPlugin = require('webpack-cleanup-plugin')
const browserConfig = {
entry: {
bundle: [
'./src/app-client.js'
]
},
output: {
path: resolve(__dirname, 'public/static'),
filename: '[name].[contenthash].js',
publicPath: '/static/'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
],
include: resolve(__dirname, 'src')
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
},
importLoaders: 2
}
},
{
loader: 'postcss-loader'
},
{
loader: 'sass-loader'
}
]
},
{
test: /\.(png|jpg)$/,
exclude: /node_modules/,
loader: 'url-loader',
options: {
limit: 8192
}
}
]
},
optimization: {
minimizer: [
new UglifyJsPlugin(),
new OptimizeCSSAssetsPlugin({})
]
},
plugins: [
new ManifestPlugin(),
new WebpackCleanupPlugin(),
new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
new CompressionPlugin({})
],
node: { fs: 'empty' }
}
module.exports = browserConfig

View File

@ -1,61 +0,0 @@
const { resolve } = require('path')
const nodeExternals = require('webpack-node-externals')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const WebpackCleanupPlugin = require('webpack-cleanup-plugin')
const serverConfig = {
entry: './src/server.js',
target: 'node',
externals: [nodeExternals()],
output: {
path: resolve(__dirname, 'build'),
filename: 'server.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader'
],
exclude: '/node_modules/'
},
{
test: /\.scss$/,
use: [
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
},
onlyLocals: true,
importLoaders: 2
}
},
{
loader: 'postcss-loader'
},
{
loader: 'sass-loader'
}
]
},
{
test: /\.(png|jpg)$/,
exclude: /node_modules/,
loader: 'url-loader',
options: {
limit: 10000
}
}
]
},
plugins: [
new MiniCssExtractPlugin(),
new WebpackCleanupPlugin()
]
}
module.exports = serverConfig

8686
yarn.lock

File diff suppressed because it is too large Load Diff