Refactor
This commit is contained in:
1
.env.local
Normal file
1
.env.local
Normal file
@ -0,0 +1 @@
|
||||
SEQUELIZE_SECRET=7a947f4b6b5cd4c769029f7b5130c85fe2d3484758c0eeee351cd2e2d2c3bd59
|
37
_server.js
Normal file
37
_server.js
Normal 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
50
dist/bundle.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bundle.js.map
vendored
2
dist/bundle.js.map
vendored
@ -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":""}
|
21
index.html
21
index.html
@ -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
13
index.js
Normal 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
4
init.js
Normal file
@ -0,0 +1,4 @@
|
||||
require('safestart')(__dirname);
|
||||
require('babel-core/register')
|
||||
require('babel-polyfill');
|
||||
require('localenv');
|
57
package.json
57
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
83
server.js
83
server.js
@ -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
73
server/api/auth.js
Normal 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;
|
61
server/api/authentication.js
Normal file
61
server/api/authentication.js
Normal 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
55
server/api/index.js
Normal 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
13
server/api/user.js
Normal 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
26
server/api/validation.js
Normal 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
66
server/index.js
Normal 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
14
server/models/Atlas.js
Normal 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
17
server/models/Document.js
Normal 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
20
server/models/Team.js
Normal 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
44
server/models/User.js
Normal 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
6
server/models/index.js
Normal 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
16
server/presenters.js
Normal 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
39
server/routes.js
Normal 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
13
server/sequelize.js
Normal 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
21
server/static/index.html
Normal 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>
|
84
server/static/service-worker.js
Normal file
84
server/static/service-worker.js
Normal 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
84
service-worker.js
Normal 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);
|
@ -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
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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; }
|
@ -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);
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import Login from './Login';
|
||||
export default Login;
|
5
src/actions/EditorActions.js
Normal file
5
src/actions/EditorActions.js
Normal file
@ -0,0 +1,5 @@
|
||||
import makeActionCreator from '../utils/actions';
|
||||
|
||||
export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW';
|
||||
|
||||
const togglePreview = makeActionCreator(TOGGLE_PREVIEW);
|
34
src/actions/SlackAuthAction.js
Normal file
34
src/actions/SlackAuthAction.js
Normal 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'));
|
||||
// })
|
||||
};
|
||||
};
|
5
src/actions/TeamActions.js
Normal file
5
src/actions/TeamActions.js
Normal file
@ -0,0 +1,5 @@
|
||||
import makeActionCreator from '../utils/actions';
|
||||
|
||||
export const UPDATE_TEAM = 'UPDATE_TEAM';
|
||||
|
||||
export const updateTeam = makeActionCreator(UPDATE_TEAM, 'team');
|
5
src/actions/UserActions.js
Normal file
5
src/actions/UserActions.js
Normal file
@ -0,0 +1,5 @@
|
||||
import makeActionCreator from '../utils/actions';
|
||||
|
||||
export const UPDATE_USER = 'UPDATE_USER';
|
||||
|
||||
export const updateUser = makeActionCreator(UPDATE_USER, 'user');
|
0
src/components/Button/Button.scss
Normal file
0
src/components/Button/Button.scss
Normal file
2
src/components/Button/index.js
Normal file
2
src/components/Button/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Button from './Button';
|
||||
export default Button;
|
60
src/components/Editor/Editor.js
Normal file
60
src/components/Editor/Editor.js
Normal 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;
|
2
src/components/Editor/index.js
Normal file
2
src/components/Editor/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Editor from './Editor';
|
||||
export default Editor;
|
34
src/components/Layout/Layout.js
Normal file
34
src/components/Layout/Layout.js
Normal 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);
|
35
src/components/Layout/Layout.scss
Normal file
35
src/components/Layout/Layout.scss
Normal 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;
|
||||
}
|
2
src/components/Layout/index.js
Normal file
2
src/components/Layout/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Layout from './Layout';
|
||||
export default Layout;
|
66
src/components/MarkdownEditor/codemirror.scss
Normal file
66
src/components/MarkdownEditor/codemirror.scss
Normal 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; }
|
||||
}
|
0
src/components/Preview/Preview.js
Normal file
0
src/components/Preview/Preview.js
Normal file
2
src/components/Preview/index.js
Normal file
2
src/components/Preview/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Preview from './Preview';
|
||||
export default Preview;
|
43
src/components/SlackAuthLink/SlackAuthLink.js
Normal file
43
src/components/SlackAuthLink/SlackAuthLink.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
6
src/components/SlackAuthLink/SlackAuthLink.scss
Normal file
6
src/components/SlackAuthLink/SlackAuthLink.scss
Normal 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;
|
||||
}
|
20
src/components/SlackAuthLink/assets/slack_icon.svg
Normal file
20
src/components/SlackAuthLink/assets/slack_icon.svg
Normal 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 |
2
src/components/SlackAuthLink/index.js
Normal file
2
src/components/SlackAuthLink/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import SlackAuthLink from './SlackAuthLink';
|
||||
export default SlackAuthLink;
|
53
src/index.js
53
src/index.js
@ -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'));
|
||||
|
62
src/reducers/EditorReducer.js
Normal file
62
src/reducers/EditorReducer.js
Normal 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
15
src/reducers/team.js
Normal 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
15
src/reducers/user.js
Normal 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;
|
@ -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>
|
||||
// )}
|
33
src/scenes/Dashboard/Dashboard.js
Normal file
33
src/scenes/Dashboard/Dashboard.js
Normal 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);
|
0
src/scenes/Dashboard/Dashboard.scss
Normal file
0
src/scenes/Dashboard/Dashboard.scss
Normal file
@ -1,2 +1,2 @@
|
||||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
63
src/scenes/Home/Home.js
Normal file
63
src/scenes/Home/Home.js
Normal 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
17
src/scenes/Home/Home.scss
Normal 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;
|
||||
}
|
26
src/scenes/Home/animation.js
Normal file
26
src/scenes/Home/animation.js
Normal 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 we’re 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
2
src/scenes/Home/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Home from './Home';
|
||||
export default Home;
|
35
src/scenes/SlackAuth/SlackAuth.js
Normal file
35
src/scenes/SlackAuth/SlackAuth.js
Normal 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);
|
2
src/scenes/SlackAuth/index.js
Normal file
2
src/scenes/SlackAuth/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import SlackAuth from './SlackAuth';
|
||||
export default SlackAuth;
|
9
src/utils/actions.js
Normal file
9
src/utils/actions.js
Normal 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
|
||||
}
|
||||
}
|
23
src/utils/base-styles.scss
Normal file
23
src/utils/base-styles.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
||||
|
@ -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'
|
||||
}),
|
||||
]
|
||||
};
|
@ -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',
|
||||
|
Reference in New Issue
Block a user