This commit is contained in:
Jori Lallo
2016-04-28 22:25:37 -07:00
parent 2f9233222d
commit cce82b3d43
79 changed files with 1495 additions and 496 deletions

1
.env.local Normal file
View File

@ -0,0 +1 @@
SEQUELIZE_SECRET=7a947f4b6b5cd4c769029f7b5130c85fe2d3484758c0eeee351cd2e2d2c3bd59

37
_server.js Normal file
View File

@ -0,0 +1,37 @@
var path = require('path');
var express = require('express');
var app = express();
var port = process.env.PORT || 3000;
if (process.env.NODE_ENV !== 'production') {
var webpack = require('webpack');
var config = require('./webpack.config.dev');
var compiler = webpack(config);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
} else {
app.use('/static', express.static('dist'));
}
// Frontend
app.get('/service-worker.js', function(req, res) {
res.header("Content-Type", "application/javascript");
res.sendFile(path.join(__dirname, 'service-worker.js'));
});
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(port, function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at ' + port);
});

50
dist/bundle.js vendored

File diff suppressed because one or more lines are too long

2
dist/bundle.js.map vendored
View File

@ -1 +1 @@
{"version":3,"file":"bundle.js","sources":["webpack:///bundle.js","webpack:///"],"mappings":"AAAA;ACi2GA;AAmhFA;AAw2GA;AA99DA;AAugFA;AAy4HA;;;;;;;;;;;;;;AA+uFA;AAguOA;AA3iLA;AA8+jBA;AA4uDA;;;;;AAwpCA;;;;;AAsDA;AA85DA;AA+nDA;AAo7GA;AAu1FA;AAghEA;AA8pEA;AA45GA;AA03HA;AAu8IA;AAu/EA;AAMA;AAMA;AAMA;AAMA","sourceRoot":""}
{"version":3,"file":"bundle.js","sources":["webpack:///bundle.js","webpack:///"],"mappings":"AAAA;ACw2GA;AA6gFA;AAm2GA;AAp+DA;AAshFA;AAk2HA;;;;;;;;;;;;;;AAyiGA;AAgnOA;AA0yTA;AAstGA;AA8tFA;;;;;AAigDA;;;;;AAsDA;AAi6DA;AAs0BA;AAy0HA;AAkqBA;AA22FA;AAuhEA;AA4pEA;AAsjEA;AAwjIA;AA+mHA;AA21KA;AAmbA;AAMA;AAMA;AAMA;AAMA","sourceRoot":""}

View File

@ -3,20 +3,19 @@
<head>
<title>Beautiful Atlas</title>
<link href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.11.0/codemirror.min.css' rel='stylesheet'>
<style type="text/css">
html, body, .viewport {
width: 100%;
height: 100%;
margin: 0;
}
.Codemirror {
height: auto;
}
</style>
</head>
<body style='display: flex; width: 100%'>
<div id="root" style='display: flex; width: 100%'></div>
<script src="/static/bundle.js"></script>
<script type="text/javascript">
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/service-worker.js')
// .then(function(reg) {
// console.log('SW registration succeeded');
// }).catch(function(error) {
// console.log('SW registration failed: ' + error);
// });
// };
</script>
</body>
</html>

13
index.js Normal file
View File

@ -0,0 +1,13 @@
require('./init');
var app = require('./server').default;
var http = require('http');
var server = http.createServer(app.callback());
server.listen(process.env.PORT || '3000');
server.on('error', (err) => {
throw err;
});
server.on('listening', () => {
var address = server.address();
console.log('Listening on %s%s', address.address, address.port);
});

4
init.js Normal file
View File

@ -0,0 +1,4 @@
require('safestart')(__dirname);
require('babel-core/register')
require('babel-polyfill');
require('localenv');

View File

@ -7,7 +7,7 @@
"clean": "rimraf dist",
"build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors",
"build": "npm run clean && npm run build:webpack",
"start": "node server.js",
"start": "cross-env NODE_ENV=development DEBUG=1 ./node_modules/.bin/nodemon --watch server index.js",
"lint": "eslint src"
},
"repository": {
@ -21,19 +21,43 @@
},
"homepage": "https://github.com/jorilallo/atlas#readme",
"dependencies": {
"express": "^4.13.4",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"body-parser": "^1.15.0"
},
"devDependencies": {
"babel-core": "^6.4.5",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.1",
"babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-react-hmre": "^1.0.1",
"babel-preset-stage-0": "^6.5.0",
"debug": "^2.2.0",
"http-errors": "^1.4.0",
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^5.7.0",
"koa": "^2.0.0",
"koa-bodyparser": "^2.0.1",
"koa-compress": "^2.0.0",
"koa-connect": "^1.0.0",
"koa-helmet": "^1.0.0",
"koa-jwt": "^1.2.0",
"koa-logger": "^2.0.0",
"koa-mount": "^2.0.0",
"koa-router": "^7.0.1",
"koa-sendfile": "^2.0.0",
"koa-webpack-dev-middleware": "^1.2.0",
"localenv": "^0.2.2",
"pg": "^4.5.3",
"pg-hstore": "^2.3.2",
"querystring": "^0.2.0",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"react-keyframes": "^0.1.4",
"safestart": "^0.8.0",
"sequelize": "^3.21.0",
"sequelize-cli": "^2.3.1",
"sequelize-encrypted": "^0.1.0",
"validator": "^5.2.0"
},
"devDependencies": {
"classnames": "^2.2.3",
"codemirror": "^5.11.0",
"cross-env": "^1.0.7",
@ -43,14 +67,20 @@
"eslint-config-airbnb": "^5.0.0",
"eslint-plugin-react": "^3.16.1",
"exports-loader": "^0.6.3",
"fetch": "^1.0.1",
"file-loader": "^0.8.5",
"fsevents": "^1.0.11",
"history": "^1.17.0",
"imports-loader": "^0.6.5",
"json-loader": "^0.5.4",
"koa-convert": "^1.2.0",
"koa-webpack-dev-middleware": "^1.2.0",
"koa-webpack-hot-middleware": "^1.0.3",
"localforage": "^1.4.0",
"lodash": "^4.3.0",
"marked": "^0.3.5",
"node-dev": "^3.1.0",
"node-sass": "^3.4.2",
"nodemon": "^1.9.1",
"normalize.css": "^3.0.3",
"react": "^0.14.7",
"react-codemirror": "^0.2.5",
@ -58,16 +88,19 @@
"react-medium-editor": "^1.6.2",
"react-redux": "^4.4.0",
"react-router": "^2.0.0",
"react-router-redux": "^4.0.4",
"redux": "^3.3.1",
"redux-logger": "^2.6.1",
"redux-persist": "^3.0.1",
"redux-storage": "^4.0.0",
"redux-storage-engine-localstorage": "^1.0.0",
"sass-loader": "^3.1.2",
"redux-thunk": "^2.0.1",
"sass-loader": "^3.2.0",
"style-loader": "^0.13.0",
"sw-toolbox": "^3.1.1",
"to-markdown": "^2.0.1",
"url-loader": "^0.5.7",
"webpack": "^1.12.12",
"webpack-dev-middleware": "^1.5.1",
"webpack-hot-middleware": "^2.6.4",
"whatwg-fetch": "^0.11.0"
"webpack-koa-hot-middleware": "^0.1.2"
}
}

View File

@ -1,83 +0,0 @@
var path = require('path');
var express = require('express');
var app = express();
var port = process.env.PORT || 3000;
if (process.env.NODE_ENV !== 'production') {
var webpack = require('webpack');
var config = require('./webpack.config.dev');
var compiler = webpack(config);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
} else {
app.use('/static', express.static('dist'));
}
// API stubs - Feel free to tear these down in favor of rolling out proper APIs
// Also `body-parser` module is included only for this
var router = express.Router();
var validJwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
function isAuthenticated(req, res, next) {
// Authenticate with JWT
if (req.headers.authorization) {
var tokenParts = req.headers.authorization.split(" ");
if (tokenParts.length === 2 &&
tokenParts[0].trim().toUpperCase() === "JWT" &&
tokenParts[1].trim() === validJwtToken
) {
return next();
}
}
// Return 401 with invalid credentials
res.status(401).json({
'error': 'Invalid JWT token'
});
}
router.post('/authenticate', function(req, res) {
if (req.body.email === 'user1@example.com' &&
req.body.password === 'test123!') {
res.json({
'jwt_token': validJwtToken,
});
} else {
res.status(400).json({
'error': 'Invalid credentials'
});
}
});
router.get('/user', isAuthenticated, function(req, res) {
res.json({
id: '93c3a6d6-3958-44c9-a668-59711befb25c',
email: 'user1@example.com',
name: 'Test User'
});
});
// Register API
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use('/api', router);
// Frontend
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(port, function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at ' + port);
});

73
server/api/auth.js Normal file
View File

@ -0,0 +1,73 @@
import Router from 'koa-router';
import httpErrors from 'http-errors';
import fetch from 'isomorphic-fetch';
var querystring = require('querystring');
import { presentUser, presentTeam } from '../presenters';
import { User, Team } from '../models';
const router = new Router();
router.post('auth.slack', async (ctx) => {
const { code } = ctx.request.body;
ctx.assertPresent(code, 'code is required');
const body = {
client_id: process.env.SLACK_KEY,
client_secret: process.env.SLACK_SECRET,
code: code,
redirect_uri: process.env.SLACK_REDIRECT_URI,
}
let data;
try {
const response = await fetch('https://slack.com/api/oauth.access?' + querystring.stringify(body));
data = await response.json();
} catch(e) {
throw httpErrors.BadRequest();
}
if (!data.ok) throw httpErrors.BadRequest(data.error);
// User
let userData;
let user = await User.findOne({ slackId: data.user_id });
if (user) {
user.slackAccessToken = data.access_token;
user.save();
} else {
// Find existing user
const userParams = { token: data.access_token, user: data.user_id }
const response = await fetch('https://slack.com/api/users.info?' + querystring.stringify(userParams));
userData = await response.json();
user = await User.create({
slackId: data.user_id,
username: userData.user.name,
name: userData.user.profile.real_name,
email: userData.user.profile.email,
slackData: userData.user,
slackAccessToken: data.access_token,
});
}
// Team
let team = await Team.findOne({ slackId: data.team_id });
if (!team) {
team = await Team.create({
slackId: data.team_id,
name: data.team_name,
});
}
// Add to correct team
user.setTeam(team);
ctx.body = { data: {
user: presentUser(user),
team: presentTeam(team),
accessToken: user.getJwtToken(),
}};
});
export default router;

View File

@ -0,0 +1,61 @@
import httpErrors from 'http-errors';
import JWT from 'jsonwebtoken';
import { User } from '../models';
export default function auth({ require = true } = {}) {
return async function authMiddleware(ctx, next) {
let token;
const authorizationHeader = ctx.request.get('authorization');
if (authorizationHeader) {
const parts = authorizationHeader.split(' ');
if (parts.length == 2) {
const scheme = parts[0];
const credentials = parts[1];
if (/^Bearer$/i.test(scheme)) {
token = credentials;
}
} else {
if (require) {
throw httpErrors.Unauthorized('Bad Authorization header format. Format is "Authorization: Bearer <token>"\n');
}
}
} else if (ctx.request.query.token) {
token = ctx.request.query.token;
}
if (!token && require) {
throw httpErrors.Unauthorized('Authentication required');
}
// Get user without verifying payload signature
let payload;
try {
payload = JWT.decode(token);
} catch(_e) {
throw httpErrors.Unauthorized('Unable to decode JWT token');
}
console.log(payload)
const user = await User.findOne({
where: { id: payload.id },
});
try {
JWT.verify(token, user.jwtSecret);
} catch(e) {
throw httpErrors.Unauthorized('Invalid token');
}
ctx.state.token = token;
ctx.state.user = user;
return next();
};
};
// Export JWT methods as a convenience
export const sign = JWT.sign;
export const verify = JWT.verify;
export const decode = JWT.decode;

55
server/api/index.js Normal file
View File

@ -0,0 +1,55 @@
import bodyParser from 'koa-bodyparser';
import httpErrors from 'http-errors';
import Koa from 'koa';
import Router from 'koa-router';
import Sequelize from 'sequelize';
import auth from './auth';
import user from './user';
import validation from './validation';
const api = new Koa();
const router = new Router();
// API error handler
api.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
let message = err.message || err.name;
if (err instanceof Sequelize.ValidationError) {
// super basic form error handling
ctx.status = 400;
if (err.errors && err.errors[0]) {
message = `${err.errors[0].message} (${err.errors[0].path})`;
}
}
if (ctx.status === 500) {
message = 'Internal Server Error';
ctx.app.emit('error', err, ctx);
}
ctx.body = { message };
}
});
api.use(bodyParser());
api.use(validation());
router.use('/', auth.routes());
router.use('/', user.routes());
// Router is embedded in a Koa application wrapper, because koa-router does not
// allow middleware to catch any routes which were not explicitly defined.
api.use(router.routes());
// API 404 handler
api.use(async () => {
throw httpErrors.NotFound();
});
export default api;

13
server/api/user.js Normal file
View File

@ -0,0 +1,13 @@
import Router from 'koa-router';
import auth from './authentication';
import { presentUser } from '../presenters';
import { User } from '../models';
const router = new Router();
router.post('user.info', auth(), async (ctx) => {
ctx.body = { data: presentUser(ctx.state.user) };
});
export default router;

26
server/api/validation.js Normal file
View File

@ -0,0 +1,26 @@
import httpErrors from 'http-errors';
import validator from 'validator';
export default function validation() {
return function validationMiddleware(ctx, next) {
ctx.assertPresent = function assertPresent(value, message) {
if (value === undefined || value === null || value === '') {
throw httpErrors.BadRequest(message);
}
};
ctx.assertEmail = function assertEmail(value, message) {
if (!validator.isEmail(value)) {
throw httpErrors.BadRequest(message);
}
};
ctx.assertUuid = function assertUuid(value, message) {
if (!validator.isUUID(value)) {
throw httpErrors.BadRequest(message);
}
};
return next();
};
}

66
server/index.js Normal file
View File

@ -0,0 +1,66 @@
import compress from 'koa-compress';
import helmet from 'koa-helmet';
import logger from 'koa-logger';
import mount from 'koa-mount';
import Koa from 'koa';
import api from './api';
import routes from './routes';
const app = new Koa();
app.use(compress());
if (process.env.NODE_ENV === 'development') {
const convert = require('koa-convert')
const webpack = require('webpack');
const devMiddleware = require('koa-webpack-dev-middleware');
const hotMiddleware = require('koa-webpack-hot-middleware');
const config = require('../webpack.config.dev');
const compile = webpack(config);
app.use(convert(devMiddleware(compile, {
// display no info to console (only warnings and errors)
noInfo: false,
// display nothing to the console
quiet: false,
// switch into lazy mode
// that means no watching, but recompilation on every request
lazy: false,
// // watch options (only lazy: false)
// watchOptions: {
// aggregateTimeout: 300,
// poll: true
// },
// public path to bind the middleware to
// use the same as in webpack
publicPath: config.output.publicPath,
// options for formating the statistics
stats: {
colors: true
}
})));
app.use(convert(hotMiddleware(compile, {
log: console.log,
path: '/__webpack_hmr',
heartbeat: 10 * 1000
})));
app.use(logger());
}
app.use(mount('/api', api));
app.use(mount(routes));
app.use(helmet.csp({
directives: {
defaultSrc: ['\'self\''],
styleSrc: ['\'self\'', '\'unsafe-inline\''],
},
}));
export default app;

14
server/models/Atlas.js Normal file
View File

@ -0,0 +1,14 @@
import {
DataTypes,
sequelize,
} from '../sequelize';
import Team from './Team';
const Atlas = sequelize.define('atlas', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
});
Atlas.belongsTo(Team);
export default Atlas;

17
server/models/Document.js Normal file
View File

@ -0,0 +1,17 @@
import {
DataTypes,
sequelize,
} from '../sequelize';
import Atlas from './Atlas';
import Team from './Team';
const Document = sequelize.define('document', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
content: DataTypes.STRING,
});
Document.belongsTo(Atlas);
Document.belongsTo(Team);
export default Atlas;

20
server/models/Team.js Normal file
View File

@ -0,0 +1,20 @@
import {
DataTypes,
sequelize,
} from '../sequelize';
const Team = sequelize.define('team', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
slackId: { type: DataTypes.STRING, unique: true },
slackData: DataTypes.JSONB,
}, {
indexes: [
{
unique: true,
fields: ['slackId']
},
],
});
export default Team;

44
server/models/User.js Normal file
View File

@ -0,0 +1,44 @@
import crypto from 'crypto';
import {
DataTypes,
sequelize,
encryptedFields
} from '../sequelize';
import Team from './Team';
import JWT from 'jsonwebtoken';
const User = sequelize.define('user', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
email: DataTypes.STRING,
username: DataTypes.STRING,
name: DataTypes.STRING,
isAdmin: DataTypes.BOOLEAN,
slackAccessToken: encryptedFields.vault('slackAccessToken'),
slackId: { type: DataTypes.STRING, unique: true },
slackData: DataTypes.JSONB,
jwtSecret: encryptedFields.vault('jwtSecret'),
}, {
instanceMethods: {
getJwtToken() {
return JWT.sign({ id: this.id }, this.jwtSecret);
},
},
indexes: [
{
unique: true,
fields: ['email']
},
],
});
const setRandomJwtSecret = (model) => {
model.jwtSecret = crypto.randomBytes(64).toString('hex');
};
User.beforeCreate(setRandomJwtSecret);
User.belongsTo(Team);
sequelize.sync();
export default User;

6
server/models/index.js Normal file
View File

@ -0,0 +1,6 @@
import User from './User';
import Team from './Team';
import Atlas from './Atlas';
import Document from './Document';
export { User, Team, Atlas, Document };

16
server/presenters.js Normal file
View File

@ -0,0 +1,16 @@
export function presentUser(user) {
return {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
avatarUrl: user.slackData.profile.image_192,
};
}
export function presentTeam(team) {
return {
id: team.id,
name: team.name,
};
}

39
server/routes.js Normal file
View File

@ -0,0 +1,39 @@
const path = require('path');
import httpErrors from 'http-errors';
import Koa from 'koa';
import Router from 'koa-router';
import sendfile from 'koa-sendfile';
const koa = new Koa();
const router = new Router();
// // error handler
// koa.use(async (ctx, next) => {
// try {
// await next();
// } catch (err) {
// ctx.status = err.status || 500;
// ctx.body = err.message;
// }
// });
// Frontend
router.get('/service-worker.js', async (ctx) => {
ctx.set('Content-Type', 'application/javascript');
const stats = await sendfile(ctx, path.join(__dirname, 'static/service-worker.js'));
if (!ctx.status) ctx.throw(httpErrors.NotFound());
});
router.get('*', async (ctx) => {
const stats = await sendfile(ctx, path.join(__dirname, 'static/index.html'));
if (!ctx.status) ctx.throw(httpErrors.NotFound());
});
koa.use(router.routes());
// 404 handler
koa.use(async () => {
throw httpErrors.NotFound();
});
export default koa;

13
server/sequelize.js Normal file
View File

@ -0,0 +1,13 @@
import Sequelize from 'sequelize';
import EncryptedField from 'sequelize-encrypted';
import debug from 'debug';
const secretKey = process.env.SEQUELIZE_SECRET;
export const encryptedFields = EncryptedField(Sequelize, secretKey);
export const DataTypes = Sequelize;
export const sequelize = new Sequelize(process.env.DATABASE_URL, {
logging: debug('sql'),
typeValidation: true,
});

21
server/static/index.html Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<title>Beautiful Atlas</title>
<link href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.11.0/codemirror.min.css' rel='stylesheet'>
</head>
<body style='display: flex; width: 100%'>
<div id="root" style='display: flex; width: 100%'></div>
<script src="/static/bundle.js"></script>
<script type="text/javascript">
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/service-worker.js')
// .then(function(reg) {
// console.log('SW registration succeeded');
// }).catch(function(error) {
// console.log('SW registration failed: ' + error);
// });
// };
</script>
</body>
</html>

View File

@ -0,0 +1,84 @@
/*
Copyright 2014 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.toolbox = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";function debug(e,n){n=n||{};var t=n.debug||globalOptions.debug;t&&console.log("[sw-toolbox] "+e)}function openCache(e){var n;return e&&e.cache&&(n=e.cache.name),n=n||globalOptions.cache.name,caches.open(n)}function fetchAndCache(e,n){n=n||{};var t=n.successResponses||globalOptions.successResponses;return fetch(e.clone()).then(function(c){return"GET"===e.method&&t.test(c.status)&&openCache(n).then(function(t){t.put(e,c).then(function(){var c=n.cache||globalOptions.cache;(c.maxEntries||c.maxAgeSeconds)&&c.name&&queueCacheExpiration(e,t,c)})}),c.clone()})}function queueCacheExpiration(e,n,t){var c=cleanupCache.bind(null,e,n,t);cleanupQueue=cleanupQueue?cleanupQueue.then(c):c()}function cleanupCache(e,n,t){var c=e.url,a=t.maxAgeSeconds,u=t.maxEntries,o=t.name,r=Date.now();return debug("Updating LRU order for "+c+". Max entries is "+u+", max age is "+a),idbCacheExpiration.getDb(o).then(function(e){return idbCacheExpiration.setTimestampForUrl(e,c,r)}).then(function(e){return idbCacheExpiration.expireEntries(e,u,a,r)}).then(function(e){debug("Successfully updated IDB.");var t=e.map(function(e){return n["delete"](e)});return Promise.all(t).then(function(){debug("Done with cache cleanup.")})})["catch"](function(e){debug(e)})}function renameCache(e,n,t){return debug("Renaming cache: ["+e+"] to ["+n+"]",t),caches["delete"](n).then(function(){return Promise.all([caches.open(e),caches.open(n)]).then(function(n){var t=n[0],c=n[1];return t.keys().then(function(e){return Promise.all(e.map(function(e){return t.match(e).then(function(n){return c.put(e,n)})}))}).then(function(){return caches["delete"](e)})})})}var globalOptions=require("./options"),idbCacheExpiration=require("./idb-cache-expiration"),cleanupQueue;module.exports={debug:debug,fetchAndCache:fetchAndCache,openCache:openCache,renameCache:renameCache};
},{"./idb-cache-expiration":2,"./options":3}],2:[function(require,module,exports){
"use strict";function openDb(e){return new Promise(function(r,n){var t=indexedDB.open(DB_PREFIX+e,DB_VERSION);t.onupgradeneeded=function(){var e=t.result.createObjectStore(STORE_NAME,{keyPath:URL_PROPERTY});e.createIndex(TIMESTAMP_PROPERTY,TIMESTAMP_PROPERTY,{unique:!1})},t.onsuccess=function(){r(t.result)},t.onerror=function(){n(t.error)}})}function getDb(e){return e in cacheNameToDbPromise||(cacheNameToDbPromise[e]=openDb(e)),cacheNameToDbPromise[e]}function setTimestampForUrl(e,r,n){return new Promise(function(t,o){var i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME);u.put({url:r,timestamp:n}),i.oncomplete=function(){t(e)},i.onabort=function(){o(i.error)}})}function expireOldEntries(e,r,n){return r?new Promise(function(t,o){var i=1e3*r,u=[],c=e.transaction(STORE_NAME,"readwrite"),s=c.objectStore(STORE_NAME),a=s.index(TIMESTAMP_PROPERTY);a.openCursor().onsuccess=function(e){var r=e.target.result;if(r&&n-i>r.value[TIMESTAMP_PROPERTY]){var t=r.value[URL_PROPERTY];u.push(t),s["delete"](t),r["continue"]()}},c.oncomplete=function(){t(u)},c.onabort=o}):Promise.resolve([])}function expireExtraEntries(e,r){return r?new Promise(function(n,t){var o=[],i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME),c=u.index(TIMESTAMP_PROPERTY),s=c.count();c.count().onsuccess=function(){var e=s.result;e>r&&(c.openCursor().onsuccess=function(n){var t=n.target.result;if(t){var i=t.value[URL_PROPERTY];o.push(i),u["delete"](i),e-o.length>r&&t["continue"]()}})},i.oncomplete=function(){n(o)},i.onabort=t}):Promise.resolve([])}function expireEntries(e,r,n,t){return expireOldEntries(e,n,t).then(function(n){return expireExtraEntries(e,r).then(function(e){return n.concat(e)})})}var DB_PREFIX="sw-toolbox-",DB_VERSION=1,STORE_NAME="store",URL_PROPERTY="url",TIMESTAMP_PROPERTY="timestamp",cacheNameToDbPromise={};module.exports={getDb:getDb,setTimestampForUrl:setTimestampForUrl,expireEntries:expireEntries};
},{}],3:[function(require,module,exports){
"use strict";var scope;scope=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,module.exports={cache:{name:"$$$toolbox-cache$$$"+scope+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/};
},{}],4:[function(require,module,exports){
"use strict";var url=new URL("./",self.location),basePath=url.pathname,pathRegexp=require("path-to-regexp"),Route=function(e,t,i,s){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=basePath+t),this.keys=[],this.regexp=pathRegexp(t,this.keys)),this.method=e,this.options=s,this.handler=i};Route.prototype.makeHandler=function(e){var t;if(this.regexp){var i=this.regexp.exec(e);t={},this.keys.forEach(function(e,s){t[e.name]=i[s+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},module.exports=Route;
},{"path-to-regexp":13}],5:[function(require,module,exports){
"use strict";function regexEscape(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var Route=require("./route"),keyMatch=function(e,t){for(var r=e.entries(),o=r.next();!o.done;){var n=new RegExp(o.value[0]);if(n.test(t))return o.value[1];o=r.next()}return null},Router=function(){this.routes=new Map,this["default"]=null};["get","post","put","delete","head","any"].forEach(function(e){Router.prototype[e]=function(t,r,o){return this.add(e,t,r,o)}}),Router.prototype.add=function(e,t,r,o){o=o||{};var n;t instanceof RegExp?n=RegExp:(n=o.origin||self.location.origin,n=n instanceof RegExp?n.source:regexEscape(n)),e=e.toLowerCase();var u=new Route(e,t,r,o);this.routes.has(n)||this.routes.set(n,new Map);var a=this.routes.get(n);a.has(e)||a.set(e,new Map);var s=a.get(e),i=u.regexp||u.fullUrlRegExp;s.set(i.source,u)},Router.prototype.matchMethod=function(e,t){var r=new URL(t),o=r.origin,n=r.pathname;return this._match(e,keyMatch(this.routes,o),n)||this._match(e,this.routes.get(RegExp),t)},Router.prototype._match=function(e,t,r){if(t){var o=t.get(e.toLowerCase());if(o){var n=keyMatch(o,r);if(n)return n.makeHandler(r)}}return null},Router.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},module.exports=new Router;
},{"./route":4}],6:[function(require,module,exports){
"use strict";function cacheFirst(e,r,t){return helpers.debug("Strategy: cache first ["+e.url+"]",t),helpers.openCache(t).then(function(r){return r.match(e).then(function(r){return r?r:helpers.fetchAndCache(e,t)})})}var helpers=require("../helpers");module.exports=cacheFirst;
},{"../helpers":1}],7:[function(require,module,exports){
"use strict";function cacheOnly(e,r,c){return helpers.debug("Strategy: cache only ["+e.url+"]",c),helpers.openCache(c).then(function(r){return r.match(e)})}var helpers=require("../helpers");module.exports=cacheOnly;
},{"../helpers":1}],8:[function(require,module,exports){
"use strict";function fastest(e,n,t){return helpers.debug("Strategy: fastest ["+e.url+"]",t),new Promise(function(r,s){var c=!1,o=[],a=function(e){o.push(e.toString()),c?s(new Error('Both cache and network failed: "'+o.join('", "')+'"')):c=!0},h=function(e){e instanceof Response?r(e):a("No result returned")};helpers.fetchAndCache(e.clone(),t).then(h,a),cacheOnly(e,n,t).then(h,a)})}var helpers=require("../helpers"),cacheOnly=require("./cacheOnly");module.exports=fastest;
},{"../helpers":1,"./cacheOnly":7}],9:[function(require,module,exports){
module.exports={networkOnly:require("./networkOnly"),networkFirst:require("./networkFirst"),cacheOnly:require("./cacheOnly"),cacheFirst:require("./cacheFirst"),fastest:require("./fastest")};
},{"./cacheFirst":6,"./cacheOnly":7,"./fastest":8,"./networkFirst":10,"./networkOnly":11}],10:[function(require,module,exports){
"use strict";function networkFirst(e,r,t){t=t||{};var s=t.successResponses||globalOptions.successResponses,n=t.networkTimeoutSeconds||globalOptions.networkTimeoutSeconds;return helpers.debug("Strategy: network first ["+e.url+"]",t),helpers.openCache(t).then(function(r){var o,u,c=[];if(n){var i=new Promise(function(t){o=setTimeout(function(){r.match(e).then(function(e){e&&t(e)})},1e3*n)});c.push(i)}var a=helpers.fetchAndCache(e,t).then(function(e){if(o&&clearTimeout(o),s.test(e.status))return e;throw helpers.debug("Response was an HTTP error: "+e.statusText,t),u=e,new Error("Bad response")})["catch"](function(){return helpers.debug("Network or response error, fallback to cache ["+e.url+"]",t),r.match(e).then(function(e){return e||u})});return c.push(a),Promise.race(c)})}var globalOptions=require("../options"),helpers=require("../helpers");module.exports=networkFirst;
},{"../helpers":1,"../options":3}],11:[function(require,module,exports){
"use strict";function networkOnly(e,r,t){return helpers.debug("Strategy: network only ["+e.url+"]",t),fetch(e)}var helpers=require("../helpers");module.exports=networkOnly;
},{"../helpers":1}],12:[function(require,module,exports){
"use strict";function cache(e,t){return helpers.openCache(t).then(function(t){return t.add(e)})}function uncache(e,t){return helpers.openCache(t).then(function(t){return t["delete"](e)})}function precache(e){Array.isArray(e)||(e=[e]),options.preCacheItems=options.preCacheItems.concat(e)}require("serviceworker-cache-polyfill");var options=require("./options"),router=require("./router"),helpers=require("./helpers"),strategies=require("./strategies");helpers.debug("Service Worker Toolbox is loading");var flatten=function(e){return e.reduce(function(e,t){return e.concat(t)},[])};self.addEventListener("install",function(e){var t=options.cache.name+"$$$inactive$$$";helpers.debug("install event fired"),helpers.debug("creating cache ["+t+"]"),e.waitUntil(helpers.openCache({cache:{name:t}}).then(function(e){return Promise.all(options.preCacheItems).then(flatten).then(function(t){return helpers.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}),self.addEventListener("activate",function(e){helpers.debug("activate event fired");var t=options.cache.name+"$$$inactive$$$";e.waitUntil(helpers.renameCache(t,options.cache.name))}),self.addEventListener("fetch",function(e){var t=router.match(e.request);t?e.respondWith(t(e.request)):router["default"]&&"GET"===e.request.method&&e.respondWith(router["default"](e.request))}),module.exports={networkOnly:strategies.networkOnly,networkFirst:strategies.networkFirst,cacheOnly:strategies.cacheOnly,cacheFirst:strategies.cacheFirst,fastest:strategies.fastest,router:router,options:options,cache:cache,uncache:uncache,precache:precache};
},{"./helpers":1,"./options":3,"./router":5,"./strategies":9,"serviceworker-cache-polyfill":15}],13:[function(require,module,exports){
function parse(e){for(var t,r=[],n=0,o=0,p="";null!=(t=PATH_REGEXP.exec(e));){var a=t[0],i=t[1],s=t.index;if(p+=e.slice(o,s),o=s+a.length,i)p+=i[1];else{p&&(r.push(p),p="");var u=t[2],c=t[3],l=t[4],f=t[5],g=t[6],x=t[7],h="+"===g||"*"===g,m="?"===g||"*"===g,y=u||"/",T=l||f||(x?".*":"[^"+y+"]+?");r.push({name:c||n++,prefix:u||"",delimiter:y,optional:m,repeat:h,pattern:escapeGroup(T)})}}return o<e.length&&(p+=e.substr(o)),p&&r.push(p),r}function compile(e){return tokensToFunction(parse(e))}function tokensToFunction(e){for(var t=new Array(e.length),r=0;r<e.length;r++)"object"==typeof e[r]&&(t[r]=new RegExp("^"+e[r].pattern+"$"));return function(r){for(var n="",o=r||{},p=0;p<e.length;p++){var a=e[p];if("string"!=typeof a){var i,s=o[a.name];if(null==s){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to be defined')}if(isarray(s)){if(!a.repeat)throw new TypeError('Expected "'+a.name+'" to not repeat, but received "'+s+'"');if(0===s.length){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to not be empty')}for(var u=0;u<s.length;u++){if(i=encodeURIComponent(s[u]),!t[p].test(i))throw new TypeError('Expected all "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=(0===u?a.prefix:a.delimiter)+i}}else{if(i=encodeURIComponent(s),!t[p].test(i))throw new TypeError('Expected "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=a.prefix+i}}else n+=a}return n}}function escapeString(e){return e.replace(/([.+*?=^!:${}()[\]|\/])/g,"\\$1")}function escapeGroup(e){return e.replace(/([=!:$\/()])/g,"\\$1")}function attachKeys(e,t){return e.keys=t,e}function flags(e){return e.sensitive?"":"i"}function regexpToRegexp(e,t){var r=e.source.match(/\((?!\?)/g);if(r)for(var n=0;n<r.length;n++)t.push({name:n,prefix:null,delimiter:null,optional:!1,repeat:!1,pattern:null});return attachKeys(e,t)}function arrayToRegexp(e,t,r){for(var n=[],o=0;o<e.length;o++)n.push(pathToRegexp(e[o],t,r).source);var p=new RegExp("(?:"+n.join("|")+")",flags(r));return attachKeys(p,t)}function stringToRegexp(e,t,r){for(var n=parse(e),o=tokensToRegExp(n,r),p=0;p<n.length;p++)"string"!=typeof n[p]&&t.push(n[p]);return attachKeys(o,t)}function tokensToRegExp(e,t){t=t||{};for(var r=t.strict,n=t.end!==!1,o="",p=e[e.length-1],a="string"==typeof p&&/\/$/.test(p),i=0;i<e.length;i++){var s=e[i];if("string"==typeof s)o+=escapeString(s);else{var u=escapeString(s.prefix),c=s.pattern;s.repeat&&(c+="(?:"+u+c+")*"),c=s.optional?u?"(?:"+u+"("+c+"))?":"("+c+")?":u+"("+c+")",o+=c}}return r||(o=(a?o.slice(0,-2):o)+"(?:\\/(?=$))?"),o+=n?"$":r&&a?"":"(?=\\/|$)",new RegExp("^"+o,flags(t))}function pathToRegexp(e,t,r){return t=t||[],isarray(t)?r||(r={}):(r=t,t=[]),e instanceof RegExp?regexpToRegexp(e,t,r):isarray(e)?arrayToRegexp(e,t,r):stringToRegexp(e,t,r)}var isarray=require("isarray");module.exports=pathToRegexp,module.exports.parse=parse,module.exports.compile=compile,module.exports.tokensToFunction=tokensToFunction,module.exports.tokensToRegExp=tokensToRegExp;var PATH_REGEXP=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))"].join("|"),"g");
},{"isarray":14}],14:[function(require,module,exports){
module.exports=Array.isArray||function(r){return"[object Array]"==Object.prototype.toString.call(r)};
},{}],15:[function(require,module,exports){
Cache.prototype.addAll||(Cache.prototype.addAll=function(t){function e(t){this.name="NetworkError",this.code=19,this.message=t}var r=this;return e.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return t=t.map(function(t){return t instanceof Request?t:String(t)}),Promise.all(t.map(function(t){"string"==typeof t&&(t=new Request(t));var r=new URL(t.url).protocol;if("http:"!==r&&"https:"!==r)throw new e("Invalid scheme");return fetch(t.clone())}))}).then(function(e){return Promise.all(e.map(function(e,n){return r.put(t[n],e)}))}).then(function(){})});
},{}]},{},[12])(12)
});
(global => {
'use strict';
// Turn on debug logging, visible in the Developer Tools' console.
global.toolbox.options.debug = true;
// Set up a handler for HTTP GET requests:
// - /\.ytimg\.com\// will match any requests whose URL contains 'ytimg.com'.
// A narrower RegExp could be used, but just checking for ytimg.com anywhere
// in the URL should be fine for this sample.
// - toolbox.cacheFirst let us to use the predefined cache strategy for those
// requests.
global.toolbox.router.get(/\.ytimg\.com\//, global.toolbox.cacheFirst, {
// Use a dedicated cache for the responses, separate from the default cache.
cache: {
name: 'youtube-thumbnails',
// Store up to 10 entries in that cache.
maxEntries: 10,
// Expire any entries that are older than 30 seconds.
maxAgeSeconds: 30
}
});
// By default, all requests that don't match our custom handler will use the
// toolbox.networkFirst cache strategy, and their responses will be stored in
// the default cache.
global.toolbox.router.default = global.toolbox.networkFirst;
// Boilerplate to ensure our service worker takes control of the page as soon
// as possible.
global.addEventListener('install',
event => event.waitUntil(global.skipWaiting()));
global.addEventListener('activate',
event => event.waitUntil(global.clients.claim()));
})(self);

84
service-worker.js Normal file
View File

@ -0,0 +1,84 @@
/*
Copyright 2014 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.toolbox = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";function debug(e,n){n=n||{};var t=n.debug||globalOptions.debug;t&&console.log("[sw-toolbox] "+e)}function openCache(e){var n;return e&&e.cache&&(n=e.cache.name),n=n||globalOptions.cache.name,caches.open(n)}function fetchAndCache(e,n){n=n||{};var t=n.successResponses||globalOptions.successResponses;return fetch(e.clone()).then(function(c){return"GET"===e.method&&t.test(c.status)&&openCache(n).then(function(t){t.put(e,c).then(function(){var c=n.cache||globalOptions.cache;(c.maxEntries||c.maxAgeSeconds)&&c.name&&queueCacheExpiration(e,t,c)})}),c.clone()})}function queueCacheExpiration(e,n,t){var c=cleanupCache.bind(null,e,n,t);cleanupQueue=cleanupQueue?cleanupQueue.then(c):c()}function cleanupCache(e,n,t){var c=e.url,a=t.maxAgeSeconds,u=t.maxEntries,o=t.name,r=Date.now();return debug("Updating LRU order for "+c+". Max entries is "+u+", max age is "+a),idbCacheExpiration.getDb(o).then(function(e){return idbCacheExpiration.setTimestampForUrl(e,c,r)}).then(function(e){return idbCacheExpiration.expireEntries(e,u,a,r)}).then(function(e){debug("Successfully updated IDB.");var t=e.map(function(e){return n["delete"](e)});return Promise.all(t).then(function(){debug("Done with cache cleanup.")})})["catch"](function(e){debug(e)})}function renameCache(e,n,t){return debug("Renaming cache: ["+e+"] to ["+n+"]",t),caches["delete"](n).then(function(){return Promise.all([caches.open(e),caches.open(n)]).then(function(n){var t=n[0],c=n[1];return t.keys().then(function(e){return Promise.all(e.map(function(e){return t.match(e).then(function(n){return c.put(e,n)})}))}).then(function(){return caches["delete"](e)})})})}var globalOptions=require("./options"),idbCacheExpiration=require("./idb-cache-expiration"),cleanupQueue;module.exports={debug:debug,fetchAndCache:fetchAndCache,openCache:openCache,renameCache:renameCache};
},{"./idb-cache-expiration":2,"./options":3}],2:[function(require,module,exports){
"use strict";function openDb(e){return new Promise(function(r,n){var t=indexedDB.open(DB_PREFIX+e,DB_VERSION);t.onupgradeneeded=function(){var e=t.result.createObjectStore(STORE_NAME,{keyPath:URL_PROPERTY});e.createIndex(TIMESTAMP_PROPERTY,TIMESTAMP_PROPERTY,{unique:!1})},t.onsuccess=function(){r(t.result)},t.onerror=function(){n(t.error)}})}function getDb(e){return e in cacheNameToDbPromise||(cacheNameToDbPromise[e]=openDb(e)),cacheNameToDbPromise[e]}function setTimestampForUrl(e,r,n){return new Promise(function(t,o){var i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME);u.put({url:r,timestamp:n}),i.oncomplete=function(){t(e)},i.onabort=function(){o(i.error)}})}function expireOldEntries(e,r,n){return r?new Promise(function(t,o){var i=1e3*r,u=[],c=e.transaction(STORE_NAME,"readwrite"),s=c.objectStore(STORE_NAME),a=s.index(TIMESTAMP_PROPERTY);a.openCursor().onsuccess=function(e){var r=e.target.result;if(r&&n-i>r.value[TIMESTAMP_PROPERTY]){var t=r.value[URL_PROPERTY];u.push(t),s["delete"](t),r["continue"]()}},c.oncomplete=function(){t(u)},c.onabort=o}):Promise.resolve([])}function expireExtraEntries(e,r){return r?new Promise(function(n,t){var o=[],i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME),c=u.index(TIMESTAMP_PROPERTY),s=c.count();c.count().onsuccess=function(){var e=s.result;e>r&&(c.openCursor().onsuccess=function(n){var t=n.target.result;if(t){var i=t.value[URL_PROPERTY];o.push(i),u["delete"](i),e-o.length>r&&t["continue"]()}})},i.oncomplete=function(){n(o)},i.onabort=t}):Promise.resolve([])}function expireEntries(e,r,n,t){return expireOldEntries(e,n,t).then(function(n){return expireExtraEntries(e,r).then(function(e){return n.concat(e)})})}var DB_PREFIX="sw-toolbox-",DB_VERSION=1,STORE_NAME="store",URL_PROPERTY="url",TIMESTAMP_PROPERTY="timestamp",cacheNameToDbPromise={};module.exports={getDb:getDb,setTimestampForUrl:setTimestampForUrl,expireEntries:expireEntries};
},{}],3:[function(require,module,exports){
"use strict";var scope;scope=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,module.exports={cache:{name:"$$$toolbox-cache$$$"+scope+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/};
},{}],4:[function(require,module,exports){
"use strict";var url=new URL("./",self.location),basePath=url.pathname,pathRegexp=require("path-to-regexp"),Route=function(e,t,i,s){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=basePath+t),this.keys=[],this.regexp=pathRegexp(t,this.keys)),this.method=e,this.options=s,this.handler=i};Route.prototype.makeHandler=function(e){var t;if(this.regexp){var i=this.regexp.exec(e);t={},this.keys.forEach(function(e,s){t[e.name]=i[s+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},module.exports=Route;
},{"path-to-regexp":13}],5:[function(require,module,exports){
"use strict";function regexEscape(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var Route=require("./route"),keyMatch=function(e,t){for(var r=e.entries(),o=r.next();!o.done;){var n=new RegExp(o.value[0]);if(n.test(t))return o.value[1];o=r.next()}return null},Router=function(){this.routes=new Map,this["default"]=null};["get","post","put","delete","head","any"].forEach(function(e){Router.prototype[e]=function(t,r,o){return this.add(e,t,r,o)}}),Router.prototype.add=function(e,t,r,o){o=o||{};var n;t instanceof RegExp?n=RegExp:(n=o.origin||self.location.origin,n=n instanceof RegExp?n.source:regexEscape(n)),e=e.toLowerCase();var u=new Route(e,t,r,o);this.routes.has(n)||this.routes.set(n,new Map);var a=this.routes.get(n);a.has(e)||a.set(e,new Map);var s=a.get(e),i=u.regexp||u.fullUrlRegExp;s.set(i.source,u)},Router.prototype.matchMethod=function(e,t){var r=new URL(t),o=r.origin,n=r.pathname;return this._match(e,keyMatch(this.routes,o),n)||this._match(e,this.routes.get(RegExp),t)},Router.prototype._match=function(e,t,r){if(t){var o=t.get(e.toLowerCase());if(o){var n=keyMatch(o,r);if(n)return n.makeHandler(r)}}return null},Router.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},module.exports=new Router;
},{"./route":4}],6:[function(require,module,exports){
"use strict";function cacheFirst(e,r,t){return helpers.debug("Strategy: cache first ["+e.url+"]",t),helpers.openCache(t).then(function(r){return r.match(e).then(function(r){return r?r:helpers.fetchAndCache(e,t)})})}var helpers=require("../helpers");module.exports=cacheFirst;
},{"../helpers":1}],7:[function(require,module,exports){
"use strict";function cacheOnly(e,r,c){return helpers.debug("Strategy: cache only ["+e.url+"]",c),helpers.openCache(c).then(function(r){return r.match(e)})}var helpers=require("../helpers");module.exports=cacheOnly;
},{"../helpers":1}],8:[function(require,module,exports){
"use strict";function fastest(e,n,t){return helpers.debug("Strategy: fastest ["+e.url+"]",t),new Promise(function(r,s){var c=!1,o=[],a=function(e){o.push(e.toString()),c?s(new Error('Both cache and network failed: "'+o.join('", "')+'"')):c=!0},h=function(e){e instanceof Response?r(e):a("No result returned")};helpers.fetchAndCache(e.clone(),t).then(h,a),cacheOnly(e,n,t).then(h,a)})}var helpers=require("../helpers"),cacheOnly=require("./cacheOnly");module.exports=fastest;
},{"../helpers":1,"./cacheOnly":7}],9:[function(require,module,exports){
module.exports={networkOnly:require("./networkOnly"),networkFirst:require("./networkFirst"),cacheOnly:require("./cacheOnly"),cacheFirst:require("./cacheFirst"),fastest:require("./fastest")};
},{"./cacheFirst":6,"./cacheOnly":7,"./fastest":8,"./networkFirst":10,"./networkOnly":11}],10:[function(require,module,exports){
"use strict";function networkFirst(e,r,t){t=t||{};var s=t.successResponses||globalOptions.successResponses,n=t.networkTimeoutSeconds||globalOptions.networkTimeoutSeconds;return helpers.debug("Strategy: network first ["+e.url+"]",t),helpers.openCache(t).then(function(r){var o,u,c=[];if(n){var i=new Promise(function(t){o=setTimeout(function(){r.match(e).then(function(e){e&&t(e)})},1e3*n)});c.push(i)}var a=helpers.fetchAndCache(e,t).then(function(e){if(o&&clearTimeout(o),s.test(e.status))return e;throw helpers.debug("Response was an HTTP error: "+e.statusText,t),u=e,new Error("Bad response")})["catch"](function(){return helpers.debug("Network or response error, fallback to cache ["+e.url+"]",t),r.match(e).then(function(e){return e||u})});return c.push(a),Promise.race(c)})}var globalOptions=require("../options"),helpers=require("../helpers");module.exports=networkFirst;
},{"../helpers":1,"../options":3}],11:[function(require,module,exports){
"use strict";function networkOnly(e,r,t){return helpers.debug("Strategy: network only ["+e.url+"]",t),fetch(e)}var helpers=require("../helpers");module.exports=networkOnly;
},{"../helpers":1}],12:[function(require,module,exports){
"use strict";function cache(e,t){return helpers.openCache(t).then(function(t){return t.add(e)})}function uncache(e,t){return helpers.openCache(t).then(function(t){return t["delete"](e)})}function precache(e){Array.isArray(e)||(e=[e]),options.preCacheItems=options.preCacheItems.concat(e)}require("serviceworker-cache-polyfill");var options=require("./options"),router=require("./router"),helpers=require("./helpers"),strategies=require("./strategies");helpers.debug("Service Worker Toolbox is loading");var flatten=function(e){return e.reduce(function(e,t){return e.concat(t)},[])};self.addEventListener("install",function(e){var t=options.cache.name+"$$$inactive$$$";helpers.debug("install event fired"),helpers.debug("creating cache ["+t+"]"),e.waitUntil(helpers.openCache({cache:{name:t}}).then(function(e){return Promise.all(options.preCacheItems).then(flatten).then(function(t){return helpers.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}),self.addEventListener("activate",function(e){helpers.debug("activate event fired");var t=options.cache.name+"$$$inactive$$$";e.waitUntil(helpers.renameCache(t,options.cache.name))}),self.addEventListener("fetch",function(e){var t=router.match(e.request);t?e.respondWith(t(e.request)):router["default"]&&"GET"===e.request.method&&e.respondWith(router["default"](e.request))}),module.exports={networkOnly:strategies.networkOnly,networkFirst:strategies.networkFirst,cacheOnly:strategies.cacheOnly,cacheFirst:strategies.cacheFirst,fastest:strategies.fastest,router:router,options:options,cache:cache,uncache:uncache,precache:precache};
},{"./helpers":1,"./options":3,"./router":5,"./strategies":9,"serviceworker-cache-polyfill":15}],13:[function(require,module,exports){
function parse(e){for(var t,r=[],n=0,o=0,p="";null!=(t=PATH_REGEXP.exec(e));){var a=t[0],i=t[1],s=t.index;if(p+=e.slice(o,s),o=s+a.length,i)p+=i[1];else{p&&(r.push(p),p="");var u=t[2],c=t[3],l=t[4],f=t[5],g=t[6],x=t[7],h="+"===g||"*"===g,m="?"===g||"*"===g,y=u||"/",T=l||f||(x?".*":"[^"+y+"]+?");r.push({name:c||n++,prefix:u||"",delimiter:y,optional:m,repeat:h,pattern:escapeGroup(T)})}}return o<e.length&&(p+=e.substr(o)),p&&r.push(p),r}function compile(e){return tokensToFunction(parse(e))}function tokensToFunction(e){for(var t=new Array(e.length),r=0;r<e.length;r++)"object"==typeof e[r]&&(t[r]=new RegExp("^"+e[r].pattern+"$"));return function(r){for(var n="",o=r||{},p=0;p<e.length;p++){var a=e[p];if("string"!=typeof a){var i,s=o[a.name];if(null==s){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to be defined')}if(isarray(s)){if(!a.repeat)throw new TypeError('Expected "'+a.name+'" to not repeat, but received "'+s+'"');if(0===s.length){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to not be empty')}for(var u=0;u<s.length;u++){if(i=encodeURIComponent(s[u]),!t[p].test(i))throw new TypeError('Expected all "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=(0===u?a.prefix:a.delimiter)+i}}else{if(i=encodeURIComponent(s),!t[p].test(i))throw new TypeError('Expected "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=a.prefix+i}}else n+=a}return n}}function escapeString(e){return e.replace(/([.+*?=^!:${}()[\]|\/])/g,"\\$1")}function escapeGroup(e){return e.replace(/([=!:$\/()])/g,"\\$1")}function attachKeys(e,t){return e.keys=t,e}function flags(e){return e.sensitive?"":"i"}function regexpToRegexp(e,t){var r=e.source.match(/\((?!\?)/g);if(r)for(var n=0;n<r.length;n++)t.push({name:n,prefix:null,delimiter:null,optional:!1,repeat:!1,pattern:null});return attachKeys(e,t)}function arrayToRegexp(e,t,r){for(var n=[],o=0;o<e.length;o++)n.push(pathToRegexp(e[o],t,r).source);var p=new RegExp("(?:"+n.join("|")+")",flags(r));return attachKeys(p,t)}function stringToRegexp(e,t,r){for(var n=parse(e),o=tokensToRegExp(n,r),p=0;p<n.length;p++)"string"!=typeof n[p]&&t.push(n[p]);return attachKeys(o,t)}function tokensToRegExp(e,t){t=t||{};for(var r=t.strict,n=t.end!==!1,o="",p=e[e.length-1],a="string"==typeof p&&/\/$/.test(p),i=0;i<e.length;i++){var s=e[i];if("string"==typeof s)o+=escapeString(s);else{var u=escapeString(s.prefix),c=s.pattern;s.repeat&&(c+="(?:"+u+c+")*"),c=s.optional?u?"(?:"+u+"("+c+"))?":"("+c+")?":u+"("+c+")",o+=c}}return r||(o=(a?o.slice(0,-2):o)+"(?:\\/(?=$))?"),o+=n?"$":r&&a?"":"(?=\\/|$)",new RegExp("^"+o,flags(t))}function pathToRegexp(e,t,r){return t=t||[],isarray(t)?r||(r={}):(r=t,t=[]),e instanceof RegExp?regexpToRegexp(e,t,r):isarray(e)?arrayToRegexp(e,t,r):stringToRegexp(e,t,r)}var isarray=require("isarray");module.exports=pathToRegexp,module.exports.parse=parse,module.exports.compile=compile,module.exports.tokensToFunction=tokensToFunction,module.exports.tokensToRegExp=tokensToRegExp;var PATH_REGEXP=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))"].join("|"),"g");
},{"isarray":14}],14:[function(require,module,exports){
module.exports=Array.isArray||function(r){return"[object Array]"==Object.prototype.toString.call(r)};
},{}],15:[function(require,module,exports){
Cache.prototype.addAll||(Cache.prototype.addAll=function(t){function e(t){this.name="NetworkError",this.code=19,this.message=t}var r=this;return e.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return t=t.map(function(t){return t instanceof Request?t:String(t)}),Promise.all(t.map(function(t){"string"==typeof t&&(t=new Request(t));var r=new URL(t.url).protocol;if("http:"!==r&&"https:"!==r)throw new e("Invalid scheme");return fetch(t.clone())}))}).then(function(e){return Promise.all(e.map(function(e,n){return r.put(t[n],e)}))}).then(function(){})});
},{}]},{},[12])(12)
});
(global => {
'use strict';
// Turn on debug logging, visible in the Developer Tools' console.
global.toolbox.options.debug = true;
// Set up a handler for HTTP GET requests:
// - /\.ytimg\.com\// will match any requests whose URL contains 'ytimg.com'.
// A narrower RegExp could be used, but just checking for ytimg.com anywhere
// in the URL should be fine for this sample.
// - toolbox.cacheFirst let us to use the predefined cache strategy for those
// requests.
global.toolbox.router.get(/\.ytimg\.com\//, global.toolbox.cacheFirst, {
// Use a dedicated cache for the responses, separate from the default cache.
cache: {
name: 'youtube-thumbnails',
// Store up to 10 entries in that cache.
maxEntries: 10,
// Expire any entries that are older than 30 seconds.
maxAgeSeconds: 30
}
});
// By default, all requests that don't match our custom handler will use the
// toolbox.networkFirst cache strategy, and their responses will be stored in
// the default cache.
global.toolbox.router.default = global.toolbox.networkFirst;
// Boilerplate to ensure our service worker takes control of the page as soon
// as possible.
global.addEventListener('install',
event => event.waitUntil(global.skipWaiting()));
global.addEventListener('activate',
event => event.waitUntil(global.clients.claim()));
})(self);

View File

@ -9,6 +9,10 @@ export const TOGGLE_EDITORS = 'TOGGLE_EDITORS';
export const ADD_REVISION = 'ADD_REVISION';
export const REPLACE_TEXT= 'REPLACE_TEXT';
export const SLACK_AUTH_PENDING = 'SLACK_AUTH_PENDING';
export const SLACK_AUTH_SUCCESS = 'SLACK_AUTH_SUCCESS';
export const SLACK_AUTH_FAILURE = 'SLACK_AUTH_FAILURE';
/*
* Other Constants
*/

View File

@ -1,13 +1,10 @@
import React from 'react';
import MarkdownIcon from '../../Components/Icons/Markdown';
import styles from './Header.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const Header = ({
activeEditors,
toggleEditors,
}) => {
return (
@ -17,16 +14,7 @@ const Header = ({
className={ cx('headerItem', 'editorToggle') }
style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}
>
<div
onClick={toggleEditors.bind(this, 'MARKDOWN')}
className={ activeEditors.includes('MARKDOWN') ? styles.active : '' }
>
<MarkdownIcon style={{ width: '32px', height: '20px', color: '#fff' }} />
</div>
<div
onClick={toggleEditors.bind(this, 'TEXT')}
className={ activeEditors.includes('TEXT') ? styles.active : '' }
>
<div>
<span className={ styles.textIcon }>Aa</span>
</div>
</div>

View File

@ -19,7 +19,7 @@
width: 150px;
padding: 12px 22px;
font-size: 13px;
font-size: 15px;
font-weight: 300;
font-family: "Atlas Grotesk", "Helvetica Neue", sans-serif;
text-align: center;

View File

@ -3,12 +3,13 @@ import Codemirror from 'react-codemirror';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/edit/continuelist';
import 'codemirror/addon/display/placeholder.js';
import Dropzone from 'react-dropzone';
import styles from './MarkdownEditor.scss';
import './codemirror.css';
import './codemirror.scss';
import { client } from '../../Utils/ApiClient';
import { client } from '../../utils/ApiClient';
class MarkdownAtlas extends React.Component {
static propTypes = {
@ -97,6 +98,7 @@ class MarkdownAtlas extends React.Component {
extraKeys: {
Enter: 'newlineAndIndentContinueMarkdownList',
},
placeholder: "# Start with a title...",
};
// http://codepen.io/lubelski/pen/fnGae

View File

@ -1,65 +0,0 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
@import url(https://fonts.googleapis.com/css?family=Cousine:400,700,700italic,400italic);
/* Custom styling */
.cm-s-atlas.CodeMirror {
background: #ffffff;
color: #202020;
font-family: 'Atlas Typewriter', 'Cousine', 'Monaco', monospace;
font-weight: 300;
height: auto;
}
.cm-s-atlas div.CodeMirror-selected {
background: #90CAF9;
}
/* Disable ondrag cursor for file uploads */
.cm-s-atlas div.CodeMirror-dragcursors {
visibility: hidden;
}
.cm-s-atlas .CodeMirror-line::selection,
.cm-s-atlas .CodeMirror-line > span::selection,
.cm-s-atlas .CodeMirror-line > span > span::selection {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::-moz-selection, .cm-s-atlas .CodeMirror-line > span::-moz-selection, .cm-s-atlas .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-atlas .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-atlas .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-atlas .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-cursor {
border-left: 2px solid #2196F3;
}
.cm-s-atlas span.cm-quote {
font-style: italic;
}
.cm-s-atlas span.cm-comment { color: #8f5536; }
.cm-s-atlas span.cm-atom { color: #aa759f; }
.cm-s-atlas span.cm-number { color: #aa759f; }
.cm-s-atlas span.cm-property, .cm-s-atlas span.cm-attribute { color: #90a959; }
.cm-s-atlas span.cm-keyword { color: #ac4142; }
.cm-s-atlas span.cm-string { color: #f4bf75; }
.cm-s-atlas span.cm-variable { color: #90a959; }
.cm-s-atlas span.cm-variable-2 { color: #788696; }
.cm-s-atlas span.cm-def { color: #d28445; }
.cm-s-atlas span.cm-bracket { color: #202020; }
.cm-s-atlas span.cm-tag { color: #ac4142; }
.cm-s-atlas span.cm-link { color: #aa759f; }
.cm-s-atlas span.cm-error { background: #ac4142; color: #505050; }
.cm-s-atlas .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-atlas .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@ -11,9 +11,7 @@ const keys = keyMirror({
// Constant values
const constants = {
API_USER_AGENT: `${name}/${version}`,
API_BASE_URL: 'http://localhost:8000/api',
LOGIN_PATH: '/login',
LOGIN_SUCCESS_PATH: '/dashboard',
API_BASE_URL: 'http://localhost:3000/api',
};
export default Object.assign(keys, constants);

View File

@ -7,8 +7,8 @@ import {
TOGGLE_HISTORY_SIDEBAR,
ADD_REVISION,
REPLACE_TEXT,
ActiveEditors,
} from '../Actions';
ActiveEditors
} from '../actions';
const activeEditors = (state = [ActiveEditors.MARKDOWN, ActiveEditors.TEXT], action) => {
switch (action.type) {
@ -103,8 +103,13 @@ const text = (state = textDefaultState, action) => {
}
};
import team from './team';
import user from './user';
export default combineReducers({
activeEditors,
historySidebar,
text,
user,
team,
});

View File

@ -1,12 +1,12 @@
import _ from 'lodash';
import Auth from './Auth';
import Constants from '../Constants';
import auth from './auth';
import constants from '../constants';
class ApiClient {
constructor(options = {}) {
this.baseUrl = options.baseUrl || Constants.API_BASE_URL;
this.userAgent = options.userAgent || Constants.API_USER_AGENT;
this.baseUrl = options.baseUrl || constants.API_BASE_URL;
this.userAgent = options.userAgent || constants.API_USER_AGENT;
}
fetch = (path, method, data) => {
@ -25,8 +25,8 @@ class ApiClient {
'Content-Type': 'application/json',
'User-Agent': this.userAgent,
});
if (Auth.getToken()) {
headers.set('Authorization', `JWT ${Auth.getToken()}`);
if (auth.getToken()) {
headers.set('Authorization', `Bearer ${auth.getToken()}`);
}
// Construct request
@ -48,7 +48,7 @@ class ApiClient {
// Handle 401, log out user
if (response.status === 401) {
Auth.logout();
auth.logout(); // replace with dispatch+action
}
// Handle failed responses
@ -81,18 +81,6 @@ class ApiClient {
return this.fetch(path, 'POST', data);
}
put = (path, data) => {
return this.fetch(path, 'PUT', data);
}
get = (path, data) => {
return this.fetch(path, 'GET', data);
}
delete = (path, data) => {
return this.fetch(path, 'DELETE', data);
}
// Helpers
constructQueryString = (data) => {

View File

@ -1,42 +1,19 @@
// Inspired by https://github.com/reactjs/react-router/blob/master/examples/auth-flow/auth.js
import Constants from '../Constants';
import History from './History';
import { client } from './ApiClient';
import constants from '../constants';
export default {
login(email, password) {
return new Promise((resolve, reject) => {
client.post('/authenticate', {
email,
password,
})
.then((data) => {
localStorage.setItem(Constants.JWT_STORE_KEY, data.jwt_token);
this.onChange(true);
resolve(data);
})
.catch((error) => {
reject(error);
});
});
setToken(token) {
localStorage.setItem(constants.JWT_STORE_KEY, token);
},
getToken() {
return localStorage.getItem(Constants.JWT_STORE_KEY);
return localStorage.getItem(constants.JWT_STORE_KEY);
},
logout() {
localStorage.removeItem(Constants.JWT_STORE_KEY);
History.push(Constants.LOGIN_PATH);
this.onChange(false);
localStorage.removeItem(constants.JWT_STORE_KEY);
},
loggedIn() {
return !!localStorage.getItem(Constants.JWT_STORE_KEY);
},
onChange() {
// This is overriden with a callback function in `Views/App/App.js`
return !!localStorage.getItem(constants.JWT_STORE_KEY);
},
};

View File

@ -1,89 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MarkdownEditor from '../../Components/MarkdownEditor';
import TextEditor from '../../Components/TextEditor';
import { toMarkdown } from '../../Utils/Markdown';
import {
updateText,
replaceText,
} from '../../Actions';
import Constants from '../../Constants';
import styles from './Dashboard.scss';
class Dashboard extends Component {
static propTypes = {
editMarkdown: React.PropTypes.func.isRequired,
editText: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
replaceText: React.PropTypes.func.isRequired,
activeEditors: React.PropTypes.arrayOf(React.PropTypes.string),
showHistorySidebar: React.PropTypes.bool.isRequired,
}
render() {
const activeEditors = this.props.activeEditors;
return (
<div className={styles.container}>
{
activeEditors.includes('MARKDOWN') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.markdown}`}
>
<MarkdownEditor
onChange={this.props.editMarkdown}
text={this.props.text}
replaceText={this.props.replaceText}
/>
</div>
) : null
}
{
activeEditors.includes('TEXT') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.text}`}
>
<TextEditor onChange={this.props.editText} text={this.props.text} />
</div>
) : null
}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
text: state.text.text,
editor: state.editor,
activeEditors: state.activeEditors,
showHistorySidebar: state.historySidebar.visible,
revisions: state.text.revisions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
editMarkdown: (text) => {
dispatch(updateText(text, 'markdown'));
},
editText: (html) => {
const text = toMarkdown(html);
dispatch(updateText(text, 'text'));
},
replaceText: (originalText, replacedText) => {
dispatch(replaceText(originalText, replacedText));
},
};
};
Dashboard = connect(
mapStateToProps,
mapDispatchToProps,
)(Dashboard);
export default Dashboard;

View File

@ -1,75 +0,0 @@
import React, { Component } from 'react';
import Auth from '../../Utils/Auth';
export default class Login extends Component {
static propTypes = {
location: React.PropTypes.object,
}
static contextTypes = {
router: React.PropTypes.object.isRequired,
}
state = {
email: '',
password: '',
error: null,
}
handleEmailChange = (event) => {
this.setState({ email: event.target.value });
}
handlePasswordChange = (event) => {
this.setState({ password: event.target.value });
}
handleSubmit = (event) => {
event.preventDefault();
Auth.login(this.state.email, this.state.password)
.then(() => {
const { location } = this.props;
if (location.state && location.state.nextPathname) {
this.context.router.replace(location.state.nextPathname);
} else {
this.context.router.replace('/dashboard');
}
})
.catch((err) => {
this.setState({ error: err.error });
});
}
render() {
return (
<div>
<h2>Login</h2>
<form action="" onSubmit={ this.handleSubmit }>
{this.state.error && (
<p>{ this.state.error }</p>
)}
<div>
<input
placeholder={ 'Email' }
onChange={ this.handleEmailChange }
/>
</div>
<div>
<input
placeholder={ 'Password' }
type={ 'password' }
onChange={ this.handlePasswordChange }
/>
</div>
<div>
<input type={ 'submit' } />
</div>
</form>
</div>
);
}
}

View File

@ -1,2 +0,0 @@
import Login from './Login';
export default Login;

View File

@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW';
const togglePreview = makeActionCreator(TOGGLE_PREVIEW);

View File

@ -0,0 +1,34 @@
import makeActionCreator from '../utils/actions';
import { push } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import auth from 'utils/auth';
import { updateUser } from './UserActions';
import { updateTeam } from './TeamActions';
export const SLACK_AUTH_PENDING = 'SLACK_AUTH_PENDING';
export const SLACK_AUTH_SUCCESS = 'SLACK_AUTH_SUCCESS';
export const SLACK_AUTH_FAILURE = 'SLACK_AUTH_FAILURE';
const slackAuthPending = makeActionCreator(SLACK_AUTH_PENDING);
const slackAuthSuccess = makeActionCreator(SLACK_AUTH_SUCCESS, 'user');
const slackAuthFailure = makeActionCreator(SLACK_AUTH_FAILURE, 'error');
export function slackAuthAsync(code) {
return (dispatch) => {
dispatch(slackAuthPending());
client.post('/auth.slack', {
code: code,
})
.then(data => {
auth.setToken(data.data.accessToken);
dispatch(updateUser(data.data.user));
dispatch(updateTeam(data.data.team));
dispatch(push('/dashboard'));
})
// .catch((err) => {
// dispatch(push('/error'));
// })
};
};

View File

@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const UPDATE_TEAM = 'UPDATE_TEAM';
export const updateTeam = makeActionCreator(UPDATE_TEAM, 'team');

View File

@ -0,0 +1,5 @@
import makeActionCreator from '../utils/actions';
export const UPDATE_USER = 'UPDATE_USER';
export const updateUser = makeActionCreator(UPDATE_USER, 'user');

View File

View File

@ -0,0 +1,2 @@
import Button from './Button';
export default Button;

View File

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MarkdownEditor from '../../components/MarkdownEditor';
import {
updateText,
replaceText,
} from '../../actions';
import constants from '../../constants';
import styles from './Editor.scss';
class Editor extends Component {
static propTypes = {
editMarkdown: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
replaceText: React.PropTypes.func.isRequired,
showHistorySidebar: React.PropTypes.bool.isRequired,
}
render() {
return (
<div className={styles.container}>
<div className={ styles.markdown }>
<MarkdownEditor
onChange={this.props.editMarkdown}
text={this.props.text}
replaceText={this.props.replaceText}
/>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
text: state.text.text,
editor: state.editor,
showHistorySidebar: state.historySidebar.visible,
revisions: state.text.revisions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
editMarkdown: (text) => {
dispatch(updateText(text, 'markdown'));
},
};
};
Editor = connect(
mapStateToProps,
mapDispatchToProps,
)(Editor);
export default Editor;

View File

@ -0,0 +1,2 @@
import Editor from './Editor';
export default Editor;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { connect } from 'react-redux';
import styles from './Layout.scss';
class Layout extends React.Component {
static propTypes = {
}
render() {
return (
<div className={ styles.container }>
<div className={ styles.header }>
<div className={ styles.teamName }>Coinbase</div>
<div className={ styles.user }>
<img src={ this.props.avatarUrl } />
</div>
</div>
<div className={ styles.content }>
{ this.props.children }
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
avatarUrl: state.user ? state.user.avatarUrl : null,
}
};
export default connect(mapStateToProps)(Layout);

View File

@ -0,0 +1,35 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 42px;
border-bottom: 1px solid #eee;
}
.teamName {
font-family: 'Atlas Grotesk';
font-weight: bold;
}
.user {
img {
height: 24px;
width: 24px;
border-radius: 12px;
}
}
.content {
flex: 1;
}

View File

@ -0,0 +1,2 @@
import Layout from './Layout';
export default Layout;

View File

@ -0,0 +1,66 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
:global {
/* Custom styling */
.cm-s-atlas.CodeMirror {
background: #ffffff;
color: #202020;
font-family: 'Atlas Typewriter', 'Cousine', 'Monaco', monospace;
font-weight: 300;
height: auto;
}
.cm-s-atlas div.CodeMirror-selected {
background: #90CAF9;
}
/* Disable ondrag cursor for file uploads */
.cm-s-atlas div.CodeMirror-dragcursors {
visibility: hidden;
}
.cm-s-atlas .CodeMirror-line::selection,
.cm-s-atlas .CodeMirror-line > span::selection,
.cm-s-atlas .CodeMirror-line > span > span::selection {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::-moz-selection, .cm-s-atlas .CodeMirror-line > span::-moz-selection, .cm-s-atlas .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-atlas .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-atlas .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-atlas .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-cursor {
border-left: 2px solid #2196F3;
}
.cm-s-atlas span.cm-quote {
font-style: italic;
}
.cm-s-atlas span.cm-comment { color: #8f5536; }
.cm-s-atlas span.cm-atom { color: #aa759f; }
.cm-s-atlas span.cm-number { color: #aa759f; }
.cm-s-atlas span.cm-property, .cm-s-atlas span.cm-attribute { color: #90a959; }
.cm-s-atlas span.cm-keyword { color: #ac4142; }
.cm-s-atlas span.cm-string { color: #f4bf75; }
.cm-s-atlas span.cm-variable { color: #90a959; }
.cm-s-atlas span.cm-variable-2 { color: #788696; }
.cm-s-atlas span.cm-def { color: #d28445; }
.cm-s-atlas span.cm-bracket { color: #202020; }
.cm-s-atlas span.cm-tag { color: #ac4142; }
.cm-s-atlas span.cm-link { color: #aa759f; }
.cm-s-atlas span.cm-error { background: #ac4142; color: #505050; }
.cm-s-atlas .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-atlas .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
.cm-s-atlas .CodeMirror-placeholder { color: rgba(0, 0, 0, 0.5); font-weight: bold; }
}

View File

View File

@ -0,0 +1,2 @@
import Preview from './Preview';
export default Preview;

View File

@ -0,0 +1,43 @@
import React from 'react';
import styles from './SlackAuthLink.scss';
export default class SlackAuthLink extends React.Component {
static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string),
}
state = {
oauthState: Math.random().toString(36).substring(7),
}
static defaultProps = {
scopes: ['identify']
}
componentDidMount = () => {
localStorage.oauthState = this.state.oauthState;
}
slackUrl = () => {
const baseUrl = 'https://slack.com/oauth/authorize';
const params = {
client_id: '30086650419.30130733398',
scope: this.props.scopes.join(" "),
redirect_uri: 'http://localhost:3000/auth/slack/',
state: this.state.oauthState,
};
const urlParams = Object.keys(params).map(function(key) {
return key + '=' + encodeURIComponent(params[key]);
}).join('&');
return `${baseUrl}?${urlParams}`;
}
render() {
return (
<a href={ this.slackUrl() } className={ styles.link }>Authorize /w Slack</a>
)
}
}

View File

@ -0,0 +1,6 @@
.link {
text-decoration: none;
background: no-repeat left/10% url(./assets/slack_icon.svg);
padding: 5px 0 4px 36px;
font-size: 1.4em;
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="148px" height="147px" viewBox="0 0 148 147" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<g>
<g>
<path d="M12.997,77.78 C7.503,77.822 2.849,74.548 1.133,69.438 C1.067,69.24 1.01,69.048 0.955,68.855 C-0.915,62.311 2.711,55.465 9.21,53.273 L113.45,18.35 C114.717,17.987 115.993,17.802 117.257,17.794 C122.897,17.75 127.679,21.096 129.437,26.314 L129.593,26.818 C131.543,33.634 126.698,39.718 120.893,41.668 C120.889,41.671 119.833,42.028 17.231,77.059 C15.844,77.53 14.421,77.768 12.997,77.78 L12.997,77.78 L12.997,77.78 Z" fill="#70CADB"></path>
<path d="M30.372,129.045 C24.835,129.085 20.165,125.857 18.469,120.82 C18.405,120.628 18.344,120.435 18.289,120.241 C16.393,113.619 20.015,106.701 26.536,104.506 L130.78,69.263 C132.127,68.813 133.518,68.583 134.917,68.57 C140.469,68.528 145.347,71.92 147.068,77.014 L147.228,77.544 C148.235,81.065 147.64,85.022 145.638,88.145 C144.146,90.467 139.44,92.511 139.44,92.511 L34.8,128.29 C33.342,128.777 31.855,129.034 30.372,129.047 L30.372,129.045 L30.372,129.045 Z" fill="#E01765"></path>
<path d="M117.148,129.268 C111.588,129.311 106.665,125.803 104.893,120.545 L70.103,17.205 L69.929,16.625 C68.044,10.035 71.669,3.161 78.166,0.971 C79.466,0.534 80.81,0.306 82.163,0.294 C84.173,0.279 86.118,0.732 87.95,1.637 C91.013,3.162 93.304,5.787 94.399,9.029 L129.186,112.36 L129.287,112.692 C131.241,119.534 127.624,126.412 121.127,128.602 C119.84,129.031 118.5,129.256 117.148,129.268 L117.148,129.268 L117.148,129.268 Z" fill="#E8A723"></path>
<path d="M65.435,146.674 C59.875,146.717 54.948,143.209 53.175,137.944 L18.394,34.608 C18.334,34.418 18.274,34.228 18.216,34.033 C16.336,27.445 19.95,20.57 26.445,18.378 C27.74,17.948 29.079,17.721 30.43,17.71 C35.991,17.666 40.915,21.173 42.687,26.433 L77.469,129.773 C77.534,129.953 77.593,130.152 77.646,130.342 C79.53,136.935 75.914,143.814 69.409,146.006 C68.117,146.437 66.78,146.662 65.431,146.673 L65.435,146.673 L65.435,146.674 Z" fill="#3EB890"></path>
<path d="M99.997,105.996 L124.255,97.702 L116.325,74.152 L92.039,82.359 L99.997,105.996 L99.997,105.996 Z" fill="#CC2027"></path>
<path d="M48.364,123.65 L72.62,115.357 L64.63,91.627 L40.35,99.837 L48.364,123.65 L48.364,123.65 Z" fill="#361238"></path>
<path d="M82.727,54.7 L106.987,46.417 L99.15,23.142 L74.845,31.285 L82.727,54.7 L82.727,54.7 Z" fill="#65863A"></path>
<path d="M31.088,72.33 L55.348,64.047 L47.415,40.475 L23.11,48.617 L31.088,72.33 L31.088,72.33 Z" fill="#1A937D"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,2 @@
import SlackAuthLink from './SlackAuthLink';
export default SlackAuthLink;

View File

@ -1,40 +1,46 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import { Router, Route, IndexRoute } from 'react-router';
import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import thunkMiddleware from 'redux-thunk';
import * as storage from 'redux-storage';
import createEngine from 'redux-storage-engine-localstorage';
import History from './Utils/History';
import createLogger from 'redux-logger';
import History from 'utils/History';
import Auth from './Utils/Auth';
import auth from 'utils/auth';
import reducers from './Reducers';
import reducers from 'reducers';
import App from './Views/App';
import Login from './Views/Login';
import Dashboard from './Views/Dashboard';
import 'utils/base-styles.scss';
import Home from 'scenes/Home';
import App from 'scenes/App';
import Dashboard from 'scenes/Dashboard';
import SlackAuth from 'scenes/SlackAuth';
// Redux
const reducer = storage.reducer(reducers);
const engine = createEngine('atlas-store');
const storageMiddleware = storage.createMiddleware(engine);
const loggerMiddleware = createLogger();
const routerMiddlewareWithHistory = routerMiddleware(History);
const createStoreWithMiddleware = applyMiddleware(storageMiddleware)(createStore);
const store = createStoreWithMiddleware(reducer);
const createStoreWithMiddleware = (createStore);
const load = storage.createLoader(engine);
load(store);
// .then((newState) => console.log('Loaded state:', newState));
// .catch(() => console.log('Failed to load previous state'));
// React router
const store = createStore(reducer, applyMiddleware(
thunkMiddleware,
routerMiddlewareWithHistory,
loggerMiddleware,
), autoRehydrate());
persistStore(store);
function requireAuth(nextState, replace) {
if (!Auth.loggedIn()) {
if (!auth.loggedIn()) {
replace({
pathname: '/login',
pathname: '/',
state: { nextPathname: nextState.location.pathname },
});
}
@ -43,7 +49,14 @@ function requireAuth(nextState, replace) {
render((
<Provider store={store}>
<Router history={History}>
<Route path="/" component={App} />
<Route path="/">
<IndexRoute component={Home} />
<Route path="/dashboard" component={Dashboard} onEnter={ requireAuth } />
<Route path="/editor" component={App} />
<Route path="/auth/slack" component={SlackAuth} />
</Route>
</Router>
</Provider>
), document.getElementById('root'));

View File

@ -0,0 +1,62 @@
const defaultTest = `# Welcome to Beautiful Atlas
This is just a small preview here's what you can do:
- Write markdown or rich text, you choose
- Dont' worry about saving
- One document for now
- More to come
`
const state = {
previousTest: null,
text: null,
unsavedChanges: false,
};
const text = (state = state, action) => {
switch (action.type) {
case UPDATE_TEXT: {
let unsavedChanges = false;
if (lastRevision && lastRevision.text !== state.text) {
unsavedChanges = true;
}
return {
...state,
unsavedChanges,
text: action.text,
};
}
case ADD_REVISION: {
// Create new revision if it differs from the previous one
if (!lastRevision || lastRevision.text !== state.text) {
const lastId = lastRevision ? lastRevision.id : 0;
return {
...state,
revisions: [
...state.revisions,
{
id: lastId + 1,
text: state.text,
created_at: action.createdAt,
},
],
unsavedChanges: false,
};
} else {
return state;
}
}
case REPLACE_TEXT: {
const newText = state.text.replace(action.originalText, action.replacedText);
return {
...state,
text: newText,
};
}
default:
return state;
}
};

15
src/reducers/team.js Normal file
View File

@ -0,0 +1,15 @@
import { UPDATE_TEAM } from 'actions/TeamActions';
const team = (state = null, action) => {
switch (action.type) {
case UPDATE_TEAM: {
return {
...action.team,
};
}
default:
return state;
}
};
export default team;

15
src/reducers/user.js Normal file
View File

@ -0,0 +1,15 @@
import { UPDATE_USER } from 'actions/UserActions';
const user = (state = null, action) => {
switch (action.type) {
case UPDATE_USER: {
return {
...action.user,
};
}
default:
return state;
}
};
export default user;

View File

@ -8,52 +8,28 @@ import styles from './App.scss';
import {
toggleEditors,
addRevision,
} from '../../Actions';
} from '../../actions';
import Header from '../../Components/Header';
import Dashboard from '../Dashboard';
import Auth from '../../Utils/Auth';
import Header from '../../components/Header';
import Editor from '../../components/Editor';
class App extends Component {
static propTypes = {
children: React.PropTypes.element,
activeEditors: React.PropTypes.array.isRequired,
toggleEditors: React.PropTypes.func.isRequired,
addRevision: React.PropTypes.func.isRequired,
unsavedChanges: React.PropTypes.bool.isRequired,
}
state = {
loggedIn: Auth.loggedIn(),
}
componentWillMount = () => {
Auth.onChange = this.updateAuth;
}
updateAuth = (loggedIn) => {
this.setState({
loggedIn,
});
}
logout = () => {
// TODO: Replace with Redux actions
Auth.logout();
}
render() {
return (
<div className={ styles.container }>
<Header
activeEditors={this.props.activeEditors}
toggleEditors={this.props.toggleEditors}
addRevision={this.props.addRevision}
unsavedChanges={this.props.unsavedChanges}
/>
<div className={ styles.content }>
<Dashboard />
<Editor />
</div>
</div>
);
@ -69,9 +45,6 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
toggleEditors: (toggledEditor) => {
dispatch(toggleEditors(toggledEditor));
},
addRevision: () => {
dispatch(addRevision());
},
@ -84,9 +57,3 @@ App = connect(
)(App);
export default App;
// {this.state.loggedIn ? (
// <a href="#" onClick={this.logout}>Logout</a>
// ) : (
// <Link to="/login">Login</Link>
// )}

View File

@ -0,0 +1,33 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import Layout from 'components/Layout';
import styles from './Dashboard.scss';
class Dashboard extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
render() {
return (
<Layout
header={<div>header!</div>}
>
holla!
</Layout>
);
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
}
export default connect(
null, mapDispatchToProps
)(Dashboard);

View File

View File

@ -1,2 +1,2 @@
import Dashboard from './Dashboard';
export default Dashboard;
export default Dashboard;

63
src/scenes/Home/Home.js Normal file
View File

@ -0,0 +1,63 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import auth from '../../utils/auth';
import SlackAuthLink from '../../components/SlackAuthLink';
import styles from './Home.scss';
class Home extends React.Component {
static propTypes = {
replace: React.PropTypes.func.isRequired,
}
componentDidMount = () => {
if (auth.loggedIn()) {
this.props.replace('/dashboard');
}
}
render() {
return (
<div className={ styles.container }>
<div className={ styles.content }>
<div className={ styles.intro }>
<p>
Hi there,
</p>
<p>
We're building the best place for engineers, designers and teams to
share ideas, tell stories and build knowledge.
</p>
<p>
<strong>**Atlas**</strong> can start as a wiki, but it's really
up to you what you want to make of it:
</p>
<p>
- Write documentation in <i>_markdown_</i><br/>
- Build a blog around the API<br/>
- Hack the frontend for your needs (coming!)<br/>
</p>
<p>
We're just getting started.
</p>
</div>
<div className={ styles.action }>
<SlackAuthLink />
</div>
</div>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ replace }, dispatch)
}
export default connect(
null, mapDispatchToProps
)(Home);

17
src/scenes/Home/Home.scss Normal file
View File

@ -0,0 +1,17 @@
.container {
display: flex;
flex: 1;
justify-content: space-around;
}
.content {
margin: 40px;
max-width: 640px;
}
.intro {
font-family: "Atlas Typewriter", Monaco, monospace;
font-size: 1.4em;
line-height: 1.6em;
margin-bottom: 40px;
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Frame } from 'react-keyframes';
let frames = [];
const p = (node) => frames.push(node);
const E = (props) => {
return (<Frame duration={props.duration || 300} component='div'>{ props.children }</Frame>);
};
const line1 = (<p>Hi there,</p>);
const line2 = (<p>We're excited to share what were building.</p>);
const line3 = (<p>We <strong>**love**</strong> Markdown,</p>);
const line4 = (<p>but we also get that it's not for everyone.</p>);
const line5 = (<p>Together with you,</p>);
const line6 = (<p>we want to build the best place to</p>);
const line7 = (<p>share ideas,</p>);
const line8 = (<p>tell stories,</p>);
const line9 = (<p>and build knowledge.</p>);
const line10 = (<p>We're just getting started.</p>);
const line11 = (<p>Welcome to Beautiful Atlas.</p>);
p(<E>{line1}{line2}{line3}{line4}{line5}{line6}{line7}{line8}{line9}{line10}{line11}</E>);
// Hmms leaving this here for now, would be nice to something
export default frames;

2
src/scenes/Home/index.js Normal file
View File

@ -0,0 +1,2 @@
import Home from './Home';
export default Home;

View File

@ -0,0 +1,35 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { slackAuthAsync } from '../../actions/SlackAuthAction';
import { client } from '../../utils/ApiClient';
class SlackAuth extends React.Component {
componentDidMount = () => {
const { query } = this.props.location
// Validate OAuth2 state param
if (localStorage.oauthState != query.state) {
return;
}
this.props.slackAuthAsync(query.code);
}
render() {
return (
<div>Loading...</div>
);
}
}
const mapDispactcToProps = (dispatch) => {
return bindActionCreators({ slackAuthAsync }, dispatch);
};
export default connect(
null,
mapDispactcToProps
)(SlackAuth);

View File

@ -0,0 +1,2 @@
import SlackAuth from './SlackAuth';
export default SlackAuth;

9
src/utils/actions.js Normal file
View File

@ -0,0 +1,9 @@
export default (type, ...argNames) => {
return function(...args) {
let action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}

View File

@ -0,0 +1,23 @@
* {
box-sizing: border-box;
}
html, body, .viewport {
width: 100%;
height: 100%;
margin: 0;
}
html, body {
font-family: 'Atlas Grotesk', 'Helvetica Neue', sans-serif;
}
a {
color: #0C77F8;
}
:global {
.Codemirror {
height: auto;
}
}

View File

@ -1,17 +1,19 @@
var path = require('path');
var webpack = require('webpack');
commonWebpackConfig = require('./webpack.config');
const commonWebpackConfig = require('./webpack.config');
developmentWebpackConfig = Object.assign(commonWebpackConfig, {
const developmentWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'eval',
entry: [
'babel-polyfill',
'webpack-hot-middleware/client',
'./src/index',
],
});
developmentWebpackConfig.plugins.push(new webpack.optimize.OccurenceOrderPlugin());
developmentWebpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
developmentWebpackConfig.plugins.push(new webpack.NoErrorsPlugin());

View File

@ -23,20 +23,20 @@ module.exports = {
include: path.join(__dirname, 'src')
},
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.scss$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass?sourceMap' },
{ test: /\.css$/, loader: 'style!css-loader' },
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }, // inline base64 URLs for <=8k images, direct URLs for the rest
{ test: /\.s?css$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass?sourceMap' },
{ test: /\.(png|jpg|svg)$/, loader: 'url-loader' }, // inline base64 URLs for <=8k images, direct URLs for the rest
{ test: /\.woff$/, loader: 'url-loader?limit=65000&mimetype=application/font-woff&name=public/fonts/[name].[ext]' },
]
},
resolve: {
root: path.resolve('./src'),
// you can now require('file') instead of require('file.json')
extensions: ['', '.js', '.json']
},
plugins: [
definePlugin,
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
'fetch': 'imports?this=>global!exports?global.fetch!isomorphic-fetch'
}),
]
};

View File

@ -6,7 +6,10 @@ commonWebpackConfig = require('./webpack.config');
productionWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'cheap-module-source-map',
entry: './src/index',
entry: [
'babel-polyfill',
'./src/index',
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',