├── .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 |
23 | {Object.values(users).map(user =>
24 | )}
25 |
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 |
21 | {channels.map(channel =>
22 | -
23 | #
24 | {' '}
25 | {channel.name}
26 |
27 | )}
28 |
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 |
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 |
;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |