├── .nvmrc ├── styles └── index.scss ├── .gitignore ├── src ├── components │ ├── message │ │ ├── timestamp.scss │ │ ├── content.scss │ │ ├── name.scss │ │ ├── avatar.scss │ │ ├── container.scss │ │ ├── name.js │ │ ├── avatar.js │ │ ├── content.js │ │ ├── timestamp.js │ │ ├── container.js │ │ └── index.js │ ├── chat │ │ ├── index.scss │ │ ├── header.scss │ │ ├── header.js │ │ ├── conversation.scss │ │ ├── index.js │ │ ├── input.scss │ │ ├── input.js │ │ └── conversation.js │ ├── sidebar │ │ ├── channels.scss │ │ ├── index.scss │ │ ├── header.js │ │ ├── users.scss │ │ ├── header.scss │ │ ├── users.js │ │ ├── user.js │ │ ├── channels.js │ │ └── index.js │ ├── rich-text │ │ ├── index.js │ │ └── visitor.js │ ├── build-info │ │ └── index.js │ └── login │ │ ├── index.js │ │ └── index.scss ├── server │ ├── favicon.js │ ├── build-info.js │ ├── error.js │ ├── logging.js │ ├── collect.js │ ├── app.js │ ├── render.js │ ├── assets.js │ └── page.js ├── styles │ ├── variables.scss │ └── index.scss ├── index.scss ├── constants.js ├── api │ ├── users.js │ └── messages.js ├── reducers │ ├── index.js │ ├── auth.js │ ├── socket.js │ ├── users.js │ └── channels.js ├── action-types.js ├── containers │ ├── root.js │ └── app.js ├── utils │ └── auth.js ├── images │ └── logo.svg ├── actions.js ├── index.js └── vendor │ └── phoenix.js ├── entry ├── server.entry.js ├── client.entry.js └── spec.entry.js ├── config ├── eslint │ ├── babel.eslintrc │ ├── docs.eslintrc │ ├── filenames.eslintrc │ ├── imports.eslintrc │ ├── next.eslintrc │ ├── react.eslintrc │ ├── style.eslintrc │ └── base.eslintrc └── webpack │ ├── partial │ ├── root.webpack.config.js │ ├── images.webpack.config.js │ ├── json.webpack.config.js │ ├── stats.webpack.config.js │ ├── compatibility.webpack.config.js │ ├── optimize.webpack.config.js │ ├── babel.webpack.config.js │ ├── adana.webpack.config.js │ ├── env.webpack.config.js │ ├── sharp.webpack.config.js │ ├── vendor.webpack.config.js │ ├── source-maps.webpack.config.js │ ├── build-info.webpack.config.js │ ├── postcss.webpack.config.js │ └── hot.webpack.config.js │ ├── server.webpack.config.babel.js │ └── client.webpack.config.babel.js ├── app.json ├── .eslintrc ├── .editorconfig ├── CONTRIBUTING.md ├── package.json ├── LICENSE.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 4.1.1 2 | -------------------------------------------------------------------------------- /styles/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | build 4 | .env 5 | -------------------------------------------------------------------------------- /src/components/message/timestamp.scss: -------------------------------------------------------------------------------- 1 | .default { 2 | color: #ccc; 3 | } 4 | -------------------------------------------------------------------------------- /entry/server.entry.js: -------------------------------------------------------------------------------- 1 | import app from 'server/app'; 2 | app.listen(process.env.PORT); 3 | -------------------------------------------------------------------------------- /src/components/message/content.scss: -------------------------------------------------------------------------------- 1 | .default { 2 | margin-bottom: 10px; 3 | color: #666 4 | } 5 | -------------------------------------------------------------------------------- /src/server/favicon.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function() { 4 | // Handle `/favicon.ico` requests 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $green: #A1C659; 2 | $light-gray: #E8E8E8; 3 | $gray: #555459; 4 | $button: #2ab27b; 5 | -------------------------------------------------------------------------------- /src/components/chat/index.scss: -------------------------------------------------------------------------------- 1 | .chat { 2 | flex: 1 1 auto; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | .slerk { 2 | display: flex; 3 | align-items: stretch; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/message/name.scss: -------------------------------------------------------------------------------- 1 | .default { 2 | display: inline-block; 3 | font-weight: 800; 4 | margin-right: 10px; 5 | margin-bottom: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/message/avatar.scss: -------------------------------------------------------------------------------- 1 | .default { 2 | float: left; 3 | border-radius: 50%; 4 | display: block; 5 | width: 40px; 6 | height: 40px; 7 | margin-left: -50px; 8 | } 9 | -------------------------------------------------------------------------------- /config/eslint/babel.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Use the power of babel for parsing. This allows for use of newer versions 3 | // of Javascript and things like JSX. 4 | "parser": "babel-eslint", 5 | } 6 | -------------------------------------------------------------------------------- /config/eslint/docs.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Enforce good documentation. 4 | "valid-jsdoc": [2, { 5 | "prefer": { 6 | "return": "returns" 7 | } 8 | }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/message/container.scss: -------------------------------------------------------------------------------- 1 | .common { 2 | padding-left: 50px; 3 | clear: left; 4 | } 5 | 6 | .default { 7 | composes: common; 8 | 9 | } 10 | 11 | .detailed { 12 | composes: common; 13 | padding-top: 20px; 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "css-wipe"; 3 | 4 | html, body { 5 | background: white; 6 | font-family: "Ubuntu"; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | :global #content { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /config/webpack/partial/root.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path'; 3 | 4 | export default function root({ context }) { 5 | return { 6 | resolve: { 7 | root: [ 8 | path.join(context, 'src') 9 | ] 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/sidebar/channels.scss: -------------------------------------------------------------------------------- 1 | .channels { 2 | padding: 1.2em; 3 | } 4 | 5 | .channel { 6 | color: white; 7 | padding: 0.2em 0; 8 | } 9 | 10 | .channelHash { 11 | color: #999; 12 | } 13 | 14 | .header { 15 | margin-bottom: 0.4em; 16 | } 17 | -------------------------------------------------------------------------------- /config/webpack/partial/images.webpack.config.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | module: { 4 | loaders: [{ 5 | test: /\.(gif|jpe?g|png|tiff|svg)(\?.*)?$/, 6 | loader: 'file-loader', 7 | }] 8 | } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const generalChannelId = '9d0bea9c-4a68-4129-bc58-0f929ee11ffd'; 2 | export const apiRoot = 'https://slerk-api.herokuapp.com/v1'; 3 | 4 | export const auth0Domain = 'slerk.auth0.com'; 5 | export const auth0ClientID = 'TgUKK4FYiuElDOQ1RYLni83wudgX0HhI'; 6 | -------------------------------------------------------------------------------- /src/server/build-info.js: -------------------------------------------------------------------------------- 1 | /* global BUILD_VERSION BUILD_COMMIT */ 2 | 3 | export default function() { 4 | return function(req, res, next) { 5 | res.set('Build-Version', BUILD_VERSION); 6 | res.set('Build-Commit', BUILD_COMMIT); 7 | next(); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /config/webpack/partial/json.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default function json5() { 3 | return { 4 | module: { 5 | loaders: [{ 6 | name: 'json5', 7 | test: /\.json5?$/, 8 | loader: 'json5-loader' 9 | }] 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /entry/client.entry.js: -------------------------------------------------------------------------------- 1 | // Core react. 2 | import { render } from 'react-dom'; 3 | 4 | // Base component. 5 | import Root from 'index'; 6 | 7 | // Get the root element. 8 | const rootEl = document.getElementById('content'); 9 | 10 | // Mount the root component. 11 | render(Root, rootEl); 12 | -------------------------------------------------------------------------------- /config/eslint/filenames.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "filenames" 4 | ], 5 | 6 | "rules": { 7 | // Enforce the idiomatic kebab-case instead of snake_case or PascalCase. 8 | // There are also sometimes casing issues on different platforms. 9 | "filenames/filenames": [2, "^[a-z.-]+$"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/users.js: -------------------------------------------------------------------------------- 1 | import xr from 'xr'; 2 | import { apiRoot } from '../constants'; 3 | 4 | export function retrieveUsers(authToken) { 5 | return xr.get(`${apiRoot}/users`, null, { 6 | headers: { 7 | Accept: 'application/vnd.api+json', 8 | Authorization: `Bearer ${authToken}` 9 | }, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/sidebar/index.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | z-index: 40; 3 | width: 250px; 4 | background: #3C413F; 5 | color: #A9ACAB; 6 | position: relative; 7 | } 8 | 9 | .credit { 10 | position: absolute; 11 | bottom: 23px; 12 | left: 25px; 13 | &, & a { 14 | color: rgba(255,255,255,0.3); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/error.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Error displayer. 4 | * @returns {[type]} [description] 5 | */ 6 | export default function error() { 7 | // TODO: Implement me! 8 | // For production – static error page. 9 | // For dev – stack trace. 10 | return function(err, req, res, next) { 11 | next(); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/server/logging.js: -------------------------------------------------------------------------------- 1 | 2 | import morgan from 'morgan'; 3 | 4 | export default function() { 5 | const env = process.env.NODE_ENV || 'development'; 6 | if (env !== 'production') { 7 | return morgan('dev'); 8 | } 9 | // TODO: Send logs somewhere useful in production. 10 | return (req, res, next) => next(); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/rich-text/index.js: -------------------------------------------------------------------------------- 1 | 2 | import mdast from 'mdast'; 3 | import { Element } from 'react'; 4 | import Visitor from './visitor'; 5 | 6 | export default function Text(attributes) : Element { 7 | return mdast().use((mdast) => { 8 | mdast.Compiler = Visitor; 9 | }).process(attributes.text, { 10 | attributes: attributes 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import channels from './channels'; 4 | import users from './users'; 5 | import socket from './socket'; 6 | import auth from './auth'; 7 | 8 | // Combine individual reducers into our top-level reducer 9 | export default combineReducers({ 10 | channels, 11 | users, 12 | socket, 13 | auth, 14 | }); 15 | -------------------------------------------------------------------------------- /config/webpack/partial/stats.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import StatsPlugin from 'stats-webpack-plugin'; 3 | 4 | export default function stats() { 5 | return { 6 | plugins: [ 7 | new StatsPlugin('stats.json', { 8 | hash: true, 9 | assets: false, 10 | reasons: false, 11 | chunks: true, 12 | source: false 13 | }) 14 | ] 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /config/webpack/partial/compatibility.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default function compatibility({ target }) { 3 | // These shims should only be available to web targets. 4 | if (target === 'web') { 5 | return { 6 | entry: { 7 | shiv: 'html5shiv/src/html5shiv-printshiv', 8 | shim: [ 'es5-shim/es5-shim', 'es5-shim/es5-sham' ] 9 | } 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/action-types.js: -------------------------------------------------------------------------------- 1 | export const MESSAGE_SEND = 'MESSAGE_SEND'; 2 | export const MESSAGE_RECEIVE = 'MESSAGE_RECEIVE'; 3 | export const MESSAGES_FETCH = 'MESSAGES_FETCH'; 4 | 5 | export const USERS_FETCH = 'USERS_FETCH'; 6 | export const USERS_UPDATE_PRESENCE = 'USERS_UPDATE_PRESENCE'; 7 | 8 | export const AUTH_COMPLETE = 'AUTH_COMPLETE'; 9 | 10 | export const SOCKET_CONNECT = 'SOCKET_CONNECT'; 11 | -------------------------------------------------------------------------------- /src/api/messages.js: -------------------------------------------------------------------------------- 1 | import xr from 'xr'; 2 | import { apiRoot, generalChannelId } from '../constants'; 3 | 4 | export function retrieveMessages(authToken, chanId = generalChannelId) { 5 | return xr.get(`${apiRoot}/channels/${chanId}/messages`, null, { 6 | headers: { 7 | Accept: 'application/vnd.api+json', 8 | Authorization: `Bearer ${authToken}` 9 | }, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /config/webpack/partial/optimize.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { optimize } from 'webpack'; 3 | 4 | export default function optimizer() { 5 | return { 6 | plugins: process.env.NODE_ENV === 'production' ? [ 7 | new optimize.DedupePlugin(), 8 | new optimize.UglifyJsPlugin({ 9 | compress: { 10 | warnings: false 11 | } 12 | }) 13 | ] : [] 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { getToken } from '../utils/auth'; 2 | import { AUTH_COMPLETE } from '../action-types'; 3 | 4 | const initialState = { 5 | token: getToken(), 6 | }; 7 | 8 | export default function channels(state = initialState, action) { 9 | if (action.type === AUTH_COMPLETE && action.payload) { 10 | return {...state, token: action.payload.token}; 11 | } 12 | return state; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/message/name.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import styles from './name.scss'; 4 | 5 | export default class Name extends Component { 6 | 7 | static propTypes = { 8 | user: PropTypes.object.isRequired 9 | }; 10 | 11 | render() : Element { 12 | const { name } = this.props.user; 13 | return {name}; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/socket.js: -------------------------------------------------------------------------------- 1 | import { SOCKET_CONNECT } from '../action-types'; 2 | 3 | const initialState = { 4 | socket: null, 5 | channel: null, 6 | }; 7 | 8 | export default function socket(state = initialState, action) { 9 | switch (action.type) { 10 | case SOCKET_CONNECT: 11 | const { socket, channel } = action.payload; 12 | return { ...state, socket, channel}; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/message/avatar.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import styles from './avatar.scss'; 4 | 5 | export default class Avatar extends Component { 6 | 7 | static propTypes = { 8 | user: PropTypes.object.isRequired 9 | }; 10 | 11 | render() : Element { 12 | const { picture } = this.props.user; 13 | return ; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/chat/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | box-shadow: 0px 0 15px 0px rgba(0, 0, 0, 0.2); 3 | border-bottom: 1px solid #d0d0d0; 4 | z-index: 20; 5 | } 6 | 7 | .channelName { 8 | font-size: 24px; 9 | margin: 1em; 10 | display: inline-block; 11 | font-weight: normal; 12 | } 13 | 14 | .topic { 15 | display: inline-block; 16 | color: #999; 17 | } 18 | 19 | .userCount { 20 | 21 | } 22 | 23 | .hashSign { 24 | color: #999; 25 | } 26 | -------------------------------------------------------------------------------- /config/webpack/partial/babel.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default function babel() { 3 | return { 4 | module: { 5 | loaders: [{ 6 | name: 'babel', 7 | test: /\.js$/, 8 | exclude: /node_modules/, 9 | loader: 'babel-loader', 10 | query: { 11 | stage: 0, 12 | optional: [ 13 | 'runtime' 14 | ], 15 | jsxPragma: 'createElement' 16 | } 17 | }] 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/message/content.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import RichText from '../rich-text'; 4 | import styles from './content.scss'; 5 | 6 | export default class Content extends Component { 7 | 8 | static propTypes = { 9 | text: PropTypes.string.isRequired 10 | }; 11 | 12 | render() : Element { 13 | const { text } = this.props; 14 | return ; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/users.js: -------------------------------------------------------------------------------- 1 | import { indexBy } from 'lodash'; 2 | import { 3 | USERS_FETCH, 4 | USERS_UPDATE_PRESENCE, 5 | } from '../action-types'; 6 | 7 | export default function users(state = {}, action) { 8 | switch (action.type) { 9 | case USERS_UPDATE_PRESENCE: 10 | const user = action.payload; 11 | return {...state, [user.id]: user}; 12 | case USERS_FETCH: 13 | const users = indexBy(action.payload, 'id'); 14 | return {...state, ...users}; 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slerk-web", 3 | "description": "Web frontend for Slerk.", 4 | "logo": "http://i.imgur.com/6MEyk9n.jpg", 5 | "keywords": [ 6 | "slack", 7 | "slerk", 8 | "startupslam" 9 | ], 10 | "env": { 11 | "API_URL": { 12 | "description": "URL to the `slerk-api` backend.", 13 | "value": "https://slerk-api.herokuapp.com" 14 | }, 15 | "NODE_ENV": { 16 | "description": "Environment the app is running in.", 17 | "value": "production" 18 | } 19 | }, 20 | "buildpacks": [ 21 | 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true 5 | }, 6 | "extends": [ 7 | "./config/eslint/base.eslintrc", 8 | "./config/eslint/react.eslintrc", 9 | "./config/eslint/babel.eslintrc", 10 | "./config/eslint/style.eslintrc", 11 | "./config/eslint/filenames.eslintrc", 12 | "./config/eslint/docs.eslintrc", 13 | "./config/eslint/imports.eslintrc", 14 | "./config/eslint/next.eslintrc" 15 | ], 16 | "rules": { 17 | "comma-dangle": 0, 18 | "no-extra-parens": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /entry/spec.entry.js: -------------------------------------------------------------------------------- 1 | 2 | let tests = require.context('../test/spec', true, /\.spec\.js$/); 3 | const specs = { }; 4 | 5 | // https://github.com/webpack/webpack/issues/834 6 | if (module.hot) { 7 | module.hot.accept(tests.id, () => { 8 | tests = require.context('../test/spec', true, /\.spec\.js$/); 9 | }); 10 | } 11 | 12 | export default function() { 13 | tests.keys().forEach((entry) => { 14 | const entries = tests(entry); 15 | if (specs[entry] !== entries) { 16 | specs[entry] = entries; 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/build-info/index.js: -------------------------------------------------------------------------------- 1 | /* global BUILD_COMMIT BUILD_VERSION */ 2 | 3 | import { Component, Element, createElement } from 'react'; 4 | 5 | /** 6 | * Display the build information. Handy for debugging and telling what version 7 | * of the code is deployed to the environment you're currently looking at. 8 | */ 9 | export default class BuildInfo extends Component { 10 | render() : Element { 11 | return
12 | Build: {BUILD_COMMIT} 13 | Version: {BUILD_VERSION} 14 |
; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/eslint/imports.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "import" 4 | ], 5 | 6 | "settings": { 7 | "import/parser": "babel-eslint", 8 | "import/resolve": { 9 | "moduleDirectory": [ 10 | "node_modules", 11 | "./src" 12 | ] 13 | } 14 | }, 15 | 16 | "rules": { 17 | "import/no-unresolved": 2, 18 | "import/named": 2, 19 | "import/default": 0, 20 | "import/no-require": 2, 21 | "import/imports-first": [2, "absolute-first"], 22 | "import/no-duplicates": 2, 23 | "import/export": 2 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/message/timestamp.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | import moment from 'moment'; 3 | 4 | import styles from './timestamp.scss'; 5 | 6 | export default class Timestamp extends Component { 7 | 8 | static propTypes = { 9 | insertedAt: PropTypes.string.isRequired 10 | }; 11 | 12 | render() : Element { 13 | const { insertedAt } = this.props; 14 | return ( 15 | 16 | {moment(insertedAt).format('h:mm A')} 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/eslint/next.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // http://eslint.org/docs/rules/prefer-const 4 | "prefer-const": 2, 5 | 6 | // http://eslint.org/docs/rules/no-const-assign 7 | "no-const-assign": 2, 8 | 9 | // http://eslint.org/docs/rules/no-dupe-class-members 10 | "no-dupe-class-members": 2, 11 | 12 | // http://eslint.org/docs/rules/no-var 13 | "no-var": 2, 14 | 15 | // http://eslint.org/docs/rules/prefer-template 16 | "prefer-template": 2, 17 | 18 | // http://eslint.org/docs/rules/jsx-quotes 19 | "jsx-quotes": [2, "prefer-single"] 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/components/message/container.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import styles from './container.scss'; 4 | 5 | export default class Container extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.node.isRequired, 9 | detailed: PropTypes.bool.isRequired 10 | }; 11 | 12 | render() : Element { 13 | const { children, detailed } = this.props; 14 | return ( 15 |
16 | {children} 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/webpack/partial/adana.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default function adana() { 3 | const env = process.env.NODE_ENV || 'development'; 4 | 5 | if (process.env.COVERAGE || env !== 'test') { 6 | return { }; 7 | } 8 | 9 | // Rewrite all the entry points to include HMR code. 10 | return { 11 | module: { 12 | loaders: [{ 13 | name: 'babel', 14 | query: { 15 | plugins: [ 16 | 'adana' 17 | ], 18 | extra: { 19 | adana: true 20 | } 21 | } 22 | }] 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/sidebar/header.js: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Element, createElement } from 'react'; 3 | 4 | 5 | import styles from './header.scss'; 6 | 7 | /** 8 | * whatami 9 | */ 10 | export default class Header extends Component { 11 | 12 | static propTypes = { 13 | 14 | } 15 | 16 | render() : Element { 17 | return
18 |

Startup Slam

19 | {/* 20 |
21 | 22 | Izaak Schroeder 23 |
24 | */} 25 |
; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/sidebar/users.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables.scss'; 2 | 3 | .users { 4 | padding: 1.2em; 5 | } 6 | 7 | .user { 8 | margin: 0.3em 0; 9 | text-overflow: ellipsis; 10 | overflow: hidden; 11 | white-space: nowrap; 12 | } 13 | 14 | .statusIndicator { 15 | background-color: white; 16 | margin: 0.1em 0; 17 | margin-right: 0.25em; 18 | width: 9px; 19 | height: 9px; 20 | display: inline-block; 21 | vertical-align: baseline; 22 | position: relative; 23 | border-radius: 100%; 24 | opacity: 0.3; 25 | } 26 | 27 | .onlineIndicator { 28 | background-color: $green; 29 | opacity: 1; 30 | } 31 | -------------------------------------------------------------------------------- /config/webpack/partial/env.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { EnvironmentPlugin } from 'webpack'; 3 | 4 | export default function env() { 5 | // Use bluebird long traces for development and testing 6 | // See: https://github.com/petkaantonov/bluebird#error-handling 7 | process.env.BLUEBIRD_DEBUG = env.NODE_ENV !== 'production'; 8 | 9 | return { 10 | plugins: [ 11 | // Export `process.env` to the app being built. Optimize your code by 12 | // checking `NODE_ENV` and set things like config variables (e.g. 13 | // `API_URL`). 14 | new EnvironmentPlugin(Object.keys(process.env)) 15 | ] 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/sidebar/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | &:hover { 3 | background: #333; 4 | } 5 | padding: 1.2em; 6 | h1 { 7 | color: #fff; 8 | font-size: 20px; 9 | } 10 | } 11 | 12 | .account { 13 | margin: 0.4em 0; 14 | > * { 15 | vertical-align: middle; 16 | } 17 | } 18 | 19 | .status + .name { 20 | margin-left: 0.4em; 21 | } 22 | 23 | .status { 24 | border-radius: 100%; 25 | width: 0.6em; 26 | height: 0.6em; 27 | display: inline-block; 28 | } 29 | 30 | .online { 31 | composes: status; 32 | background: #688E3D; 33 | } 34 | 35 | .idle { 36 | composes: status; 37 | background: #eab700; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/chat/header.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import styles from './header.scss'; 4 | 5 | /** 6 | * whatami 7 | */ 8 | export default class Header extends Component { 9 | 10 | static propTypes = { 11 | title: PropTypes.string.isRequired, 12 | topic: PropTypes.string.isRequired, 13 | } 14 | 15 | render() : Element { 16 | return
17 |

18 | # 19 | [title] 20 |

21 | [topic] 22 |
; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/root.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, createElement, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { completeAuthentication } from '../actions'; 5 | import App from './app'; 6 | import Login from '../components/login'; 7 | 8 | class RootApp extends Component { 9 | 10 | static propTypes = { 11 | token: PropTypes.string, 12 | } 13 | 14 | componentWillMount() { 15 | this.props.dispatch(completeAuthentication()); 16 | } 17 | 18 | render() : Element { 19 | return createElement(this.props.token ? App : Login); 20 | } 21 | } 22 | 23 | // Wire up auth state to compontent 24 | export default connect(state => ({ token: state.auth.token }))(RootApp); 25 | -------------------------------------------------------------------------------- /src/components/sidebar/users.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, createElement, PropTypes } from 'react'; 2 | 3 | import User from './user'; 4 | 5 | import styles from './users.scss'; 6 | 7 | /** 8 | * I am your worst nightmare. 👻 9 | */ 10 | export default class Users extends Component { 11 | 12 | static propTypes = { 13 | users: PropTypes.object.isRequired, 14 | } 15 | 16 | render() : Element { 17 | const { users } = this.props; 18 | 19 | return ( 20 |
21 |

Users

22 | 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/sidebar/user.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, createElement, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import styles from './users.scss'; 5 | 6 | /** 7 | * Worst nightmare. 👻 8 | */ 9 | export default class Users extends Component { 10 | 11 | static propTypes = { 12 | user: PropTypes.object.isRequired, 13 | } 14 | 15 | render() : Element { 16 | const { user } = this.props; 17 | const indicatorStyle = classnames(styles.statusIndicator, { 18 | [styles.onlineIndicator]: user.online 19 | }); 20 | 21 | return ( 22 |
  • 23 | 24 | {user.name} 25 |
  • 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/sidebar/channels.js: -------------------------------------------------------------------------------- 1 | 2 | import { Component, Element, createElement } from 'react'; 3 | 4 | 5 | import styles from './channels.scss'; 6 | 7 | /** 8 | * whatami 9 | */ 10 | export default class ChannelList extends Component { 11 | 12 | static propTypes = { 13 | 14 | } 15 | 16 | render() : Element { 17 | const channels = [{name: 'general'}]; 18 | return
    19 |

    Channels

    20 | 29 |
    ; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/sidebar/index.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, createElement, PropTypes } from 'react'; 2 | 3 | import Header from './header'; 4 | import Channels from './channels'; 5 | import Users from './users'; 6 | 7 | import styles from './index.scss'; 8 | 9 | /** 10 | * whatami 11 | */ 12 | export default class Sidebar extends Component { 13 | 14 | static propTypes = { 15 | users: PropTypes.object.isRequired, 16 | } 17 | 18 | render() : Element { 19 | return ( 20 |
    21 |
    22 | 23 | 24 |
    25 | Made with ♥ by MetaLab 26 |
    27 |
    28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/webpack/partial/sharp.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default function() { 3 | return { 4 | module: { 5 | loaders: [{ 6 | test: /\.(gif|jpe?g|png|tiff|svg)(\?.*)?$/, 7 | loader: 'sharp-loader', 8 | query: { 9 | name: '[name].[hash:8].[ext]', 10 | presets: { 11 | favicon: { 12 | size: 32, 13 | format: 'png' 14 | }, 15 | default: { 16 | format: [ 'webp', 'png', 'jpeg' ], 17 | density: [ 1, 2, 3 ] 18 | }, 19 | prefetch: { 20 | format: 'jpeg', 21 | mode: 'cover', 22 | blur: 100, 23 | quality: 30, 24 | inline: true, 25 | size: 50 26 | } 27 | } 28 | } 29 | }] 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/server/collect.js: -------------------------------------------------------------------------------- 1 | 2 | export default function collect(root, stats) { 3 | const base = root.charAt(root.length) !== '/' ? `${root}/` : root; 4 | 5 | // Order the chunks so commons chunks come first. 6 | const files = stats.chunks 7 | .slice() 8 | .sort((a, b) => a.entry === b.entry ? b.id - a.id : b.entry - a.entry) 9 | .reduce((list, chunk) => list.concat(chunk.files), []); 10 | 11 | const result = { 12 | scripts: [], 13 | styles: [] 14 | }; 15 | 16 | files.forEach(file => { 17 | const path = base + file; 18 | if (/\.js$/.test(file)) { 19 | result.scripts.push({ 20 | type: 'text/javascript', 21 | path 22 | }); 23 | } else if (/\.css$/.test(file)) { 24 | result.styles.push({ 25 | type: 'text/css', 26 | path 27 | }); 28 | } 29 | }); 30 | 31 | return result; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/chat/conversation.scss: -------------------------------------------------------------------------------- 1 | .conversation { 2 | flex: 1; 3 | position: relative; 4 | padding: 1em; 5 | display: flex; 6 | overflow-y: auto; 7 | } 8 | 9 | .messages { 10 | height: 100%; 11 | } 12 | 13 | .dateBreakNormal { 14 | font-weight: 800; 15 | background: #fff; 16 | text-align: center; 17 | padding: 0.3em; 18 | overflow: hidden; 19 | &::before, &::after { 20 | background-color: #ddd; 21 | content: ""; 22 | display: inline-block; 23 | height: 1px; 24 | position: relative; 25 | vertical-align: middle; 26 | width: 50%; 27 | } 28 | &::before { 29 | right: 0.8em; 30 | margin-left: -50%; 31 | } 32 | &::after { 33 | left: 0.8em; 34 | margin-right: -50%; 35 | } 36 | } 37 | 38 | .dateBreakActive { 39 | composes: dateBreakNormal; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | right: 0; 44 | } 45 | -------------------------------------------------------------------------------- /config/webpack/partial/vendor.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { optimize } from 'webpack'; 3 | 4 | export default function({ target }) { 5 | return { 6 | externals: target === 'node' ? [(context, request, cb) => { 7 | // TODO: Make this work properly. 8 | if (/^[a-z\-0-9]+$/.test(request)) { 9 | return cb(null, `commonjs ${request}`); 10 | } 11 | cb(); 12 | }] : [ ], 13 | plugins: target !== 'node' ? [ 14 | // This performs the actual bundling of all the vendor files into their 15 | // own package. See the vendor entry above for more info. 16 | new optimize.CommonsChunkPlugin({ 17 | name: 'vendor', 18 | filename: '[name].[hash].js', 19 | minChunks: (module) => { 20 | return module.resource && 21 | module.resource.indexOf('node_modules') !== -1; 22 | } 23 | }) 24 | ] : [ ] 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/message/index.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | import Container from './container'; 3 | 4 | /* 5 | // Uncomment to access available components. 6 | import Avatar from './avatar'; 7 | import Name from './name'; 8 | import Timestamp from './timestamp'; 9 | import Content from './content'; 10 | */ 11 | 12 | /** 13 | * Message component 14 | */ 15 | export default class Message extends Component { 16 | 17 | static propTypes = { 18 | user: PropTypes.object.isRequired, 19 | /* eslint-disable camelcase */ 20 | inserted_at: PropTypes.string, 21 | /* eslint-enable camelcase */ 22 | text: PropTypes.string, 23 | // If we should show the avatar, time, and name 24 | detailed: PropTypes.bool, 25 | }; 26 | 27 | render() : Element { 28 | return ( 29 | 30 | [message] 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Auth0 from 'auth0-js'; 2 | 3 | import { auth0Domain, auth0ClientID } from '../constants'; 4 | const AUTH0_TOKEN_KEY = 'auth0_token'; 5 | 6 | const auth0 = new Auth0({ 7 | domain: auth0Domain, 8 | clientID: auth0ClientID, 9 | callbackOnLocationHash: true 10 | }); 11 | 12 | // Naive implementation; doesn't check token expiry or 13 | // if the token has been revoked. It however, should 14 | // be okay in context of the presentation however. 15 | export function getToken() { 16 | return localStorage.getItem(AUTH0_TOKEN_KEY); 17 | } 18 | 19 | export function setToken(token) { 20 | return localStorage.setItem(AUTH0_TOKEN_KEY, token); 21 | } 22 | 23 | export function startLogin(provider) { 24 | auth0.login({ connection: provider }); 25 | } 26 | 27 | // TODO: Potentially pass up any errors (user canceled, etc.) 28 | export function parseHash(hash) { 29 | const result = auth0.parseHash(hash); 30 | return result && result.id_token; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/chat/index.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, createElement, PropTypes } from 'react'; 2 | import { takeRight } from 'lodash'; 3 | 4 | import Header from './header'; 5 | import Conversation from './conversation'; 6 | import Input from './input'; 7 | 8 | import styles from './index.scss'; 9 | 10 | /** 11 | * whatami 12 | */ 13 | export default class Chat extends Component { 14 | 15 | static propTypes = { 16 | channel: PropTypes.object.isRequired, 17 | messageSend: PropTypes.func.isRequired, 18 | } 19 | 20 | render() : Element { 21 | const { channel, messageSend } = this.props; 22 | 23 | // Only display the last x messages for perf reasons 24 | const messages = takeRight(channel.messages, 100); 25 | 26 | return
    27 |
    28 | 29 | 30 |
    ; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /config/webpack/partial/source-maps.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { BannerPlugin } from 'webpack'; 3 | 4 | export default function sourcemaps(options) { 5 | const env = process.env.NODE_ENV || 'development'; 6 | return { 7 | // Embed source map support for sane debugging. This kinda cheats by 8 | // writing source map hooks at the top of every entrypoint. 9 | plugins: options.target === 'node' && env !== 'production' ? [ 10 | new BannerPlugin('require("source-map-support").install();', { 11 | raw: true, 12 | entryOnly: false 13 | }) 14 | ] : [], 15 | 16 | // Use `eval` style source-maps for development since they're extremely 17 | // fast to generate. 18 | // Use full source-maps for production builds. This also helps prevent 19 | // prying eyes from poking into the code by allowing the map file to be 20 | // hosted separately and privately (e.g. on S3). 21 | devtool: env === 'production' ? 'source-map' : 'eval' 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/login/index.js: -------------------------------------------------------------------------------- 1 | import { Element, Component, createElement } from 'react'; 2 | import { startLogin } from '../../utils/auth'; 3 | 4 | import logo from '../../images/logo.svg'; 5 | import styles from './index.scss'; 6 | 7 | /** 8 | * Login w/ Auth0 9 | */ 10 | export default class Login extends Component { 11 | 12 | onAuthenticate() { 13 | startLogin('github'); 14 | } 15 | 16 | render() : Element { 17 | return ( 18 |
    19 | 25 |
    26 |
    27 | 28 |

    Sign in to Slerk™

    29 | 32 |
    33 |
    34 |
    35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | 2 | // Express. 3 | import express from 'express'; 4 | 5 | // Middleware. 6 | import compression from 'compression'; 7 | import assets from './assets'; 8 | import logging from './logging'; 9 | import buildInfo from './build-info'; 10 | 11 | // Create the application. 12 | const app = express(); 13 | 14 | // Add assorted middleware. 15 | app.use(logging()); 16 | app.use(compression()); 17 | app.use(buildInfo()); 18 | app.use(assets()); 19 | 20 | /* eslint import/no-require: 0 */ 21 | let render = require('./render'); 22 | 23 | if (module.hot) { 24 | module.hot.accept('./render', function() { 25 | render = require('./render'); 26 | }); 27 | } 28 | 29 | app.all('*', (req, res, next) => { 30 | render({ 31 | path: req.path, 32 | ...req.assets 33 | }).then(({ markup, status /* scripts, styles */ }) => { 34 | res 35 | .status(status) 36 | .set('Content-Type', 'text/html; charset="utf-8"') 37 | .send(markup); 38 | }, next); 39 | }); 40 | 41 | // Fire. 42 | export default app; 43 | -------------------------------------------------------------------------------- /config/webpack/partial/build-info.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import { DefinePlugin } from 'webpack'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import { execFileSync } from 'child_process'; 6 | 7 | // If this fails with `fatal: Needed a single revision` it means you have not 8 | // yet committed anything to the repo. 9 | function git(root, ...args) { 10 | return execFileSync('git', args, { 11 | cwd: root 12 | }); 13 | } 14 | 15 | function pkg(root) { 16 | return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); 17 | } 18 | 19 | export default function buildInfo({ context }) { 20 | // TODO: Any other useful information? 21 | const commit = process.env.SOURCE_VERSION || 22 | git(context, 'rev-parse', '--verify', 'HEAD').toString('utf8'); 23 | const version = pkg(context).version; 24 | 25 | return { 26 | plugins: [ 27 | new DefinePlugin({ 28 | BUILD_COMMIT: JSON.stringify(commit), 29 | BUILD_VERSION: JSON.stringify(version) 30 | }) 31 | ] 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/chat/input.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | z-index: 20; 3 | display: flex; 4 | flex-direction: row; 5 | box-shadow: 0px 0 15px 0px rgba(0, 0, 0, 0.2); 6 | border-top: 1px solid #d0d0d0; 7 | 8 | > * { 9 | border: none; 10 | border-top: solid 1px #c0c0c0; 11 | border-bottom: solid 1px #c0c0c0; 12 | font-size: 14px; 13 | display: block; 14 | 15 | 16 | &:first-child { 17 | border-left: solid 1px #c0c0c0; 18 | border-top-left-radius: 6px; 19 | border-bottom-left-radius: 6px; 20 | } 21 | 22 | &:last-child { 23 | border-right: solid 1px #c0c0c0; 24 | border-top-right-radius: 6px; 25 | border-bottom-right-radius: 6px; 26 | } 27 | } 28 | } 29 | 30 | 31 | .upload { 32 | padding: 0 10px; 33 | appearance: none; 34 | background: #fff; 35 | border-right: solid 1px #c0c0c0; 36 | } 37 | 38 | .emoji { 39 | background: #fff; 40 | padding: 0 10px; 41 | } 42 | 43 | .text { 44 | appearance: none; 45 | flex: 1 1 auto; 46 | padding: 0.5em; 47 | margin: 1em; 48 | } 49 | -------------------------------------------------------------------------------- /config/webpack/server.webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import nearest from 'find-nearest-file'; 2 | import partial from 'webpack-partial'; 3 | import path from 'path'; 4 | 5 | // No matter where we are, locate the canonical root of the project. 6 | const root = path.dirname(nearest('package.json')); 7 | 8 | const config = { 9 | id: 'server', 10 | entry: { 11 | server: path.join(root, 'entry', 'server.entry.js') 12 | }, 13 | target: 'node', 14 | context: root, 15 | // Output controls the settings for file generation. 16 | output: { 17 | libraryTarget: 'commonjs2', 18 | filename: '[name].js', 19 | path: path.join(root, 'build', 'server'), 20 | chunkFilename: '[id].js' 21 | } 22 | }; 23 | 24 | // Extend the default webpack configuration with any partials you want. 25 | // e.g. partial(config, 'babel', 'compatibility'); 26 | export default partial( 27 | config, 28 | 'root', 29 | 'env', 30 | 'build-info', 31 | 'hot', 32 | 'babel', 33 | 'images', 34 | 'postcss', 35 | 'json', 36 | 'vendor', 37 | 'source-maps', 38 | 'optimize' 39 | ); 40 | -------------------------------------------------------------------------------- /src/components/chat/input.js: -------------------------------------------------------------------------------- 1 | import { Component, Element, PropTypes, createElement } from 'react'; 2 | 3 | import styles from './input.scss'; 4 | 5 | /** 6 | * Input for chat. 7 | */ 8 | export default class Input extends Component { 9 | 10 | static propTypes = { 11 | messageSend: PropTypes.func.isRequired, 12 | } 13 | 14 | // Initial state for the component instance 15 | state = { 16 | value: '', 17 | } 18 | 19 | onKeyPress(ev) { 20 | // On enter, submit the chat msg and clear the field 21 | if (ev.keyCode === 13) { 22 | alert('Implement me'); 23 | } 24 | } 25 | 26 | onChange(ev) { 27 | this.setState({value: ev.target.value}); 28 | } 29 | 30 | render() : Element { 31 | return
    32 | 40 |
    ; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/webpack/client.webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import nearest from 'find-nearest-file'; 2 | import partial from 'webpack-partial'; 3 | import path from 'path'; 4 | 5 | // No matter where we are, locate the canonical root of the project. 6 | const root = path.dirname(nearest('package.json')); 7 | 8 | const config = { 9 | id: 'client', 10 | entry: { 11 | client: path.join(root, 'entry', 'client.entry.js') 12 | }, 13 | target: 'web', 14 | context: root, 15 | // Output controls the settings for file generation. 16 | output: { 17 | filename: '[name].[chunkhash].js', 18 | publicPath: '/assets', 19 | path: path.join(root, 'build', 'client'), 20 | chunkFilename: '[id].[chunkhash].js' 21 | } 22 | }; 23 | 24 | // Extend the default webpack configuration with any partials you want. 25 | // e.g. partial(config, 'babel', 'compatibility'); 26 | export default partial( 27 | config, 28 | 'root', 29 | 'env', 30 | 'build-info', 31 | 'hot', 32 | 'babel', 33 | 'images', 34 | 'postcss', 35 | 'json', 36 | 'vendor', 37 | 'source-maps', 38 | 'optimize', 39 | 'compatibility', 40 | 'stats' 41 | ); 42 | -------------------------------------------------------------------------------- /src/server/render.js: -------------------------------------------------------------------------------- 1 | 2 | // Promises. 3 | import Promise from 'bluebird'; 4 | 5 | // React. 6 | import { createElement } from 'react'; 7 | import { renderToStaticMarkup, renderToString } from 'react-dom/server'; 8 | 9 | // Prevent XSS. 10 | import escape from 'htmlescape'; 11 | 12 | // Generic page layout. 13 | import Page from './page'; 14 | import Root from '../'; 15 | 16 | function body({ scripts = [] }) { 17 | // TODO: This is where any flux magic would go. 18 | return new Promise((resolve) => { 19 | const state = { }; 20 | resolve({ 21 | status: 200, 22 | title: 'Slerk™', 23 | markup: renderToString(Root), 24 | scripts: [{ 25 | id: 'state', 26 | type: 'text/json', 27 | content: escape(state) 28 | }, ...scripts] 29 | }); 30 | }); 31 | } 32 | 33 | function html(props) { 34 | const markup = renderToStaticMarkup(); 35 | return Promise.resolve({ 36 | ...props, 37 | markup: `${markup}` 38 | }); 39 | } 40 | 41 | export default function render(props) { 42 | return Promise.all([ 43 | body(props) 44 | ]).then(([body]) => { 45 | return html({ 46 | ...props, 47 | ...body 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/server/assets.js: -------------------------------------------------------------------------------- 1 | 2 | import { Router } from 'express'; 3 | import serveStatic from 'serve-static'; 4 | import { readFileSync } from 'fs'; 5 | import path from 'path'; 6 | import collect from './collect'; 7 | 8 | const env = process.env.NODE_ENV || 'development'; 9 | const router = new Router(); 10 | 11 | let assets = { }; 12 | 13 | function middleware(req, res, next) { 14 | req.assets = assets; 15 | next(); 16 | } 17 | 18 | router.use(middleware); 19 | 20 | // Enable dynamic updating of assets in development environment. 21 | if (env === 'development') { 22 | process.on('assets', ([url, stats]) => { 23 | assets = collect(url, stats); 24 | }); 25 | } else { 26 | const assetPath = path.join(process.cwd(), 'build', 'client'); 27 | const assetUrl = process.env.ASSET_URL || '/assets'; 28 | 29 | assets = collect(assetUrl, JSON.parse(readFileSync(path.join( 30 | assetPath, 'stats.json' 31 | ), 'utf8'))); 32 | 33 | // If assets are being served locally then include them. 34 | if (assetUrl.charAt(0) === '/') { 35 | // TODO: Don't include source-maps, etc. 36 | router.use(assetUrl, serveStatic(assetPath, { 37 | index: false, 38 | lastModified: true, 39 | maxAge: '100 years', 40 | redirect: false, 41 | fallthrough: false 42 | })); 43 | } 44 | } 45 | 46 | export default function() { 47 | return router; 48 | } 49 | -------------------------------------------------------------------------------- /config/eslint/react.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react" 4 | ], 5 | "rules": { 6 | "react/display-name": 0, 7 | "react/jsx-boolean-value": 2, 8 | "react/jsx-no-undef": 2, 9 | "react/jsx-sort-props": 0, 10 | "react/jsx-sort-prop-types": 0, 11 | "react/jsx-uses-react": [2, { 12 | "pragma": "createElement" 13 | }], 14 | "react/jsx-uses-vars": 2, 15 | "react/no-did-mount-set-state": [2, "allow-in-func"], 16 | "react/no-did-update-set-state": 2, 17 | "react/no-multi-comp": 2, 18 | "react/no-unknown-property": 2, 19 | "react/prop-types": 2, 20 | "react/react-in-jsx-scope": 0, 21 | "react/self-closing-comp": 2, 22 | "react/wrap-multilines": 0, 23 | "react/sort-comp": [2, { 24 | "order": [ 25 | "displayName", 26 | "propTypes", 27 | "contextTypes", 28 | "childContextTypes", 29 | "mixins", 30 | "statics", 31 | "defaultProps", 32 | "constructor", 33 | "getDefaultProps", 34 | "getInitialState", 35 | "state", 36 | "getChildContext", 37 | "componentWillMount", 38 | "componentDidMount", 39 | "componentWillReceiveProps", 40 | "shouldComponentUpdate", 41 | "componentWillUpdate", 42 | "componentDidUpdate", 43 | "componentWillUnmount", 44 | "/^on.+$/", 45 | "/^handle.+$/", 46 | "/^get.+$/", 47 | "/^update.+$/", 48 | "everything-else", 49 | "/^render.+$/", 50 | "render" 51 | ] 52 | }] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/login/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variables.scss"; 2 | 3 | .login { 4 | background-color: #f5f5f5; 5 | height: 100%; 6 | } 7 | 8 | .nav { 9 | position: fixed; 10 | top: 0; 11 | width: 100%; 12 | height: 70px; 13 | z-index: 99; 14 | background: #FFF; 15 | box-shadow: 0 1px 1px rgba(0,0,0,.1); 16 | 17 | h1 { 18 | float: left; 19 | margin: 1.5em 0 0 0.5em; 20 | } 21 | 22 | img { 23 | width: 30px; 24 | height: 30px; 25 | float: left; 26 | margin: 20px 0 0 20px; 27 | } 28 | } 29 | 30 | .page { 31 | padding: 6rem 2rem; 32 | } 33 | 34 | .card { 35 | background-color: white; 36 | border-radius: .25rem; 37 | box-shadow: 0 1px 0 rgba(0,0,0,.25); 38 | padding: 3rem; 39 | border: 1px solid $light-gray; 40 | text-align: center; 41 | width: 400px; 42 | margin: 0 auto; 43 | 44 | img { 45 | height: 200px; 46 | } 47 | 48 | h1 { 49 | font-size: 2rem; 50 | font-weight: 400; 51 | line-height: 2.5rem; 52 | letter-spacing: -1px; 53 | text-align: center; 54 | color: $gray; 55 | margin-top: 0.25em; 56 | margin-bottom: 1.25em; 57 | } 58 | 59 | button { 60 | padding: 14px 2pc 1pc; 61 | font-size: 1.5rem; 62 | font-weight: 600; 63 | width: 100%; 64 | background: $button; 65 | color: white; 66 | line-height: 1.2rem; 67 | text-decoration: none; 68 | cursor: pointer; 69 | text-shadow: 0 1px 1px rgba(0,0,0,.1); 70 | border: none; 71 | border-radius: .25rem; 72 | box-shadow: none; 73 | position: relative; 74 | display: inline-block; 75 | vertical-align: bottom; 76 | text-align: center; 77 | white-space: nowrap; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/server/page.js: -------------------------------------------------------------------------------- 1 | 2 | import { createElement, PropTypes, Component } from 'react'; 3 | 4 | function script({ path, content, type, id }, i) { 5 | return path ? 6 |